Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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