Repository navigation
Phase B: harness/web improvements (re-open after merge mishap) - #102
Conversation
…dize worker layout
- inbox::* -> session-inbox::* - hooks::publish_collect -> hook-fanout::publish_collect Updates README, registry/index.json, turn-orchestrator callsites, and whitelists per-worker config.yaml files in .gitignore.
…nd description - Introduced constants for skill ID, metadata, and sub-skills in `lib.rs`. - Updated the description in `start.rs` to remove an unnecessary argument. - Implemented skill registration with retry logic and graceful shutdown handling in `main.rs`.
Replace Duration::from_secs(180)/(60) and from_secs(3 * 60) with from_mins(3)/from_mins(1) in skill-register retry loops for auth-credentials, llm-budget, and session-tree.
- Changed trigger type from "subscribe" to "durable:subscriber" in multiple modules. - Updated function IDs to use the "iii::durable::publish" format for consistency across the codebase.
Phase A item #3. Includes new test expected_workers_includes_approval_gate and the existing drift test passes.
…mirror on parent-read failure
…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.
# Conflicts: # .gitignore
CI runs Rust 1.95 with stricter lints. Replace .map(...).unwrap_or(0) with .map_or(0, ...) in both harness/tests/common/mod.rs and turn-orchestrator/tests/common/mod.rs nonce() helpers.
# Conflicts: # harness/src/main.rs # harness/web/src/App.tsx # harness/web/src/reducer.ts # harness/web/src/types.ts
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (41)
📝 WalkthroughWalkthroughThis PR introduces a per-browser subscription fanout layer for the harness that pushes live state changes (sessions, approvals, costs, workers) to connected browsers via WebSocket, and replatforms the web UI from SSE to a push-based architecture using the iii-browser-sdk. It includes new UI components for command menus, function palettes, message forking/repair, session export, and live status dashboards. ChangesHarness Fanout & State Management
Web UI Replatforming
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Suggested reviewers
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
Resolves squash-merge fake conflicts: Phase A/B content (PR #100, #102) landed on main as squash commits while feat/iii-native-harness retained the original commits + the tool->function rename + iii-native-harness work. Resolutions favor feat side for files where Phase A/B content was the sole source of conflict; main's unique content (skills filesystem support, storage worker, etc.) merged automatically. Removed turn-orchestrator/src/states/tools.rs (renamed to functions.rs).
What's in the box
WS transport cutover + composer power + bus function palette + live status panel + session UX, layered on Phase A's (#100) foundation.
Spec:
docs/superpowers/specs/2026-05-06-harness-web-improvements-design.mdStep 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 with backpressure (atomic in-flight counter, drop+resync on overflow, ≤10/s coalesce on cost).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,/repair,/fork,/export md|json. Skills appear as/<skill-id>.@-mention browses files from cwd viashell::filesystem::ls. Plain monospace path insert.↑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::changed.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 — Skipped
After step B,
bridge.tsis a 30-line Promise façade overiii-client— kept as the public API surface; deletion would be cosmetic churn across ~15 files.Bug fixes from end-to-end testing
Three fix commits at the tip address bugs surfaced by running the live demo:
chore(harness): add approval-gate to demo.sh WORKERS; commit session-tree Cargo.lock— Phase A oversight (demo.sh predated approval-gate addition).chore(harness/web): tsc noEmit so Vite owns the JS emit— without this,tscemits stale .js next to .tsx and Vite's resolver picks the .js, so dev runs serve pre-Phase-B compiled output.fix(harness/web): agent_end shape, dedupe, and main layoutturn-orchestrator/crates/harness-types/src/agent_event.rs) emits bareAgentMessage[]; reducer now tolerates both bare and wrapped shapes.pushUnkeyedprevents the message_end + agent_end double-render..mainswitched from grid (template-rows blew up with 8 children) to flex column.Plus:
fix(tests): clippy::map_unwrap_or in nonce() helpers— required for CI's Rust 1.95 strict-warnings.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 warningsclean (incl. CI's Rust 1.95 lints)demo.shstack: WS connection only, no SSE, no 4s pollingPragmatic deviations from spec
Worth flagging — none are blockers, all have rationale:
llm-budget::summarydoesn't exist — cost pump synthesizes frombudget::list;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).Summary by CodeRabbit
New Features
/forkand/repaircommands.UI/UX