Repository navigation
Explorer: group finished subagents, show running counts - #12
Draft
katulevskiy wants to merge 83 commits into
Draft
katulevskiy wants to merge 83 commits into
katulevskiy wants to merge 83 commits into
Conversation
* Question panel: paint an opaque background so the transcript doesn't show through * Question panel: frost it like the composer pill instead of a new opaque fill The panel stands in for the composer pill but was the only input surface painting a translucent fill without the backdrop blur, so the transcript showed through unblurred. Wrap it in frost::frosted and use the pill's composer_surface_bg, matching the pill and queue tray on every material and wallpaper tint, and drop the separate input_solid_bg token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Wing <contact@winglee.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A provider CLI installed as a Homebrew cask or formula
(Caskroom/<token>/<version>/… or Cellar/<token>/<version>/{bin,libexec}/…)
gets an Update action that runs `<prefix>/bin/brew upgrade --cask|--formula
<token>` as argv. The brew binary is the one in the prefix that owns the CLI.
The installable version comes from formulae.brew.sh and `brew info`. When an
upstream release is newer than Homebrew's, the row names the brew command.
Update still runs it, but idle auto-update waits for Homebrew. npm globals
under Homebrew's Node keep their own updaters. Detection is macOS/Linux only.
Co-Authored-By: grahamwinter <wwinterr@pm.me>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix subagent spawn identity across resumes and recover stale chips * Claude resume: scan native history off the runtime, skip sidechains Long sessions' transcripts reach 100+ MB; parsing every line as JSON on a tokio worker before each cold resume stalled the runtime. Read with std I/O under spawn_blocking, parse only lines that can name a spawn or its agent, stop on read errors, and ignore a subagent's own nested spawns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Wing <contact@winglee.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Appearance: add background positioning and zoom * Wallpaper: keep the preload queue when a background's framing changes The preload key compared the whole background setting, so applying a crop or zoom flushed three decoded wallpapers and re-decoded them. Key on the managed path instead, and extend the warm-switch test to cover it. Also rustfmt the new background-mask code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Wing <contact@winglee.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
PR caches are scoped to their merge ref, so no other PR could read them, and main never saved one because in-progress main runs were cancelled. Only main writes the Rust caches now (PRs restore them), main runs are no longer cancelled, and Cursor compatibility also runs on pushes to main so it seeds its cache.
New macos.yml holds all macOS CI work: updater/preview/native UI tests and the WebKit fixtures from one nextest compile on the dev profile, the gpui frame-source test, and the iOS checks (Rust mobile core tests and the Swift bindings check now on Linux, simulator tests on macOS behind the original path filter). scripts/ci/run-macos-fixture.sh wraps the native fixtures with bounded retries.
macos-frame-recovery, ios-tests and their path-filter job move to macos.yml; the preview networking tests on macOS run there as well.
…sh#632) * Fix drag-select failing when several transcripts share a frame The markdown selection registry was one thread-local list that every transcript's frame reset cleared. With a side chat or subagent tab painted beside the main chat, only the last-painted transcript kept its text in the registry, so a drag anchored in any other one resolved to nothing. Triple-click still worked because its span is stored at mouse-down. Give each transcript a surface id (its entity id). Its reset clears only its own entries, entries are stamped with the surface they were painted in, and a drag resolves only against the surface it started in. Adds a two-pane regression test. * Keep forked panes' selections apart and prune closed panes' entries A side chat's forked history repeats its parent's entry ids, so both transcripts paint the same selection keys and a selection in one washed the other too. The selection now records the surface it began on; the wash and drag resolution use that instead of a separate thread-local. A surface that stops painting (closed side chat or subagent tab) never resets again, so its registry entries lived for the rest of the session and every frame's reset walked them. Drop a surface's entries once a full cycle of another surface's resets passes without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Wing <contact@winglee.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A composite action installs the toolchain, a checksum-pinned cargo-nextest and apt packages (bounded retries, cached .debs), and wires the main-seeded Rust cache. scripts/ci/test-core.sh runs the harness, engine, sync, update, doc and preview tests as one build and one nextest run. nextest.toml retries the two tests that are timing-sensitive under nextest. Not used by any workflow yet.
- ui-tests: session-sync-regressions becomes core-tests (one build for the harness, engine, sync, update, doc and preview tests); ui-tests and linux-browser use nextest, the dev profile and a shared rust-cache - cursor-compatibility: the Rust half runs from core-tests on PRs and main; it only runs here for release, cursor-sdk-update dispatch and manual runs - preview-tests: the Linux leg moves into core-tests - linux-installer: install desktop-file-utils with bounded apt retries
* feat(proto): define workspace entry mutation contracts * feat(engine): move workspace entries without replacement * feat(engine): delete workspace entries and publish mutations * feat(files): coordinate workspace mutations across open surfaces * feat(files): reuse context menus for copy path and add to chat * feat(files): rename entries inline and confirm deletion * feat(files): preserve workspace drag origin and enable Windows handles * feat(files): move tree entries with scoped drop targets * feat(files): polish tree drag feedback and navigation * test(files): cover workspace mutations and composer drag regressions * fix(files): style delete menu action as destructive * fix(files): reuse icons for all tree context menu actions * fix(files): show only entry name in delete confirmation * fix(files): give delete dialog more spacing and shorten action label * fix(files): refresh mutation revisions after completed saves * fix(files): refresh editor presentation and syntax after rename * fix(files): show rename caret and preserve text input focus * fix(files): copy full paths from the owning workspace * fix(engine): compare canonical workspace paths on Windows * Route chat drops through the shared dropzone with origin checks The branch replaced the main chat's chat_dropzone() call from zeronsh#612 with an inline copy, so the main chat validated drag origins but side chats did not. Both now go through chat_dropzone(), which hands workspace payloads to attach_workspace_drag() for the target composer. The dropzone fixture now stamps tree/search payloads with a live explorer origin, and also checks that an origin-less (stale) payload attaches nowhere. Also correct the Windows drag note: tree rows drag on every platform; there is no dedicated drag handle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Wing <contact@winglee.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Fix command and skill labels in the message queue * Test queue row labels for commands, skills and files Extract the row label into queue_row_text so the projection is covered by a unit test alongside the attachment-trailer handling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Wing <contact@winglee.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Right panel tabs: close button on the right The ✕ replaced the surface icon on hover in the leading slot. The icon now stays put on the left, the title takes the free space, and the ✕ fades in on hover in a trailing slot at the tab's right edge (same size, color and hover wash). The unsaved dot moves into that slot and swaps with the ✕ on hover. Chip padding is now symmetric (4px). * Right panel tabs: label the close button and pin its placement Give the trailing close slot a "Close tab" tooltip, a button role and an accessible label, matching the terminal drawer's "Close terminal" tooltip. Add a regression test that the close sits after the title at the chip's right edge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Wing <contact@winglee.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Compact model picker (on by default, General settings): effort slider with fast-mode toggle, provider button + provider page (Starred first), per-model effort memory, Cursor effort options on the slider, page-scoped keyboard shortcuts, per-edge/per-pixel edge fades (zui 667d0aa). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CI: seed the Rust cache from main and stop cancelling main runs
CI: run Windows tests as parallel jobs on a cheaper release profile
…e, nightly release-profile run - ui-tests.yml also triggers on scripts/*cursor-sdk* and cursor-*.yml, so the Rust half that moved out of cursor-compatibility still runs for those PRs. - linux-ci installs nextest after rust-cache restores ~/.cargo/bin, so the checksum-verified binary is the one used. - release-profile-tests.yml runs the Linux core and UI suites daily with --release (no cache save), backstopping the dev-profile switch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the expected conflicts: ui-tests.yml keeps core-tests and drops the iOS changes job (now in macos.yml) and the macOS/iOS-only path filters; preview-tests.yml drops the networking job, whose Rust tests now run in core-tests (Linux) and macos-native (macOS). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… nextest download - zeron-text's coretext tests are #![cfg(target_os = "macos")]; moving the mobile core tests to Linux silently dropped them. macos-native now runs binary_id(zeron-text::coretext). - run-macos-fixture.sh no longer writes /etc/hosts: preview hostnames are meant to resolve through WebKit's per-domain proxy, so a DNS fallback could let the fixture pass with that path broken. Retries and warnings stay. - nextest download: https-only, TLS 1.2+, retries, extract only cargo-nextest. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
macos-14 is two releases behind what macos-latest provides (macOS 26), and zeron-text's CoreText tokenizer check fails there (136/20000 differ, limit 20) while passing on macos-latest in main's runs. Keep the native, frame-source and iOS jobs on the image the old jobs used. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI: run Linux tests with nextest on a dev profile, one shared build
Lead with the desktop downloads per platform and present the curl installer as the headless (Linux server) option. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
deepwiki.com/badge.svg now sits behind a Vercel bot checkpoint, so GitHub's image proxy gets an HTML challenge page and renders a broken image. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sidebar's project filter still listed one row per checkout ("anara @
work-metal", "anara @ work-laptop"). It now lists one row per repository
across devices — named for its representative checkout, tagged with the
devices holding it ("@ work-metal · work-laptop", or "@ N devices") — and
filtering on it shows every checkout's sessions, in the list, the
Archived shelf and pin-drag validation alike.
The setting still stores one checkout's space id (this device's first),
so existing saved filters keep working and simply widen to their
repository; new sessions from the filter land on that local checkout.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Contribution Managerneeds triage · medium risk · untriaged Open review workspace · Revision Next actions:
This summary is maintained by the review app. Decisions and test evidence are recorded in the workspace; the app does not merge PRs. |
…#816) * Pin chip padding to Geist so insets don't follow the UI font A chip reserves its icon slot and insets with non-breaking spaces shaped in the interface font. The counts are tuned to Geist, whose space is 0.25em; with a monospaced UI font like Geist Mono (0.6em) every chip grew ~2.4x wider padding, leaving a wide gap between the icon well and the label. The padding runs are now shaped in the bundled Geist in the composer, sent user bubbles and queue rows, while the label keeps the interface font. Geist and Geist Mono share vertical metrics, so line boxes don't move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Center chips on their label's caps whatever the interface font paint_chip assumed Geist's metrics put the label's cap height on the row's middle. Other interface fonts move the baseline (the line takes the tallest face's ascent and descent) and GPUI rounds it to a device pixel, so chips could sit a pixel off their label (Noto Sans Mono: 1px low). Each chip row now moves its pill to the label face's cap middle, from the shaped line's metrics and the rounded baseline, in the composer, sent bubbles and queue rows. Also: tests that the padding, and only the padding, is shaped in Geist in the composer, bubbles and queue overrides; the stale "narrow" side-pad comment; the bubble's run building is its own function for the test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: wing-anara <wing@anara.com>
…eronsh#826) * test(ios): reproduce fixture fallback for unknown chat routes * fix(ios): return to sessions when a chat cannot be opened --------- Co-authored-by: José Gurruchaga <jsgrrchg@outlook.com>
* test(codex): document and probe native voice capabilities * feat(proto): define local voice session contracts * fix(harness): bound realtime media and prioritize control messages * feat(codex): attach realtime voice to an idle thread runtime * feat(engine): own voice leases and enforce subscription eligibility * feat(mcp): create standalone sessions on selected hosts Add chat/side kinds with compatible defaults, resolve projects within the selected device, and validate that host's harness and model catalogs before writing. Route transcript reads to the chat host and preserve send baselines across waits so missing session rows and delayed replies cannot return an old response. Cover invalid requests, remote catalogs, mixed batches, wait races, two-engine execution and interrupt delivery. Add an isolated MCP stdio smoke fixture. * feat(doc): persist canonical voice transcripts once * docs(mcp): document standalone and remote session workflows * feat(audio): add native capture and streaming playback * feat(audio): handle echo cancellation and voice interruptions * feat(ui): coordinate voice ownership over local engine RPC * feat(ui): extract the Bezel orb into a native Zeron component * feat(ui): replace the composer with a central orb during voice sessions * feat(mcp): constrain voice orchestration to subscription policy * build(voice): add microphone permissions and offline audio CI * test(voice): verify lifecycle, billing boundaries and rollout gates * fix(voice): preserve text transport and idle billing boundaries * feat(codex): connect subscription voice through native WebRTC * feat(engine): activate native voice ownership and preserve delegated work * feat(ui): enable native voice with central orb and retained drafts * fix(mcp): keep voice delegation on subscription-backed Codex * test(voice): document native transport and add opt-in connectivity smoke * fix(voice): close audit races and validate project subscription context * Fix Codex voice initialization and macOS microphone permissions * Allow Codex voice agents to delegate through upstream MCP * Make voice a persistent orchestrator with a dedicated stage * Add call controls and elapsed time to the voice stage * Remove decorative rings around the voice orb * Add voice selection menu and simplify stage artwork * Remove voice status labels beneath the orb * Fix dictation and realtime voice integration after rebase * Move Codex voice style selection into Voice settings * Restore window dragging from the voice stage titlebar * Open the voice stage immediately when starting a call * Smooth voice orb motion and speaking transitions * Remove the green audio pulse from the voice microphone control * Show the live voice orb above the orchestrator chat composer * Use full chat IDs in MCP reply instructions * Correlate MCP replies by message IDs instead of remote timestamps * Keep the voice stage open when launched from Settings * test(voice): add a split-host Codex voice compatibility probe * feat(voice): package a standalone desktop media runtime * feat(proto): define client-media voice control contracts * refactor(codex): separate voice signaling from local audio * feat(engine): own remote voice sessions with ephemeral leases * feat(rpc): route versioned voice controls to execution hosts * feat(voice): coordinate local media with remote Codex control * feat(ui): start voice on a selected remote Codex host * test(voice): cover cancelled preparations and stale session controls * test(voice): prepare desktop rollout validation and offline CI * feat(ios): add a pinned native WebRTC compatibility endpoint * feat(mobile): expose remote voice sessions through UniFFI * feat(ios): bridge native audio lifecycle to remote voice calls * feat(ios): add remote voice controls and registered host selection * test(voice): cover relay and iOS lifecycle and prepare live acceptance Add half-open and native callback regressions, stage-only diagnostics, complete runtime notices, and development test instructions. Keep real Mac/Fedora/iPhone acceptance opt-in for user validation. * refactor(orb): share the thinking-orb engine through zeron-orb Move the renderer-agnostic engine, presets, motion and a new OrbAnimator (clock, audio response, state crossfades) into crates/orb. The GPUI widget now delegates to the animator, so desktop and mobile paint identical frames. * feat(voice): rebuild iOS remote voice around the desktop orb and phone-call audio - Share the call view (phase, caption, levels, speaking, orb state) in voice-session; the client derives speaking because client media has no host meter. Keep owner-event headroom so level meters never overflow it. - Title remote orchestrator chats "Voice session" like local ones. - UniFFI: typed call state and end reasons instead of JSON, a distinct denied-microphone failure, and OrbRenderer frames from zeron-orb. - iOS: OrbView paints the desktop orb; start voice from the New session bar (live strip while a call runs), the session nav bar and the iPad sidebar; full-screen call stage with caption, transcript (flagged when Codex waits for an answer), mute, route picker and hang-up. - Phone-call audio: proximity sensor moves output to the earpiece, headsets win, calls survive screen lock, headset loss re-routes instead of ending. Install the WebRTC audio configuration globally so it is not reverted when the audio unit starts. * fix(voice): install the macOS voice runtime under codex-resources/voice Codex's helper only initializes from a codex-resources/voice directory and exits with code 23 on initializeRuntime anywhere else, which the desktop reported as "Voice could not connect". Bundle the runtime at Contents/Resources/codex-resources/voice, reject other locations up front, and report a helper that cannot start as an unavailable runtime. Ship the runtime byte-for-byte with OpenAI's Developer ID signatures instead of re-signing it ad hoc. * fix(ios): reopen the call stage from the live strip and redo voice settings The live strip in the bottom bar is a plain UIControl, which never sends .primaryActionTriggered, so tapping it did nothing; listen for touchUpInside. UI tests cover reopening the stage after the transcript and after minimizing. Voice settings now follow the Settings list style: the resting orb, the devices Codex can run on with their status, the voice as a pop-up menu, and what to expect during a call. * fix(ios): resume the orb animation when the app becomes active again At willEnterForeground the app still reports .background, so the display link never restarted and the orb stayed frozen after returning from Home. * fix(ios): guard voice preview state in Release builds * feat(voice): veil streamed caption words in on desktop and iPhone Move the transcript's renderer-agnostic streaming veil into zeron-veil so the voice call caption can use the same chunk fades on both platforms. A new CaptionVeil keeps the veil aligned with the ellipsized tail and fades the previous utterance out when the speaker turn changes; reduced motion shows text directly. Desktop recolors GPUI runs; iOS gets UTF-16 spans through CaptionFader and paints an attributed caption on a display link. The desktop transcript keeps its behavior; the iOS transcript is untouched. * fix(ui): step the voice stage aside when opening any session Opening a session only hid the stage indirectly, when the selection changed, so clicking the thread already under the stage (or the one you came from Settings) did nothing. open_chat now hides the stage explicitly; the call keeps running in the background. * feat(voice): reuse host orchestrator chats across calls * fix(voice): reserve orchestrator atomically and preserve rotation retries * feat(voice): rotate the orchestrator transcript after 1000 entries * feat(voice): package remote media runtime for Linux and Windows * feat(voice): use the installed Codex helper everywhere and stop bundling it Remote voice now plays audio through the client's own standalone Codex installation, like local voice, instead of a runtime packaged inside Zeron. The resolver moves to zeron-voice-media and is shared by the harness and the desktop media endpoint; ZERON_VOICE_MEDIA_DIR remains a development override. macOS, Linux and Windows packages no longer contain Codex binaries, GStreamer/GLib or the Visual C++ runtime, so Zeron takes on none of their redistribution or update obligations. package-voice-runtime.py stays as a development projection tool. Missing-runtime messages point to Codex's official installer, since npm installs lack the voice runtime. Notices and disclosures: - THIRD_PARTY_NOTICES: full MIT chain for the orb (Bezel, gpui-thinking-orbs, thinking-orbs), Codex NOTICE, OpenAI trademark and non-affiliation statement, and WebRTC for iOS. - The iOS app carries the orb's MIT notice; the Linux tarball now ships LICENSE and THIRD_PARTY_NOTICES.md. - Microphone purpose strings on macOS and iOS state that voice sessions stream audio to OpenAI; dictation stays on device. - Docs describe the voice data flow, and the acceptance checklist lists the legal and privacy decisions that block a public release. * feat(ios): show the voice call in the Dynamic Island and on the Lock Screen * fix(voice): hide the voice trigger when this device has no Codex The microphone in the sidebar footer only shows when Codex is installed and enabled on this device, following the same catalog the composer offers. It re-reads after Settings → Agents changes and when returning to a window that hides it. A start without Codex now asks for an install instead of a retry. * test(voice): seed the real rotation threshold in the remote rotation test * feat(voice): sound the start and end of a call on desktop and iPhone A call sounds as it is placed, before the microphone opens, and again when the user hangs up or a connected call ends. A failure to connect stays quiet and explains itself. Nothing sounds in between. The iPhone plays them as system sounds, so they follow the ringer switch. * perf(voice): skip the rate limits round trip before each call The pre-call probe read account/rateLimits/read, a ~1 s backend round trip, and ran twice per call (prepare and negotiate). Its result was never shown: the native backend makes the final usage decision. Dropping it saves about 1.7 s per call on desktop and iPhone. * perf(voice): release the orchestrator runtime 5 minutes after hang-up The voice orchestrator's Codex runtime and MCP servers hold ~200 MB. Its idle window was the generic 30 minutes, counted from the runtime's start, so a call ending late in the window left a runtime reaped right after the hang-up. It now idles 5 minutes, restarted at each hang-up. Hang-ups wake the reaper directly, so even a short call restarts the window, and a reap armed before a call began rechecks for it instead of cutting the call. * fix(voice): enable client-owned remote voice by default * fix(harness): read whole resumed histories and report why stdout failed 509818e capped every JSON-RPC line at 8 MiB on the transport shared by text chats. A resumed long chat returns its history in one response (54-71 MiB measured), so resume failed and was reported as an exit even though Codex was alive. Lift the cap (voice bounds its own messages) and keep the read error's cause for every waiting and later request. * fix(harness): name the rejected request in JSON-RPC errors again dca9a05 made peer rejections structured but dropped the method from their message, so chats showed 'JSON-RPC rejection -32600: …' without saying which request failed. Prefix the method again and render it as upstream did, keeping the code available for auth detection. * test: skip POSIX mode check on Windows hosts * fix: launch Codex batch shims from plain paths and answer account/read in fake * refactor(engine): remove the superseded local session RPC surface The client-media protocol replaced these methods; nothing calls them anymore. * refactor(harness): restore the shared JSON-RPC transport Return the stdio transport every agent uses to its upstream behavior (unbounded writes, no timeouts or polling timers) and keep only an additive notification route that is registered while a subscriber exists. * fix(ui): let the full-window stage show the window glass On translucent appearances the stage painted an opaque fill unlike the new-thread page. Drop the fill there and fade out the page it covers. * fix(ui): show the full-window stage over Settings Settings replaces the workspace layout and never mounted the stage, so opening it from there did nothing. Mount it beside the settings sidebar and fade the page it covers. * fix(voice-session): keep the live caption on the newest turn Turns arrive interleaved: a user final often lands after the answer has started streaming. Keep each turn's text separately and caption the newest one, so a late final never takes the caption back and deltas never restart it. Desktop and mobile now share this state. * ci: build the desktop UI only in the UI workflow The offline-linux job compiled zeron-ui without the WebKitGTK and JSON-GLib packages its build script requires, so it failed every run. Drop its UI step; the UI workflow already runs those lib tests and now also runs the orb geometry test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Claude CLI lists Haiku 4.5 as claude-haiku-4-5-20251001 while the curated catalog uses claude-haiku-4-5, so the exact-ID overlay added a second "Haiku 4.5" row to the model picker. Treat a -YYYYMMDD snapshot of a curated ID as that curated row. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eronsh#818) * Cut startup memory from ~700 MB to ~200 MB on large local profiles - Registry: local-only replicas apply writes directly instead of queueing them forever (178k queued ops / 70 MB snapshot on an affected profile); coalesce the unsent tail per row with an edge, preserving push notifications, the per-op size limit and multi-op batches; compact bloated snapshots on load. - Docs store: cap the WAL, truncate it at open, and VACUUM once when free pages dominate (again after registry compaction). - Boot: read run journals backwards from the end; share one notify watcher for diff_sync and spaces with batched changes and a stream bridge; skip archived chats unless a transcript stream has them open. - UI: notify only when watched state actually changed; fire clock-driven transitions (stale, device offline, send grace) at their deadlines instead of ticking every second. - Fix the e2e generated-image test on macOS (/var vs /private/var). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address review: watcher races, Linux inotify sharing, deferred VACUUM - fs_watch: release the batch worker flag under the same lock as the "no new changes" check, so a registration racing the worker's exit is never lost. - fs_watch: give inotify/kqueue one watcher per installed root. A shared instance let a checkout symlinking into another (npm link) alias its watch descriptors, and a failed recursive watch leaked its partial watches for the life of the process. macOS/Windows keep one watcher. - fs_watch: rescan a live registration whose covering OS watch was replaced (a nested path taking over from a dropped recursive root, a mode change); nothing observed the swap. - diff_sync: serialize every discard on one lock, whether or not the checkout has an entry, so two discards of one root never interleave. - registry: fold writes into rows only for Local profiles, which can never attach an edge. Re-seeding folded rows as full upserts on a later attach could revive rows another device deleted meanwhile. - store: keep only the WAL truncate in DocsStore::open; the engine runs the thresholded VACUUM on a blocking task after boot instead of on the boot path (engine assembly, iOS client, local import). - ui: refresh the clock-dependent presentation caches when a clock transition fires, so the frame that reverts it (a revived session or device) notifies and re-arms the next deadline; side chats now watch clock transitions too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Wing <contact@winglee.io>
…zeronsh#813) * Embed the browser on Windows and preview images in tool calls - Browser tabs render pages inside Zeron on Windows (WebView2), matching macOS and Linux. - Expanded tool calls preview the images they touch and free them on collapse. Right-click any transcript image to copy its path or the image itself. - Paths, commands and output in expanded tool calls can be selected and copied. - Fix: answering an agent question with multiple lines now grows the card instead of spilling text below it. * Address review: repin zui upstream, harden the Windows browser and previews - Depend on zeronsh/zui dce5c1f (zui#15 merged) and gpui-component 47ccd07 instead of patching every gpui crate to a personal fork. - Tool image previews: refuse UNC/device/drive-relative paths in the UI and, before any filesystem call, lexically jail requested paths in the engine (canonicalizing \\host\share opened an SMB session and leaked the user's NTLM hash); read chunks on the blocking pool; bound decodes with the shared raster limits; release keys a chip stops showing; decode base64 off the UI thread; copy sanitized SVG; cap output lines. - Windows browser: fail closed on unreadable navigation targets; police subframes and cancel external-scheme launches; open tabs only for user-initiated popups (handled first); deny permission requests; disable web messages and host objects; cancel downloads silently unless the page itself is the file; recover from a browser-process exit on Reload; give each BrowserData its own InPrivate profile (no persistent fallback); sweep only folders of exited processes; never remount from an older composition generation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Wing <contact@winglee.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ronsh#820) * engine: turn snapshots stay out of the checkout's object database The "Latest turn" snapshot ran `git add -A` under a throwaway index, which writes a blob for every unignored file into the checkout's own .git/objects. Nothing referenced those blobs, nothing bounded them, and the git add had no timeout. A checkout with unignored build directories collected 20 GB of loose objects that way, and git's auto-maintenance then tried to repack them after every agent commit: 23 concurrent pack-objects holding 117 GB on a 24 GB machine. Each snapshot now writes into its own object directory under the data dir, with the checkout's database as a read-only alternate, and that directory is deleted with the snapshot. A turn-diff capture layers its snapshot on the turn's, so it only writes what changed. Stores left by earlier runs are swept at start. Snapshots are also bounded: untracked files over 8 MiB and untracked directories over 10,000 files or 256 MiB are left out, and the whole snapshot runs under a 120 s timeout with kill-on-drop children. * Address review: partial budget, stable layered diffs, snapshot lifetime - Over the untracked budget, leave out the largest units until the rest fits instead of every untracked path: one big directory or a pile of assets no longer hides the files a turn creates. - Layer the "Latest turn" capture on the turn-start tree (read-tree into the throwaway index) and leave out what either side left out, so a directory crossing the budget mid-turn keeps its turn-start state instead of diffing as thousands of added or deleted files. - Drop a chat's turn snapshot (and its on-disk store) when the chat is gone or archived without a viewer, and when a new turn's snapshot fails or its cwd is no longer a checkout, instead of diffing against an older turn. - capture_git children are kill_on_drop (all read-only), so a timed-out snapshot takes its ls-files calls down too. - Store paths are absolute: git resolves GIT_OBJECT_DIRECTORY against the checkout, so a relative data dir broke every snapshot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Wing <contact@winglee.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…r do (zeronsh#748) * Harden the Claude and Codex adapters: no early "done", Stop kills, sends never do Builds on the unmerged runtime-survival work (6b25087c, branch zeron/prevent-sub-agent-collapse): Cancel/sends no longer tear down the agent process, perTaskStopAffordance, images ride the steering mailbox, background work holds the idle reaper. Premature "done" (sound + notification mid-turn), two Claude causes reproduced live on CLI 2.1.286 and seen 27 times in real journals: - a `now` steer aborts the turn and the CLI emits a result for it; that result was held, then released by a 5s quiet timer while the steered turn ran a long tool. - `--resume` of a session whose background tasks died emits two empty results while the new prompt is still queued. Every stdin user line now carries a uuid and the CLI's command_lifecycle frames decide the turn end: a result while one of our messages is still written/queued is a boundary, superseded when that message starts. Codex: only the turn in flight settles (stale/duplicate completions are ignored), steers confirm on their userMessage item via clientUserMessageId, a stop waits for turn/started (an earlier turn/interrupt is rejected and the turn ran on), and commands still running past their turn report as background work. Stop is the one teardown: the user's Stop ends the turn and the whole runtime, including shells the CLIs start in their own process groups (snapshot the descendant tree, kill it after the agent). Sends, steers, "send now" and model/effort switches keep the runtime: Claude adopts new config with set_model/apply_flag_settings, Codex on the next turn/start. Stop/Send ordering (found by the new chaos suite, every harness): a Stop could run before the send queued ahead of it (ignored, or the earlier steer re-ran after it), and a send after Stop landed in the dying runtime and was dropped. Commands now act in the order sent, and a runtime being torn down takes no prompt (late arrivals re-dispatch). Also: no false "limit reached" error when overage carries a rejected window. Tests: fake-CLI scenarios captured from live traffic (each fails on the old code), mock-engine race tests, and opt-in live suites against the real CLIs: runtime_survival_live and adapter_chaos_live (10 invariant- checked scenarios; 30/30 Claude Sonnet 5.5, 30/30 Codex gpt-6-luna). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address review: OpenCode stop/attachments, Codex config, orphan replay, parked Stop - OpenCode: a turn stop the server rejects or never acknowledges tears the runtime down (mailbox closed first) instead of reporting the turn stopped while it may still run. The stopped-turn idle check now unwraps v1's /global/event envelope; it never matched, so every stop and interrupt waited out the 2s grace. - OpenCode: messages queued mid-turn keep their images. - Codex: a message carrying a new model/effort/tier mid-turn waits for its own turn/start with that configuration; turn/steer carries none. - Engine: an accepted-but-unconfirmed message replays with the request it was sent with (images, configuration, id), not the chat's latest request. - Engine: Stop ends a parked runtime that still holds background work. - pi_rpc tests: new RunControls/SteerMessage fields (CI compile failure). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Every harness adopts model switches in place; reaper waits out the wake Live-tested all nine local harnesses (runtime_survival_live): Claude and Codex already kept their process across a model switch; the other seven restarted it. Now: - OpenCode: the model and effort ride the next prompt (2.x: set on the session first). A switch sent mid-turn starts its own prompt. - Pi: set_model / set_thinking_level on the live process before the message it was sent with. - ACP (Grok, Devin, Hermes, Antigravity): the session's model, effort and options are selected again over the protocol between turns; a switch sent mid-turn waits for the session to go quiet, ahead of later messages. A set_config_option answer without configOptions now records the value set. - Cursor: the shim names the new model on the switching message's send and every send after it; it never joins a batch or native steer with the messages sent before it. Engine: when background work drains, the idle reaper allows a full window before reaping. The agent reports the drained set a moment before the turn its completion wakes, and reaping in that gap lost the wake (zeronsh#831, caught by the new live repro). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Durability matrix across all harnesses, and what it caught The chaos suite now runs every local harness (claude, codex, cursor, opencode, grok, devin, pi, hermes, antigravity) and adds queued bursts, Stop while idle, a long silent command, an agent crash mid-turn, two chats at once and tool approvals. `restart_durability` quits the app mid-turn and force-kills it mid-turn (a separate process group, SIGKILLed), then checks nothing reads as still running, the killed turn resumes and finishes, and the conversation keeps its context. Prompts are conversational: Grok refuses exact-token demands as injection. Fixed along the way: - A force-killed turn whose reply never reached disk (the doc saves on a debounce) was never resumed: recovery only looked for a streaming entry. - A force-killed Claude chat in a symlinked folder (/var, /tmp on macOS) resumed in a fresh session, its history gone: the stored cwd is the resolved path until a turn's Done; resume now compares directories. - Sends held for a turn-boundary agent (Hermes, Antigravity) lost their images and model, and each handover pinged "done" while they waited. They now run as sent, and no turn ends with a completion while a message the user already sent is still on its way (held, or still in the command log after the agent outran a burst). - A Stop did not cancel messages sent before it that were held for the turn's end, or whose dispatch was still in flight: the next send thawed them, or they started the stopped work again in a fresh runtime. - ACP agents, Pi, OpenCode and Cursor only killed the agent's process group: a command the agent ran in its own session (Devin) outlived Stop and quitting the app. Teardown now takes the whole descendant tree, as Claude and Codex already did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drop a needless borrow in the ACP config selection Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Stop is one boundary: no early Done, no replayed steer (review) From the adversarial review (both reproduced against simulated providers first, each test fails without its fix): - Claude: "Send now" over a turn end held for a queued steer released that Done the moment the stop was asked, before the CLI answered. The host then sent the replacement, and the stopped turn's late result settled it early and cleared its pending delivery. The stopped turn's one Done now goes out when the stop settles (its own result, or the settle window), and a result arriving just after it from that turn is dropped. - ACP: a stop left a steer's in-flight `_session/steering` call alive; its promptRequired answer, once the turn was cancelled, queued the cancelled message again and it ran after all. The stop now drops the call, and steering answers are ignored while a turn stops. CI: `tree_teardown_reaches_shells_in_their_own_process_group` raced the fixture's setpgid (two descendants existing did not mean the group did). The fixture now reports its pid once it leads its group, and is killed even when an assertion fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Claude: never drop a replacement's own end as a stopped turn's late result 8c575d4 dropped any result arriving within the stop's settle window unless the CLI had confirmed a new message started. A CLI that sends neither command_lifecycle nor a replay never confirms one, so a quick replacement's own result was dropped and the chat stayed Working (reported in review, reproduced with a simulated CLI). A result is now dropped as the stopped turn's only when nothing has happened since the stop's Done: no message written, no output streamed. Anything else takes the ordinary path — held while a message is unconfirmed, settled by the existing fallback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Claude: judge a stopped turn's late result before it expands e29fb82 counted every non-Done event as fresh activity, but a result frame expands to Usage then Done: a stale result's own Usage marked it fresh, and it was forwarded as a second Done (an Errored one fails the parked session and its runtime). Reported in review, reproduced with a simulated CLI. The stale check now runs on the result frame before it expands, and drops it whole (usage included). Activity since the stop is a message written or genuine content — text, reasoning, tool calls and results, questions, subagent events — never usage or a completed-message marker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eronsh#777) * Fix durable remote send and host wake recovery * Couple accepted chat rows to durable server host wakes * Make Linux browser captures wait for mapped and presented frames --------- Co-authored-by: Wing <contact@winglee.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Subagents section lists running subagents as plain rows, then folds everything finished into a Finished dropdown (closed by default) with Completed and Failed lists inside it. Each list collapses on its own and pages at ten rows. A "● N" pill shows how many subagents are running: on sidebar chat rows (in place of the working spinner), under the titlebar's explorer button, and beside the Subagents title. To badge chats that are not open, the session row now carries `running_subagents`, published by the engine as subagents open and settle. It is optional on the wire and stale rows count as zero, like the working indicator.
The pill hung under the button and crossed the titlebar hairline. Mark the button itself instead: its face breathes in the activity tone and a small count badge sits on its corner, both drawn over the button so titlebar layout is unchanged.
The pill no longer replaces the row's own activity indicator, so compact titles stop shifting. In compact rows it stands where the time stamp is while subagents run; in regular rows it leads the status label.
The titlebar badge had no explicit line height and used the UI font, so its digit sat about a pixel high; the sidebar pill's digits sat half a pixel low. Both now draw their count through one helper: mono digits in a box exactly as tall as the pill or badge, with a small optical lift (and, for the badge, a rightward shift) measured from rendered captures at one, two and three characters.
Geist Mono's digits are centred on the line box and each digit's ink is centred in its advance (checked from the font tables), so with the line height equal to the box height nothing needs nudging. The earlier optical offsets were fitted to two monitor scales and gave the same result there as none; at a whole-number scale they would only add error. Residual misalignment is device-pixel rounding of the baseline, at most half a device pixel.
… 99+ badge The pill's dot was a box centred in the pill while the digits' vertical position followed the text engine's own metrics and rounding, so at fractional display scales the two could differ by a device pixel. The pill now paints its background, dot and digits in one canvas: the digits' baseline is snapped to a device pixel and the dot is centred on the digits' middle from that baseline, so they share a centre at any scale. A capped label ends in '+', whose thin arms leave the eye a wider gap than a digit does; trim the padding after it by a pixel in both the pill and the titlebar badge.
The badge was a laid-out box, whose text landed on a rounded device pixel and read as the digit sitting left of centre. Paint the badge and its digits in one canvas instead: the digits are centred on the badge's real middle, with the baseline placed as in the sidebar pill. Measured for every digit 1-9 on 1.25x and 1.875x captures: centred to within half a device pixel, exactly for most.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wing-anara
force-pushed
the
subagents-finished-groups
branch
from
October 8, 2026 18:28
fdb0d69 to
c7dfc37
Compare
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.
Mirror of zeronsh#647, opened on this fork to run CI. Not for merging.
Problem
The Explorer's Subagents list mixes everything in one column. Once a few subagents have finished they crowd out the ones still running, and nothing anywhere else in the app says a chat has subagents running: the sidebar shows the same working spinner whether the parent is thinking or ten subagents are.
Change
Subagents section
Running pill (● N)
To badge chats that are not open, the session row now carries
running_subagents: the engine publishes it as subagent sinks open and settle, and clears it when the run ends. It is#[serde(default)]on the wire, written in both the workspace and registry docs, and read throughview::running_subagents, which counts a row that has not been refreshed withinSESSION_STALE_MSas zero, like the working indicator. The UI folds the staleness-checked count into its session presence key so a stale count clears without a new frame.Screenshots
Rendered with GPUI from a fake transcript (three running, two completed, one failed). Left: Finished closed, the default. Right: Finished opened.
The explorer button with the panel closed (its face pulses):
Sidebar, compact (left) and regular (right): the first two rows have 3 and 2 subagents running, the fourth has 12 while its parent is idle.
Counts of one, two and three characters ("99+" is the cap). The badge and the sidebar pill draw their digits in Geist Mono, whose metrics centre the digits on the line (checked from the font tables). Both are painted rather than laid out, so the digits are placed exactly: in the pill the dot is centred on the digits' middle from the same snapped baseline (0.0 device pixels apart on a 1.875x capture), and the badge's digits are centred on its real middle. The padding after the "+" of a capped count is trimmed by a pixel, since its thin arms read as extra space.
Every single digit in the badge, captured at 1.875x. Measured against the badge on 1.25x and 1.875x captures, each digit is centred to within half a device pixel, and exactly for most.
The images live on a separate
pr-assets-subagent-groupsbranch of my fork, so they are not part of this diff.Tests
files::subagents: grouping, default open state, slot counts as groups open and close, paging.view::running_subagents: counts while fresh, zero once stale or absent.workspacedoc round trip carries the count.e2e::session_row_counts_running_subagents_until_the_run_ends: a scripted run with two subagents reports 2, then 1 as one settles, then 0 when the run ends.cargo test -p zeron-ui --libpasses (1361). Run in parallel it occasionally fails one of the text-selection tests (a different one each time; they share a global selection); it passes with--test-threads=1.Notes