Skip to content

plan: workspace status bar (sandbox · cwd · git · context) (parent) #538

Description

@btipling

Plan header

Field Value
Status HANDOFF-READY
Date 2026-08-15
Type parent
Parent N/A
Branch plan/status-bar (per-phase branch suffix)
Layers DOM app/*,lib/* · harness native/harness/** (Zig) · Vercel app/api/**,lib/agent/**
Reusability impact none (all seams are existing session/sandbox/env seams; no host URL hardcoded)
Production mutate? no — no data/secret mutate, no env cutover
Cloud ops path N/A — pure code (Wasm + host + additive bridge + read-only server probe route); no GHA
Living docs docs/feature-divide.md, docs/harness-limits.md, docs/sandbox.md, docs/session-model.md, docs/agent-stream.md, AGENTS.md, README.md

Review notes (2026-08-16)

Reviewed per the repo plan-review skill (AGENTS.md + docs/feature-divide.md loaded;
every baseline claim verified against live main). Verdict: HANDOFF-READY.
All four caps are NEW (generous by default, no change to any existing cap →
no human cap-approval gate). Cloud ops N/A is correct (no Production mutate), and the
three phase issues (#541/#540/#539) conform to the phase map below; Depends-on lines are
correct (Phase 2 and Phase 3 each depend only on Phase 1). No Blockers/Majors.

  • Minor — probe-bind precedence locked (envelope-authoritative). GET /api/harness/status
    must resolve the bind from the caller envelope meta.activeSandboxId via the
    resolveSessionStore → isEnvelopeStore → readEnvelope seam first — the same
    server-authoritative rule the merged meta(#532): built-in meta sandbox tools on /api/agent #535/agent-route B1 fix established. The host
    ?sandboxId= param is a Redis-safe carry only (mirror of parseQuerySandboxId in
    app/api/sandboxes/route.ts), never an override: a stale/foreign host param must not
    cause a probe against a different sandbox. Phase 2 (plan: workspace status bar — phase 2 — git branch/commit probe #540) adopts this exact precedence.
  • Minor — probe rate-limit is per-instance best-effort. On Vercel serverless the
    "bounded in-process per-user clock" is per-instance / ephemeral, not global across
    instances. The cap still blocks a single hot loop, and the host cadence is the primary
    throttle
    ; the exec stays bounded and per-slot fail-soft holds. Worded below accordingly
    (do not present it as a hard global limit).
  • Nit — reserve primary header controls. The existing header holds lifecycle + model
    label + Next (a primary action). Status slots must yield before those on narrow
    canvases; the model/lifecycle/Next controls are never in the slot drop pool, so primary
    action geometry stays stable (feature-divide / layout-stability).
  • Nit — % of window condition is safe. The model catalog does not currently carry
    maxContext/token-limit metadata (verified), so the phase-3 design correctly degrades to
    absolute tokens only — the "only when known" guard never invents a denominator.
    Enforce this in plan: workspace status bar — phase 3 — context/usage slot (implements #327) #539's host fold: never compute a denominator unless catalog metadata
    explicitly provides it.

Summary

Ship the workspace status bar from product issue #328 as the authoritative
harness chrome: a compact status strip in the Wasm header band showing
sandbox identity · cwd · git branch/commit · context/usage. Product source of
truth is in-canvas (Wasm), per feature-divide; the host may mirror a short summary
but must never host a competing status panel. Context comes from provider usage
(#327), never client token math. Split into 3 implementable phases with distinct
gates (chrome/wire · new server git-probe route · usage plumb).

Goals

# Goal Success signal
1 Persistent Wasm status bar with sandbox · cwd · git (when available) Slots render on-canvas after hydrate + each turn; no secrets; ~390px usable
2 Context/usage slot is provider usage, not a client estimator Number comes from the API usage field; labeled; missing usage → hides or marks estimated
3 Values refresh across turns / cwd change / session restore without manual refresh Status updates when RunAgentResult.cwd/activeSandboxId land and after a git probe
4 Graceful degradation Missing git / no sandbox → muted/hidden slots, never broken layout

Non-goals / out of scope

  • Full git UI (log / diff browser / checkout picker) — harness: workspace status bar (sandbox · cwd · git · context) #328 non-goal kept.
  • Replacing the admin sandbox roster or /settings/sandbox.
  • Live billable $ spend — optional later via TPX cost.
  • Re-architecting session carrier semantics (cwd/sandbox already ride meta).
  • Forbidden wiring: the status bar is NOT a second DOM chat/status panel; the
    git probe is NOT an unbounded agent exec on every frame; no Gateway/sandbox
    secrets (token/base_url) ever leave the server.

Architectural decisions

Decision Options considered Choice Why
Where the status bar lives A: Wasm header band (reuse existing frame() header row) / B: dedicated thin strip under header / C: DOM-only A — extend the existing Wasm header band (header_h = TOUCH_H+24 in ui.zig) with right-aligned status slots Matches #328 "header band or reserved chrome strip"; product truth stays in-canvas per feature-divide; zero new band layout
How status reaches Wasm A: additive bridge fields + version bump / B: overload a message kind / C: DOM punctuation-only A — new additive bridge state exports (inv_set_status_slot* / getters), protocol bump 12 → 13 Mirrors the shipped model-catalog push (setModelCataloginv_push_model_catalog_entry); additive (old fields intact) so node time is bounded; protocol is additive-only
Git branch/commit source A: new server read-only probe route against the resolved bind / B: force an agent tool exec in-turn / C: client-side guess A — a new session-gated GET /api/harness/status route that resolves the active bind and runs a bounded, argv-only read-only git probe Git is a sandbox fact (server-side per feature-divide); a model turn to probe git is unbounded/expensive; branch+SHA must be authoritative, rate-limited
Bind source for the probe A: envelope meta.activeSandboxId (server-authoritative) / B: host ?sandboxId= query param A — envelope wins; query param is a Redis-safe carry only Mirrors the merged #535 agent-route precedence (envelope beats body/host carry); a stale/foreign host param must never probe a wrong bind. The ?sandboxId= param follows parseQuerySandboxId exactly as in app/api/sandboxes/route.ts
Context/usage source A: provider usage plumbed from chat/agent completions (#327) / B: client char×const estimator A — capture AI SDK usage on /api/chat + /api/agent (JSON + stream done) and carry a bounded usage summary to host → bridge #327 is the source of truth; #328 explicitly prefers implementing #327 as the bar's context segment; never silent fake math
Sandbox identity display A: reuse GET /api/sandboxes options+active (already non-secret) / B: new projection A — host already builds the bind from /api/sandboxes; slot shows backend/name/id from that projection projectOption already excludes base_url/token; no new identity surface is needed
Cadence guard for git probe A: server-side min-interval rate limit (cap) / B: trust host timers A — server-side STATUS_PROBE_MIN_INTERVAL_MS cap; per-instance best-effort on Vercel serverless (not a global lock), primary throttle is the host cadence Host timers are per-tab and resettable; the read-only exec must be bounded server-side so a refresh loop or stale tab can't hammer the sandbox. The cap still blocks single-path hot loops; keep per-slot fail-soft on rate exhaustion

Layer placement

Concern Layer Path(s) Rationale
Status slot paint (sandbox/cwd/git/context) Wasm native/harness/src/ui.zig header band + new status.zig Wasm owns product chrome
Additive bridge state (status slots) Wasm + DOM native/harness/src/bridge.ziglib/harnessBridge.ts bridge protocol v13
Host push (fold RunAgentResult.cwd/activeSandboxId, /api/sandboxes identity, usage) DOM app/harness/HarnessHost.tsx, lib/harnessChat.ts host bridges session→Wasm
Git probe (read-only exec on resolved bind) Vercel backend app/api/harness/status/route.ts, lib/agent/statusProbe.ts git is a sandbox/server fact
Usage capture (chat + agent) Vercel backend lib/chatServer.ts, lib/agent/runAgent.ts, lib/agent/agentStream.ts, lib/agentApi.ts #327 source of truth

Current baseline (live code)

Claim Path / symbol Notes
Wasm header band exists; right-align slots pattern in-repo native/harness/src/ui.zig fn frame() (749), header_h = TOUCH_H+24 (778); lifecycle + build-id + model label + Next rendered in the band verified
Bridge additive model-catalog push (pattern to mirror) lib/harnessBridge.ts setModelCataloginv_push_model_catalog_entry (521/528) verified
Bridge protocol currently v12 lib/harnessBridge.ts:13 HARNESS_PROTOCOL_VERSION=12; native/harness/src/bridge.zig:20 PROTOCOL_VERSION=12 verified — bump to 13
RunAgentResult already carries cwd, sandboxId, activeSandboxId lib/agent/runAgent.ts RunAgentResult (cwd ~150, sandboxId ~152, activeSandboxId ~161); set on generate+stream paths verified — phase 1 seed free
Host folds post-turn activeSandboxId app/api/agent/route.ts (~431-437 envelope read), lib/harnessChat.ts success reconcile folds agentResult.activeSandboxId ?? sandboxId verified (#535 B1 fix)
Non-secret sandbox inventory + active descriptor app/api/sandboxes/route.ts projectOption (id/name/slug/backend/status/image/canRead/canWrite/usable/granted) + active.tools; ?sandboxId= Redis-safe carry via parseQuerySandboxId (line 32-54, 87) verified — probe route mirrors the same param convention
Server resolve bind seam lib/tenancy/resolveSandbox.ts resolveAgentSandbox (150) + ResolvedAgentSandbox (39); DI-root bound client factories in lib/di/index.ts verified — probe route can reuse
No usage plumbing anywhere yet grep usage/totalUsage across lib/agent/*, lib/chatServer.ts, /api/{chat,agent} → none #327 greenfield
No git-probe surface grep rev-parse/git branch/workspaceProbe across lib/app → none new route needed
Model catalog does not carry maxContext/token-limit lib/gateway/byokProviders.ts / /api/models — no max-context field phase-3 % of window only ever degrades to absolute tokens
Session meta reserved keys carry logicalCwd + activeSandboxId AGENTS.md session-carrier row; lib/sessionStore.ts/lib/sessionCloudCaps.ts verified
Host already ticks a ~1Hz mm:ss Busy chip + lifecycle chip app/harness/HarnessHost.tsx (busy/setLifecycle, thinking · ${formatElapsedSeconds(...)}), lib/elapsedTime.ts verified — don't regress

Design

Wasm status strip (phase 1). Extend the header band with a right-aligned
horizontal pack of text slots rendered from additive bridge state:
sandbox (backend/short id) · cwd · git (branch@sha + optional * dirty)
· context (N tok). Palette only: TEAL default, WARM for git-dirty/busy,
EMBER only for genuine errors (sandbox down / out-of-date). Each slot
truncates with + tooltip/expand-on-tap; slots drop in priority order when
the viewport is narrow (~390px: git first, then cwd, then context — sandbox is
last to hide). The existing primary header controls (lifecycle + model label +
Next) are never in the drop pool
— status slots yield before any of those, so
primary-action geometry stays stable on narrow canvases. Missing value → muted
/hidden, never a broken layout or h-gutter.

Additive bridge v13. Add read/write state exports (e.g. inv_set_status_slot,
inv_status_slot_len/copy) for each in bridge.zig + harnessBridge.ts. Old
exports untouched; host clears slots on bridge.reset() / Clear / New session.

Host fold. On hydrate and after each successful turn, the host
(1) resolves sandbox identity from the /api/sandboxes response it already
fetches, (2) copies RunAgentResult.cwd and the effective activeSandboxId,
(3) pushes them into the Wasm status slots. SessionPicker/Clear resets slots.
Context slot updated when a usage summary is present (phase 3).

Git probe (phase 2). New session/user-gated GET /api/harness/status:
requireSessionUser + read the caller envelope meta.activeSandboxId via the
existing resolveSessionStore → isEnvelopeStore → readEnvelope seam (this envelope
wins
over any ?sandboxId= carry, per the B1/#535 precedence), resolve the bind via
resolveAgentSandbox, then run a bounded, argv-only, read-only
git rev-parse --abbrev-ref HEAD + git rev-parse --short HEAD + optional
git status --porcelain (dirty flag) through the resolved bind client. Output is
truncated to a cap; a non-git repo / no bind / exec error → empty slots (fail
soft). Server-side min-interval rate limit (STATUS_PROBE_MIN_INTERVAL_MS) as a
per-instance best-effort clock (primary throttle is the host cadence; the cap
blocks single-path hot loops — return cached last value, never 429-spam, never
exec when limited).
Host calls after turns/cwd changes and on a cadence; not every frame.

Context/usage (phase 3, implements #327). Capture AI SDK usage
(prompt/completion/total, cached when present) at completion time on /api/chat
and /api/agent (JSON result + stream final chunk). Carry a small bounded
usage summary on RunAgentResult / AgentSuccess / the stream done event;
host folds it into the context slot (and may mirror). Missing usage → hide the
slot or mark estimated — never present client math as API truth. Absolute
tokens only; % of window only when model max-context is known from catalog, else
no fake denominator (the catalog currently exposes no max-context, so v1 is
absolute-only by default).

Edge cases: empty session (no envelope) → sandbox ; Wasm load fail → host
chips still work (no regression); API 4xx/5xx → probe/usage absent, slots mute;
refresh/restore → slots reseed from envelope + /api/sandboxes; mobile ~390px →
slot priority ordering; Clear/new session → bridge.reset() clears slots.

Cloud ops path

N/A — no Production mutate. New GET /api/harness/status is read-only against
the resolved bind; no schema/DB/env/deploy change. No GHA; no workflow_dispatch.
(Note the git-probe route reuses existing DI-bound sandbox clients — no new
runner/env.)

Living docs plan

Surface Change Notes
docs/feature-divide.md Add Workspace status bar row to the ownership table + a Status bar note in Data flow Wasm primary, host mirror only; no dual status panel
docs/harness-limits.md Document status slots, slot-priority order on narrow canvases (primary controls never dropped), git-probe cadence/rates, usage-source rule timeless; no phase/issue theater
docs/sandbox.md Document the read-only git probe route + server-side rate limit (per-instance best-effort) server fact
docs/session-model.md Note status reseeds from envelope meta.{logicalCwd,activeSandboxId} + /api/sandboxes on restore + bridge v13
docs/agent-stream.md Add the usage summary on done/JSON result when phase 3 ships
AGENTS.md Ownership-table rows: status bar chrome, git probe route, usage plumb; bump bridge-protocol mention to v13 + status seam per-surface
README.md No change (visitor entry unchanged) N/A

Implementation order

Per-phase branches off main, each a non-merged PR, landed in order:

  1. Phase 1 — status bar chrome + sandbox/cwd slots + additive bridge v13 + host fold + docs.
  2. Phase 2 — server git-probe route + rate limit + host cadence + git slot + docs.
  3. Phase 3 — usage capture + summary carrier + context slot (implements harness: show live context size from model usage (not client math) #327) + docs.

Each phase builds on the prior (phase 2/3 depend on phase 1's slot bridge).

Testing

# Case Layer Type Command / method
1 Status slots serialize/deserialize over bridge v13 (additive) Wasm+DOM integration lib/harnessBridge.test.ts round-trip; wasm-int loads v13
2 Slot priority ordering at ~390px Wasm operator new-harness.wasm smoke
3 Cwd/activeSandbox fold populates slots after turn DOM unit lib/harnessChat.test.ts
4 Bridge reset on Clear/New clears slots DOM+Wasm unit/integration host test + wasm-int
5 Git probe: branch+SHA parsed; dirty flag; non-repo / dead bind → empty server unit lib/agent/statusProbe.test.ts (store-fakes)
6 Git probe rate-limited server-side (per-instance best-effort; cached value; never exec when limited) server unit statusProbe test (clock fake)
7 /api/harness/status auth edge + envelope read + resolve; envelope beats ?sandboxId= carry server integration app/api/harness/status/route.test.ts
8 Usage summary captured on chat + agent JSON + stream done server unit runAgent, chatServer, agentStream, agentApi tests
9 Missing usage → slot hidden/estimated, never fake total; no denominator without catalog max-context DOM unit host test
10 No secrets on wire (probe returns no base_url/token) server unit statusProbe + route test
11 Minimum locked: full vitest + typecheck + build + wasm-int (fails closed) per phase all gate npm test (= di-gate + vitest run directly), npm run typecheck, npm run build, build-harness on runner

Definition of done

  • Wasm status bar persists across turns / restore; no secrets; mobile usable
  • Additive bridge v13 (old fields intact); both sides in sync
  • Git probe route is read-only, user+session-gated, rate-limited; envelope-authoritative bind; fail-soft empty
  • Context slot uses provider usage (or hides/estimates) — harness: show live context size from model usage (not client math) #327 implemented
  • Tests green in agent workspace/CI (full vitest, typecheck, build, build-harness); di-gate clean
  • Cloud ops: explicit N/A stated (no Production mutate, no GHA)
  • Living docs: listed surfaces updated (feature-divide, harness-limits, sandbox, session-model, agent-stream, AGENTS.md; README N/A)
  • Palette-only colors, EMBER reserved for errors; no dual DOM status panel

Risks & mitigations

Risk Mitigation
Bridge v13 drift (host old ≠ wasm new) Additive-only; both-sides guard + wasm-int fails closed
Git probe rate unlimited → hammer sandbox Server-side STATUS_PROBE_MIN_INTERVAL_MS cap (per-instance best-effort) + host cadence as primary throttle (see Caps)
Probe hits the wrong bind via stale host param Envelope meta.activeSandboxId wins; ?sandboxId= is a Redis-safe carry only
Usage missing (provider returns none) Hide or estimated-label; never fake total
A single git probe error blanks the bar Per-slot fail-soft; one bad probe never clears other slots
Primary header controls crowded at ~390px Status slots yield before lifecycle/model/Next; those are never in the drop pool
Laptop-only ops creep in No prod mutate → Cloud ops N/A; docs teach Actions/browser/cloud paths
Reusability regression Reuses session/sandbox/env seams; no host URL/account hardcoding

Caps table

Cap / ceiling Value Rationale Code location
STATUS_PROBE_MIN_INTERVAL_MS (NEW) 2000 server-side git-probe min-interval so a refresh loop/stale tab cannot exec hammer the sandbox; generous vs real cadence; per-instance best-effort on serverless lib/sessionCloudCaps.ts
STATUS_SLOT_MAX_BYTES (NEW) 96 per-slot display cap (cwd path / label) — truncate with ; far under argv/bridge ceilings lib/sessionCloudCaps.ts + bridge.zig
STATUS_GIT_PROBE_OUT_MAX_BYTES (NEW) 512 bounded git-probe stdout (branch + SHA + porcelain dirty); fail-soft on overrun lib/agent/statusProbe.ts
USAGE_SUMMARY_MAX_BYTES (NEW) 96 bounded usage summary JSON carrier (prompt/completion/total/cached) — stays under the stream event / JSON-result payload ceilings lib/agent/agentStream.ts + lib/agentApi.ts

All caps are NEW (not changes to existing caps), generous by default vs the
transport ceiling of their carriers (bridge state, argv, Runtime.request body/SSE
event). No change to any existing cap → no human cap-approval gate for this
plan.

Open questions

None — all in-scope decisions locked above. (#328's only non-goal — operator-triggered
sandbox switch — remains covered by the shipped meta.sandbox_switch, not this bar.)

Phase issues

Phase map

Phase Issue Deliverable Depends on
1 #541 Status bar chrome + sandbox/cwd slots + additive bridge v13 + host fold
2 #540 Server read-only git-probe route + rate limit + git slot Phase 1
3 #539 Usage capture + context slot (implements #327) Phase 1

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions