-
feat(x): robot-driven code mode, unified chat, tabless navigation, voice everywhere (#855)
发布于
2026-08-13 09:06:23 +00:00 - feat(x): todo view keyboard accessibility + new-chat affordances
Pre-existing work-in-progress from the working tree, committed separately
from the code/nav/voice overhaul that follows: focus-visible states for
hover-only controls, Enter/Space to edit items, and optional onNewChat /
onFocusComposer hooks on TodoView.Co-Authored-By: Claude Fable 5 noreply@anthropic.com
- feat(x): robot-driven code mode, unified chat, tabless navigation, voice everywhere
One holistic overhaul of four coupled problems:
Code mode — direct drive removed; everything goes through Rowboat, and the
Rowboat path actually works now:- A code session IS a chat session on the turns runtime (same id). The old
rowboat mode was broken at the foundation: sessions lived on the legacy
run runtime while the chat pane had moved to the new one, so binding
loaded nothing and sends failed. - code_agent_run resolves the session's agent/cwd/policy/engine-model
server-side from the turn's sessionId (new ToolExecutionContext.sessionId
threading), and keys the ACP conversation per session so follow-up turns
resume the coding agent instead of cold-starting. - Deleted: codeSession:sendMessage IPC, CodeSessionService.sendMessage,
suppressReplay (replay muting is now unconditional), CodeChat,
use-code-chat, ResizableRightPane, the "Who drives" picker and the
header mode Switch. CodeSession.mode is gone from the schema. - Status tracker rewritten on the turn event spine; pre-overhaul session
metas get chat-session files backfilled at boot; launch-code-task drives
via sessions.sendMessage with a settle watcher.
Unified chat — the Code view's right pane is the same assistant chat as
everywhere (full composer: attachments, mentions, model picker,
permissions, voice). Code sessions appear in chat recents like any
conversation, carrying the repo pin.Navigation — the editor-style tab strip and all _rowboat* sentinel tabs
are gone. Every section is plain view state funneled through one
closeAllSections() reset, which kills the stuck-section bug family by
construction (Code was the one section bulk clears couldn't close — the
"sometimes clickable, sometimes not" reports). Single chat binding via
bindChatToRun(); one markdown editor keyed by path; expandedFrom is a
ViewState snapshot so closing full-screen chat restores ANY section; the
dock toggle works from every section; body pointer-events guard clears
Radix modal leaks on navigation.Voice — the Home composer gains mic + call; transcripts route to the
composer that owns the mic (a spoken to-do lands on the list, not in a
chat); ownership token released on every cancel path; dock recording UI
gated on ownership; the code chat's rogue second microphone died with it.Verified: typecheck + lint clean; 607 core / 107 shared / 313 renderer
tests green; live Playwright drive of the dev app (11/11 checks: boot,
section round-trips, session creation on the new runtime, dock binding +
pin, dock toggle in Code, mic/call on Home and the code dock).Co-Authored-By: Claude Fable 5 noreply@anthropic.com
- feat(x): one hover mode — unified surface, no pop-out button, companion chat decoupled
The companion ("hover mode") had grown two different surfaces and a hard
coupling to the app window's chat. This unifies all of it:One surface for every entry point
- The composer call button now starts the SAME companion voice session
(Skipper card: mascot + pins + text panel) that ⌥⇧Space summons — it
used to open the mascot-less pill via the 'share' preset, which was the
reported confusion. Screen share moved into the call menu and stays one
tap away on the Skipper's share pin (sticky). - Surface choice is now derived from devices only (camera on → pill, the
self-view surface; otherwise → Skipper card). The old tuckOrigin
write-once flag — which made the card unreachable from composer-started
calls and raced device acquisition on a 5s time-box — is gone. - startHoverCall() is the one flow behind the ⌥⇧Space relay, the tuck
handle, and the call button; re-asserts the companion surface when
already in a call (fixes the inert re-tuck), and the pinned path
recreates a destroyed companion window instead of wedging. - ⌥⇧Space is never a silent no-op: if the app never answers the tuck
relay, main falls back to the text card after 1.5s.
Pop-out button removed
- The dock header's pop-out duplicated hover mode — button, quickAsk:popOut
IPC, and the dead 'send-text' popout action are gone.
Companion chat decoupled from the app chat
- The companion holds its OWN session binding (hoverRunId + a second
SessionChatStore). Bar submits and call utterances create/continue that
session via a lean sender (same per-turn config: voice flags, frames,
search/code, fast-thinking default) — never the app's visible chat. - The TTS pipeline, call response panel, activity line, and quick-ask
answer mirror all read the hover session's store, so app navigation
mid-call never retargets or silences the conversation. - The bar's chip/switcher/new-chat rebind only the companion; switching
chats in the app no longer moves the hover conversation, and vice versa.
Calls started from a composer seed the companion from that chat, then
the bindings are independent. "Open in Rowboat" is the one deliberate
bridge (binds the app chat to the companion's conversation). - The bar's model/effort picks are companion-scoped instead of silently
overwriting the app composer's selection.
Verified: typecheck + lint clean, 313 renderer tests green, live Playwright
drive (7/7): call button → Skipper card, tuck relay → same card, companion
submit creates its own session with the app window untouched.Co-Authored-By: Claude Fable 5 noreply@anthropic.com
- feat(x): call button means "float THIS chat" — seed, retarget, defer-bind, card ↗
Completes the hover/companion binding model:
-
The composer's call button always floats the chat it lives on. No call →
starts the hover session bound to that chat (seeded from the tab binding,
which is authoritative — the old runId mirror missed fresh chats and
minted an orphan hover conversation). Call live on ANOTHER chat → the
button stays a call button ("bring this chat into the call") and clicking
re-points the live call: same devices, same Skipper, only the conversation
switches (stale reply audio cancelled). Call live on THIS chat → End call.
The composer now gets a per-chat callOnThisChat signal instead of the
global inCall flag deciding the button. -
Fresh-chat defer-bind: starting a call on an unbound chat arms a capture
of that chat's identity; when the first utterance/bar message creates the
hover session, the originating chat binds to it too (identity-checked so
a user who switched away is never hijacked). One conversation, both
surfaces — verified live: the companion's message renders in the app chat. -
↗ "Open in Rowboat" now shows on the Skipper card too (it was gated off
the primary hover surface), and lands the conversation in the SIDE PANE
instead of the maximized full view. -
Deleting the companion's chat clears the hover binding.
Verified: typecheck + lint clean, 313 renderer tests green, live Playwright
drive 9/9: seed-no-new-session, chip shows seeded chat, mid-call retarget
affordance + End-call flip, card ↗, fresh-chat defer-bind with the shared
conversation visible in the app.Co-Authored-By: Claude Fable 5 noreply@anthropic.com
- fix(x): review follow-ups — server-side pins, history migration, tracker fixes, binding bugs
Addresses Ramnique's review of #855, holding the changes to the "coding is
strictly a tool call" principle:Retiring code-specific special-casing
- "Is this a code session" is no longer a client decision: the session
layer merges authoritative composition pins (agent + cwd) server-side on
every sendMessage, via a DI-injected provider the layer knows nothing
about. Composer, voice, quick-ask, and background senders now all
assemble the identical code-mode prompt; the renderer chip is cosmetic. - Old code sessions keep their history: the runs→turns migration now
treats code_session runs as the chat sessions they are (the skip and the
copilot_chat-only session-shape check were obsolete); code-* timeline
rows drop, the conversation survives, and migrated files archive out.
The boot backfill is now the fallback for metas with no legacy run and
runs exactly once (marker file) instead of rescanning every launch. - The status tracker still reads the turn spine — session-level status
cannot see a coding agent's mid-turn approval card (the turn keeps
RUNNING while code_agent_run blocks on the ask) — but it no longer
misbehaves: events are processed strictly in arrival order (interleaved
async handling could apply transitions out of order and pin a spinner),
and an answered ask-human (async tool_result) now flips needs-you back
to working, so the badge stops lying and completion notifications fire.
Concrete bugs
- Background coding tasks can't hang 90 minutes on an approval no one will
answer: SendMessageConfig gains humanAvailable (default true); the task
runner passes false so gated tools auto-deny and ask-human is
unavailable; a suspended settle fails fast; the watch timer is cleared. - Code rail ↔ dock desync: the stale boundCodeSessionRef guard is gone
(bindChatToRun already dedupes on the live binding) and re-clicking the
already-selected rail session re-notifies, so "click the session again"
always re-asserts the binding. - Drafts survive A→B→A: rebinding uses a deterministic chat identity (the
session id) instead of a fresh uuid, so a half-typed message is restored
when you come back. - Ending a call releases the companion's mirrored binding, so the second
session store unsubscribes instead of double-processing the chat forever. - The body pointer-events safety net keys on the whole view state, so
file→file navigation also clears a leaked modal lock. - Stale popout device state is cleared on unpin — the next summon no
longer flashes the camera pill before morphing to the card.
Docs: turn-runtime-design.md no longer claims the legacy runtime serves
code mode; ANALYTICS.md code_session rows updated.Verified: typecheck + lint clean; 607 core / 313 renderer tests green
(migration test now asserts code_session history survives); live drives
23/23 across code smoke, hover binding, rail re-click, and draft survival.Co-Authored-By: Claude Fable 5 noreply@anthropic.com
- fix(x): drop the Video call menu entry; kill the last hover-remnant flash
-
The call menu loses "Video call" — the hover companion and Practice cover
the real uses; the preset stays valid for the engine (practice shares the
camera path, and the pill remains the self-view surface when a practice
call is minimized). -
The "old hover without the hat" sighting was the summoned text card — the
only hatless mascot left — flashing when a ⌥⇧Space summon's device
startup outran the 1.5s "nothing answered" fallback. The app now ACKS the
tuck relay the moment it starts handling it (quickAsk:tuckAck), which
cancels the fallback timer; the timer is also cleared on pin and on a
deliberate card show. Every remaining companion surface is intentional:
Skipper card / tucked mascot / pill tile all wear the on-duty hat, and
the hatless summoned card appears only as the explicit no-voice fallback.
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
- feat(x): tap right ⌘ while the mascot speaks = stop; hold stays one-gesture barge-in
While the assistant is speaking, a quick tap of right ⌘ used to silence the
audio and then drop the user into LOCKED hands-free capture — a hot mic
nobody asked for, whose next sound gets submitted. The gesture now
disambiguates the intent:- Tap while speaking → STOP the mascot: full interrupt (audio, the queued
reply backlog, and generation — an audio-only mute would go quiet and
then resume a beat later as new sentences streamed in), mic left closed. - Tap again (in silence) → hands-free listening, as always.
- Hold while speaking → unchanged one-gesture barge-in (silence + talk,
release sends) — interrupting to speak must never become a two-step.
Also fixes a decoupling miss this made load-bearing: the interrupt's
stop-generation half still checked the APP chat's processing state and
stopped ITS run — speech comes from the companion session, so the
interrupt now stops that turn (applies to the Skipper/pill stop button and
the full-screen call's interrupt too).Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Co-authored-by: Claude Fable 5 noreply@anthropic.com
下载附件