# Service Chat Web Console Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a standalone local HTML console that connects to the existing service websocket, submits natural-language tasks with the current `submit_task` payload, and leaves the browser-helper/runtime path untouched. **Architecture:** The change stays fully at the presentation edge. A new self-contained HTML file under `frontend/service-console/` reuses the current websocket protocol from `src/service/protocol.rs`, while one narrow Rust integration test guards the page's protocol shape and forbids any reference to `browser-helper.html`, callback-host endpoints, or the browser websocket. No Rust runtime logic changes are part of this slice. **Tech Stack:** HTML, CSS, vanilla JavaScript, Rust integration tests, std::fs, Cargo test --- ## File Map - Create: `frontend/service-console/sg_claw_service_console.html` - Standalone local page with inline CSS and JavaScript - Connects to the existing service websocket at `ws://127.0.0.1:42321` by default - Sends existing `ClientMessage::SubmitTask` JSON - Renders inbound `ServiceMessage` rows only - Create: `tests/service_console_html_test.rs` - Source guard for the standalone page - Verifies file location, allowed protocol usage, and forbidden helper/callback references - Reference: `src/service/protocol.rs` - Existing websocket message shape to mirror exactly - Reference: `src/bin/sg_claw_client.rs` - Existing terminal client behavior to mirror for `submit_task` - Reference: `docs/superpowers/specs/2026-04-06-service-chat-web-console-design.md` ## Scope Guardrails - Do not modify `src/service/server.rs`. - Do not modify `src/browser/callback_host.rs`. - Do not modify `src/browser/callback_backend.rs`. - Do not modify `src/bin/sg_claw_client.rs`. - Do not add an HTTP server. - Do not connect the new page to `ws://127.0.0.1:12345`. - Do not reference `/sgclaw/browser-helper.html` or `/sgclaw/callback/*` anywhere in the new page. ### Task 1: Add a failing source-guard test for the standalone page **Files:** - Create: `tests/service_console_html_test.rs` - Reference: `docs/superpowers/specs/2026-04-06-service-chat-web-console-design.md` - [ ] **Step 1: Write the failing test** Create a focused integration test that resolves the HTML path from `CARGO_MANIFEST_DIR` and asserts the file contract. ```rust use std::fs; use std::path::PathBuf; #[test] fn service_console_html_stays_on_service_ws_boundary() { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let html_path = manifest_dir .join("frontend") .join("service-console") .join("sg_claw_service_console.html"); let source = fs::read_to_string(&html_path) .expect("service console html should exist"); assert!(source.contains("ws://127.0.0.1:42321")); assert!(source.contains("submit_task")); assert!(!source.contains("/sgclaw/browser-helper.html")); assert!(!source.contains("/sgclaw/callback/ready")); assert!(!source.contains("/sgclaw/callback/events")); assert!(!source.contains("/sgclaw/callback/commands/next")); assert!(!source.contains("/sgclaw/callback/commands/ack")); assert!(!source.contains("ws://127.0.0.1:12345")); } ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cargo test --manifest-path "D:/data/ideaSpace/rust/sgClaw/claw-new/Cargo.toml" service_console_html_stays_on_service_ws_boundary --test service_console_html_test -- --exact ``` Expected: FAIL because the HTML file does not exist yet. - [ ] **Step 3: Keep the test narrow** Before writing production code, confirm the test guards only the approved boundary: ```text - file exists at frontend/service-console/sg_claw_service_console.html - service websocket default is present - submit_task payload marker is present - no helper-page path - no callback-host endpoints - no browser websocket URL ``` Do not turn this into an end-to-end browser test. - [ ] **Step 4: Commit the red test** ```bash git add tests/service_console_html_test.rs git commit -m "test: add service console html boundary guard" ``` ### Task 2: Implement the standalone HTML console with the approved boundary **Files:** - Create: `frontend/service-console/sg_claw_service_console.html` - Reference: `src/service/protocol.rs:6` - Reference: `src/bin/sg_claw_client.rs:16` - Test: `tests/service_console_html_test.rs` - [ ] **Step 1: Create the HTML file with the minimal structure** Write one self-contained page with: ```html sgClaw Service Console
未连接
``` Keep all CSS and JavaScript inline. Do not add external assets or a build step. - [ ] **Step 2: Implement websocket connect/disconnect behavior** Add the smallest possible JS behavior, including explicit disconnect on the same button so the UI matches the approved connect/disconnect contract: ```javascript let socket = null; function appendRow(kind, text) { // append a visible row to #messageStream } function updateUiState() { const connected = socket && socket.readyState === WebSocket.OPEN; document.getElementById('connectBtn').textContent = connected ? '断开' : '连接'; document.getElementById('sendBtn').disabled = !connected; document.getElementById('connectionState').textContent = connected ? '已连接' : '未连接'; } function connectOrDisconnectService() { if (socket && socket.readyState === WebSocket.OPEN) { socket.close(); return; } const url = document.getElementById('wsUrl').value.trim() || 'ws://127.0.0.1:42321'; socket = new WebSocket(url); updateUiState(); socket.addEventListener('open', () => { appendRow('status', 'service websocket connected'); updateUiState(); }); socket.addEventListener('close', () => { appendRow('status', 'service websocket disconnected'); updateUiState(); }); socket.addEventListener('error', () => appendRow('error', 'service websocket error')); socket.addEventListener('message', handleMessage); } ``` Do not add retry loops or background reconnect logic. - [ ] **Step 3: Implement submit_task sending with the current message shape** Mirror the terminal client payload shape exactly and show inline validation for empty input: ```javascript function setValidation(message) { document.getElementById('validationText').textContent = message; } function sendTask() { const instruction = document.getElementById('instructionInput').value.trim(); if (!socket || socket.readyState !== WebSocket.OPEN) { return; } if (!instruction) { setValidation('请输入任务内容。'); return; } setValidation(''); socket.send(JSON.stringify({ type: 'submit_task', instruction, conversation_id: '', messages: [], page_url: '', page_title: '' })); } ``` Do not add new fields. Do not add conversation replay logic in this slice. - [ ] **Step 4: Render existing inbound service messages only** Handle the current `ServiceMessage` variants with a minimal dispatcher: ```javascript function handleMessage(event) { const message = JSON.parse(event.data); switch (message.type) { case 'status_changed': appendRow('status', message.state); break; case 'log_entry': appendRow('log', message.message); break; case 'task_complete': appendRow(message.success ? 'complete' : 'error', message.summary); break; case 'busy': appendRow('error', message.message); break; default: appendRow('error', 'unknown service message: ' + event.data); } } ``` Keep the composer enabled during in-flight work so repeated submits surface the existing `busy` response instead of inventing a frontend queue. - [ ] **Step 5: Keep the helper boundary explicit in the source** Before running tests, inspect the HTML source and confirm: ```text - no /sgclaw/browser-helper.html - no /sgclaw/callback/* - no ws://127.0.0.1:12345 - no browser websocket register frame logic ``` If any such string appears, remove it before testing. - [ ] **Step 6: Run the source-guard test to verify green** Run: ```bash cargo test --manifest-path "D:/data/ideaSpace/rust/sgClaw/claw-new/Cargo.toml" service_console_html_stays_on_service_ws_boundary --test service_console_html_test -- --exact ``` Expected: PASS - [ ] **Step 7: Commit the standalone page** ```bash git add frontend/service-console/sg_claw_service_console.html tests/service_console_html_test.rs git commit -m "feat: add standalone service chat console" ``` ### Task 3: Run the focused verification sweep **Files:** - Verify: `tests/service_console_html_test.rs` - Reference: `src/service/protocol.rs` - Reference: `src/bin/sg_claw_client.rs` - [ ] **Step 1: Re-run the source-guard test** Run: ```bash cargo test --manifest-path "D:/data/ideaSpace/rust/sgClaw/claw-new/Cargo.toml" service_console_html_stays_on_service_ws_boundary --test service_console_html_test -- --exact ``` Expected: PASS - [ ] **Step 2: Manually inspect disconnected-send and validation markers in the HTML source** Before broader verification, confirm the page source clearly contains all three UI-local rules: ```text - connect button can disconnect an open websocket - send button starts disabled while disconnected - empty instruction shows inline validation text ``` This inspection stays source-level; do not add extra backend tests for it in this slice. - [ ] **Step 3: Run an existing service protocol regression for safety** Run the narrow existing protocol coverage to prove the page did not require backend changes: ```bash cargo test --manifest-path "D:/data/ideaSpace/rust/sgClaw/claw-new/Cargo.toml" submit_task_client_message_converts_into_shared_runner_request --test service_ws_session_test -- --exact ``` Expected: PASS - [ ] **Step 4: Run an existing terminal-client regression for safety** Run: ```bash cargo test --manifest-path "D:/data/ideaSpace/rust/sgClaw/claw-new/Cargo.toml" client_submits_first_user_line_to_service --test service_task_flow_test -- --exact ``` Expected: PASS - [ ] **Step 5: Commit only if verification required any code change** ```bash git add frontend/service-console/sg_claw_service_console.html tests/service_console_html_test.rs git commit -m "test: tighten service console verification" ``` If verification required no code changes, do not create an extra commit. ### Task 4: Perform the manual smoke check **Files:** - Verify live behavior only; no new code required - [ ] **Step 1: Start the existing service binary** Run: ```bash cargo run --manifest-path "D:/data/ideaSpace/rust/sgClaw/claw-new/Cargo.toml" --bin sg_claw -- --config-path "D:/data/ideaSpace/rust/sgClaw/sgclaw_config.json" ``` Expected: service starts and prints its ready line with the service websocket listen address. - [ ] **Step 2: Open the standalone page directly** Open: ```text D:/data/ideaSpace/rust/sgClaw/claw-new/frontend/service-console/sg_claw_service_console.html ``` Expected: the page loads through the browser as a local file and shows the default websocket URL `ws://127.0.0.1:42321`. - [ ] **Step 3: Connect, disconnect, and reconnect once** Expected: ```text - message stream shows websocket connected - clicking the same button disconnects the websocket cleanly - message stream shows websocket disconnected - send button is disabled again while disconnected - reconnect succeeds without reloading the page ``` - [ ] **Step 4: Submit one natural-language task** Use a small harmless instruction such as: ```text 打开百度 ``` Expected: ```text - empty textarea send attempt first shows inline validation without sending a websocket frame - page sends one submit_task payload after valid input - page receives and renders status/log/task_complete or busy rows ``` - [ ] **Step 5: Confirm the helper boundary stayed untouched** Verify from the page source and observed behavior: ```text - the page never loads /sgclaw/browser-helper.html - the page never calls /sgclaw/callback/* - the page never connects to ws://127.0.0.1:12345 ``` If the task itself triggers browser automation, that remains owned by the existing Rust runtime rather than by the page. - [ ] **Step 6: Commit only if the manual pass required code changes** ```bash git add frontend/service-console/sg_claw_service_console.html tests/service_console_html_test.rs git commit -m "fix: tighten standalone service console smoke flow" ``` If the manual pass required no code changes, do not create an extra commit.