Skip to content

feat(daemon+cli): aceclaw dashboard subcommand — daemon serves bundled dist (#446)#477

Merged
xinhuagu merged 10 commits into
mainfrom
feat/446-dashboard-subcommand
May 3, 2026
Merged

feat(daemon+cli): aceclaw dashboard subcommand — daemon serves bundled dist (#446)#477
xinhuagu merged 10 commits into
mainfrom
feat/446-dashboard-subcommand

Conversation

@xinhuagu

@xinhuagu xinhuagu commented May 3, 2026

Copy link
Copy Markdown
Owner

Closes #446. One command (aceclaw dashboard) replaces the 5-step manual setup.

Changes

Daemon

  • WebSocketBridge serves bundled dashboard at / from classpath /META-INF/dashboard with SPA fallback. Friendly 404 in -Pno-dashboard builds.
  • Same-origin allowlist (http://localhost:{port}, http://127.0.0.1:{port}) computed at start — bundled dashboard connects with zero allowedOrigins config; cross-site origins still rejected with 1008.
  • health.status reports dashboard: { enabled, url, bundled } so CLI can discover the URL instead of hard-coding 3141.
  • DEFAULT_WEBSOCKET_ENABLED → true. Bind host stays localhost; same-origin gate keeps cross-site browsers out.

CLI

  • New aceclaw dashboard subcommand: auto-starts daemon, queries health, prints URL, opens browser via native open/xdg-open/rundll32 (more reliable than Desktop.browse in headless JVMs). --no-open flag for SSH.
  • REPL startup prints dashboard: once for zero-friction discovery.

Build

  • aceclaw-dashboard is now a Gradle module that runs npm ci + npm run build with proper input/output caching.
  • :aceclaw-daemon:processResources copies dist/ into META-INF/dashboard/.
  • -Pno-dashboard escape hatch for backend devs without Node 20.

Tests

  • WebSocketBridgeTest.acceptsSameOriginHandshakeWithoutAllowedOriginsConfig pins the new behavior.

Refinements beyond the original issue spec

Posted as comments on #446: same-origin gate, default-on for webSocket, URL printed on browser-open failure, --no-open flag, npm ci, -Pno-dashboard, startup hint.

Test plan

  • ./gradlew test — green
  • ./gradlew :aceclaw-daemon:processResources bundles dashboard files
  • ./gradlew :aceclaw-daemon:processResources -Pno-dashboard skips bundling
  • aceclaw dashboard --no-open auto-starts daemon, prints URL
  • curl / SPA fallback / /assets/* all return 200
  • WS same-origin accept; cross-site reject with 1008
  • Manually verify execution tree renders in browser (reviewer)
  • CI green on Linux/macOS/Windows

Summary by CodeRabbit

  • New Features

    • CLI dashboard command prints the dashboard URL and can open it in your browser.
    • REPL startup prints the dashboard URL once when available.
  • Changes

    • Dashboard frontend is built, optionally bundled into the daemon, and auto-detected/served at startup.
    • WebSocket bridge enabled by default with loopback-only safety unless explicitly opted in/out.
  • Tests

    • Added tests covering dashboard reporting, WebSocket defaulting, and same-origin handshake.

@qodo-code-review

Copy link
Copy Markdown
ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds a bundled production dashboard to the daemon (classpath-served SPA), accepts same-origin WebSocket handshakes, exposes dashboard metadata via health.status, wires Gradle/npm build and optional packaging of the dashboard module, and adds a CLI dashboard subcommand that prints and can open the dashboard URL.

Changes

Dashboard Support Infrastructure

Layer / File(s) Summary
Build & Module Setup
settings.gradle.kts, build.gradle.kts, aceclaw-dashboard/build.gradle.kts, aceclaw-daemon/build.gradle.kts
Adds aceclaw-dashboard project, npm npmCi/npmBuild tasks, -Pno-dashboard/no-dashboard gating, and copies dist/ into daemon resources (META-INF/dashboard/) when enabled.
Configuration Defaults
aceclaw-daemon/src/main/java/dev/aceclaw/daemon/AceClawConfig.java
Changes default webSocket.enabled to true, adds webSocketEnabledExplicit tracking, loopback-host detection helper, and enforces loopback-only safety gate when not explicitly opted-in.
Data Shape / API
aceclaw-daemon/src/main/java/dev/aceclaw/daemon/RequestRouter.java
Adds RequestRouter.DashboardInfo record and setDashboardInfo(...); extends health.status JSON to include a dashboard object with enabled, bundled, and optional url.
Core Implementation
aceclaw-daemon/src/main/java/dev/aceclaw/daemon/WebSocketBridge.java
Detects bundled dashboard classpath resource, serves static assets with SPA fallback when present (or a plain-text 404 otherwise), computes and maintains a sameOriginAllowlist based on bound port, updates origin-allowance to accept same-origin Origin values, exposes dashboardBundled(), and clears allowlist on stop.
Daemon Wiring
aceclaw-daemon/src/main/java/dev/aceclaw/daemon/AceClawDaemon.java
Publishes initial placeholder DashboardInfo during wiring (enabled=false), and after webSocketBridge.start() republishes enabled=true with a normalized host/port-built dashboardUrl (normalizes 0.0.0.0/:: to localhost, brackets IPv6 as needed).
CLI Integration
aceclaw-cli/src/main/java/dev/aceclaw/cli/AceClawMain.java
Registers nested DashboardCommand subcommand; REPL startup prints dashboard: <url> once when health.status.dashboard reports enabled && bundled && url; aceclaw dashboard re-queries health.status, validates dashboard shape/flags, prints the URL, and optionally opens it in the system browser (with --no-open).
Tests
aceclaw-daemon/src/test/java/dev/aceclaw/daemon/*
Adds WebSocketBridgeTest verifying same-origin WS handshake acceptance with empty allowedOrigins, AceClawConfigTest cases for WebSocket defaulting and gating, and RequestRouterTest assertions for health.status dashboard fields.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI
    participant Daemon
    participant Browser
    User->>CLI: aceclaw dashboard
    CLI->>Daemon: GET /health/status
    Daemon-->>CLI: { dashboard: { enabled, bundled, url } }
    CLI->>Browser: open(url)
    Browser->>Daemon: GET / (same-origin)
    Daemon-->>Browser: index.html + assets
    Browser->>Daemon: WebSocket /ws (Origin matches same-origin)
    Daemon-->>Browser: WS connected + events
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • #442: Adds the aceclaw-dashboard frontend module and build scaffold that is bundled by the daemon in this change.
  • #441: Prior WebSocket/dashboard plumbing changes that this PR extends (overlaps on bridge/health wiring).
  • #448: Other WebSocketBridge changes (event buffering/snapshot logic) that may intersect with origin/serving behavior.

Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Block Major Correctness And Security Risks ❌ Error Same-origin allowlist hardcoded to localhost addresses only, excluding explicitly configured non-loopback hosts advertised by daemon, breaking the bundled dashboard feature for non-loopback deployments. Add buildSameOriginAllowlist method that includes configured host alongside loopback addresses with proper IPv6 bracket handling.
Require Test Coverage For New Logic ❌ Error New DashboardCommand in AceClawMain.java lacks test coverage; no test files exist in aceclaw-cli module. AceClawDaemon dashboard initialization lacks corresponding test verification. Add AceClawMainTest.java to test DashboardCommand invocation, daemon startup, and URL discovery. Add dashboard initialization tests to AceClawDaemonTest.java to verify the end-to-end dashboard flow.
Docstring Coverage ⚠️ Warning Docstring coverage is 45.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a dashboard subcommand to the CLI and enabling the daemon to serve a bundled dashboard distribution.
Linked Issues check ✅ Passed All primary objectives from issue #446 are met: daemon serves bundled dashboard with same-origin WebSocket, CLI dashboard subcommand with browser opening, Gradle dashboard module with npm integration, and -Pno-dashboard escape hatch for backend developers.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #446 objectives: dashboard bundling, WebSocket configuration changes, CLI subcommand, Gradle build tasks, and supporting test coverage. No unrelated modifications detected.
No Api Breaking Changes Without Version Bump ✅ Passed PR adds new public APIs (record, methods, subcommand) without modifying existing public API signatures, so no breaking changes requiring version bumps.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/446-dashboard-subcommand

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get your free trial and get 200 agent minutes per Slack user (a $50 value).


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the security label May 3, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 51ae2b0bc4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +555 to +559
boolean dashboardBundled = WebSocketBridge.dashboardBundled();
String dashboardUrl = config.webSocketEnabled()
? "http://" + config.webSocketHost() + ":" + config.webSocketPort()
: "";
router.setDashboardInfo(new RequestRouter.DashboardInfo(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Report dashboard enabled only after bridge startup succeeds

health.status is now populated from config values before the bridge is actually started, so it can report dashboard.enabled=true and a URL even when webSocketBridge.start() later fails (for example, if the WS port is already in use and the daemon continues without the bridge). In that case aceclaw dashboard prints/opens a dead URL instead of surfacing that the dashboard is unavailable, which breaks the new discovery flow and makes failures hard to diagnose.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
aceclaw-daemon/src/test/java/dev/aceclaw/daemon/WebSocketBridgeTest.java (1)

244-269: ⚡ Quick win

Add a 127.0.0.1-origin assertion in the same zero-config test.

This test currently validates Origin: http://localhost:{port} only. Since runtime behavior allows both localhost and 127.0.0.1, covering both here will reduce regressions in same-origin allowlist logic.

Suggested test extension
     var ws = HttpClient.newHttpClient().newWebSocketBuilder()
             .header("Origin", "http://localhost:" + port)
             .buildAsync(URI.create("ws://127.0.0.1:" + port + "/ws"), listener)
             .get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);

     assertThat(connected.await(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue();
     bridge.broadcast("sess-1", "stream.text", Map.of("delta", "hello"));
     assertThat(queue.poll(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)).isNotNull();

+    var queue127 = new LinkedBlockingQueue<String>();
+    var ws127 = HttpClient.newHttpClient().newWebSocketBuilder()
+            .header("Origin", "http://127.0.0.1:" + port)
+            .buildAsync(URI.create("ws://127.0.0.1:" + port + "/ws"), new BufferingListener(queue127::add))
+            .get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+    bridge.broadcast("sess-1", "stream.text", Map.of("delta", "hello-127"));
+    assertThat(queue127.poll(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)).isNotNull();
+
     ws.sendClose(WebSocket.NORMAL_CLOSURE, "bye").get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+    ws127.sendClose(WebSocket.NORMAL_CLOSURE, "bye").get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@aceclaw-daemon/src/test/java/dev/aceclaw/daemon/WebSocketBridgeTest.java`
around lines 244 - 269, Extend the test
acceptsSameOriginHandshakeWithoutAllowedOriginsConfig to also assert that an
Origin of "http://127.0.0.1:{port}" is accepted: after the existing
localhost-based connection and broadcast assertions (using bridge, port, queue,
listener), open a second WebSocket using
HttpClient.newWebSocketBuilder().header("Origin", "http://127.0.0.1:" + port)
and connect to ws://127.0.0.1:{port}/ws, then wait for the connection via the
same connected CountDownLatch or a new latch, broadcast via
bridge.broadcast("sess-2", "stream.text", …) and assert the queue receives a
message; finally close that second ws with sendClose. This mirrors the existing
localhost flow but uses "127.0.0.1" to verify same-origin acceptance for the
loopback IP.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@aceclaw-cli/src/main/java/dev/aceclaw/cli/AceClawMain.java`:
- Around line 638-658: Add a null-guard at the start of openInBrowser to
validate the url parameter: call Objects.requireNonNull(url, "url") (and import
java.util.Objects if needed) before constructing the cmd or ProcessBuilder so
downstream calls never receive a null; keep the existing behavior of starting
the helper process with ProcessBuilder and returning true on pb.start() (and
returning false on IOException) without introducing a wait-and-check for the
helper process exit code.

In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/AceClawDaemon.java`:
- Around line 550-560: The health payload currently uses
config.webSocketEnabled() for the dashboard "enabled" flag, which can be
inaccurate if the bridge fails to start; change the logic so that
RequestRouter.DashboardInfo passed to router.setDashboardInfo uses the bridge's
actual runtime state (e.g., WebSocketBridge.isRunning() or the success/failure
result from the bridge startup sequence) instead of the config value, and update
the dashboard URL only when the bridge is confirmed running; ensure you adjust
the place(s) that call router.setDashboardInfo (and any initial default) so the
enabled flag and URL reflect the real runtime availability after the bridge
startup completes or fails.

In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/WebSocketBridge.java`:
- Around line 226-238: The current friendly fallback only registers
instance.get("/", ...) when dashboardBundled is false, so non-root dashboard
URLs still return Javalin's default 404; change this to register a global 404
handler or a catch-all GET route when dashboardBundled is false. Specifically,
replace or augment the instance.get("/", ...) usage with a Javalin 404 error
handler (instance.error(404, ctx -> ...)) or a wildcard GET route (e.g.,
instance.get("/*", ctx -> ...)) so any unhandled path returns the same
plain-text message and status/content-type; keep the same message and use the
dashboardBundled check to only register this handler in -Pno-dashboard builds.
- Around line 249-252: Clear sameOriginAllowlist in stop() to remove stale ports
and ensure you pre-seed sameOriginAllowlist with "http://localhost:{port}" and
"http://127.0.0.1:{port}" before calling instance.start(port) (or any start
sequence) when the port is known; update the code paths that assign
sameOriginAllowlist (the constructor/initializer where sameOriginAllowlist =
Set.of(...)) to instead set these entries early on when starting and ensure
stop() empties or resets sameOriginAllowlist so old origins cannot be authorized
during restart windows.

---

Nitpick comments:
In `@aceclaw-daemon/src/test/java/dev/aceclaw/daemon/WebSocketBridgeTest.java`:
- Around line 244-269: Extend the test
acceptsSameOriginHandshakeWithoutAllowedOriginsConfig to also assert that an
Origin of "http://127.0.0.1:{port}" is accepted: after the existing
localhost-based connection and broadcast assertions (using bridge, port, queue,
listener), open a second WebSocket using
HttpClient.newWebSocketBuilder().header("Origin", "http://127.0.0.1:" + port)
and connect to ws://127.0.0.1:{port}/ws, then wait for the connection via the
same connected CountDownLatch or a new latch, broadcast via
bridge.broadcast("sess-2", "stream.text", …) and assert the queue receives a
message; finally close that second ws with sendClose. This mirrors the existing
localhost flow but uses "127.0.0.1" to verify same-origin acceptance for the
loopback IP.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e263c1c9-08fd-4b51-b783-1e2769eabb81

📥 Commits

Reviewing files that changed from the base of the PR and between c8c3870 and 51ae2b0.

⛔ Files ignored due to path filters (1)
  • README.md is excluded by !**/*.md
📒 Files selected for processing (10)
  • aceclaw-cli/src/main/java/dev/aceclaw/cli/AceClawMain.java
  • aceclaw-daemon/build.gradle.kts
  • aceclaw-daemon/src/main/java/dev/aceclaw/daemon/AceClawConfig.java
  • aceclaw-daemon/src/main/java/dev/aceclaw/daemon/AceClawDaemon.java
  • aceclaw-daemon/src/main/java/dev/aceclaw/daemon/RequestRouter.java
  • aceclaw-daemon/src/main/java/dev/aceclaw/daemon/WebSocketBridge.java
  • aceclaw-daemon/src/test/java/dev/aceclaw/daemon/WebSocketBridgeTest.java
  • aceclaw-dashboard/build.gradle.kts
  • build.gradle.kts
  • settings.gradle.kts

Comment thread aceclaw-cli/src/main/java/dev/aceclaw/cli/AceClawMain.java
Comment thread aceclaw-daemon/src/main/java/dev/aceclaw/daemon/AceClawDaemon.java Outdated
Comment thread aceclaw-daemon/src/main/java/dev/aceclaw/daemon/WebSocketBridge.java Outdated
Comment thread aceclaw-daemon/src/main/java/dev/aceclaw/daemon/WebSocketBridge.java Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 35040a096b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

case "0.0.0.0", "::", "::0" -> "localhost";
default -> webSocketBridge.host();
};
String dashboardUrl = "http://" + urlHost + ":" + webSocketBridge.port();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bracket IPv6 host when publishing dashboard URL

When webSocket.host is set to an IPv6 literal like ::1, this builds dashboardUrl as http://::1:<port>, which is not a valid HTTP URL format for IPv6 hosts. As a result, aceclaw dashboard prints and tries to open an unusable URL even though the bridge is running. The host should be normalized to bracketed form (e.g. http://[::1]:3141) before concatenating the port.

Useful? React with 👍 / 👎.

Comment on lines +248 to +250
this.sameOriginAllowlist = Set.of(
"http://localhost:" + this.port,
"http://127.0.0.1:" + this.port);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include IPv6 loopback in same-origin allowlist

The new same-origin whitelist only includes http://localhost:<port> and http://127.0.0.1:<port>. If the daemon is bound to IPv6 loopback and the dashboard is opened at http://[::1]:<port>, the browser Origin will be http://[::1]:<port>, which is rejected by isOriginAllowed unless users add manual config. That breaks the zero-config bundled dashboard flow for IPv6 localhost setups.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
aceclaw-daemon/src/main/java/dev/aceclaw/daemon/WebSocketBridge.java (1)

231-238: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a real catch-all for the -Pno-dashboard fallback.

Line 232 only matches the literal /, so /foo and other deep links still return Javalin’s default 404 instead of the rebuild guidance this mode is supposed to provide. Switch this to a 404 handler or wildcard GET fallback so every unmatched dashboard route gets the same message.

In Javalin, does app.get("/") match only the exact root path, and what is the recommended way to return a custom 404 body for unmatched GET routes?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/WebSocketBridge.java` around
lines 231 - 238, The current fallback registers instance.get("/") which only
matches the exact root and misses deep links; replace this with a catch-all 404
handler so every unmatched dashboard route returns the rebuild guidance. When
dashboardBundled is false, register a global 404 handler (use
instance.error(404, ...) or a wildcard GET like instance.get("/*", ...)) instead
of instance.get("/"), and keep the same status, contentType, and result message;
reference the dashboardBundled flag and the existing instance.get("/", ctx -> {
... }) block when making the change.
aceclaw-daemon/src/main/java/dev/aceclaw/daemon/AceClawDaemon.java (1)

561-570: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Populate dashboardInfo from the running bridge, not config.

Lines 566-570 advertise the dashboard before webSocketBridge.start() runs. If the bridge fails to bind, health.status still reports enabled=true with a live URL, and if the configured port is 0 it reports http://...:0 instead of the bound port. Initialize this pessimistically, then update it after the start attempt from webSocketBridge.isRunning() and webSocketBridge.port().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/AceClawDaemon.java` around
lines 561 - 570, The current code sets DashboardInfo from config before the
WebSocketBridge has started, causing enabled=true and the configured port
(possibly 0) to be advertised even if the bridge fails or binds to a different
port; change the initialization so router.setDashboardInfo(...) is populated
pessimistically using WebSocketBridge.dashboardBundled() and disabled/empty URL
(enabled=false, dashboardUrl="") before start, then after calling
webSocketBridge.start() update the DashboardInfo again by querying
webSocketBridge.isRunning() and webSocketBridge.port() to set enabled and the
actual URL (use "localhost" for host resolution like the existing switch) via
router.setDashboardInfo(new RequestRouter.DashboardInfo(...)). Ensure any place
that previously relied on config.webSocketEnabled()/config.webSocketPort() for
advertised values uses the live webSocketBridge.isRunning()/port() after start.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/AceClawDaemon.java`:
- Around line 562-568: The dashboard URL builder uses urlHost from
config.webSocketHost() and naively concatenates ":"+port, which produces invalid
URLs for IPv6 literals like "::1"; update the logic in AceClawDaemon (where
urlHost is set and dashboardUrl constructed) to detect IPv6 literals (e.g., host
contains ':' and is not already bracketed) and wrap the literal in square
brackets before appending the port (so "[" + urlHost + "]:" + port); ensure
existing bracketed hosts are left unchanged and keep the same
config.webSocketEnabled() gate.

---

Duplicate comments:
In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/AceClawDaemon.java`:
- Around line 561-570: The current code sets DashboardInfo from config before
the WebSocketBridge has started, causing enabled=true and the configured port
(possibly 0) to be advertised even if the bridge fails or binds to a different
port; change the initialization so router.setDashboardInfo(...) is populated
pessimistically using WebSocketBridge.dashboardBundled() and disabled/empty URL
(enabled=false, dashboardUrl="") before start, then after calling
webSocketBridge.start() update the DashboardInfo again by querying
webSocketBridge.isRunning() and webSocketBridge.port() to set enabled and the
actual URL (use "localhost" for host resolution like the existing switch) via
router.setDashboardInfo(new RequestRouter.DashboardInfo(...)). Ensure any place
that previously relied on config.webSocketEnabled()/config.webSocketPort() for
advertised values uses the live webSocketBridge.isRunning()/port() after start.

In `@aceclaw-daemon/src/main/java/dev/aceclaw/daemon/WebSocketBridge.java`:
- Around line 231-238: The current fallback registers instance.get("/") which
only matches the exact root and misses deep links; replace this with a catch-all
404 handler so every unmatched dashboard route returns the rebuild guidance.
When dashboardBundled is false, register a global 404 handler (use
instance.error(404, ...) or a wildcard GET like instance.get("/*", ...)) instead
of instance.get("/"), and keep the same status, contentType, and result message;
reference the dashboardBundled flag and the existing instance.get("/", ctx -> {
... }) block when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0b72b500-c6f4-4c20-896a-d5358f2a44b8

📥 Commits

Reviewing files that changed from the base of the PR and between 51ae2b0 and cbf7439.

⛔ Files ignored due to path filters (2)
  • .github/workflows/ci.yml is excluded by !.github/**
  • .github/workflows/release.yml is excluded by !.github/**
📒 Files selected for processing (4)
  • aceclaw-cli/src/main/java/dev/aceclaw/cli/AceClawMain.java
  • aceclaw-daemon/src/main/java/dev/aceclaw/daemon/AceClawDaemon.java
  • aceclaw-daemon/src/main/java/dev/aceclaw/daemon/WebSocketBridge.java
  • aceclaw-dashboard/build.gradle.kts
✅ Files skipped from review due to trivial changes (1)
  • aceclaw-dashboard/build.gradle.kts
🚧 Files skipped from review as they are similar to previous changes (1)
  • aceclaw-cli/src/main/java/dev/aceclaw/cli/AceClawMain.java

Comment thread aceclaw-daemon/src/main/java/dev/aceclaw/daemon/AceClawDaemon.java Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8c75826f76

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +248 to +250
this.sameOriginAllowlist = Set.of(
"http://localhost:" + this.port,
"http://127.0.0.1:" + this.port);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow default-port origins without explicit :port

The same-origin allowlist is built only as http://localhost:<port> and http://127.0.0.1:<port>, but browsers serialize origins on default HTTP port 80 without the port suffix (for example http://localhost). If users set webSocket.port to 80, the bundled dashboard page loads from the daemon but its WebSocket handshake is rejected by isOriginAllowed, breaking the zero-config flow unless they manually add allowedOrigins.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 30717c1255

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* user config. Bind host stays {@code localhost} so it's never exposed off-machine.
* Users who explicitly set {@code webSocket.enabled = false} keep their override.
*/
private static final boolean DEFAULT_WEBSOCKET_ENABLED = true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve opt-in when non-loopback WebSocket host is configured

Switching DEFAULT_WEBSOCKET_ENABLED to true silently turns on the bridge for existing configs that set webSocket.host but never set webSocket.enabled (previously defaulted off). In that scenario, a host like 0.0.0.0 or another non-loopback address becomes network-reachable by default, and WebSocketBridge.isOriginAllowed still permits clients with no Origin header, so remote non-browser clients can connect without being in allowedOrigins. This is a security regression introduced by the default flip and should remain opt-in unless the host is explicitly loopback.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5b911b3d5e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2386 to +2387
} else if (configuredHost.contains(":")) {
urlHost = "[" + configuredHost + "]";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid double-bracketing IPv6 host in dashboard URL

When webSocket.host is already configured in bracketed IPv6 form (for example [::1]), this branch wraps it again and publishes http://[[::1]]:<port>, which is not a valid URL and breaks aceclaw dashboard/startup hints for that config. This case is reachable because config parsing explicitly treats [::1] as a loopback host, so URL normalization should detect and preserve already-bracketed literals instead of blindly adding brackets.

Useful? React with 👍 / 👎.

@xinhuagu xinhuagu merged commit 5b17f8e into main May 3, 2026
6 of 7 checks passed
@xinhuagu xinhuagu deleted the feat/446-dashboard-subcommand branch May 3, 2026 10:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(daemon+cli): aceclaw dashboard subcommand — daemon serves dist, single-process UX

1 participant