Device-local tabs + spaces as a searchable sidebar filter - #14
Closed
wingleeio wants to merge 10 commits into
Closed
Device-local tabs + spaces as a searchable sidebar filter#14wingleeio wants to merge 10 commits into
wingleeio wants to merge 10 commits into
Conversation
Contributor
Author
Tabs detach from session existence: a tab is a device-local viewport onto a synced session (UiSettings.openTabs, the vec is the order). Closing a tab is local-only — the session keeps running and stays in the sidebar, where a click reopens it; archiving stays an explicit sidebar-row action. The strip goes cross-space and gains a context menu (close / close others / close left / right). Old files migrate by seeding openTabs from the last space's sessions. Spaces stop being a navigation spine: the sidebar section becomes a searchable dropdown (ref-picker recipe) with "All spaces", filtering only the session list and hosting space management. The new-session canvas gains a space picker chip defaulting to the sidebar filter, else the last selected space; picking a space re-keys the ref selector and harness/model catalogs through the existing selected_space observer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The label now flexes, so long space names ellipsize against the trigger width instead of pushing the caret out of the row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wingleeio
force-pushed
the
tabs-spaces-restructure
branch
from
August 4, 2026 08:44
d9f7abf to
de7f886
Compare
anchored_menu pins to the trigger's top-left, which covered the button. New anchored_menu_below primitive pins the floating layer to the trigger's bottom edge — a dropdown proper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
The selected row's wash (menu_row active styling) is the selection signal; the trailing check fought the @device tag for the row end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
The selected row's wash (menu_row active styling) is the selection signal everywhere — ref/checkout/model pickers, the canvas space picker, and the account device menu — matching the space-filter dropdown. menu_check retires with its last caller. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
…-down Dropdowns/menus previously vanished the frame their state dropped. Every menu state is now a popover::Popup<T> with an exit phase: close paths funnel through a begin_close that keeps the card mounted ~100ms playing menu-out (fade + 2px lift, interactivity deadened), then a reap timer drops the state. The exit progress reads the Popup's closing instant at render time rather than with_animation's element-state clock — that clock replays from 0 on a missed frame, which flashed the dying menu back to full opacity. The frosted backdrop blur ignores element_opacity, so its radius rides the same progress down to zero instead of popping off at unmount. Also: clicking an open trigger now just closes its menu (the outside- click dismissal no longer immediately reopens via the same click), the same just-dismissed guard the user menu already carried. Converted: spaces filter dropdown, user menu, session/tab/space context menus, composer pickers, settings device menu. The @-mention completion stays instant on purpose (keystroke-driven dismissal would read as lag). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The host device was only visible inside the space pickers' rows. The sidebar filter trigger now shows it right after the space name (caret stays at the right edge), and the composer's space chip shows it as the muted suffix — amber "· offline" when the host's heartbeat is stale, same as the rows. Formatting shared via AppState::space_device_tag. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both the sidebar filter dropdown and the composer space picker listed spaces in sync-arrival order. They now share AppState::spaces_sorted (case-insensitive by display name, id tie-break): search still ranks prefix matches first — alphabetical within each rank — and the picker's opening highlight indexes the sorted order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Author
davidhs26
added a commit
to davidhs26/comet
that referenced
this pull request
Aug 21, 2026
* linux: draw caption controls under client-side decorations
On Linux the window had no minimize/maximize/close at all wherever the
compositor ends up in client-side-decoration mode (GNOME/Mutter has no
SSD support; gpui's WindowControlArea hit-testing is Windows-only, so
the Windows caption cluster never applied). Meanwhile leaving
window_decorations unset requested SERVER decorations, which stacked a
compositor titlebar on top of zeron's own chrome under sway/labwc.
- Request client decorations on Linux (WindowOptions.window_decorations)
to match the unified-titlebar design; the compositor can still
override via xdg-decoration and the shell re-resolves every frame.
- Draw zeron-styled caption buttons (24px cluster rhythm, hand-drawn
linear glyphs) with explicit handlers - minimize_window / zoom_window /
remove_window - since the native control-area hit-test is inert on
Linux. Mounted at the root like the Windows cluster so they survive
gates and the splash.
- Follow the desktop's button layout (GNOME button-layout via
cx.button_layout, observe_button_layout_changed): right side by
default; "close:..." layouts put buttons on the left and the app
cluster shifts past them at the shared 2px rhythm. Controls the
compositor can't do (per wm_capabilities) drop out; close always
stays. Server decorations draw nothing - no double controls.
- Reserve titlebar right padding for the caption strip (tabs strip,
changes-pane header, settings bar) via titlebar_right_pad.
- Give gate/splash pages a real drag strip on Linux/Windows
(titlebar_drag_region - the WindowControlArea::Drag surface alone is
dead on Linux).
Rig-verified under labwc (CSD: all three buttons work - minimize
unmaps, zoom maximizes and flips to the restore glyph, close exits;
close hover shows the red wash) and sway (SSD: no double controls);
left-side layout verified end-to-end through xdg-desktop-portal-gtk
with button-layout "close:".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(proto): model checkout change request status
* feat(engine): resolve GitHub pull requests with gh
* feat(engine): stream cached checkout change request status
* feat(ui): track pull requests for local and remote sessions
* feat(ui): show pull request badges in sidebar and composer
* feat(ios): support streaming device RPC
* feat(ios): show host pull requests in session surfaces
* test: cover pull request status across device boundaries
* fix(ui): frost pull request tooltip
* fix(engine): resolve gh from login shell path
* fix(engine): preserve links on unsupported streams
* fix(ui): retry pull request watch after host upgrade
* fix(engine): use origin for untracked branches
* fix(engine): resolve uncached remote default branch
* fix(engine): evict inactive change request cache entries
* test(engine): isolate login shell gh path fixture
* github: add FUNDING.yml so the repo shows a Sponsor button
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat: adopt external pi sessions into workspace (ID01-420) (#1)
* feat: adopt external pi sessions into workspace (ID01-420)
Add pi_adopt module that discovers pi harness sessions created outside
the Zeron engine (CLI runs, pi-worker) and surfaces them as regular
workspace chats so every client (iOS included) can list and open them.
Discovery reads two sources:
- pi-acp session map (~/.pi/pi-acp/session-map.json)
- pi agent session store (~/.pi/agent/sessions/*/*.jsonl) as fallback
Each session becomes a Chat row with deterministic UUIDv5 id, resolved
space (created if needed), and idle Session row. Rows are idempotent —
re-scans never duplicate or rewrite existing rows.
Feature flag: ZERON_ADOPT_PI_SESSIONS=1 (off by default). With the flag
absent, the binary behaves identically to upstream.
Includes integration tests (2 passing) and proper CancellationToken-based
shutdown.
* docs: notas de implementación y verificación de pi_adopt
* fix(pi-adopt): post-review fixes — skip adoption without space, downgrade map warn to debug
- If resolve_space returns None (create_space transient error), skip
adoption for this scan instead of writing an invisible chat row
- Downgrade recurring session-map warning to debug (45s spam)
- Update NOTAS with adversarial review findings
* fix(pi-adopt): don't guess a dangling space_id after create_space
Review (kimi-coding/k3): if create_space no-ops on an existing
(device,path) and the re-read fails, the derived UUIDv5 would
dangle and SpacesSync's orphan sweep would delete the chat.
Also: run the scan in spawn_blocking, log skip reasons at debug,
and document the spec-5 warning→debug deviation.
---------
Co-authored-by: David <davidhsutton@gmail.com>
* ci: fork gate (check + engine tests); upstream deploy/release manual-only
Los workflows heredados apuntan a la infra de zeron.sh (R2/releases) y no
aplican al fork; quedan en workflow_dispatch. El gate del fork es ci.yml:
cargo check --workspace + tests del engine. (ID01-424)
* landing: add nine new tweets to the marquee
Five to the top row, four to the reverse row, avatars mirrored into
assets/tweets. Marquee durations scaled (72s->117s, 80s->126s) so the
longer tracks keep the same scroll speed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* harness: native Claude driver over stream-json (A1)
Resurrect the pre-ACP claude driver (ca05336^) and modernize it against
CLI 2.1.228, replacing the claude-agent-acp adapter for HarnessId::ClaudeCode:
- Permissions ride the stdio control channel (--permission-prompt-tool
stdio, validated live: can_use_tool requests arrive and allow/deny
responses are honored). Tools auto-allow (unattended parity);
AskUserQuestion round-trips through the engine input bridge.
- Done is the CLI's own result frame, eagerly — background subagents
never hold the turn. The CLI's native wake turn (second init on the
same session id + second result, live-verified) flows through the same
stream; the engine's existing parked-session resume path turns it into
the done→Working→done wake with no engine changes.
- Subagent frames (top-level parent_tool_use_id) are wrapped in the new
AgentEvent::Subagent variant instead of dropped — never folded into
the parent feed (they'd split contiguous text around phantom tool
calls, and their message boundaries must not rotate the parent's
assistant message id). The parent doc fold ignores them; routing to
per-subagent docs lands with the subagent-visualization commit.
- --thinking-display summarized (newer models emit no readable thinking
otherwise) alongside --verbose/--include-partial-messages.
Tests: fake-CLI scenarios (init dedupe, tagging, eager-done+wake, ask-
user round-trip, steering, interrupt escalation, error mapping), a
verbatim live-captured 2.1.228 background-subagent frame replay, and an
ignored live-CLI smoke.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* net: cap mobile warm dials, overlap checkpoint with backfill, flush sends at state
Three cold-open wire cuts for thin links (450kbps target):
- iOS caps background room dials at 8 (attention order). All 46 sessions
still hydrate from disk instantly; sidebar status rides the registry
room; an undialed chat connects the moment it's opened. Foreground and
path-recovery kick sweeps skip dial-held stores — no more 46-socket
stampede on every flap.
- Both clients send the rows request BEFORE the checkpoint HTTP fetch and
stream the backfill in parallel with the download, buffering rows until
the import lands (order pinned by test). The download used to push the
whole join back by its own duration.
- Both clients flush pending pushes right after the state answer instead
of after full convergence — a message typed offline now lands ~2 RTTs
after the socket does, not after checkpoint + backfill (server batchId
dedupe makes replays no-ops; deadlock-shaped regression test).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ios: checkpoint downloads survive redials, registry single-dial, registry-first dial gating
On-device NLC (Edge profile) round 2 fixes:
- ChatRoomClient: one checkpoint download per chat that OUTLIVES socket
generations (a saturated link trips the silence lease mid-download —
pongs queue behind the blob — and the redial used to discard the bytes
and start over, looping a fresh-chat open forever). Partial downloads
Range-resume across attempts; download progress defers the silence
lease (120s backfill deadline stays as the backstop); a completed blob
applies even if its socket died (CRDT import + max-cursor are socket-
independent), so the next session joins RowsOnly.
- RegistryClient.kick: don't redial while a handshake is in flight —
foregrounded() fires on scene activation at launch and killed the dial
mid-TLS every cold open, doubling the spinner (6.9s → measured single
dial 4.5s on Edge). Mirrors ChatRoomClient's existing guard.
- AppModel.preloadSessions: warm chat dials wait for the registry to
connect (10s cap) — the sidebar's own handshake+state gets the thin
pipe to itself; open views still dial instantly via releaseDial.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* sync: pull-first HTTPS transport — 1-RTT bootstrap, WS demoted to enhancement
The airplane-wifi fix. The WS needs 4+ serial round trips (DNS, TCP, TLS,
upgrade, hello) before byte one, and hostile middleboxes strip the upgrade
entirely — the user-reported 'never connects on the plane'. A plain HTTPS
GET is 1 RTT on a warm connection and indistinguishable from a webpage to
any proxy. So: push/pull/poke.
Edge (worker + DOs):
- GET /registry/{org}/rows?since= — the WS hello's exact delta answer
(full/gcFloor rules shared with handleHello); ?beat=1 doubles as a
presence beat. POST /registry/{org}/push — the WS push twin via a shared
applyPushBatch core (validate, atomic apply, rows broadcast to live
sockets); LWW clocks make replays no-ops.
- GET /chat2/{id}/rows?after= — one request collapsing connect→hello→
state→rowsReq→backfill: u32-LE length-prefixed frames (state with
frontier payload, rows, rowsDone), byte-identical encoding to the WS.
POST /chat2/{id}/rows?batchId= — push twin, batchId-deduped.
Desktop (Rust): RegistryTransport/ChatTransport seams; with a transport,
construction resolves immediately (local-first), an HTTP pull bootstraps
in ~1 RTT while the socket dials, and every backoff cycle pushes pending +
pulls over HTTPS — a WS-hostile network degrades to ≤30s polling instead
of silence. Engine wires both via the existing bearer/reqwest plumbing
(registry: URL derived from the WS provider; chat: EdgeChatTransport).
iOS (Swift): WorkspaceStore pull-first bootstrap + 20s poll-while-down +
HTTP flush of pending writes; the header spinner now means 'server state
reached this session' (flips on the ~1-RTT pull, not the 4-RTT socket).
ChatRoomClient.pullSync: HTTP push of pending sends (permanent rejections
retire, mirroring the error-frame path), framed rows pull applied through
the exact socket path, checkpoint fetch shared via fetchInFlight.
Validation: new edge/scripts/pullpush-check.mjs green against wrangler dev
(push/ack/no-op replay, delta-at-cursor, presence beat, framed pull,
excludeOwn, batchId dedupe); existing smoke + chat2-check (65) + reset
suites green; zeron-sync 34, engine+rpc 180; iOS sim build clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* sync: audit fixes — heal waits for known server state, iOS retires pushes only on edge verdicts
Pre-merge audit findings:
- With immediate-ready construction, the host's bootstrap heal could read
all-zero placeholder stats 3s after construction (server state not yet
known on a slow link) and upload a spurious full-doc checkpoint. New
ChatStatsSnapshot.server_known gates it; the heal waits (bounded 2min)
for a real state answer.
- iOS pullSync retired pending batches on ANY http 400/413 — a captive
portal answering a POST with junk would silently drop a real message on
exactly the networks this path serves. Retirement now requires a parsed
edge error code (bad_push/too_large/empty), mirroring the WS error-frame
discipline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* harness: native Codex driver over app-server JSON-RPC (A2)
Resurrect the pre-ACP codex driver and modernize it against codex-cli
0.146.1 (the app-server API is experimental — the pin is noted in the
module header), replacing the codex-acp adapter for HarnessId::Codex:
- Done stays turn/completed (turn.status == "failed" now maps to an
errored Done even without a turn.error; the error notification's
nested params.error.message shape is accepted alongside the old flat
one — both live-verified).
- Approvals ride the app-server request RPCs as before (wire policy
"never" for unattended parity, stray requests round-trip as yes/no
questions); the experimental item/tool/requestUserInput content
questions now reach the input bridge and answer keyed by wire id.
- Subagents are full child threads. A registered child's notifications
route through an EXPLICIT table (route_child_notification), pure and
tested: item lifecycles AND message/reasoning deltas become tagged
AgentEvent::Subagent events (live capture shows child threads DO
stream deltas on this wire — token-level live transcripts without
touching the rollout files), child turn/thread bookkeeping is
consumed so it can never settle the parent turn, and unknown methods
fall through to the parent path — fail open, never silent loss (the
reference implementation shipped two bugs from a catch-all swallow:
a child error left the agent running forever, and a swallowed
serverRequest/resolved stuck approvals). Registration comes from
subAgentActivity items on the parent thread; the root's own activity
marker never registers (registering the root as its own child
intercepted its turn/completed and hung the thread Working).
Live-validated: basic turn end-to-end (marker file written, clean
turn/completed) and a two-child fan-out capture archived under
tests/fixtures/codex/. Ignored live smoke included.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* v0.2.4
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pi-adopt): filter noise sessions (ephemeral cwds + utility prompts) (#2)
Skip adoption of sessions in /tmp-style ephemeral workdirs (E2E test
workspaces) and of machine-to-machine utility prompts (title generator).
Observed in production: first rollout adopted title-gen sessions and
pw2-e2e workspaces, flooding the mobile sessions list. (ID01-420)
Co-authored-by: David <davidhsutton@gmail.com>
* harness: Cursor driver via the pinned @cursor/sdk shim (A3)
Move HarnessId::Cursor off ACP (that surface strips subagent transcripts
— live-verified: task rawOutput is {durationMs, isBackground} only) onto
Cursor's SDK, per Cursor's own guidance to integrators.
What wire the SDK wraps, investigated first: @cursor/sdk@1.0.28 does NOT
spawn the cursor-agent binary — it is Cursor's agent runtime bundled
in-process, speaking proprietary protobuf/ConnectRPC to api2.cursor.sh
with client-side tool execution (sandbox helper binaries, MCP spawning).
There is no speakable stdio wire to drive from Rust, so the driver is a
thin zeron-owned Node shim (shim.mjs, JSONL frames over stdio) around the
PINNED SDK, materialized into a managed npm install
(adapter_install::ensure_installed_shim) and spawned as `node <shim>`.
- Done comes from the SDK run's terminal result — a crisp turn end by
construction; steers queue and deliver at turn boundaries (parity with
the prior ACP behavior).
- Subagents: tool-call-delta carries the child's FULL nested transcript
(NestedTaskUpdate) tagged by the spawning task's callId; the shim
forwards it tagged and the driver wraps it as AgentEvent::Subagent —
token-level live, no file tailing needed for cursor.
- The permission-free print surface is never used. askQuestion is
disallowed at agent creation: the SDK exposes no public answer channel
(SDKRequestMessage carries only a request id), so a question would
block the run forever. Known gap vs the ACP ask_question bridge.
- AUTH: the SDK's credentials are SEPARATE from `cursor-agent login`
(verified live — Agent.create succeeds unauthed, the turn then fails
with "Invalid User API Key"). Turn/startup errors carry the exact fix
(set CURSOR_API_KEY) as the error chip.
- Beta churn: unknown SDK update kinds are ignored, never fatal — a
drifted SDK degrades the stream, not the chat.
Shim validated live against the real SDK (ready frame + clean actionable
auth error end-to-end); driver covered by fake-shim scenarios (mapping +
tagging, steer-as-next-turn, interrupt, fatal auth, crash tail).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* landing: point downloads at v0.2.4
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* harness/engine: retire adapter machinery for claude/codex/cursor (A4)
AcpHarness now serves ONLY agents built ground-up on ACP — Grok and
Hermes — plus pi via pi-acp until a native driver exists.
Removed with the claude/codex/cursor ACP paths:
- The claude-agent-acp / codex-acp specs, managed-install prewarm
entries, and constructors (the pi pin keeps the npm install machinery;
the cursor shim reuses it for @cursor/sdk).
- The `_session/turn_ended` extension handler — the upstream adapter
dependency (adapter PR #997) is gone; the native claude driver forwards
the CLI's own wake-turn result instead.
- The Claude cost-frame settle machinery (COST_HINT_GRACE, turn-end cost
arming, the quiet-settle exemption): all of it compensated for the
adapter dropping prompt replies, which the native driver cannot do.
The blanket quiet settle stays for the remaining ACP agents.
- Cursor's ACP extension surface (cursor/ask_question, create_plan,
update_todos, task, generate_image handlers + the parameterized model
picker opt-in and variant-id plumbing).
Engine: new Harness::deterministic_turn_end capability — native drivers
whose every turn shape (user-prompted AND agent-initiated wakes) ends
with a wire Done return true, and drive_run stops arming the 120s/20s
quiesce watchdogs for them (ZERON_TURN_QUIESCE_MS still forces a window
on for diagnostics). ACP agents keep the watchdog backstop.
Tests: claude/codex/cursor ACP suites and the cost-frame/turn-ended/
quiet-exemption tests removed with the machinery they pinned; managed-
install tests repointed at the pi pin; the login-shell resolution test
now proves the native claude driver consults the same PATH snapshot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* harness: Grok ACP hardening (A5)
Grok stays on ACP — its only surface with supervised permissions
(headless is --always-approve only) — hardened against the field
reports of silent non-response and multi-second stalls:
- Spawn args are now `grok --no-auto-update agent --no-leader stdio`.
Flag placement verified against 1.0.4 (--no-auto-update is top-level;
--no-leader lives on the agent subcommand — NOT the spellings the
investigation guessed). --no-leader starts a fresh agent even when
`[cli] use_leader` makes `agent stdio` ATTACH to a shared leader via
~/.grok/leader.sock; --no-auto-update kills the launch-time update
check. Field-hang REPRO (leader on a private socket, SIGSTOP, attach):
no initialize response for 10s then a client-timeout death on 1.0.4
(older builds hung forever — the reported signature); the same wedged
machine completes a turn in ~1s with --no-leader. npm fallback pin
bumped to @xai-official/grok@1.0.4 to match the verified flags.
- `_x.ai/session/prompt_complete` is now the AUTHORITATIVE turn end
(live-confirmed on 1.0.4, including the echo of a zeron-minted
`_meta.promptId`): the notification swaps the in-flight prompt future
for a synthesized response, so the ENTIRE existing response-settle arm
(steering settlement, drain ordering, park/queued-steer) runs
unchanged and the possibly-hung RPC drains discarded in the
background. Stale completions (already-settled prompt ids, ring of
32), foreign sessions, and out-of-order ids can never settle a newer
turn. The prompt response remains the fallback — whichever lands
first wins, exactly once.
- Prompt-stall watchdog (spec-gated, grok 30s;
ZERON_ACP_PROMPT_STALL_MS overrides): TOTAL wire silence after a
prompt — healthy grok acknowledges within milliseconds — surfaces a
visible error chip naming the likely wedge and closes the run with an
errored Done, instead of indefinite Working. Any wire traffic clears
it; stderr already feeds the harness log via the rolling tail.
- Usage: grok settles token counts on the response `_meta`
(inputTokens/outputTokens — verified live); usage_from_response reads
it alongside the adapters' first-class `usage` object.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* subagent visualization: per-subagent docs, spawn chips, right-pane tab (B)
Main doc stays light: subagent content NEVER enters the parent chat doc.
The spawn tool call becomes the index chip — subagent_ref (the subagent
doc id), subagent_status (running/done/failed; distinct from `resolved`,
which the eager-done policy fires while the subagent still runs), and a
one-line live tail — all additive doc fields old readers ignore.
Engine (drive_run): tagged AgentEvent::Subagent traffic routes to a
per-subagent doc, id `{chatId}--sub--{toolUseId}` (hashed when unclean/
over the edge ID_RE budget — same id names the ChatRoom and the frozen
blob). Sinks fold events into one streaming entry, flushed on the same
coalesced STREAM_COMMIT_MS tick as the parent; the held doc Arc pins the
doc warm while running. The chip refreshes through the live fold while
its segment streams, else in place via the new
SessionDoc::update_subagent_chip (newest-entry-first scan — the parked/
wake norm), with tail stamps throttled to ~1/s. On the tagged terminal
event the entry finalizes and the transcript FREEZES: entries JSON
uploads as a static R2 blob (blob/{chatId}/{subDocId}) so viewers of
finished subagents never wake the room; dropping the sink unpins the
doc and the room idles. Every failure path degrades to chip-only.
Routing runs BEFORE the parked gate, so a parked session's background
traffic reaches the subagent doc without un-parking the chat.
UI: RightSurface::Subagent tab beside Diff/Terminal, opened from an
"Open subagent" affordance in the chip's existing blob-affordance slot
(analytic heights untouched). Strictly read-only Transcript::for_doc
instances — no composer, no steering, no permission UI, no own-turn
machinery, no global attachment-protection writes. Running subagents
watch their doc over the unchanged WatchDocMessages RPC (any doc id);
finished ones fetch the frozen blob first, doc watch as fallback.
Closing the tab drops the watch task, which cancels the server-side
watch and unpins the doc from the WARM_DOC_CAP LRU.
zeron-doc: SegmentWriter::resume/into_state (cross-flush sink seam) and
the in-place chip update API.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Also: the cursor shim's crash message now waits briefly for the reaped
exit status (try_wait raced the child's death — the error chip read
"still running" with no stderr tail).
* ios: pull's checkpoint fetch must drain the socket's frame buffer
The HTTPS pull and the socket handshake race on a frontier-missing chat:
the pull's inline fetch set fetchInFlight, the socket's handleState saw it,
skipped its own fetch, and armed checkpointBuffer expecting the completion
path to drain it — which the inline fetch never did. Every socket row
(backfill AND live broadcasts) buffered until the 120s backfill deadline
redialed: a frozen transcript on exactly the fresh-chat open the pull was
meant to speed up (user report, 2026-08-18). Route the pull's fetch
through completeCheckpointFetch, whose tail applies the blob and drains
whatever buffer a racing socket armed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* sync: pull-path failures are loud on both platforms
The 2026-08-17 cellular incident was undiagnosable from device logs: the
phone's sockets fleet-wedged on 2-bar 5G while HTTPS demonstrably worked
(sends + uploads flowed), the pull path failed to fill the transcript —
and logged NOTHING, because every failure exit was a silent try?/guard.
That violates this codebase's own rule (roomLog header: sync must never
fail silently). Every pull/push exit now names its failure (no-token,
transport error, http status, malformed body, frontier-still-missing),
and the Rust offline-sync failures log at warn instead of debug so real
users' daemon.log shows them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* sync: empty checkpoint frontier is NOT contained — fetch, never skip history
Root cause of the 'Add Tweets' invisible-transcript incident (2026-08-18),
diagnosed from room stats + phone log archives + the absent on-device doc:
the room's checkpoint (seq 5, rows 1-5 trimmed) carries an empty frontier
label, and both clients treated empty-frontier as 'already contained' — so
every FRESH reader skipped the checkpoint, imported rows 6..75 raw, and
Loro parked everything depending on the chat's founding ops: transcript
invisible past the reader's own-lineage rows, no import errors (parking
is not an error), and snapshot export fails with pending ops so the doc
never persists. Converged devices (synced from row 1 pre-trim) unaffected,
which is why the bug only shows on new devices/fresh installs.
- Both clients: drop the empty-frontier shortcut; empty fails the decode
path → not contained → fetch. Fetching is always safe (full-state merge;
an empty-doc seed applies as a no-op) — the shortcut contradicted the
adjacent comment's own principle ('never silently skips history').
Applying the poisoned room's real checkpoint bytes heals it client-side
with no server repair.
- Edge: reject checkpoint POSTs with an empty frontier when seqCovered>0
(empty stays legal for the M1 empty-doc seed) so no new rooms are
poisoned; chat2-check codifies the guard (66 checks green).
- Regression tests: EngineChatSink::contains_frontier(&[]) must be false;
plan-level decision-table entry for the fresh-reader fetch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* sync: an encoded-empty frontier is a vacuous claim — fetch, never skip
The first frontier fix (zero-length bytes) didn't heal the poisoned room:
the phone's next cold open STILL skipped the fetch cleanly. Mechanism
proven by failing test: the room's frontier is a VALID ENCODING of an
EMPTY version vector — every doc vacuously 'includes' empty, so the
containment check passed for readers holding nothing and they skipped the
chat's founding ops anyway. One representation deeper than the last fix.
Both platforms now treat a decoded-but-empty version vector as NOT
contained (a size>0 checkpoint claiming empty state is a contradiction),
with an info log at the decision so any future skip names itself.
Regression tests: encoded-empty frontier → fetch; real contained frontier
(doc with actual ops) still short-circuits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* sync: cursor amnesty — a cursor above the checkpoint is re-verified, once
The 'Add Tweets' wedge, closed with artifact evidence: the phone's
persisted doc was EXACTLY the 2728-byte checkpoint (state at seq 5, which
includes the user's own message) under cursor=75. Rows 6-75 had imported
as PENDING during the frontier-bug era (deps missing → Loro parks
silently, import reports success, no logs), the cursor advanced anyway,
and parked ops vanish on export — so the persisted doc lied under a
cursor that guaranteed nothing below it would ever be re-read. Every
transport then truthfully reported 'converged' forever.
Fix (both platforms): once per client, on the first state answer, clamp
the cursor to the room's checkpointSeq. Rows since the checkpoint refetch
(re-imports are no-ops; cost bounded by the trim policy to the last
checkpoint window), converting any lying cursor into a true one — heals
every wedged replica with no server change and no data-dir surgery, and
keeps healing any future parked-history divergence from any cause.
iOS grows a clampCursor delegate (persists doc+cursor in the same write,
C2 rule); Rust clamps shared.cursor at the state site both transports
share. Amnesty logs at info so a clamp names itself.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* v0.2.5
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* landing: point downloads at v0.2.5
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(pi-adopt): repair tests + k3 findings (ID01-426) (#3)
Tests: non-ephemeral cwds (the filter itself broke tempdir-based tests);
/tmp adoption asserted as filtered. Findings: /private/var/tmp coverage,
shared TITLE_PROMPT_PREFIX const with titles.rs, info-level utility skip,
extra ephemeral tests, module doc on operator-side purge.
Implementer: glm-5.3 (timed out at 40m but left complete work).
Local gate: targeted tests (unit+integration pi_adopt) + workspace check;
full engine suite runs in CI only (hangs locally against the live engine).
Co-authored-by: David <davidhsutton@gmail.com>
* subagent viz: three live-rig findings fixed
The PR's media rig session (real claude + codex through the built app)
surfaced three gaps the fixture suites missed; all three are fixed and
pinned by regression tests:
- update_part_fields dropped the chip fields: a spawn chip mutating
while its segment still streamed (the codex fan-out shape) lost
subagentRef/subagentStatus/subagentTail at SegmentWriter::sync — only
the parked in-place path persisted them. Live-segment chips now carry
the fields.
- claude 2.1.228 emits NO tagged terminal frame for a finished
background subagent: completion arrives as an untagged system
task_notification carrying the spawning tool's id (shape pinned from
the live capture). The normalizer now surfaces it as the subagent's
tagged Done, so chips flip done/failed and transcripts freeze instead
of reading running forever.
- claude streams no tagged partial deltas: a subagent's text arrives
only as full text blocks on its tagged assistant frames, which were
dropped — subagent transcripts were tool-chips-only. Tagged text
blocks now emit as attributed text (message granularity; the wire
offers nothing finer).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* subagent tabs: contextual titles + top-aligned streaming runway
User feedback on the PR build:
- Tab names are contextual. The root cause was the harness: Claude's
spawn decode named the chip a bare "Agent", so the tab inherited it.
The Agent/Task decode now names the chip after the task's description
("Agent: scan repo") — codex (agent-path leaf) and cursor already did.
The tab-title fallback also digs description/prompt out of the stored
tool input for older docs whose chips predate the decode, and every
derived title caps at 40 chars for the fixed-width strip.
- Subagent transcripts start from the TOP and stream downward. Override
instances build their list Top-aligned — the notes-page shape
structurally, with the empty space below as the runway — instead of
Bottom-aligned, which pinned short/starting transcripts to the foot
of the pane. The whole glue-trap surface (end-sentinel re-glue,
distance-0 restick, pad/hold machinery) is a Bottom-alignment
property, so none of it is needed or armed here: the pin starts off,
the wheel restick is gated off for overrides, and row 0 keeps only
the turn gap (the pane already pads for the titlebar — the full
window-chrome pad double-counted and started content ~66px down).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ui): round project picker section corners
* feat(pi): step-boundary steering — descriptor truth-up for the patched pi-acp adapter
The local pi-acp adapter now implements the _session/steering ACP
extension, bridging to pi native RPC steer (delivered before the next
LLM call). Runtime detection stays dynamic via
initialize._meta.steering.supported, so an unpatched adapter degrades
to boundary delivery instead of breaking.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ScYmkStp1WkbMwchzaXjcb
* subagent UX: real terminal statuses, bare tab titles, designed chip
User feedback round — statuses were wrong and the chip read as bolted-on;
all fixed now, none deferred:
- Codex and Cursor subagents get REAL terminal signals. Codex children
never see thread/closed in real fan-outs (it needs the collab
close_agent tool), so chips stayed "running" after the child settled —
a child's turn/completed / turn/failed / turn/aborted now routes as
the subagent's tagged Done (completed/errored/interrupted). Cursor's
SDK has no separate terminal frame for the nested transcript, so the
task tool's end doubles as the tagged Done. Claude already settles via
task_notification. The engine skips minting a subagent doc for a
Done-only event (a terminal that beats registration is chip-only, not
an empty frozen doc).
- Tab titles are the BARE task description — the "Agent: " prefix ate
most of a fixed-width tab. Genus stripping is word-boundary-safe
("Taskmaster" survives), first-line-only, 40-char capped; "Subagent"
is the last resort.
- The chip is designed, not annotated: state is visual — the app's
mini gradient spinner (the sessions-sidebar dot loader) pulses in the
trailing slot while running, done is the ordinary quiet chip, failed
takes the danger tint; the "· running/· done" text suffixes are gone.
The affordance row is "Show subagent transcript", rendered from the
same base as "Show full output". The Subagent tab's strip icon swaps
to the mini spinner while its doc streams (the history fetch-all
pattern), settling back on completion.
- Clipped running-tail bug root-caused: the tail bypassed the
single_line whitespace fold every other chip detail passes through,
so raw text wrapped inside the fixed 30px header and clipped. It now
folds like every detail line; height accounting is unchanged by
construction (no new visible rows).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* subagent polish: Agent chip label + codex message paragraphing
Two cosmetic findings from the live verification pass:
- Spawn chips read "Tool · Agent: scan repo" — two labels fighting. The
Unknown decode convention every native driver uses ("Agent[: <desc>]")
now labels the chip "Agent" with the description as the detail.
- Codex emits several assistant messages per turn (commentary, final
answer) whose deltas carry no separator, so consecutive messages
rendered concatenated ("…waiting.Beta's 90-second…"). Each completed
agentMessage now closes a paragraph, in the parent transcript and in
child subagent docs alike.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* subagent tab: scroll follow + jump pill; chip quiets down; top fade
Feedback batch on the subagent surfaces:
- The tab now has the main transcript's follow UX. Top alignment stays
(the start-from-top runway is untouched) — verified against the pinned
gpui source that the list glue trap is Bottom-alignment-only: under
Top, past-end offsets always materialize concrete, so a parked spring
cannot re-glue and hard-track growth. The pin/spring/restick machinery
is distance-based and runs unchanged on top: streaming overflow
follows the end with the same glide, wheel-up releases to a held
offset, and the jump pill — extracted into a shared element, not
copied — renders pane-locally for the active tab and re-engages
follow on click. Frozen transcripts open unpinned at the top.
- The chip's live tail is REMOVED end to end. Rewriting the header per
delta batch grew the parent doc's oplog for the whole subagent run and
rendered as mid-stream markdown fragments. The fold and the engine's
in-place stamps are lifecycle-only now; the running chip is the static
task description plus the working spinner. subagent_tail stays in the
schema (and the row-cache fingerprints) for docs that carry it.
- The affordance row reads "Open subagent".
- The pane transcript gets the top overflow fade (edge_faded, top edge
only — nothing sits at the pane's bottom), gated on real overflow so a
short top-anchored transcript shows no fade band.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* shell: jump pills go glass
Both scroll-to-bottom pills (one shared element since the subagent-pane
round) rendered as an opaque raised surface — one of the few non-glass
floating elements left. On glass appearances the pill now frosts at the
composer tier (radius-clipped backdrop blur 16) under the standard
floating-card tint, hover fading the on-glass wash in OVER the tint —
compositing the wash rather than mixing toward it, which would have
thinned the pill see-through on hover. Opaque appearances keep the
raised-surface blend. Frost opens its own scene layer at the pill's
paint position, so blur + pill composite over the scrolling transcript
in both hosts; size, label, motion, and click behavior unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* spaces: reach other drives from the add-project palette
The folder browser only ever started at home, so a project on an
external SSD or second disk meant walking up to / and hunting for the
mount by hand (if you even knew where your platform puts them). The
palette's rail now has a Locations section under Devices: Home plus the
device's mounted drives, served by a new relay-forwardable ListDrives
RPC (macOS /Volumes with the boot-volume symlink resolved; Linux
/proc/mounts under /media, /run/media, /mnt plus a System row for /;
Windows drive letters). Clicking one rebrowses the palette at that
root, the row owning the browsed path carries the selection wash, and
the drive's mount folds into a breadcrumb named after the drive the
same way home folds into the device crumb. Hard-drive + home glyphs
hand-drawn in the Solar Linear style.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* subagent tab: every open lands at the end
Opening a tab now lands on the latest content universally, not only for
live tabs. Frozen transcripts arm a one-shot end-landing that fires in
sync on the FIRST non-empty row set — whichever feed delivers it (blob
snapshot or watch replay) and whenever it resolves — via the item-
anchored scroll_to_end (a past-the-end offset the next layout
materializes by walking back from the last item; pixel offsets would
land short on freshly-spliced unmeasured rows). The flag clears on
fire, so later growth never re-asserts and the viewport belongs to the
user immediately. `pinned` stays untouched: a frozen doc that grows
after the landing appends below with no auto-follow — landing at the
end and following it remain distinct states. Short content still rests
top-anchored (the past-end offset clamps to the top under Top
alignment), so the runway behavior is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* subagent iconography: bot glyph for tabs and spawn chips
Hand-drawn bot head (antenna + eyes + ears) in the Solar Linear style —
the embedded set had no robot glyph. The Subagent tab drops the chat
bubble for it, and the spawn chip's icon tile matches (the "Agent"
Unknown convention drew the generic widget glyph, inconsistent with its
own tab). Chat-bubble usages that ARE chat (question chip, comments,
composer) stay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ui: real frosted floats on Linux (wgpu backdrop blur)
Theme::is_frost() separates scene-level frost (floating surfaces
blurring in-app content) from is_glass() window vibrancy: the wgpu
renderer now rasterizes BackdropBlur like Metal does, so Linux gets
frosted popovers/modals and the composer pill without compositor
vibrancy. The window chrome itself stays opaque off macOS (no
blur-behind guarantee outside KWin).
- composer pill + question panel: paint their backdrop blur and
translucent tint on Linux; drop the opaque shadow slab
- popovers/modals/settings cards: frosted tint over the blur
- macOS unchanged (is_frost == is_glass there)
Requires the gpui rev bump to wingleeio/zed comet/edge-fade-horizontal
with the wgpu BackdropBlur port (PR to that fork); rev points at the
port branch until it merges.
* spaces: enumerate custom mounts and accept typed paths (PR #144 feedback)
Linux drive partitions live wherever the user mounted them — /disk2 is
as much a drive as /media/user/T7. The Linux walk now also takes any
block-device mount at a depth-one non-FHS point (squashfs/erofs images
excluded, so snaps stay out; the system's own split /boot//home//var
partitions are plumbing, not drives). And the browse root was never
meant to be a cage: a query starting with / or ~ now navigates —
typing / jumps to the root and each segment/ descends shell-style,
while a pasted path (or ⏎ on one) jumps in one hop, ~ expanding to the
device's home. A typed path that doesn't exist shows the engine's real
folder error instead of blaming the device's connectivity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* v0.2.6
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* landing: point downloads at v0.2.6
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chat2: claim rooms on HTTP first-contact, not just WS join
The /rows GET/POST twins predate the pull-first HTTPS transport and sat
behind an owner gate that only /ws and checkpoint POST could satisfy. A
brand-new chat on a WS-hostile network deadlocked: the sender's HTTP
push 404s (room unclaimed), the host's HTTP pull 404s, and the only
claimants — WS join and the checkpoint heal (gated on a server state
that the 404'd pull can never learn) — never run. Every remote new-chat
send hung on "Sending…" until some WS dial happened to succeed.
First authed toucher now owns the room on any HTTP contact, the same
client-minted-id discipline /ws already applies; foreign users still
get 403.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* grok: subagent visualization over ACP — lifecycle wire events + disk-tailed transcripts
Grok subagents rendered as plain tool calls because the ACP path never
emitted AgentEvent::Subagent: grok's frames carry no parent_tool_use_id
(tagging documented but unimplemented as of 1.0.4), the subagent's interior
transcript never rides the parent's wire, and the lifecycle updates
(subagent_spawned/progress/finished) arrive on grok's extension channel
_x.ai/session_notification, which the harness dropped entirely.
The new SubagentTracker correlates the spawn_subagent tool call (via
_meta["x.ai/tool"]) with its subagent_spawned update — binding through the
subagent_id echoed in the spawn's completion output, falling back to
description-FIFO for sync spawns — then tails the child session's
chat_history.jsonl (located by session id under ~/.grok/sessions, one glob
level to dodge the cwd encoding) into tagged transcript events: reasoning
summaries, assistant text, typed tool calls, tool results.
subagent_finished settles the chip with a tagged Done after a bounded
drain; a torn-down session settles running tails as Interrupted. Every
disk parse fails soft to chip + final-output-only — the format is
vendor-private. Spawn chips are named "Agent: {description}", claude-driver
parity.
Live-validated against grok 1.0.4 (examples/grok_subagent_probe.rs): full
tagged stream — reasoning, text, Exec("echo viz-probe-ok && sleep 3"),
result, Done — attributed to the spawn chip on a real background subagent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(engine): per-harness model allowlist for ListModels (#5)
Trim the picker to ~/.zeron/model-allowlist.json at the RPC seam.
Missing/corrupt/empty config or zero matches pass through so the
catalog never goes empty. Deepseek ids with an extra slash match
deepseek-payg/*.
Co-authored-by: David <davidhsutton@gmail.com>
* feat(ios): create a space from the new-session menu (#6)
The folder browser already existed. This wires New space into the +
menu, adds a typed path (engine-validated via ListFolders), and
clears a stale gitDetected when jumping paths.
Co-authored-by: David <davidhsutton@gmail.com>
* fix(engine): deaf-socket tripwire retries with backoff + sweeps chat rooms (#7)
* fix(engine): deaf-socket tripwire retries with backoff + sweeps chat rooms
The presence tripwire fired one registry redial and disarmed ("one probe +
one redial, ever"), so a redial that landed on another dead socket left the
engine dark forever. Chat2 rooms were never touched at all, so a completed
turn's final message sat undelivered on the phone (2026-08-18 incident,
11:37-11:51 UTC: redial requested, never recovered, summary never arrived).
- keep the tripwire armed while presence stays dark: repeat redials with a
doubling interval (60s base, 15 min cap), log each attempt and recovery
- new deaf-escalation hook wired to DocHost::probe_open_chats — a probe is
free on a healthy room; a deaf room misses its probe deadline and redials
itself (chat rooms ride the same relay as the registry room)
Gate: cargo check --workspace --all-targets OK; zeron-engine --lib 76/76.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtyrsrkwhZsVP7QvPKUGLr
* fix(engine): invoke presence hooks outside reg/room guards + test the ladder
Review finding (K3, CRITICAL): the deaf-escalation hook ran under the
registry `reg` guard (match-scrutinee temporary in publish()) plus the
room/presence_seen guards, while DocHost's open/eviction paths take
`handles` before `reg` — an AB-BA deadlock armed exactly during the long
dark-presence spells the watchdog exists for.
- publish() binds read_all() outside the match so `reg` drops before
overlay_presence (also moves the pre-existing peer-alive hook out from
under `reg`)
- check_presence_deafness returns the decision; overlay_presence invokes
the hook after releasing room/presence_seen, mirroring peer_alive
- the tripwire state machine is now PresenceWatch::tick (pure) with unit
tests for the probe->redial ladder, doubling backoff + cap, and re-arm
Gate: cargo check --workspace --all-targets OK; zeron-engine --lib 79/79.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtyrsrkwhZsVP7QvPKUGLr
---------
Co-authored-by: David <davidhsutton@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(engine): keep task timer across park/self-continued (ID01-423) (#8)
* fix(engine): keep task timer across park/self-continued resume (ID01-423)
started_at now tracks the user task, not each internal turn. A parked
persistent session keeps the base; self-continued output does not
restamp; user dispatch/steer does. Only a true settle (run handle gone)
clears it.
* fix(engine): don't double-restamp timer on Steered; skip settle if a new run owns the chat
* fix(engine): hold runs lock across settle_status so a new dispatch cannot be clobbered to Idle
---------
Co-authored-by: David <davidhsutton@gmail.com>
* feat(pi): foreground subagents tool (ID01-432) (#9)
* feat(pi): foreground subagents tool (ID01-432)
Versioned copy of ~/.pi/agent/extensions/subagents.ts plus unit tests.
Foreground only; background is ID01-433.
* fix(pi): keep SIGKILL timer referenced so timeout tests settle
* fix(pi): clear SIGKILL timers on settle; cap stdout incrementally
---------
Co-authored-by: David <davidhsutton@gmail.com>
* feat(pi): background subagents + status (ID01-433) (#11)
* feat(pi): background subagents + status (ID01-433)
* fix(pi): per-entry kill + batch token so background reuse stays clean (ID01-433)
killAll no longer sticks a runtime-wide flag that would mark later batches
killed. Sweep only touches entries owned by the same dispatch token.
* fix(pi): close k3 findings on background subagents (ID01-433)
Foreground workers stop spawning after killAll via killEpoch. Pre-aborted
signals reject instead of returning a silent dispatched list. Status
distinguishes queued vs running. Duplicate batch ids fail in foreground.
installSubagents covers steer/nextTurn/shutdown delivery.
* fix(pi): drop dead abort/dup branches after resolveTasks (ID01-433)
---------
Co-authored-by: David <davidhsutton@gmail.com>
* cursor: in-app Connect via the SDK's browser login + accounts integration
The @cursor/sdk conversion (#136) left cursor unusable without a manually
minted CURSOR_API_KEY — the SDK's credentials are separate from
`cursor-agent login`, and the shim's only guidance was an error pointing at
cursor.com/settings. But the SDK ships its own PKCE browser login
(Cursor.auth.login, verified live on 1.0.28): it mints a named, revocable,
90-day user API key and persists it to ~/.cursor/sdk/auth.json.
This drives that flow from the Accounts settings page:
- shim login mode (`node <shim> login <store-path>`): runs Cursor.auth.login
with openBrowser:false, streaming {"ev":"auth-url"} then "logged-in" /
"fatal" JSONL; the minted key lands in an engine-chosen store file, never
the live login. Keys are dashboard-named "zeron — <hostname>".
- agent_accounts grows a Cursor provider: detection reads the SDK's
StoredSdkCredentials (email + key expiry; expired keys warn and show
"Key expired" instead of failing runs mysteriously), slots snapshot the
store file, switching rewrites it — the codex slot machinery, generalized
(LoginFlow::Codex → LoginFlow::Spawned, shared child wiring). First
connect on a device with no usable live login auto-activates the minted
key, so runs work immediately after the browser round-trip.
- Accounts page renders the Cursor card (Connect Cursor, browser-poll
dialog with cursor-specific copy); run-time auth errors now point at
Settings → Accounts instead of only the env var. CURSOR_API_KEY still
wins as the headless override.
Live-validated: the real shim in login mode emits the loginDeepControl URL
immediately and polls Cursor's backend (browser completion covered by the
fake-shim integration test; full round-trip needs a real account login).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* v0.2.7
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* landing: point downloads at v0.2.7
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* cursor: live model discovery + typed model options actually applied
The picker showed only Auto + Composer 2.5 — a static catalog from #136,
built on the assumption that Cursor.models.list() required auth. It
doesn't (verified live, logged out, 1.0.28): the endpoint returns the full
catalog — 37 models with typed parameter definitions (thinking, context,
effort, reasoning, fast, Auto's optimize_for), display names, aliases, and
default variants.
- shim models mode (`node <shim> models`): prints the live catalog as one
JSONL frame.
- CursorHarness::models(): spawns it (15s bound), maps ModelListItem →
picker Model — parameters become ModelOption choice sets, isDefault
variants set the default choice, the bare `default` alias twin of the
parameterized Auto entry is skipped. Successful non-empty catalogs cache
(OnceCell); failures fall back to a minimal static pair, uncached so the
next picker open retries.
- Model options now REACH the run: the run frame carries modelOptions and
the shim folds them into the SDK's ModelSelection params — the old
static "optimizeFor" choice was picker decoration that never left the
app. Legacy stored optimizeFor/speed picks translate to
optimize_for/cost instead of sending ids the backend never knew.
Live-validated end-to-end through the real harness path: 36 models with
options and catalog defaults (examples/cursor_models_probe.rs, kept for
pin-bump revalidation).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* v0.2.8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* landing: point downloads at v0.2.8
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* cursor: isolate each SDK run's agent store — concurrent runs no longer lock
"cursor agent failed to start: database is locked" (user report): the SDK's
default local store is SQLite keyed by WORKSPACE (getDefaultSdkStateRoot(cwd)),
designed for one process per workspace. Zeron runs concurrent shims on the
same cwd as a matter of course — a chat turn and its title generation start
together, and two chats can share a worktree — so the second Agent.create
finds index.db locked and dies. Reproduced live: 3 concurrent shims, one
ready, two fatal with exactly the reported error.
A SHARED JSONL store is not the fix — JsonlLocalAgentStore serializes writes
only process-wide and rewrites whole files on update. Instead every run gets
its own JsonlLocalAgentStore directory under ~/.zeron/cursor-state/agents/
(one agent per store, so the backend's small-catalog caveat never applies,
and the native sqlite3 dependency drops out), and a one-writer marker file
(by-agent/<agentId> → dir, atomic tmp+rename) lets Agent.resume from a later
process find the agent's store. Pre-isolation agents have no marker and
resume from the SDK default store exactly as before. ZERON_CURSOR_STATE_DIR
overrides the base for tests.
Live-validated (real SDK, 1.0.28): 4 concurrent creates on one cwd all reach
ready; resume round-trips through the marker in a fresh process; markerless
resume routes to the default store.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* v0.2.9
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* landing: point downloads at v0.2.9
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(pi): subagents usage/cost + budget (ID01-434) (#12)
* feat(pi): subagents usage/cost + budget (ID01-434)
* feat(pi): subagents usage/cost + budget (ID01-434)
Cada hijo reporta usage (session-dir descartable) y el batch una línea
summary (wall vs cpu, costo o n/d, PAYG, skipped). Tope opcional
PI_SUBAGENTS_MAX_COST_USD. Review k3: path traversal en task.id,
leak de dirs, pid en batchKey, budget re-leído por batch.
---------
Co-authored-by: David <davidhsutton@gmail.com>
* fix(pi): subagents heartbeat + role timeout floors + honest timeout message (#13)
Los hijos corren pi -p (output solo al final): con timeouts bajos elegidos por
el orquestador LLM morian a los 5 min con output vacio, y el silencio >30s
hacia que el engine parkee el turno (chat mudo en la app). Heartbeat cada 20s
via onUpdate (tasks vivas + elapsed), pisos de timeout por rol que clampean el
timeoutMs pedido (implement/hard 900s, review 600s, research/grunt 300s;
deps.timeoutFloorsMs={} para tests) y mensaje de timeout que explica que la
task probablemente seguia trabajando. Hallazgo k3 aplicado: emitUpdate con
try/catch (teardown del host no tumba el batch).
Diagnostico y spec: Linear ID01-434 (comentario 2026-08-18). Review: k3 APROBADO.
Claude-Session: https://claude.ai/code/session_01LWydxxHbUSos6jJr2XfqjL
Co-authored-by: David <davidhsutton@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* opencode: ACP harness with subagent viz off the sidecar event bus
Adds OpenCode (SST's opencode, `opencode acp`) as an ACP harness — the
new-agent recipe end to end: HarnessId variant, AcpAgentSpec (no npm shim:
the opencode-ai package ships a native binary, so the CLI itself is the
ACP server), registry_lazy slot, harness_slug, pixel-"o" brand mark, and
the settings/picker arms. Model discovery rides the existing config-option
probe (verified: the model select lists every configured provider with no
auth); no thought_level option exists, so the effort ladder is empty
(hermes parity). Steering is turn-boundary; loadSession resume works.
Subagent virtualization: opencode's `task` tool call rides the parent
session/update stream, but the child transcript never touches the ACP
wire, and 1.18 moved session storage to SQLite — so no grok-style JSONL
tail. Instead the harness exploits the fact that `opencode acp` doubles as
opencode's HTTP server: runs pass `--port <free>` (the shared default 4096
loses silently under concurrency) and OpencodeTracker tails the `/event`
SSE bus. session.created (parentID + "{description} (@agent subagent)"
title) binds children to task chips ahead of the completion's
authoritative rawOutput.metadata.sessionId; message.part.updated/delta map
to tagged AgentEvent::Subagent traffic (token-level text, typed tool
chips with results); the ACP task completion settles the chip with a
tagged Done after a short drain. Everything fails soft to chip + final
<task_result> output. A normalize arm keeps the "Agent: {description}"
chip naming across the completion frame, which drops rawInput but keeps
the spawn signature in rawOutput.metadata.
Live-validated against opencode 1.18.18 through the real harness path
(examples/opencode_subagent_probe.rs, kept for bump revalidation) with a
mock OpenAI-compatible provider forcing deterministic spawns: single task
and two concurrent tasks both stream fully tagged interior transcripts
attributed to the right chips. Engine needed zero changes (generic
Subagent routing from the native-drivers work).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(pi): subagents streaming real via --mode rpc (ID01-461) (#14)
Los hijos pasan de 'pi -p' (output opaco hasta el final) a 'pi --mode rpc':
prompt por stdin JSONL y parseo de los eventos del stdout (text_delta,
toolcall_end, tool_execution_start, message_end, agent_settled). El heartbeat
del padre ahora muestra QUE esta haciendo el hijo, no solo cuanto lleva vivo.
Modo print queda como fallback (deps.childMode / PI_SUBAGENTS_CHILD_MODE=print).
Correcciones de 3 rondas de review adversarial (modelo distinto al implementer):
- stdin sin handler 'error': un EPIPE async tumbaba el pi PADRE.
- cierre: stdin.end() primero + reaper TERM/KILL y resolucion en 'exit' (no mas
hijos huerfanos fuera de 'live'; usage leido con el jsonl flusheado).
- gracia post-settle solo si el cwd tiene .pi-verify.json (verify-gate reinyecta
turnos de reparacion): cancela con cualquier evento, 15s, cero costo si no aplica.
- timeout manda sobre el cierre ordenado salvo que el cierre haya empezado antes
(no reportar exit 0 un task que reventó el tiempo, ni 124 uno que ya termino).
- cap de linea JSONL 1M chars con descarte reportado; buffers de salida acotados
de forma continua; rpcError transitorio limpiado solo en retry del assistant;
timers de señales en un Set por task (no mas aliasing en pendingKills).
- si el pi instalado no soporta --mode rpc: hint accionable + stdout crudo.
67/67 tests (incluye reaper sin huerfano, EPIPE del runtime, settle post-abort,
linea gigante, error transitorio, multi-mensaje). E2E real en la Beelink.
Linear: ID01-461. Review: APROBADO.
Claude-Session: https://claude.ai/code/session_01LWydxxHbUSos6jJr2XfqjL
Co-authored-by: David <davidhsutton@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* send: worktree creation rides the durable command plane; relay calls get deadlines
New-worktree sends wedged on "Sending…" forever while the session ran
remotely anyway: the composer's blocking CreateWorktree relay RPC had no
timeout, so a silently lost relay reply (the DO's auto-pong keeps a dead
host socket looking healthy) hung the send task after the remote had
already created the worktree and — via a retry — run the session.
Two fixes, defense in depth:
- RunRequest grows an additive `worktree: Option<WorktreeSpec>`: the HOST
materializes the worktree at command-drain time, repoints the chat row's
cwd, and stamps the actual zeron/<name> branch. The composer no longer
makes ANY relay call on the send path — createChat and QueueCommand are
local, so a new-worktree send is exactly as durable as a plain send. The
resolved request (spec taken, cwd = worktree path) is what gets journaled,
so resumes and steer→new-turn fallbacks reuse the checkout; a duplicate
spec-carrying Run reuses the chat's existing worktree instead of minting
another. Old hosts ignore the unknown field and degrade to the main
checkout instead of failing.
- Relay-forwarded unary calls get a reply deadline (30s interactive, 120s
CreateWorktree, 15m network git/update). On expiry the peer link is
invalidated — the zombie socket re-dials — and the caller sees a
retryable error instead of hanging forever.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* harness: native drivers rediscover slash commands
The ACP→native cutover (#136) silently dropped slash-command discovery
for claude/codex/cursor: the adapters used to advertise
availableCommands, and the native drivers inherited the trait's empty
default — only ACP-path agents (grok/hermes/pi, now opencode) kept a
populated composer popover.
- claude: short-lived probe sends the `initialize` control request on
the stream-json channel (the same handshake the Agent SDK's query()
opens) and reads commands + argument hints out of the
control_response. No user message is written, so no model turn and no
API cost (verified live on 2.1.228: 48 commands in ~1s). The probe
needs `--verbose` — mandatory with --print + stream-json output.
- codex: short-lived `codex app-server` handshake + `skills/list`, the
only invocable-listing method on the 0.146.x wire (custom
~/.codex/prompts are not exposed; verified against the full method
list). Skills are deduped across cwd groups; the interface's
shortDescription wins over the model-facing paragraph.
- cursor: documented no-op — @cursor/sdk 1.0.28 has no slash-command
surface, so the empty default is the honest answer.
Both discoveries cache on success only (ACP parity), time-bound at 10s,
and tear the child down immediately. Fake-CLI tests cover parsing,
dedupe, hint mapping, and caching; live `#[ignore]`d smokes cover the
real binaries.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* v0.2.10
* landing: point downloads at v0.2.10
* accounts: keep the list order stable — always
Two ways the accounts page reshuffled, both gone:
- Same-millisecond logins (issue #161): the sort key was created_at
alone, ms-resolution; a tie fell back to read_dir enumeration order,
which is filesystem-arbitrary for UUID-named slot files — the flaky
cursor_slot_swap_round_trip flip. New slots now get stamped strictly
after every sibling (max+1 bump in write_slot), and the sort carries
the slot id as a deterministic tie-break for pre-existing collided
files.
- Switching accounts moved the switched-to card to the top: the engine
deliberately lists in creation order ("never active-first"), but the
UI's provider_accounts re-sorted active-first, defeating it. The
re-sort is removed; the Active badge already marks the live account,
and a list that reshuffles under the click reads as broken.
Fixes #161.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* harness: opencode probe example builds again post-worktree field
PR #159 added RunRequest.worktree and updated every initializer except
the opencode probe example, which landed in parallel via #157 — the
cross-PR race broke `cargo test --workspace` (examples compile there;
no CI job builds them).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* transcript: spawn chips link to the subagent tab, not an accordion
The spawn chip's expandable body only repeated the subagent's own
transcript (and its footer 'Open subagent' row was easy to miss). The
chip is now a plain link: clicking anywhere on it opens the subagent
tab, and the trailing tile shows a top-right open-arrow (new
arrow-up-right icon, arrow-up rotated 45°) instead of the accordion
chevron. ChipAffordance collapses to the blob-fetch struct now that
the subagent slot is gone.
ZERON_MOCK_SUBAGENT=1 makes the mock harness fan out two scripted
subagents (tagged AgentEvent::Subagent traffic) — the data-side way to
put spawn chips and their openable docs on screen in the demo rig.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* attachments: 10x chunks, parallel window, overall deadline, live progress
An image send could crawl indefinitely on a flapping link while reading
only "Sending…": per-chunk timeouts retry 3x, so chunks that succeed on
attempt 2-of-3 never trip the consecutive-failure abort, and a ~3MB
screenshot was ~70 sequential 45KB relay round trips — hours of lawful
crawling with no feedback and no overall bound (2026-08-18 user report;
text-only sends are all-local and instant, which is why only image sends
looked hung).
- UPLOAD_CHUNK_B64_CHARS 60_000 → 680_000 (~510KB binary per chunk,
~35% headroom under Cloudflare's 1MB WebSocket message cap). Chunk
slot files engine-side are size-agnostic; no engine change.
- Three chunks in flight (seq slots are…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.





Restructures the shell around wing's proposal: tabs stop being synced session state, spaces stop being a navigation spine.
What changed
Tabs are now device-local. A tab is a local viewport onto a synced session (
ui-settings.json→openTabs; the vec is the drag order). Closing a tab is local-only — the session keeps running on its host and stays in the sidebar; clicking a sidebar row (re)opens it as a tab. Archiving is a separate, explicit action in the session row's context menu (unchanged — it was already there). The strip is cross-space and gets a context menu: close, close others, close to the right, close to the left (rows that would close nothing are omitted). Existing installs migrate by seedingopenTabsfrom the last selected space's sessions, so nobody's tabs vanish on upgrade.Spaces are a searchable dropdown (same recipe as the ref picker: ranked substring search, keyboard nav) that filters only the sidebar session list. "All spaces" is the first row; "Add space…" (the existing ⌘K palette) is the last; rename/remove live on row context menus inside the dropdown.
New sessions.
+opens the canvas defaulting to the sidebar's filter space — or the last selected space under "All". The composer gains a space chip on the canvas only; picking a space re-keys the ref selector, worktree plan, and harness/model catalogs through the existingselected_spaceobserver (no new invalidation paths).The synced Loro doc schema is untouched — this is purely a view-layer restructure.
Screenshots
Cross-space tab strip; sidebar sessions with the dropdown trigger on top:
The spaces dropdown — searchable, "All spaces", device tags, "Add space…":
Filtered to
soccertcg— note the twocomet-space tabs stay open (the filter never touches the strip):New-session canvas defaults to the filter space, with the new space chip:
The canvas space picker, and the ref footer re-keying after switching to a git space (
Current checkout / mainappears becausecomethas git;soccertcgdidn't):Tab context menu (last tab → no "close to the right"):
Demo video
Filter → open-from-sidebar →
+with space pick → send (mock harness streams) → close the streaming tab locally (session keeps running in the sidebar) → reopen with transcript intact → close others:MP4 version · assets live on the
assets/tabs-spacesbranch, not in this diff.Verification
cargo test --workspacegreen (settings round-trip covers the new fields; tab-neighbor logic keeps its unit tests).🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.