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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +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 **v18** 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) |
| 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** |
| 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
1 change: 1 addition & 0 deletions docs/feature-divide.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ optional login chrome).
| Busy / error presentation for turns | **Wasm** | EMBER for errors |
| Whole-turn `mm:ss` clock (Busy) | **Wasm** (busy row) fed by the **DOM** host | The host owns the only reliable wall-clock (no WASI clock in Wasm) and ticks it ~1 Hz, pushing the elapsed seconds into the Wasm busy row via protocol **v14** `inv_set_turn_elapsed` (plan #567). The canvas appends `Waiting for model… · 0:42` in-canvas while a turn runs; reset to 0 on Ready/Stop/error so no `0:00` lingers. Composer/Stop stay **Wasm** |
| 2×4 busy spinner (plan #574) | **Wasm paint** fed by the **DOM** host | **Wasm** paints a 2×4 WARM rectangle grid left of `Waiting for model…` (clockwise pulse; pure LUT `busy_spinner.zig`, zero I/O/alloc in the frame path). **DOM host** drives the pulse phase on the same Busy ticker at **`HARNESS_BUSY_TICK_HZ` = 10 Hz** (`HarnessBridge.setBusyTick` → additive `inv_set_busy_tick`; the v14 `mm:ss` clock is fed every 10th tick ≈ 1 Hz). **Reduced motion** (read fresh at each busy start): the host skips only the per-tick pulse push, grid static at phase 0 — the `mm:ss` **clock keeps ticking** (no reduced-motion clock regression). Idle/Stop/error clears both to 0. Old host + new Wasm degrades to a static grid (busy_tick stays 0) |
| Keyboard (keymap table, leader, help overlay) | **Wasm** | All chords in `native/harness/src/keymap.zig` + one dispatcher `ui/keymap_dispatch.zig`; in-canvas help panel `ui/help_overlay.zig`. **DOM adds no shortcut UI / React cheatsheet / `window` keydown** |
| Empty / onboarding copy for agent | **Wasm** | |
| Asteronica canvas theme | **Wasm** | `palette.zig` |
| Frame loop / WebGL | **Wasm** | dvui |
Expand Down
16 changes: 14 additions & 2 deletions docs/harness-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,25 @@ click/tap scrolls the transcript so the message is back in view near the top.

## Keyboard & focus

All harness chords are rows of one keymap table
(`native/harness/src/keymap.zig`) and every key event is dispatched by a single
walk of `dvui.events()` before the composer textEntry is built
(`native/harness/src/ui/keymap_dispatch.zig`). `ui.zig` and `queue_band.zig`
never scan keys inline. A **reserved browser chord** (new tab, reload,
find/devtools, copy/paste, back/forward, tab-switch) is **never marked handled**
— the canvas yields it to the browser, even if a row name overlaps (fail
closed).

| Chord | Action |
|-------|--------|
| **Enter** (composer focused) | Insert a newline (composer is multi-line) |
| **Ctrl+Enter** / **Cmd+Enter** (composer focused) | Send prompt when idle; **enqueue** a follow-up when Busy |
| **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** (harness focused, Busy) | Cancel the in-progress turn (same as ■ Stop). Queue-row editor: dismisses editor first (does not abort turn). Idle: no-op |
| Tab | DOM nav / Clear (canvas uses pointer + dvui focus) |
| **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**. Idle: no-op (textEntry / dvui menus keep it) |
| **Ctrl+**/**Cmd+/** | Toggle the in-canvas **help overlay** (TEAL panel over the transcript band) |
| **Ctrl+Shift+Space** | Arm the **leader** prefix (800 ms). Within the window press **`?`** to toggle help; **Escape** cancels; an unmatched key swallows (never lands in the prompt); a reserved browser chord 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) |
| Composer focus | Requested on ready and after each send |

## Touch / mobile (~390px)
Expand Down
14 changes: 14 additions & 0 deletions native/harness/build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,20 @@ pub fn build(b: *std.Build) void {
test_rich.dependOn(&b.addRunArtifact(composer_history_tests).step);
}

// Host unit tests for keymap.zig (plan #741): the single chord table +
// reserved-browser deny-list + leader machine. Pure, no dvui, no bridge.
{
const keymap_tests = b.addTest(.{
.name = "keymap",
.root_module = b.createModule(.{
.root_source_file = b.path("src/keymap.zig"),
.target = host_target,
.optimize = optimize,
}),
});
test_rich.dependOn(&b.addRunArtifact(keymap_tests).step);
}

// Host unit tests for cwd_slot.zig (plan #579, adversarial review #584
// Minor L6): the "."-hidden predicate. Pure, no dvui.
{
Expand Down
Loading
Loading