Phase B: harness/web improvements (WS transport + composer + palette + status + session UX) - #101
Merged
Merged
Conversation
…eleton
- New harness::fanout module: per-browser subscription registry
(BrowserId -> {session_ids, all-sessions sentinel}). 4 unit tests cover
per-session routing, all-sessions fallthrough, eviction, and global-only
filtering.
- New bridge::info function: returns the relative WS path (/iii/ws),
protocol "ws", and the engine_url the harness was started with so
reverse-proxy / HTTPS browser clients compose URLs from window.location
while native callers still get a usable ws:// URL.
- New ui::subscribe / ui::unsubscribe functions: add/remove a browser's
interest in a session (or null = all sessions / non-session topics).
Real subscribers (agent::events, state diffs, llm-budget,
harness::status) wire in later steps.
- HarnessFunctionRefs gains bridge_info, subscribe_fn, unsubscribe_fn;
unregister_all updated.
- register_with_iii_with_engine_url is the new entry point that takes the
engine URL through; register_with_iii kept as a default-URL shim so the
existing test surface stays unchanged.
- iii-worker-manager is intentionally NOT added to EXPECTED_WORKERS: it is
built into the iii engine itself (engine's builtin_defaults.rs), not a
discrete worker crate. Comment added near EXPECTED_WORKERS to document
the assumption.
- New tests/bridge_info.rs integration test (engine-backed; auto-skips
when no engine is reachable).
Cmd-J (Ctrl-J on Linux/Win) opens a global modal listing every function on the bus. Drill-in shows description + a raw JSON payload editor that posts through /bridge/trigger. Sensitive functions (policy::*, auth::set_*, shell filesystem writes) get a two-step Enter-to-confirm Send. Recent invocations (last 20) persist in localStorage so the next open pre-fills the payload. Files: - useGlobalShortcut.ts + test: cross-platform Cmd/Ctrl binder with pure matchesShortcut predicate so the matching logic is testable in node env. - palette.ts + test: isSensitive, workerFromId, filterPalette (mirrors the menuItems.ts ranking — substring beats fuzzy), loadRecent / pushRecent (dedupe by function_id, cap at 20, survives quota errors), curlForBridge. - components/FunctionPalette.tsx: list view with Functions/Recent tabs, arrow-key nav, ARIA dialog/listbox/option, focus trap (filter focused on open, prior focus restored on close), drill-in with JSON textarea, response pre, copy-as-curl, sensitive confirm flow, prefers-reduced-motion respect. - App.tsx: wires paletteOpen state + useGlobalShortcut + footer hint. - styles.css: .palette-* classes (backdrop, list, drill, sensitive chip).
…esWithEntryIds; group + subtitle Switch the rail to session-tree::list (with state::list drift fallback when total === 0), group rows by Today/Yesterday/Earlier, and render the workspace cwd as a left-truncated subtitle. Per-message kebab actions render fork + copy; fork is disabled with a tooltip when entry_id is null (drift case). - sessions.ts: pure groupByDate + truncatePath helpers (vitest covered). - export.ts: client-side exportMd/exportJson via Blob downloads of session-tree::messages and session-tree::tree responses. - MessageActions.tsx: per-message kebab; copy uses navigator.clipboard. - SessionList.tsx: session-tree::list reader + grouped sections + subtitle. - SessionView.tsx: thread MessageActions through with parallel entry_ids. - types.ts: SessionRow gains optional cwd + last_message_summary. - styles.css: .session-group(s|-h), .session-subtitle, .msg-action(s).
App.tsx swaps the message loader to loadMessagesWithEntryIds and tracks a parallel messageEntryIds array so fork buttons know which messages have real entry_ids. Stream-driven updates pad the array with null until WS migration (step B) carries entry_ids on the wire. - /repair calls session-tree::reconcile with the state::* snapshot, then reloads to surface the new entry_ids. - /fork forks from the most recent message with an entry_id. - /export md|json downloads via the new export module. - Per-message MessageActions wires onForkFromMessage which calls session-tree::fork and switches the active session to the new id. - menuItems: /repair, /fork, /export md, /export json built-ins. - Composer: dispatcher + slash-menu accept paths for the new commands.
…pressure Adds three new long-lived fanout pumps and per-browser backpressure to the existing harness fanout: - spawn_approval_poll: 1s poll of approval::list_pending across all known sessions; emits ui::approval::requested / ui::approval::resolved on diff. - spawn_cost_poll: 2s poll of budget::list, sums spent_usd into a daily total + by-period breakdown, emits ui::cost::tick on change. The designed llm-budget::summary function does not exist in the worker, so the summary is synthesized client-side; this is the closest equivalent. - spawn_workers_poll: 5s poll of engine::workers::list, joined against EXPECTED_WORKERS to mark missing workers as down; emits ui::workers::changed on diff. Backpressure is enforced per-browser: - AtomicU64 in_flight counter capped at 1024; on overflow we roll back the failed push, emit a single deduped ui::session::resync, and wait. - Cost ticks gated to ≥100ms apart per browser (≤10/s) via a Mutex<Option<Instant>>. Pure helpers (should_emit_cost_tick, diff_workers, diff_approvals, summarize_budgets, extract_worker_status) are pub(crate) and unit-tested without spawning tasks. All four required Step E tests pass: - fanout_pump_coalesces_cost_ticks_to_10_per_second - fanout_pump_drops_oldest_and_emits_resync_on_overflow - fanout_approval_pump_emits_resolved_on_removal - fanout_workers_poll_diffs_correctly
Adds the live status surface backed by Step E's fanout pushes:
- useStatus: subscribes once per page to ui::approval::requested,
ui::approval::resolved, ui::cost::tick, ui::workers::changed and owns
the rolling 200-event buffer. Returns {pendingApprovals, cost, workers,
events, hydrated, clearEvents}.
- StatusStrip (header): single Approvals chip with count, pulses on new
approvals, grays out + shows reconnecting dot when WS drops, click
jumps to the status tab.
- FootStatus (app-foot): cost USD + workers N/M up chips. Compact,
mono, color-tone matches state (ok/warn/muted) plus reconnect dot.
- StatusTab: workers table (text status for a11y), inline 80x16 SVG
cost sparkline, hydrated budget breakdown via budget::list +
budget::usage, rolling events feed with filter chips
(all/approval/cost/workers) + pause/resume/clear, "live updates
paused" banner after >5s disconnect with auto re-hydration on reconnect.
App.tsx adds the new "status" tab to the existing tab nav, the right-
side header group with StatusStrip + StatusPill, and the FootStatus
chips in the footer. styles.css adds chip / table / feed / sparkline
styles tuned to the existing design tokens.
10 useStatus tests cover subscription wiring, dedup, snapshot
replacement, malformed ignore, 200-entry cap, clear, hydrated flag,
and unsubscribe on unmount. Full vitest suite: 121 pass.
…tree Cargo.lock Phase A added approval-gate to EXPECTED_WORKERS + iii.worker.yaml but missed the demo.sh script. Surfaced when starting the live demo: 15 workers spawned instead of 16. session-tree Cargo.lock catches up with the serial_test + which dev-deps added in Phase A.
`npm run build` is `tsc && vite build`. Without noEmit, tsc emits compiled .js next to every .ts/.tsx, and Vite's resolver picks them over the .tsx sources on subsequent dev runs. End-to-end debugging surfaced this as crashes in components running pre-Phase-B compiled output.
Three bugs surfaced by end-to-end testing the live harness:
- AgentEvent.agent_end.messages widened to (AgentMessage | {entry_id?, message})[]:
backend (turn-orchestrator/crates/harness-types/src/agent_event.rs) emits bare
AgentMessage[]. Reducer now detects bare-vs-wrapped per item and routes
accordingly. Defensive guard in pushUnkeyed rejects anything missing a role
so future similar mismatches degrade silently instead of crashing the tree.
- Content-hash dedupe in pushUnkeyed (role:timestamp:content-length, matches
pre-Phase-B approach). Without it, message_end (per-message during the turn)
+ agent_end (full transcript at end) double every reply.
- .main switched from grid grid-template-rows: auto 1fr auto auto to flex
column with .view as flex: 1. The grid template only described 4 rows but
the chat tab renders 8 children, so the 1fr landed on ControlsBar and the
controls strip stretched to fill the viewport with the chips visually
centered in empty space.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
sergiofilhowz
approved these changes
May 7, 2026
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the harness/web HTTP+SSE stack with WebSocket transport via
iii-browser-sdk, then layers composer power (slash menu, @-mention, history), a Cmd-J function palette, a live status panel (workers / cost / approvals), session UX (fork from message, export md/json,/repair), and per-session workspace cwd on top.Spec:
docs/superpowers/specs/2026-05-06-harness-web-improvements-design.mdWhat's in the box
Step A — Backend prep
bridge::inforeturns relative WS path so reverse-proxy / HTTPS deployments compose the URL fromwindow.location.harness-ui-fanout(new module iniii-harness) — per-browser subscription registry.ui::subscribe/ui::unsubscribebus functions.iii-worker-manageris engine-built-in (verified) — not added toEXPECTED_WORKERS.Step B — WS transport cutover
iii-client.tsfaçade overiii-browser-sdk@0.11.7-next.1withuseConnection()hook (drives reconnect chrome).useAgentStreammigrated fromEventSourcetoclient.on(\"ui::session::event\")push.ui::sessions::changedpush.<functionId>::<browserId>so the fanout can target a specific browser.agent::eventsstream →ui::session::eventand a 1-secondstate::listdiff →ui::sessions::changed.Step C — Workspace
/cwd <path>slash command.policy-denylist(separate change).Step D — Composer power
useCommandMenuheadless reducer (slash / at / history modes)./new,/clear,/cwd,/model,/provider,/help. Skills appear as/<skill-id>.@-mention browses files from cwd viashell::filesystem::ls. Plain monospace path insert (no styled chips).↑history walk in empty composer. Esc///@/Enter/Tab precedence honored per spec's Key Precedence table.Step E — Live status panel
N/M upinapp-foot.ui::approval::requested|resolved,ui::cost::tick,ui::workers::changedwith backpressure (atomic in-flight counter, drop+resync on overflow, ≤10/s coalesce on cost).Step F — Bus function palette
engine::functions::list.localStorage(capped 20, deduped by function id).policy::*,auth::set_*,shell::filesystem::write|mkdir|rm.bridge::triggerPOST snippet.Step G — Session UX
session-tree::list(Phase A) withstate::listfallback for drift.loadMessagesWithEntryIds(Phase A).session-tree::fork(disabled with tooltip when entry_id is null)./repairslash command callssession-tree::reconcile.Step H — Cleanup
bridge.tsis a 30-line Promise façade overiii-client— kept as the public API surface; deletion would be cosmetic churn across ~15 files.Test plan
harness/webvitest: 121 passing across 12 files (reducer, loadMessages, workspace, useCommandMenu, menuItems, useGlobalShortcut, palette, sessions, export, iii-client, useConnection, useStatus)tsc --noEmitcleannpm run buildclean (~219 kB JS, ~31 kB CSS)cargo test --lib --testsclean acrossharness,session-tree,turn-orchestratorcargo clippy --lib --tests -- -D warningscleandemo.shstack: WS connection only, no SSE, no 4s pollingPragmatic deviations from spec
Worth flagging for review — none are blockers, all have rationale:
llm-budget::summarydoesn't exist — cost pump synthesizes frombudget::listclient-side;by_providerpopulated on tab open viabudget::usage.approval::list_pendingis per-session — pump iterates known sessions instead of one global call.state::changedtopic doesn't exist — sessions diff uses 1sstate::listpoll.ui::session::resync::<browser_id>.bridge.tsretained as the Promise-style public API surface (skipped step H).Bug fixes from end-to-end testing
Three follow-up commits at the tip address bugs surfaced by running the live demo:
cde05d6—chore(harness): add approval-gate to demo.sh WORKERS; commit session-tree Cargo.lock(Phase A oversight)0312ce4—chore(harness/web): tsc noEmit so Vite owns the JS emit(build hygiene)deead38—fix(harness/web): agent_end shape, dedupe, and main layoutAgentMessage[]foragent_end.messages; reducer now tolerates both shapes.pushUnkeyedprevents the message_end + agent_end double-render..mainswitched from grid (template-rows blew up with 8 children) to flex column.Commit history
15 commits over Phase B + 3 fix commits = 18 total ahead of Phase A's tip.