Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ invincible/
| Cloud multi-device harness session (Redis multi-session, `/api/sessions*`, hybrid local+cloud; **phase 0 #515 envelope + Blob transcript carrier**) | `app/api/sessions/*` (+ `app/api/sessions/[id]/envelope/*`, `[id]/transcript/*`), `lib/sessionRepository.ts`, `lib/sessionCloudCaps.ts`, `lib/sessions/*` (+ `lib/sessions/blobStore.ts`, `blobStores.ts`), `lib/tenancy/harnessSessionsRedis.ts`, `lib/tenancy/harnessSessions.ts` (archive read + shared validator), `lib/di/index.ts` (root), `app/harness/HarnessHost.tsx`, `middleware.ts`, [docs/session-model.md](docs/session-model.md), [docs/bring-your-own.md](docs/bring-your-own.md), [SECURITY.md](SECURITY.md) — one-shot Postgres→Redis backfill: GHA **`sessions-redis-backfill`** (idempotent per-user marker); Postgres `harness_sessions` is a read-only archive. P1/GAP-1 (#452): session-carrier `meta.{logicalCwd,activeSandboxId}` folds into the PUT body and restores on pull/adopt; **plan #616 (source #610)** adds the reserved `meta.selectedModel` session carrier for the selected model pick (restore by id after the model catalog push; server **drops a poisoned value to unset**, never a 400). **Phase 0 (#515):** the transcript lives in **Vercel Blob** (`BLOB_READ_WRITE_TOKEN` / BYO S3-R2 seam) pointed to by `meta.transcriptPointer` on the small Redis envelope (`harness:envelope:*`); server mints short-lived scoped upload URLs for **client→Blob** uploads; legacy full-record GET stays for roll-forward while old blobs stay small. Envelope upsert/read: `PUT`/`GET /api/sessions/:id/envelope`; mint/read: `POST`/`GET /api/sessions/:id/transcript` |

| Harness stream chrome (Thinking collapse/caps, live tools) | `lib/harnessChat.ts`, `native/harness/src/ui.zig` (facade + `frame`; transcript band owner), `native/harness/src/ui/thinking.zig` (Thinking kind), `native/harness/src/transcript_split.zig` (collapsible left rail + session list in the transcript band), `native/harness/src/session_catalog.zig` (v17 catalog + pending switch), protocol **v20** in `lib/harnessBridge.ts` (Stop cancel v9; Thinking kind v8; tool-run kind 6 v10→live paint v11; skill-attach kind 7 v12; **status-slot store v13**; **turn-clock feed v14**; **v14 addendum** `inv_set_busy_tick`; **v16** model persist; **v17** session-rail catalog + pending switch; **v18** `inv_queued_count` submit-queue depth; **v19** `inv_set_queue_promote_allowed` — host arms a one-shot per-terminal scalar so a Stop/Esc/error/timeout Ready **never drains the queue**; only idle ▶ / Ctrl+Enter with an empty composer + non-empty queue promotes, plan #760; **v20** `inv_queued_insert_front` — **turn retry that never drains the queue** (plan #759): the host retries a retryable agent-turn error up to `TURN_RETRY_ATTEMPTS`=5 (NEW cap) with bounded backoff via the additive `classify` seam (`lib/sandbox/resilience.ts`), then gives up onto `Lifecycle.Error` (a failed turn is never terminal for the Wasm promote gate — `ui.zig` promotes only on successful Ready), inserting `Continue the current turn` at the queue head (`inv_queued_insert_front`) when non-empty; permanent `PERMANENT_TURN_STATUS` statuses (400/401/403/404/413/422) give up after one attempt (no loop); 408/429/5xx and timeout/empty retry up to 5 attempts — but **1 attempt** once the live stream has painted a ring row past the user line (fail-closed: replaying would re-run tools / duplicate bubbles). **In-canvas Pause (submit-queue hold):** a Wasm-internal `queue_paused` latch folded into `submit_queue.canPromote` (via `bridge.tryPromoteQueued`, the single promote seam) holds **every promote path** — auto-promote on successful Ready and idle empty-▶ / empty Ctrl+Enter Play — so the next turn reads from the composer; typed send + FIFO contents + enqueue/edit/remove/Clear unaffected; **auto-clears when the FIFO empties**; TEAL `· paused` toggle on the queue-band header (`n>0`); **no new export / no protocol bump / no cap change** (Wasm-ephemeral like the queue) |
| Keyboard shortcuts (keymap, leader, help overlay) | `native/harness/src/keymap.zig` (single chord table + reserved-browser deny-list + leader machine; **NEW caps** `KEYMAP_MAX`=64, `LEADER_WINDOW_MS`=800), `native/harness/src/ui/keymap_dispatch.zig` (one per-frame walk of `dvui.events()`, handled-marking, leader dvui-timer arm/expiry), `native/harness/src/ui/help_overlay.zig` (in-canvas TEAL panel), wired in `native/harness/src/ui.zig` (dispatch before textEntry; overlay paint) + `ui/queue_band.zig` (scan removed; `queue_save`/`cancel_queue_edit` routed via dispatcher). **DOM adds no keyboard UI / `window` keydown / React cheatsheet** |
| Keyboard shortcuts (keymap, leader, help overlay) | `native/harness/src/keymap.zig` (single chord table + reserved-browser deny-list + leader machine; **NEW caps** `KEYMAP_MAX`=64, `LEADER_WINDOW_MS`=800), `native/harness/src/ui/keymap_dispatch.zig` (one per-frame walk of `dvui.events()`, handled-marking, leader dvui-timer arm/expiry), `native/harness/src/ui/help_overlay.zig` (modal `floatingWindow` **wide two-column table** over the transcript band — fixed chord column + remaining-width help column; wheel/trackpad scrolls the list **inside** the panel, never the transcript; a backdrop click-outside closes it; every looping widget uses a loop-unique `id_extra`, no duplicate-id red outlines), `native/harness/src/ui/metrics.zig` (help-overlay size = band fractions `HELP_OVERLAY_W_FRACTION`/`H_FRACTION` + `_MIN_*`/`_FLOOR_*` floors + `HELP_OVERLAY_CHORD_COL_W`; the fixed 460×320 `HELP_OVERLAY_W/H` cap is retired), wired in `native/harness/src/ui.zig` (dispatch before textEntry; overlay paint) + `ui/queue_band.zig` (scan removed; `queue_save`/`cancel_queue_edit` routed via dispatcher). **DOM adds no keyboard UI / `window` keydown / React cheatsheet** |
| Workspace status bar (protocol v13 status-slot store; bridge overall **v14** — plan #538/#541 + Phase 2 git #540 + Phase 3 context/usage #539, **two-line bottom status bar under the composer — #554/#555/#570**) | `native/harness/src/{bridge,ui,model_picker,model_catalog}.zig` (status-slot store + two-line 64 px bar directly **below the composer**: **line 1** = identity (spinner · `h:{build-id}` · model menu) relocated from the deleted header band, **line 2** = `paintStatusSlots` right-aligned slot pack — header merged by plan #570; each line has explicit 32 px height so the model picker (`PICKER_TRIGGER_H`=32) fits and slots never clip; sandbox/cwd/git + context/usage slots — context painted generically via `STATUS_SLOT_DROP_ORDER`), `lib/harnessBridge.ts` (`StatusSlot`, `setStatusSlot`/`getStatusSlot`/`clearStatusSlot`/`clearStatusSlots`, `STATUS_SLOT_MAX_BYTES` mirror), `lib/harnessChat.ts` (`foldStatusSlots` — folds `activeSandboxId` + `cwd` + **context/usage** (`formatUsageSummary`, re-sanitized on read) after hydrate, after **every** agent turn — success **and** fail (403-clear / committed `change_dir` repaint the pack — PR #543), and **live mid-turn on tool results** (Phase 2 #627 / #625: a confirmed `change_dir` or successful `meta_sandbox_switch` repaints sandbox/cwd immediately, plus the host persists via `onSessionPatch`); context default **hidden** on missing usage, abort/cancel carries the prior honest value forward; host-ellipsized to the byte cap before the wire; `refreshGitStatusSlot` — host polls the read-only `GET /api/harness/status` probe on a ~10 s cadence **and** on-demand after a successful `exec` or `meta_sandbox_switch` mid-turn (not only the cadence), fail-soft keeps the last git value on transient error/429), `app/harness/HarnessHost.tsx` (Clear/New clears the pack; wires the git cadence + `onSessionPatch` → persist), `app/api/harness/status/route.ts` (read-only git probe: envelope-authoritative bind (`meta.activeSandboxId` wins over Redis-safe `?sandboxId=` carry), `resolveSandbox` → bounded argv-only read-only git at the bind workspace root via `lib/agent/statusProbe.ts`, per-instance rate cap `STATUS_PROBE_MIN_INTERVAL_MS`; middleware matcher + in-route `requireSessionUser` dual gate; never mutates a session/envelope — no Production write), `lib/agent/statusProbe.ts` (`STATUS_GIT_PROBE_OUT_MAX_BYTES`=512, fail-soft `{}`), `lib/sessionCloudCaps.ts` (`STATUS_SLOT_MAX_BYTES` = 96 + `STATUS_PROBE_MIN_INTERVAL_MS` = 2000 — client-safe single sources), **context/usage carrier:** `lib/agent/usageSummary.ts` (bounded provider-usage mapper `mapProviderUsage` / read-side `sanitizeUsageSummary` / host `formatUsageSummary`, `USAGE_SUMMARY_MAX_BYTES` = 96 — NEW cap), emitted **live mid-stream** from `finish` parts (aggregate only — never `finish-step` per-step counts) in `lib/agent/agentStream.ts` (SSE `usage` event), reconciled at the final `done.usage` / JSON result / chat result in `lib/agent/runAgent.ts` (+ `app/api/chat/route.ts`), parsed by `lib/agentApi.ts` / `lib/chatApi.ts`, mirrored on `SessionSnapshot.usage` (`lib/sessionStore.ts`; reserved cloud `meta.usage` JSON string, drop-to-unset on poison), docs: [docs/feature-divide.md](docs/feature-divide.md), [docs/harness-limits.md](docs/harness-limits.md), [docs/agent-stream.md](docs/agent-stream.md), [docs/session-model.md](docs/session-model.md) | |
| Tool-run aggregation + expandable transcript control (#325) | `lib/agent/agentStream.ts` (backend `tool_result.preview` — bounded/redacted L2 detail), `lib/toolRun.ts` (encode/decode, host aggregation, `meaningfulDetail` preview→`detail`, `mergeToolRunPayloads`/`encodeToolRunPayload` hydrate coalesce), `lib/harnessChat.ts` (stream/JSON aggregation → kind 6 `tool_run`, **live-painted**: a tool event opens/grows ONE card immediately via `update_last` — grouping keys off the host's `lastRingRowIsToolRun` flag, the only ring writer: grow iff the last ring row is a tool-run, else a NEW card at `1`; a thinking/assistant/user/error row last is a separator; commit-once is removed; reload coalescing of consecutive `tool_run` rows via `coalesceToolRunMessages` in `pushSessionToBridge`), `lib/sessionStore.ts` role `tool_run`, `native/harness/src/rich/toolrun.zig` (decode), `native/harness/src/ui/toolrun.zig` (`paintToolRun` — **headerless**: no `tools` kind band; 📋 copy on the header row; status glyphs as the single channel from embedded faces, `✓`/`✗` DejaVu symbols + `…` Noto; L2 preview in Vera Sans Mono for command/output tools **or any multi-line detail**, body otherwise; short single-line results → static label, no blank expander), `native/harness/src/bridge.zig` + `lib/harnessBridge.ts` (protocol **v11**; additive test-only ring readback `inv_message_*_at`), protocol **v11**; expand state + stick-to-bottom reuse dvui `reorder_tree.zig` / `scrolling.zig` idioms |

Expand Down
23 changes: 22 additions & 1 deletion docs/harness-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,34 @@ closed).
| **Ctrl+Enter** / **Cmd+Enter** (queue-row editor) | **Save** the queued-item edit |
| **↑** / **↓** (composer focused) | **↑** on an empty composer loads the newest user message; further ↑ walk older user rows. **↓** walks forward; ↓ past the newest restores the in-progress draft. Only user messages appear (assistant/thinking/tool/system/error/skill rows never load). Works while Busy. Ring window only (Load earlier for prompts that have aged out) |
| **Escape** | Cancel the in-progress turn when Busy (same as ■ Stop); **dismisses a queue-row editor first** (does not abort the turn); **closes the help overlay** (wins over busy cancel); **disarms the leader**. A Busy cancel **consumes nothing** — the submit queue stays exactly as-is (no promote on the Stop terminal; only an explicit idle ▶ / Ctrl+Enter with an empty composer + non-empty queue starts the next item). Idle: no-op (textEntry / dvui menus keep it) |
| **Ctrl+**/**Cmd+/** | Toggle the in-canvas **help overlay** (TEAL panel over the transcript band) |
| **Ctrl+**/**Cmd+/** | Toggle the in-canvas **help overlay** — a modal **wide two-column table** over the transcript band. See **Help overlay** below |
| **Ctrl+I** | Arm the **leader** prefix (800 ms). Within the window press **`?`** to toggle help; **Ctrl+I** re-arms (a bare `i` does **not** — it swallows and disarms); **`t`** toggles **thinking default-collapsed** (collapse thinking by default, including the active Busy turn; in-memory only, resets to on on reload/New/Clear); **Escape** cancels; an unmatched key swallows (never lands in the prompt); a reserved browser chord (Ctrl+Shift+I Inspect) still yields to the browser |
| Tab / Ctrl+Left / Ctrl+Right | DOM nav / text caret (not harness chords; Ctrl+Left/Right are word-jumps, left to the textEntry) |
| **Shift+click** (composer / queue-row editor focused; desktop only) | **Range-selects** the text in that `textEntry` field — a stock web behavior the canvas was missing. Click to place the caret (or drag-select a base range), then **Shift+click** to extend the selection from an anchor edge to the clicked point; further Shift+clicks keep extending from the same anchor edge, and a Shift+click never counts toward the next word/line double-click. The range uses the **existing TEAL `text_select` fill** (no new hex). **Ctrl/Cmd+C** then copies that substring to the system clipboard (the chord stays browser-reserved). A **plain** (non-shift) click still moves the caret and clears the range. Touch/mobile has no shift key — **📋** stays the reliable copy path and canvas Shift+click is documented desktop-only |
| **Shift+click** (read-only body text; desktop only) | **Range-selects** text inside a single **read-only** `textLayout` (transcript rich-MD bodies, fenced code, thinking/preview/skill rows, tool-run detail): a plain click anchors, **Shift+click** extends the highlighted range from that anchor, and **Ctrl/Cmd+C** copies the active slice. **A link is still a link first** — a **Shift+click (or plain click) on a link inside a body navigates / copies per the **Links** row and does *not* extend the range**; Shift+click range-selection is for non-link body text (use 📋 for reliable body copy). Selection never spans two `textLayout`s / messages. See Transcript copy / paste → Read-only body text Shift+click |
| Composer focus | Requested on ready and after each send |

### Help overlay

Opened by **Ctrl+/**/**Cmd+/** (or **Ctrl+I** leader then `?`); closed by **Esc**
(wins over busy cancel), **Ctrl+/**/**Cmd+/** (toggle), New/Clear, or a **backdrop
click-outside** (it is a modal). It is a **modal in-canvas `floatingWindow`**
subwindow that fills most of the transcript band (`HELP_OVERLAY_W_FRACTION` /
`HELP_OVERLAY_H_FRACTION` + `_MIN_*` / `_FLOOR_*` floors; the old fixed 460×320
`HELP_OVERLAY_W/H` cap is retired). It is a real **two-column table**: a fixed
chord column (`HELP_OVERLAY_CHORD_COL_W`) plus a remaining-width help column, so
every chord and every help string starts on a stable x. One row per distinct
action in `KEY_TABLE` order; context-off rows stay grey (**WARM-muted**, never
EMBER). **Wheel / trackpad over the panel scrolls the help list** inside the
panel (`ctx_scroll`) — the modal captures it, so the transcript's
`state.transcript_scroll` never moves while it is open. Every looping widget uses
a loop-unique `id_extra` (no duplicate-id red outlines). **Reopening resumes at
the top**: `ctx_scroll` resets on every close path (Esc, backdrop, toggle, and
New/Clear). Help copy wraps to the leftover column width (no per-row height cap,
so a long help string on a ~390 px band is *shown wrapped*, never clipped/one
line). ~390 px stays on-canvas with internal scroll (matches the repo no-h-scroll
policy).

## Touch / mobile (~390px)

| Topic | Behavior |
Expand Down
8 changes: 6 additions & 2 deletions native/harness/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,12 @@ pub fn build(b: *std.Build) void {
// against stale "Space" copy. `rowChord` is a hardcoded parallel switch —
// not derived from the keymap table — so without a test a revert to the
// pre-#761 "Leader Space" strings would ship while keymap.zig tests stay
// green. help_overlay imports dvui (via mixed_text), so wire dvui_testing
// (no frame, and no web-backend: the module's imports stop at
// green. Since #781 the suite also drives the overlay modal `floatingWindow`
// through the dvui TESTING backend (`dvui.testing.init/.settle/.step/paint`)
// to lock the wide two-column table, wheel-stays-in-panel, backdrop-close,
// and key-through-modal behavior — the frame mounts a transcript stand-in
// exactly as ui.zig does. help_overlay imports dvui (via mixed_text), so
// wire dvui_testing (still no web-backend: the module's imports stop at
// mixed_text/unicode_face, which are bridge-free).
{
const help_overlay_tests = b.addTest(.{
Expand Down
Loading
Loading