Files
claw/docs/superpowers/plans/2026-04-14-helper-page-lifecycle-fix-v2-plan.md
木炎 c60cd308ca feat: service console auto-connect, settings panel, and batch of enhancements
- Auto-connect WebSocket on page load in service console
- Settings modal for editing sgclaw_config.json (API key, base URL, model, skills dir, etc.)
- UpdateConfig/ConfigUpdated protocol messages for remote config save
- save_to_path() for SgClawSettings serialization
- ConfigUpdated handler in sg_claw_client binary
- Protocol serialization tests for new message types
- HTML test assertions for auto-connect and settings UI
- Additional pending changes: deterministic submit, org units, lineloss xlsx export, browser script tool, and docs

🤖 Generated with [Qoder][https://qoder.com]
2026-04-14 14:32:46 +08:00

4.8 KiB

Helper Page Lifecycle Fix v2 — Same-Connection Close + Open

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: Prevent orphaned helper pages across process restarts by closing existing ones before opening new ones, all on the same WebSocket connection.

Architecture: In bootstrap_helper_page, after registering with the browser WS, send sgHideBrowerserClosePage (best-effort, silently ignored if no page exists), then send sgHideBrowerserOpenPage. Change use_hidden_domain to true.

Tech Stack: Rust, tungstenite, SuperRPA browser WS protocol


Task 1: Add close-before-open in bootstrap_helper_page

Files:

  • Modify: src/browser/callback_host.rs:345-374 (bootstrap_helper_page function)

  • Step 1: Add close command before open command in bootstrap_helper_page

Replace the current bootstrap_helper_page function. After recv_bootstrap_prelude, send the close command first, then the open command:

fn bootstrap_helper_page(
    browser_ws_url: &str,
    request_url: &str,
    helper_url: &str,
    use_hidden_domain: bool,
) -> Result<(), PipeError> {
    let (mut websocket, _) = connect(browser_ws_url)
        .map_err(|err| PipeError::Protocol(format!("browser websocket connect failed: {err}")))?;
    configure_bootstrap_socket(&mut websocket)?;
    websocket
        .send(Message::Text(
            r#"{"type":"register","role":"web"}"#.to_string().into(),
        ))
        .map_err(|err| PipeError::Protocol(format!("browser websocket register failed: {err}")))?;
    let _ = recv_bootstrap_prelude(&mut websocket);

    // Close any orphaned helper page from a previous process run.
    // Best-effort: if no page exists, the browser silently ignores this.
    let (open_action, close_action) = if use_hidden_domain {
        ("sgHideBrowerserOpenPage", "sgHideBrowerserClosePage")
    } else {
        ("sgBrowerserOpenPage", "sgBrowserClosePage")
    };
    let close_payload = json!([request_url, close_action, helper_url]).to_string();
    let _ = websocket.send(Message::Text(close_payload.into()));

    let payload = json!([
        request_url,
        open_action,
        helper_url,
    ])
    .to_string();
    websocket
        .send(Message::Text(payload.into()))
        .map_err(|err| PipeError::Protocol(format!("helper bootstrap send failed: {err}")))?;
    Ok(())
}

Key changes from current code:

  • After recv_bootstrap_prelude, add the close command (best-effort, ignore errors)

  • Compute both open_action and close_action from use_hidden_domain flag

  • Send close first, then open on the same WebSocket connection

  • Step 2: Change use_hidden_domain to true in server.rs

In src/service/server.rs, at the start_with_browser_ws_url call, change false to true:

                    match LiveBrowserCallbackHost::start_with_browser_ws_url(
                        browser_ws_url,
                        &bootstrap_url,
                        Duration::from_secs(15),
                        BROWSER_RESPONSE_TIMEOUT,
                        true, // use_hidden_domain: hidden domain for invisible helper
                    ) {
  • Step 3: Build

Run: cargo build 2>&1 Expected: 0 errors.

  • Step 4: Run callback_host tests

Run: cargo test --lib -- callback_host 2>&1 Expected: 12 tests pass (including live_callback_host_sends_bootstrap_open_page_command which still checks for sgBrowerserOpenPage because the test passes false, and live_callback_host_hidden_domain_sends_hide_open_page_command which passes true).

Note: The test passes false for use_hidden_domain, so the close command will use sgBrowserClosePage. The test's fake WebSocket server will receive both the close and open frames. The test only checks that sgBrowerserOpenPage is present, which is still true.

  • Step 5: Commit
git add src/browser/callback_host.rs src/service/server.rs
git commit -m "fix(callback_host): close orphaned helper page before opening new one on same WS"

Task 2: Full verification

Files: None (verification only)

  • Step 1: Full test suite

Run: cargo test 2>&1 Expected: All tests pass except pre-existing lineloss_period_resolver_prompts_for_missing_period failure.

  • Step 2: Verify key behavioral changes

Manually confirm:

  1. bootstrap_helper_page sends close command before open command (both on same WS connection)
  2. use_hidden_domain is true in server.rs — helper page opens in hidden domain
  3. Drop for LiveBrowserCallbackHost remains simple (shutdown only, no close attempt)
  4. cached_host is still in mod.rs outer loop (process-internal deduplication)