Make the generate lane's bar preference helper-wide

The maintainer still saw the bottom bar: two tabs were connected to the
helper, the tab that won the roll call hid its bar, and the tab on
screen never did. The hide was also applied only at Go, so the wait
before it showed the bar as well.

The helper now owns the preference. `impeccable live --no-live-bar`
posts `/live-bar` right after the helper is up, so the bar never
appears in any tab; an agent target carrying `hideLiveBar` sets the
same flag before the target goes out. The helper broadcasts
`live_bar` to every connected tab, answers `hideLiveBar` on every
`connected` frame (reloads, later tabs) and on `/status`, and the flag
lives as long as the helper. The overlay just follows: no per-tab
memory, no session scoping, the variant controls still show. The same
preference skips the overlay's "No PRODUCT.md found" connect notice,
which sent the user to init inside a lane that runs without context by
design.

Verified in a real Chromium session with two tabs, screenshots at each
stage: idle (bar in both), after Go (bar gone in both), after reloading
both, after the accept during the bake, after a reload after that.

Written with AI assistance (Claude).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Abdul Wahab
2026-09-15 05:45:49 +05:00
committed by Abdul Wahab
co-authored by Claude Fable 5
parent 7c42d0feba
commit 1a699913e4
9 changed files with 151 additions and 40 deletions
+27
View File
@@ -611,9 +611,19 @@ fn agent_target_forwards_the_hidden_bar_request_to_the_overlay() {
let s = Server::start("hide-bar");
let mut a = Overlay::connect(s.port, &s.token, "tab-a");
a.next(|m| m["type"] == "connected");
let mut b = Overlay::connect(s.port, &s.token, "tab-b");
b.next(|m| m["type"] == "connected");
let held = s.hold(serde_json::json!({ "hideLiveBar": true }));
// Helper-wide and first: every connected tab hears it before the target.
assert_eq!(a.next(|m| m["type"] == "live_bar")["hidden"], serde_json::json!(true));
assert_eq!(b.next(|m| m["type"] == "live_bar")["hidden"], serde_json::json!(true));
let pushed = a.next(|m| m["type"] == "agent_target");
assert_eq!(pushed["hideLiveBar"], serde_json::json!(true), "{pushed}");
// A tab connecting later learns it on connect.
let mut c = Overlay::connect(s.port, &s.token, "tab-c");
assert_eq!(c.next(|m| m["type"] == "connected")["hideLiveBar"], serde_json::json!(true));
let (_, status) = post_json(s.port, "/status", serde_json::json!({ "token": s.token }));
let _ = status;
let target_id = pushed["targetId"].as_str().unwrap().to_string();
post_json(s.port, "/agent-target-result", serde_json::json!({ "token": s.token, "targetId": target_id, "ok": true, "sessionId": "aabbccdd" }));
held.join().unwrap();
@@ -628,3 +638,20 @@ fn agent_target_forwards_the_hidden_bar_request_to_the_overlay() {
assert_eq!(status, 400, "{body}");
assert_eq!(body["error"], serde_json::json!("agent_target: hideLiveBar must be a boolean"));
}
#[test]
fn live_bar_route_sets_the_helper_wide_preference() {
let s = Server::start("live-bar");
let mut a = Overlay::connect(s.port, &s.token, "tab-a");
assert_eq!(a.next(|m| m["type"] == "connected")["hideLiveBar"], serde_json::json!(false));
let (status, body) = post_json(s.port, "/live-bar", serde_json::json!({ "token": s.token, "hidden": true }));
assert_eq!(status, 200, "{body}");
assert_eq!(body["hidden"], serde_json::json!(true));
assert_eq!(a.next(|m| m["type"] == "live_bar")["hidden"], serde_json::json!(true));
let (status, body) = post_json(s.port, "/live-bar", serde_json::json!({ "token": s.token, "hidden": "yes" }));
assert_eq!(status, 400, "{body}");
let (status, body) = post_json(s.port, "/live-bar", serde_json::json!({ "token": "nope", "hidden": true }));
assert_eq!(status, 401, "{body}");
let mut b = Overlay::connect(s.port, &s.token, "tab-b");
assert_eq!(b.next(|m| m["type"] == "connected")["hideLiveBar"], serde_json::json!(true));
}
+30
View File
@@ -271,6 +271,16 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
// reports `devUrl`; `--allow-missing-context` reports `contextMissing`
// and a `contextNote` for the files it let the boot proceed without.
let want_dev_url = args.iter().any(|a| a == "--dev-url");
// `--no-live-bar`: the generate lane wants no bottom bar in any tab for
// this helper's lifetime; tell the helper now, before any page connects.
let no_live_bar = args.iter().any(|a| a == "--no-live-bar");
let live_bar_hidden = if no_live_bar {
let port = server_info.get("port").and_then(Value::as_u64).unwrap_or(0);
let token = server_info.get("token").and_then(Value::as_str).unwrap_or("");
request_live_bar_hidden(port, token)
} else {
false
};
let token_for_probe = match server_info.get("token") {
Some(Value::String(s)) => s.clone(),
_ => String::new(),
@@ -314,6 +324,9 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
"_instructions": boot_instructions(&self_cmd),
});
if let Some(obj) = payload.as_object_mut() {
if no_live_bar {
obj.insert("liveBarHidden".into(), json!(live_bar_hidden));
}
if want_dev_url {
obj.insert("devUrl".into(), dev_url.map(Value::String).unwrap_or(Value::Null));
}
@@ -338,6 +351,23 @@ fn run_inject(args: &[String], cwd: &str, io: &Io) -> String {
/// JS: ensureServerRunning(cwd): reuse a live `server.json` record, else
/// spawn `live-server --background` (part 3) and parse its output.
/// Ask the running helper to keep the overlay's global bar hidden for its
/// lifetime (`POST /live-bar`). True when the helper acknowledged.
fn request_live_bar_hidden(port: u64, token: &str) -> bool {
if port == 0 || token.is_empty() {
return false;
}
let agent = ureq::AgentBuilder::new()
.timeout(std::time::Duration::from_millis(3000))
.build();
agent
.post(&format!("http://127.0.0.1:{}/live-bar", port))
.set("Content-Type", "application/json")
.send_string(&json!({ "token": token, "hidden": true }).to_string())
.map(|res| res.status() == 200)
.unwrap_or(false)
}
fn ensure_server_running(cwd: &str, io: &Io) -> Option<Value> {
if let Some((info, _)) = read_live_server_info(cwd, &io.env) {
if let Some(pid) = info.pid {
+32
View File
@@ -185,6 +185,7 @@ pub fn run(args: &[String], io: &mut Io) -> i32 {
pending_agent_targets: Vec::new(),
next_agent_target_timer_gen: 0,
resolved_agent_targets: Vec::new(),
hide_live_bar: false,
shutting_down: false,
cleaned_up: false,
log_tx,
@@ -750,6 +751,7 @@ fn handle_connection(shared: Shared, mut stream: TcpStream, mut ticket: Ticket)
"connectedClients": st.sse_clients.len(),
"pendingEvents": pending,
"agentPolling": st.agent_polling_connected(),
"hideLiveBar": st.hide_live_bar,
"activeSessions": sessions,
"manualEdits": st.manual_edit_status(),
});
@@ -966,6 +968,7 @@ fn handle_connection(shared: Shared, mut stream: TcpStream, mut ticket: Ticket)
("/agent-target-claim", "POST") => {
handle_agent_target_claim_post(&shared, &mut stream, &cors, &req, &token_now)
}
("/live-bar", "POST") => handle_live_bar_post(&shared, &mut stream, &cors, &req, &token_now),
_ => respond(&mut stream, &cors, text_res(404, None, "Not found")),
}
}
@@ -1070,6 +1073,7 @@ fn handle_sse(
"type": "connected",
"hasProjectContext": has_ctx,
"agentPolling": st.agent_polling_connected(),
"hideLiveBar": st.hide_live_bar,
"activeSessions": st.active_session_summaries(),
}))
.unwrap_or_default()
@@ -2778,6 +2782,9 @@ fn handle_agent_target_post(
}
if msg.get("hideLiveBar").and_then(Value::as_bool) == Some(true) {
payload.insert("hideLiveBar".into(), json!(true));
// Helper-wide, before the target goes out: every tab hides now, the
// acting one included, and later connections hide on connect.
st.set_live_bar_hidden(true);
}
let (target_id, rx) = st.register_agent_target(payload);
drop(st);
@@ -2829,6 +2836,31 @@ fn handle_agent_target_result_post(
respond(stream, cors, json_res(200, json!({ "ok": true, "delivered": delivered })));
}
/// `POST /live-bar` `{token, hidden}`: the helper-wide bar preference, set
/// by `impeccable live --no-live-bar` at boot so the bar never appears, and
/// by an agent target carrying `hideLiveBar` (see handle_agent_target_post).
fn handle_live_bar_post(
shared: &Shared,
stream: &mut TcpStream,
cors: &[(String, String)],
req: &Request,
token: &str,
) {
let Some(msg) = agent_target_body(stream, cors, req, token) else {
return;
};
let Some(hidden) = msg.get("hidden").and_then(Value::as_bool) else {
respond(
stream,
cors,
json_res(400, json!({ "error": "live_bar: hidden must be a boolean" })),
);
return;
};
lock(shared).set_live_bar_hidden(hidden);
respond(stream, cors, json_res(200, json!({ "ok": true, "hidden": hidden })));
}
/// JS: handleAgentTargetClaimPost
fn handle_agent_target_claim_post(
shared: &Shared,
+14
View File
@@ -130,6 +130,11 @@ pub struct ServerState {
/// that answered it; anything else, including a target this record no
/// longer holds, is refused, so eviction can never reopen a request.
pub resolved_agent_targets: Vec<(String, Option<String>)>,
/// The generate lane asked this helper to keep the overlay's global bar
/// out of the way (`live --no-live-bar` or an agent target carrying
/// `hideLiveBar`). Helper-wide and for its lifetime: every connected
/// tab hides on the broadcast, every later connection on `connected`.
pub hide_live_bar: bool,
pub last_poll_at: i64,
pub timed_out_apply_ids: Vec<(String, TimedOutApply)>,
pub next_poll_id: u64,
@@ -474,6 +479,15 @@ impl ServerState {
}
/// JS: broadcast(msg)
/// Flip the helper-wide bar preference and tell every connected tab.
pub fn set_live_bar_hidden(&mut self, hidden: bool) {
if self.hide_live_bar == hidden {
return;
}
self.hide_live_bar = hidden;
self.broadcast(&json!({ "type": "live_bar", "hidden": hidden }));
}
pub fn broadcast(&mut self, msg: &Value) {
let data = format!(
"data: {}\n\n",
+3 -2
View File
@@ -1481,6 +1481,7 @@ Binds `127.0.0.1:PORT`. CORS: if request has `Origin` and (origin is loopback ht
| `POST /manual-edit` | | 410 `{"error":"/manual-edit is removed; use /manual-edit-stash and /manual-edit-commit for staged copy edits."}` |
| `POST /agent-target` | body JSON `token` mismatch → 401 `{"error":"Unauthorized"}`; invalid JSON → 400 `{"error":"Invalid JSON"}` | Agent-initiated targeting (the `generate` command). Validation (400 `{"error":<msg>}`, messages verbatim): `agent_target: selector is required`, `agent_target: selector too long` (>1000 chars), `agent_target: invalid action (valid: <VISUAL_ACTIONS joined ', '>)`, `agent_target: count must be 1-8`, `agent_target: text must be a string of at most 500 chars`, `agent_target: index must be a positive integer (1-based)`, `agent_target: prompt must be a string of at most 2000 chars`, `agent_target: dryRun must be a boolean` / `agent_target: hideLiveBar must be a boolean`. No SSE client → 200 `{ok:false, error:'no_browser_connected'}`. Otherwise mint an 8-hex `targetId`, broadcast `agent_target` (see 6.2), and **hold the response** until `/agent-target-result` resolves it, every connected overlay has declined (busy roll call, see `/agent-target-claim`), or `IMPECCABLE_AGENT_TARGET_TIMEOUT_MS` (default 15000) elapses: busy verdict `{ok:false, error:'busy', state, reason}` from the first report when any report exists, else `{ok:false, error:'browser_timeout', timeoutMs}`. The held reply is 200 `{targetId, ...result}`; shutdown resolves every held request with `{ok:false, error:'server_stopping'}`. |
| `POST /agent-target-result` | 401 / 400 Invalid JSON | `targetId` (non-empty string) required else 400 `{"error":"agent_target_result: missing targetId"}`; the remaining body fields (minus `token`) resolve the held request; 200 `{ok:true, delivered:boolean}` (`delivered:false` when nothing awaits that id). A `generate` event on `POST /events` may carry `agentTarget: {targetId, result}`: once the event is accepted, the server resolves that pending target with `result` (the envelope is stripped before journaling and never reaches the poller), so a page that dies between Go and its result cannot leave the request pending for a second Go elsewhere; whichever of the event and the result post lands first answers. An accepted generate event carrying the envelope is journaled and queued with `origin: "agent"`, and `live-poll` renders that event's `_instructions` as the fast path (identity from the event's `element.computedStyles` / `cssCustomProperties` / `parentContext`, the action's three dimensions, no parameter knobs unless the prompt asks, one edit, reply done) instead of the interactive planning pointer and the action-reference read. The envelope also carries `clientId`: a generate event naming a target that another page now holds (a live lease, this page's having lapsed while it captured) or that was already answered, with a different session or with none (a timeout or a failure verdict the CLI has reported), or that the helper neither holds nor remembers (never issued by it, or evicted from its bounded record of answered targets), is refused with 409 `{"error":"agent_target_already_served", targetId, sessionId?}` and journals nothing, and the overlay drops that local session; the answering session's own event is welcome. |
| `POST /live-bar` | 401 / 400 Invalid JSON | `hidden` (boolean) required else 400 `{"error":"live_bar: hidden must be a boolean"}`. Sets the helper-wide bar preference; on a change broadcasts `{type:'live_bar', hidden}` to every SSE client. `GET /status` and the SSE `connected` frame carry it as `hideLiveBar`. Answers `{ok:true, hidden}`. |
| `POST /agent-target-claim` | 401 / 400 Invalid JSON | `targetId` and `clientId` (non-empty strings) required else 400 `{"error":"agent_target_claim: missing targetId or clientId"}`. Roll call plus a first-wins lease, so exactly one overlay acts on a broadcast target. Unknown or resolved target → `{ok:true, granted:false, pending:false}` (ends a rescuer's retry loop). `eligible !== true` → record `{state, reason, result?}` under `clientId` (replacing an earlier report; `result` is the overlay's resolution verdict when `reason` is `no_match`, i.e. its page cannot resolve the selector), release the lease if this client holds it, answer `{ok:true, granted:false, pending}` (`pending` false once the request resolved, so a declining overlay knows whether to keep watching for a change of its word), then complete the roll call when no owner holds the lease and reports ≥ connected overlays. Verdict precedence: a report whose `reason` is not `no_match` (a tab that could serve later) → `{ok:false, error:'busy', state, reason}` at once; when every report is `no_match` the roll call stays open for `IMPECCABLE_AGENT_TARGET_RESOLVE_GRACE_MS` (default 3000) after each overlay's first such report (a late reporter extends the grace by the full window; a page whose element mounts late keeps re-checking while its decline answers `pending:true`, an eligible claim drops its stale report, and the overlay declines rather than posting a result when the element is gone after its claim), then answers the first report's `result` (e.g. `no_match` with `rawMatchCount`, `invalid_selector`); the timeout uses the same precedence when any report exists. `eligible === true` → drop this client's earlier report; `granted` when no owner, the same owner (renew), or the lease lapsed (`IMPECCABLE_AGENT_TARGET_CLAIM_LEASE_MS`, default 3000); answer `{ok:true, granted, pending:true}`. |
| anything else | | 404 `Not found` |
@@ -1733,7 +1734,7 @@ Conventions: every script's "run directly" guard is `process.argv[1]` ending wit
6. Reuse server if `server.json` pid alive, else `node live-server.mjs --background`; failure → `{ok:false,error:'server_start_failed'}` exit 1.
7. `node live-inject.mjs --port P --token T`; not ok → `{ok:false,error:'inject_failed',detail:<json|raw>,serverPort}` exit 1.
8. Drift scan: `.html` files under `public, src, app, pages` (skipping ignored dirs/dot-dirs) not in resolved files and not user-excluded → `configDrift = {orphans:[≤20], orphanCount, hint:'N HTML file(s) exist but aren\'t in config.files. Consider adding them, or use a glob pattern like "public/**/*.html".'}` else `null`.
9. Success: `{ok:true, serverPort, serverToken, pageFiles:[…resolved], liveConfigPath, configDrift, targetPath, projectRoot:appRoot, repoRoot, roots:{manifest}, hasProduct:true, product:<text>, productPath:rel, hasDesign:true, design:<text>, designPath:rel, hasSurfaceBrief, surfaceBrief:<text|null>, surfaceBriefPath:rel|null, _instructions:'Open the app URL that serves a pageFiles entry (never serverPort; that is the helper). Then start the poll loop per your harness policy in live.md and re-run node <scripts>/live-poll.mjs immediately after every event or reply. Every event carries _instructions: follow them; they are the authoritative next step with real ids and paths filled in. A poll that is running is a poll you are SERVICING: never announce you are waiting and idle your turn; stay on the exec session until it returns an event, and never end a turn while a poll is outstanding.'}`. Surface brief resolved from `.impeccable/surfaces` under appRoot, contextRoot, repoRoot (first hit). With `--dev-url` (and only then) the payload also carries `devUrl`, the origin of the dev server serving this app right now, found by fetching `/` on the candidate origins (`http://127.0.0.1:<p>/` and `http://localhost:<p>/` for p in 5173, 3000, 4321, 8080, 4173, 3001, 5174, 8000, 4200, 5000, 1234, probed in parallel with sub-second timeouts; `IMPECCABLE_DEV_URL_CANDIDATES` replaces the list with a comma-separated one) and keeping the first whose document contains the injected `live.js?token=<serverToken>` tag; `null` when none does. Without either flag the payload and the boot's work are exactly as before: no probe runs and neither key appears.
9. Success: `{ok:true, serverPort, serverToken, pageFiles:[…resolved], liveConfigPath, configDrift, targetPath, projectRoot:appRoot, repoRoot, roots:{manifest}, hasProduct:true, product:<text>, productPath:rel, hasDesign:true, design:<text>, designPath:rel, hasSurfaceBrief, surfaceBrief:<text|null>, surfaceBriefPath:rel|null, _instructions:'Open the app URL that serves a pageFiles entry (never serverPort; that is the helper). Then start the poll loop per your harness policy in live.md and re-run node <scripts>/live-poll.mjs immediately after every event or reply. Every event carries _instructions: follow them; they are the authoritative next step with real ids and paths filled in. A poll that is running is a poll you are SERVICING: never announce you are waiting and idle your turn; stay on the exec session until it returns an event, and never end a turn while a poll is outstanding.'}`. Surface brief resolved from `.impeccable/surfaces` under appRoot, contextRoot, repoRoot (first hit). With `--no-live-bar` the boot POSTs `/live-bar` `{token, hidden:true}` to the (started or reused) helper and the payload carries `liveBarHidden` (true when the helper acknowledged); the helper then hides the overlay's global bar in every tab for its lifetime. With `--dev-url` (and only then) the payload also carries `devUrl`, the origin of the dev server serving this app right now, found by fetching `/` on the candidate origins (`http://127.0.0.1:<p>/` and `http://localhost:<p>/` for p in 5173, 3000, 4321, 8080, 4173, 3001, 5174, 8000, 4200, 5000, 1234, probed in parallel with sub-second timeouts; `IMPECCABLE_DEV_URL_CANDIDATES` replaces the list with a comma-separated one) and keeping the first whose document contains the injected `live.js?token=<serverToken>` tag; `null` when none does. Without either flag the payload and the boot's work are exactly as before: no probe runs and neither key appears.
- Tests: `tests/live-target-context.test.mjs`, `tests/live-roots.test.mjs`, `tests/live-e2e.test.mjs` (`session.liveBoot` for `appDir` fixtures), `tests/live-recovery-commands.test.mjs`.
#### `live-server.mjs` -> `impeccable live-server`
@@ -1799,7 +1800,7 @@ Conventions: every script's "run directly" guard is `process.argv[1]` ending wit
#### `live-generate.mjs` -> `impeccable live-generate`
- **Invoked from**: `skill/reference/generate.md` (the `generate` command), after `impeccable live` booted the helper and the agent opened the app page: `impeccable live-generate --selector "section.pricing" --action bolder --count 3`.
- **Args**: `--selector <css>` (required), `--text <snippet>`, `--index <n>` (1-based), `--action <name>` (default `impeccable`), `--count <n>` (default 3, 1-8), `--prompt <text>`, `--dry-run`, `--wait-for-browser <ms>`, `--no-live-bar` (body `hideLiveBar:true`; the overlay hides the helper's global bar for the session it starts and restores it when that session ends, persisting the choice across reloads in its session cache), `--target <path>` (consumed by `enterLiveRoot`), `--help`. A flag without a value → stdout `{"ok":false,"error":"missing_flag_value","flag":"--x"}`, exit 1.
- **Args**: `--selector <css>` (required), `--text <snippet>`, `--index <n>` (1-based), `--action <name>` (default `impeccable`), `--count <n>` (default 3, 1-8), `--prompt <text>`, `--dry-run`, `--wait-for-browser <ms>`, `--no-live-bar` (body `hideLiveBar:true`: the helper sets its lifetime-wide `hideLiveBar` preference, broadcasts `{type:'live_bar', hidden:true}` to every connected overlay before the target goes out, and answers `hideLiveBar:true` on every later `connected` frame; the overlay hides its global bar accordingly and skips its "No PRODUCT.md found" connect notice, the variant controls still show, and only the helper stopping ends it), `--target <path>` (consumed by `enterLiveRoot`), `--help`. A flag without a value → stdout `{"ok":false,"error":"missing_flag_value","flag":"--x"}`, exit 1.
- **Env**: `IMPECCABLE_SELF` (how the boot and poll verbs are spelled in `_instructions`).
- **Behavior**: `enterLiveRoot`; local verdicts first, each pretty-printed JSON on stdout with `_instructions`, exit 1: `selector_required`, `invalid_action` (+`action`, `validActions`), `invalid_count` (+`count`), `invalid_index` (+`index`), `invalid_wait` (+`wait`); no `server.json` (or one without port/token) → `server_not_running`. With `--wait-for-browser`, `GET /status` once a second until `connectedClients > 0` or the budget ends (`no_browser_connected` + `waitedMs`); an unanswered `/status``server_unreachable`. Then `POST /agent-target` with `{token, selector, action, count, text?, index?, prompt?, dryRun?}` under a 20 s client cap: a transport timeout → `request_timeout` (+`detail`, browser_timeout instructions), any other transport failure → `server_unreachable` (+`detail`); a non-2xx answer → `{ok:false, error:<body.error or http_<status>>, ...body}`; an unparseable body → `bad_server_response` (+`status`). A 2xx answer is printed as received plus `_instructions` for `ok` (dry run or started session, naming `impeccable live-poll`), `no_browser_connected`, `browser_timeout`, `invalid_selector`, `no_match` (wording depends on `rawMatchCount`), `ambiguous`, `index_out_of_range`, `busy`, `go_failed`, `server_stopping`; exit 0 when `ok:true`, else 1. `_instructions` are regenerated locally from the verdict, never taken from the wire.
- **Tests**: `tests/oracle/cases/live-generate.mjs` (local verdicts, no-browser), `tests/live-agent-target.test.mjs` (protocol matrix against the binary), `crates/cli/tests/agent_target.rs`, `tests/live-e2e.test.mjs` (`agentTargetScenario`).
+3 -3
View File
@@ -38,10 +38,10 @@ Done when you hold an action from the vocabulary, a count from 1 to 8, and the e
## Step 2: Boot and open the page
One command. Pass `--target` with the file that renders the element when the request or the project makes it obvious; skip it otherwise. Always pass both flags: `--allow-missing-context` lets the boot proceed when PRODUCT.md or DESIGN.md is absent (it changes nothing when both exist), and `--dev-url` asks the boot to find the dev server. Neither touches a plain `live` session.
One command. Pass `--target` with the file that renders the element when the request or the project makes it obvious; skip it otherwise. Always pass all three flags: `--allow-missing-context` lets the boot proceed when PRODUCT.md or DESIGN.md is absent (it changes nothing when both exist), `--dev-url` asks the boot to find the dev server, and `--no-live-bar` tells the helper to keep its bottom bar hidden in every tab for its lifetime and to skip the overlay's missing-context notice (the variant controls still show; both are back on the next plain `live` boot). None of them touches a plain `live` session.
```bash
{{scripts_path}}/impeccable live --target src/App.jsx --allow-missing-context --dev-url
{{scripts_path}}/impeccable live --target src/App.jsx --allow-missing-context --dev-url --no-live-bar
```
Read three fields of the output and nothing else:
@@ -64,7 +64,7 @@ One command. Derive the selector from what the user said and what you already kn
{{scripts_path}}/impeccable live-generate --selector "#pricing" --action bolder --count 3 --no-live-bar
```
Flags: `--selector` (required), `--action`, `--count`, `--prompt`, `--text` (keep only matches whose visible text contains a snippet), `--index` (1-based pick among matches), `--dry-run`, `--wait-for-browser <ms>`, `--no-live-bar` (always pass it: the helper's bottom bar stays hidden for this session, and only the variant controls show; the bar returns when the session ends).
Flags: `--selector` (required), `--action`, `--count`, `--prompt`, `--text` (keep only matches whose visible text contains a snippet), `--index` (1-based pick among matches), `--dry-run`, `--wait-for-browser <ms>`, `--no-live-bar` (always pass it too: if this helper was booted without the flag, the target itself tells it to hide the bar in every tab from now on).
Every verdict carries `_instructions`; follow them over your recollection of this file. Two deserve naming:
+22 -28
View File
@@ -7215,26 +7215,18 @@
// actOnAgentTarget around its handleGo call, read once by handleGo.
let agentTargetForGo = null;
// An agent target that asked for the helper's bottom bar to stay out of
// the way (`live-generate --no-live-bar`) hides it for the life of this
// helper instance in this tab: through the reloads a session causes, the
// accept, and the bake that follows, until the helper stops and takes
// the whole overlay with it. The variant controls still show. Keyed on the
// helper token so the next `impeccable live` starts with the bar again.
function liveBarHiddenKey() {
return 'impeccable-live:hide-bar:' + TOKEN;
}
// The helper's word on its global bar. The generate lane asks the helper
// to keep it out of the way (`impeccable live --no-live-bar`, or an agent
// target carrying hideLiveBar), and the helper tells every connected tab
// at once (`live_bar`) and every later connection on `connected`, so the
// bar stays hidden in every tab, through reloads, the accept, and the
// bake, until the helper stops and takes the overlay with it. The variant
// controls still show.
let liveBarHiddenByHelper = false;
function rememberLiveBarHidden() {
try { sessionStorage.setItem(liveBarHiddenKey(), '1'); } catch { /* storage may be unavailable */ }
}
function forgetLiveBarHidden() {
try { sessionStorage.removeItem(liveBarHiddenKey()); } catch { /* storage may be unavailable */ }
}
function liveBarHiddenForThisHelper() {
try { return sessionStorage.getItem(liveBarHiddenKey()) === '1'; } catch { return false; }
function applyLiveBarPreference(hidden) {
liveBarHiddenByHelper = hidden === true;
setLiveBarHidden(liveBarHiddenByHelper);
}
function setLiveBarHidden(hidden) {
@@ -7486,10 +7478,6 @@
handleGo();
agentTargetForGo = null;
if (state === 'GENERATING' && currentSessionId) {
if (msg.hideLiveBar === true) {
rememberLiveBarHidden();
setLiveBarHidden(true);
}
reply({
ok: true,
matchCount: resolved.matchCount,
@@ -7525,8 +7513,11 @@
let msg; try { msg = JSON.parse(e.data); } catch { return; }
switch (msg.type) {
case 'connected':
applyLiveBarPreference(msg.hideLiveBar === true);
hasProjectContext = !!msg.hasProjectContext;
if (!hasProjectContext) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000);
// The generate lane runs without PRODUCT.md by design and never
// sends the user to init, so its quiet chrome skips this notice.
if (!hasProjectContext && !liveBarHiddenByHelper) showToast(`No PRODUCT.md found. Variants will be brand-agnostic. Run ${IMPECCABLE_COMMAND} init to generate one.`, 7000);
console.log('[impeccable] Live mode connected.');
syncAgentPollingUi(!!msg.agentPolling);
startAgentStatusPoll();
@@ -7536,6 +7527,9 @@
syncPageInteractionCursor();
syncPageChatFocus('sse-connected');
break;
case 'live_bar':
applyLiveBarPreference(msg.hidden === true);
break;
case 'agent_polling':
syncAgentPollingUi(!!msg.connected);
break;
@@ -11983,9 +11977,9 @@ void main() {
// Listen for detection results AND ready signal
window.addEventListener('message', onDetectMessage);
updateGlobalBarState();
// A generate lane asked this helper to keep the bar out of the way; every
// reload the session causes rebuilds the bar, so re-apply it here.
if (liveBarHiddenForThisHelper()) setLiveBarHidden(true);
// The helper may already have said the bar stays hidden (a connect
// that raced the bar build, or a reload mid-lane): re-apply it here.
if (liveBarHiddenByHelper) setLiveBarHidden(true);
}
function updateGlobalBarState() {
@@ -12188,7 +12182,7 @@ void main() {
// not refuse every target the next connection hears.
busyDeclinedTargets.clear();
agentTargetsSeen.clear();
forgetLiveBarHidden();
liveBarHiddenByHelper = false;
stopAgentStatusPoll();
hideAgentPollTooltip();
if (agentPollTooltipEl) {
+7
View File
@@ -860,8 +860,15 @@ describe('POST /agent-target', { skip: ENGINE_BIN ? false : ENGINE_MISSING_MESSA
const held = postJson(server, '/agent-target', {
token: server.token, selector: 'h1', action: 'bolder', count: 3, hideLiveBar: true,
});
assert.equal((await tabA.next((m) => m.type === 'live_bar')).hidden, true, 'every connected tab hears the helper-wide preference first');
const pushed = await tabA.next((m) => m.type === 'agent_target');
assert.equal(pushed.hideLiveBar, true, 'the overlay is told to keep the bottom bar out of the way');
const tabB = await openSseClient(server, { clientId: 'tab-b' });
try {
assert.equal((await tabB.next((m) => m.type === 'connected')).hideLiveBar, true, 'a later connection learns it on connect');
} finally { tabB.close(); }
const status = await (await fetch(`http://127.0.0.1:${server.port}/status?token=${server.token}`)).json();
assert.equal(status.hideLiveBar, true, '/status carries it');
await postJson(server, '/agent-target-result', { token: server.token, targetId: pushed.targetId, ok: true, sessionId: 'aabbccdd' });
await (await held).json();
const refused = await postJson(server, '/agent-target', {
+13 -7
View File
@@ -905,22 +905,28 @@ describe('live-browser source contracts', () => {
);
assert.match(
SOURCE,
/if \(msg\.hideLiveBar === true\) \{\s*rememberLiveBarHidden\(\);\s*setLiveBarHidden\(true\);/,
'an agent target that asks for it hides the global bar and remembers that for this helper instance',
/case 'connected':\s*applyLiveBarPreference\(msg\.hideLiveBar === true\);/,
'every connection, including a reload or a second tab, takes the helper\'s word on the bar',
);
assert.match(
SOURCE,
/function liveBarHiddenKey\(\) \{\s*return 'impeccable-live:hide-bar:' \+ TOKEN;/,
'the choice is keyed on the helper token, so the next live boot shows the bar again',
/case 'live_bar':\s*applyLiveBarPreference\(msg\.hidden === true\);\s*break;/,
'a helper-wide change reaches every connected tab at once',
);
assert.match(
SOURCE,
/updateGlobalBarState\(\);\s*\/\/[^\n]*\n\s*\/\/[^\n]*\n\s*if \(liveBarHiddenForThisHelper\(\)\) setLiveBarHidden\(true\);/,
'every reload rebuilds the bar and re-applies the hide',
/hasProjectContext = !!msg\.hasProjectContext;\s*\/\/[^\n]*\n\s*\/\/[^\n]*\n\s*if \(!hasProjectContext && !liveBarHiddenByHelper\) showToast\(/,
'the lane\'s quiet chrome also skips the "No PRODUCT.md" notice, which would send the user to init',
);
assert.match(
SOURCE,
/updateGlobalBarState\(\);\s*\/\/[^\n]*\n\s*\/\/[^\n]*\n\s*if \(liveBarHiddenByHelper\) setLiveBarHidden\(true\);/,
'a bar built after the helper spoke still ends up hidden',
);
assert.ok(!/sessionStorage\.getItem\('impeccable-live:hide-bar/.test(SOURCE), 'no per-tab memory: the helper is the single source of truth');
assert.ok(!/releaseHiddenLiveBar/.test(SOURCE), 'no session end brings the bar back: the accept and the bake that follows stay bar-free');
const teardownBody = SOURCE.match(/function teardown\(\) \{[\s\S]*?\n \}/)?.[0] || '';
assert.match(teardownBody, /forgetLiveBarHidden\(\);/, 'only the helper stopping forgets the choice');
assert.match(teardownBody, /liveBarHiddenByHelper = false;/, 'only the helper stopping resets it');
assert.match(
SOURCE,
/if \(claim\.granted\) \{ noteAgentTarget\(msg\.targetId, 'acting'\); actOnAgentTarget\(msg\); return; \}/,