Files
claw/docs/superpowers/plans/2026-04-06-service-chat-web-console-plan.md
木炎 bdf8e12246 feat: align browser callback runtime and export flows
Consolidate the browser task runtime around the callback path, add safer artifact opening for Zhihu exports, and cover the new service/browser flows with focused tests and supporting docs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 21:44:53 +08:00

13 KiB

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.

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:

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:

- 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
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:

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>sgClaw Service Console</title>
</head>
<body>
  <div id="app">
    <input id="wsUrl" value="ws://127.0.0.1:42321" />
    <button id="connectBtn">连接</button>
    <div id="connectionState">未连接</div>
    <div id="messageStream"></div>
    <textarea id="instructionInput"></textarea>
    <div id="validationText"></div>
    <button id="sendBtn" disabled>发送任务</button>
  </div>
</body>
</html>

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:

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:

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:

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:

- 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:

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
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:

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:

- 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:

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:

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
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:

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:

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:

- 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:

打开百度

Expected:

- 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:

- 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
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.