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

118 lines
4.8 KiB
Markdown

# 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:
```rust
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`:
```rust
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**
```bash
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)