One-time startup migration that scans ~/.rowboat/runs/*.jsonl and converts
each legacy run into the new event-sourced runtime, so existing chats and
agent history are visible and continuable after the runtime rewrite.
Mapping (a run is one whole conversation; turn boundaries are user messages):
- copilot_chat run -> 1 session (sessionId = original runId) + one turn per
user message (turn ids <runId>-tNNN).
- every other run -> a single standalone turn whose id IS the original runId,
so live-note (lastRunId) and background-task (runs.log)
history views resolve it via sessions:getTurn with no
renderer change.
- code_session runs are skipped (Code mode still uses the old runtime).
- convert.ts: pure convertRun(); synthesizes a reduceTurn/reduceSession-legal
event log (exact request refs, permission replay, denials -> runtime isError
results, reasoning parts preserved) and validates via the reducers before
returning.
- migrate.ts: defensive IO runner. Successful runs are moved to runs-archive/
(that move is the idempotency guard); failures are left in place (still
served by the runs:fetch fallback) and retried next launch. Per-run
try/catch never blocks boot. Writes config/runs-migration.json.
- main.ts: runs before sessions.initialize() and logs an [runs-migration]
summary (N turns across M sessions, skipped/failed counts).
- Tests: 13 cases over 4 real (redacted) run fixtures — conversion, session
chaining, deny->error, reasoning preservation, transcript fidelity,
archive-on-success, skip, quarantine, idempotency, and read-back through the
real FSTurnRepo/FSSessionRepo.
Dry-run on real data: 19 runs -> 3 sessions + 21 turns, 0 failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
apps/x gains npm test — builds @x/shared first (the core and renderer
suites import its dist), then runs the shared, core, and renderer
vitest suites in dependency order; per-suite test:shared/test:core/
test:renderer scripts for targeted runs.
.github/workflows/x-tests.yml runs that harness on every pull request
touching apps/x (plus pushes to main and manual dispatch): pnpm 10 /
Node 22, frozen lockfile, pnpm store cached against apps/x's lockfile.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Decision: defer code-mode unification; no deletions. The legacy runs
runtime (runs/, AgentRuntime/streamAgent, runs:* IPC, App.tsx legacy
tab state) remains solely for code-mode sessions. AGENTS.md gets the
authoritative carve-out section: what stays and why, the no-new-callers
rule (headless work uses agents/headless.ts), the temporary fallbacks
that die with unification, and the scoped future project (rowboat-mode
code prompts as composition turns; direct-mode ACP streams as a
delegated turn kind). Turn-runtime design doc status updated to
implemented-and-live.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Analytics: the new runtime emitted usage into durable events but never
reported it to PostHog (only the permission classifier self-reported).
New IUsageReporter seam on the turn loop — invoked once per completed
model call, after the durable append, failure-isolated. The real bridge
emits the same llm_usage event as the old loop; useCase/subUseCase come
from the AsyncLocalStorage context the stage-6 headless runners already
establish via withUseCase, defaulting to copilot_chat for UI-driven
session turns (matching the old createRun default). Capturing at the
loop covers ALL turns — the session bus never sees headless ones.
Voice: the new renderer path rendered <voice> tags literally and never
fired TTS. The live overlay now extracts completed <voice> blocks
(robust to tags split across deltas) into chatState.voiceSegments;
App speaks new segments via the existing useVoiceTTS when voice output
is on, skipping segments streamed before a session became active. Tags
are stripped from both the streaming message and persisted assistant
messages, mirroring the legacy display-time strip.
Tests: usage report assertion in the runtime suite; four voice tests
(split-delta extraction, per-call scan reset, state stripping+segments,
persisted-message stripping).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Background-task and live-note history views called the legacy runs:fetch
for ids that are now turn ids (ENOENT after stage 6). New shared loader
src/lib/agent-transcript.ts fetches sessions:getTurn first and falls
back to runs:fetch so pre-migration histories stay readable (fallback
dies with the runs runtime in stage 7); turn transcripts render through
the same buildTurnConversation used by the chat views. Unit-tested
(turnToTranscript mapping + failure surfacing).
The chat-log download used runs:downloadLog with what is now a session
id. Added sessions:downloadLog (concatenates the session's turn logs
into one JSONL via the save dialog); the sidebar tries it first and
falls back to runs:downloadLog for legacy background tabs.
Remaining renderer runs:* callers are code-mode only (use-code-chat,
runs:events feed) — the deliberate stage-7 carve-out.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New src/agents/headless.ts wraps the turn runtime in the old headless
calling convention: startHeadlessAgent returns the turn id immediately
(callers record it in pointer files / bus events before completion) and
a done promise settling with { outcome, state, summary };
runHeadlessAgent awaits it. throwOnError reproduces the old
waitForRunCompletion({ throwOnError }) semantics via HeadlessRunError;
summary reproduces extractAgentResponse (last assistant text);
toolInputPaths replaces the run-bus tool-invocation subscriptions by
reading invoked calls from durable turn state. Model overrides pair the
caller's model id with the app-default provider. Unit-tested against an
injected fake runtime (7 tests).
Migrated all nine callers:
- background-tasks/runner: handle start wrapped in withUseCase so tools
(notify-user) read the use case via AsyncLocalStorage
- knowledge/live-note/runner: same shape, gains withUseCase
- pre_built/runner, knowledge/agent_notes: run-and-wait
- knowledge/tag_notes, label_emails, build_graph: edited/created paths
now come from turn state (toolInputPaths) instead of bus streaming
- knowledge/inline_tasks (both sites): summary text feeds the existing
marker parsing unchanged
- agent-schedule/runner: fire-and-forget start with
AbortSignal.timeout(TIMEOUT_MS); dropped the now-unused
runsRepo/agentRuntime/idGenerator plumbing
Code-mode sessions remain on the runs infrastructure (deliberate
carve-out until stage 7 scoping); agents/utils.ts stays for
launch-code-task's extractAgentResponse. The notify-user useCase gate
already prefers ALS with a best-effort fetchRun fallback, so it works
unchanged for turn ids.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Third application of the reference mechanism: session turns whose
system prompt + tools are byte-identical to the context predecessor's
materialized snapshot persist { agentId, model, inheritedFrom } instead
of ~70KB per turn (measured: turn 2 of a session is now ~1.1KB total).
Inheritance is decided at createTurn by equality against the
materialized predecessor; the model stays concrete, and on
materialization the inherited record's own agentId/model win — a rule
the new test matrix caught as a real bug (the chain base's model was
overriding a mid-session model switch). Reducer invariants: inherited
snapshots must reference the context predecessor; tool identity arrives
via invocation events.
Test matrix per review: prompt-diff -> full snapshot, tools-diff ->
full snapshot, model-switch -> inherits with concrete model, multi-hop
chains materialize, standalone turns never inherit, unreadable
predecessor falls back to full, cyclic inheritance is corruption,
sessions denormalize the model from inherited snapshots.
The inspector now handles sessions too (auto-detected): overview with
per-turn status/size/input preview, --turns to cascade full turn
inspection. Documented in a new repo-root AGENTS.md (storage layout,
reference model, inspector usage, invariants) with a CLAUDE.md pointer.
Breaking for dev turn files: wipe ~/.rowboat/storage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two debuggability follow-ups from reviewing real turn files:
- Request refs are now compact strings — 'context' | 'input' |
'assistant:<index>' | 'toolResult:<toolCallId>' — so a raw JSONL line
reads naturally: {"messages": ["assistant:0", "toolResult:toolu_…"]}.
Same exact-match reducer invariants, via parseRequestRef.
- npm run inspect-turn -- <turnId|path> [callIndex] [--full]: prints,
per model call, the EXACT provider payload (resolved system prompt,
tool list, wire-form messages with user-message context woven in, and
the response/failure) — rebuilt by the same composer the loop sends
through. This is the missing viewer for the derived-not-duplicated
request design: the file stores facts once; the inspector shows what
the model actually received.
Breaking for turn files written since the previous commit (dev only):
wipe ~/.rowboat/storage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
model_call_requested carried three duplications that dominated turn-file
size (measured on a real 647KB two-step turn): the system prompt (28KB)
and the 38-tool snapshot (40KB) repeated per call despite being byte-
identical to turn_created.agent.resolved, and messages re-inlining the
whole current-turn transcript (230KB re-stating a 207KB tool result).
ModelRequest is now a list of references into the turn's own events —
call 0: [{context}?, {input}]; call N: [{assistant: N-1}, ...that
batch's toolResults in source order]; re-issue after an interruption:
[]. Every referenced byte exists exactly once in the file; the measured
turn drops to ~285KB and each further model call costs ~200 bytes. The
reducer's ordering invariant got stricter: reference lists are matched
exactly against the transcript.
Debuggability is now byte-for-byte at the wire level: ResolvedModel
gains encodeMessages (the structural->wire conversion — user-message
context weaving, attachment rendering, tool-result enveloping, i.e.
convertFromMessages), and composeModelRequest rebuilds the exact
provider payload (resolved system prompt + wrapped tools + materialized
prefix + resolved refs, encoded). The loop transmits exactly the
composer's output, so the durable file plus the composer reproduce what
the model received — pinned by a property test asserting composed ==
sent for every call, and a 2KB size guard on request events. A bridge
test demonstrates the woven wire form (no raw userMessageContext).
Breaking for existing dev turn files (same schemaVersion, pre-release):
wipe ~/.rowboat/storage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two follow-ups from live testing:
1. Permission/ask-human buttons were disabled while a turn waited for
input. PermissionRequest disables via isProcessing, whose old-runtime
meaning was 'agent busy right now' (false while awaiting approval);
the sessions meaning is 'turn not settled' (true while suspended).
Cards in both chat views now take the isThinking flag — actively
working disables, waiting on the user enables — while the composer
and stop button stay on isProcessing.
2. The middle-pane (full-screen) chat rendered blank: App.tsx has its
own conversation block whose activeChatTabState was still assembled
from legacy standalone states. activeChatTabState is now the single
hook-backed source of truth for the active tab; the sidebar props,
the middle pane, the merged background-tab map, and the submit guard
all derive from it (plus hook-driven activeIsProcessing/
activeIsThinking), so both views land on identical data.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ChatSidebar assembles the active tab's view state from individual props,
not from chatTabStates — and the four permission/ask-human maps were
still fed from legacy standalone state that nothing populates under the
sessions runtime. Pending permission requests (recorded correctly in the
turn file) therefore never rendered, leaving the chat on the
'Thinking…' shimmer.
- Feed pendingAskHumanRequests / allPermissionRequests /
permissionResponses / autoPermissionDecisions from the sessions hook
(legacy fallback until stage 7).
- New optional isThinking prop on ChatSidebar: waiting on a
permission/ask-human no longer shimmers (isProcessing still blocks
the composer, per the state contract).
- Session load failures are now visible: an error item renders in the
chat instead of a silent blank, and the store logs load/reload
failures to the console.
Verified the full data path against the real on-disk session (the
suspended list-downloads turn): conversation renders and the pending
permission card map carries the request, under StrictMode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The chat stuck at 'Thinking…' with a completed turn on disk: the stores
subscribed to the sessions:events feed in their constructors while
cleanup was effect-managed, so React StrictMode's dev-mode
mount -> cleanup -> mount cycle tore down the one subscription and never
restored it. The renderer then loaded a mid-turn snapshot and was deaf
to every event after (the backend turn completed fine).
Feed attachment is now an effect-managed, idempotent connect() on both
stores; hooks return its disconnect as the effect cleanup. New tests pin
the bug: a StrictMode-style connect -> cleanup -> connect store test, and
the hook tests now render under a StrictMode wrapper.
Also removed the composer's runs:fetch model-freeze effect (the ENOENT
noise): sessions carry model/permission per message, so the picker stays
live for existing chats.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The active chat tab now renders from useSessionChat; background tabs keep
the legacy cached view state until stage 7 retires it.
- Send path: sessions:create + sessions:sendMessage with the structural
UserMessage (userMessageContext carries datetime + middle-pane — the
cache-safe per-message tier) and per-message turn config (model
override + composition: workDirId/voice/search/codeMode, permissionMode
-> autoPermission). Attachments/mentions ride as real content parts —
the old 'as unknown as string' cast is gone.
- Stop / permission / ask-human handlers route through the hook against
the latest turn; permission scope is passed as audit metadata (session
grants are a deferred v2 feature). Stop state clears when the turn
settles.
- Chat history lists sessions (sessions:list/delete); selecting one just
switches the active session — the hook loads the conversation, so the
200-line run-log replay in loadRun is gone.
- Work-dir persistence unchanged: the per-chat sidecar file is keyed by
the session id, which the resolver reads via composition.workDirId.
Renderer tsc/vite build clean; all 324 workspace tests pass; renderer
lint identical to baseline (108 pre-existing findings, none in new code).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The wire and state layer for the UI cutover; App.tsx integration follows
separately.
- shared/ipc.ts: eleven sessions:* invoke channels + the sessions:events
push feed (SessionBusEvent via z.custom, like runs:events). The generic
preload bridge needs no changes.
- main: sessions:* handlers as thin pass-throughs to the DI'd sessions
service; startSessionsWatcher forwards the session bus to all windows;
startup awaits the session-index scan before the renderer can list.
- renderer architecture per review guidance — all logic in framework-
agnostic, dependency-injected modules; hooks are thin
useSyncExternalStore subscriptions; components will consume pre-digested
view models:
- client.ts: narrow SessionsClient over window.ipc (fakeable).
- feed.ts: one shared sessions:events consumer with fan-out (factory
for tests).
- turn-view.ts: pure derivations — live overlay (deltas accumulate,
canonical events clear), TurnState -> ConversationItem[], and the
session chat state (permission/ask-human maps re-manufactured in the
runs-era shapes so existing components render unchanged;
isProcessing/isThinking contract preserved).
- store.ts: SessionChatStore (seed via getSession/getTurn, shared
reduceTurn over live events, prior-turn freezing, unknown-turn
reconciliation, stale-load guard, action routing) + SessionListStore.
- hooks/useSessionChat + useSessions: thin wrappers, deps injectable.
- renderer test infra added from scratch (vitest + jsdom +
testing-library); 29 tests across stores, pure views, feed, and hooks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five bridges adapt existing app code to the runtime seams, each unit-
tested against injected fakes:
- RealAgentResolver: loadAgent + dynamic builders -> immutable
ResolvedAgent. Model precedence override > agent > app default; tool
descriptors from BuiltinTools (zod -> JSON schema) and MCP
attachments (toolId schemes builtin:/mcp:server:tool); ask-human as
the async requiresHuman tool. System prompt composed via
composeSystemInstructions — extracted verbatim from streamAgent so
old and new runtimes share one implementation — driven by the new
opaque RequestedAgent.overrides.composition (session-sticky inputs
preserve provider prefix caching; per-message context stays on
UserMessage.userMessageContext as today).
- RealModelRegistry: models.json -> live AI SDK model; one streamText
step (stopWhen: stepCountIs(1)) normalized into deltas, durable step
events, and the completed assistant message.
- RealToolRegistry: execTool dispatch (builtins + MCP) with per-call
abortRegistry bracketing and tool-output-stream -> durable progress.
- RealPermissionChecker: getToolPermissionMetadata rules (command
allowlist, workspace file boundaries); session grants deferred.
- RealPermissionClassifier: classifyToolPermissions with conversation
context now threaded through the classifier batch.
Interface refinements (doc updated): ToolExecutionContext gains
turnId/toolCallId; classifier takes a batch with turnId + messages;
TurnRuntime dep renamed bus -> lifecycleBus and SessionsImpl bus ->
sessionBus for strict-PROXY DI. Container registers the whole stack
(FS repos rooted at WorkDir/storage); the legacy agentRuntime
registration became lazy to survive the new import-order cycle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SessionsImpl per session-design.md, constructor-injected and tested
entirely against a mocked ITurnRuntime:
- sendMessage: per-session lock, typed TurnNotSettledError while the
latest turn is non-terminal (terminal statuses including failed/
cancelled allow continuation), context as { previousTurnId } ref,
turn-file-first write ordering (a failed session append leaves a
benign orphan turn and no advance), denormalized agent/model on
turn_appended, default title from the first message.
- Dedicated respondToAskHuman endpoint (async_tool_result wrapper);
respondToPermission / deliverAsyncToolResult pass turn-runtime
rejections through. stopTurn aborts a live advance or cancels an
at-rest turn; resumeTurn re-enters idle turns (never at startup).
- In-memory index: startup scan (session files + each latest turn for
status; corrupt files yield errored entries without aborting),
write-through updates, deletion guard against late-settle
resurrection. Bus events (turn-event / index-changed) defined in
@x/shared as the renderer IPC contract.
- FSSessionRepo: date-partitioned append-only JSONL + list/delete;
deleteSession removes the session file only.
- runHeadlessTurn: standalone turns (sessionId null, auto permission,
no human) for background/knowledge/scheduled callers.
29 new tests covering the session-design §13 matrix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No behavior change — all 67 turn tests pass untouched. The ~620-line
advance method becomes an internal TurnAdvance class holding the
per-invocation context, with one named method per spec phase
(applyInput, closeInterrupted*, evaluatePermissions, classifyBatch,
denyUnresolvedWithoutHuman, executeAllowedTools, suspendIfPending,
completeIfFinished, failIfExhausted, runModelStep, cancel). The main
loop now reads as a direct transcription of the design doc's §18
algorithm; TurnRuntime itself is reduced to construction and
createTurn/getTurn/advanceTurn plumbing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The TurnRuntime per turn-runtime-design.md: constructor-injected,
container-ignorant, no per-turn state — every advanceTurn reconstructs
from the JSONL log, so normal execution and crash recovery share one
path.
- runtime.ts: createTurn/advanceTurn/getTurn; durable-barrier appends
before side effects; sequential sync tools with progress; async tool
exposure + durable suspension; permission pipeline (checker →
optional classifier batch → human/deny fallback, fail-closed on
checker errors); cancellation with synthetic results; recovery that
re-issues interrupted model calls and continues after interrupted
sync tools; model-call-limit exhaustion with machine-readable code.
- fs-repo.ts: date-partitioned append-only JSONL, strict line
validation, path-traversal rejection, per-turn in-process locking.
- stream.ts: hot execution stream — buffers before the consumer
attaches, outcome independent of consumption, close-drops-events.
- context-resolver.ts: chain-walking materialization of context
references with cycle detection.
- Seam interfaces for stage-4 bridges: model/tool registries,
permission checker/classifier, agent resolver, lifecycle bus, clock.
- shared reducer: track classification failures durably so failed
classifications are never re-run.
67 new tests: all nine §26 end-to-end scenarios (mocked deps,
recovery boundaries seeded directly) plus repo/stream/resolver suites.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Durable contracts for the new turn/session runtime, shared by core and
renderer:
- turns.ts: zod schemas for all 16 durable turn events, TurnContext
(previousTurnId ref | inline messages), ModelRequest with contextRef
and current-turn-only messages, ephemeral delta types, reduceTurn
enforcing every spec invariant, and pure derivations
(deriveTurnStatus, turnTranscript, outstanding work).
- sessions.ts: session_created/turn_appended/title_changed schemas,
reduceSession, and the SessionIndexEntry projection.
- vitest wiring for packages/shared (config, build-excluded tests);
92 tests covering happy paths, recovery-shaped histories, and one
named test per corruption invariant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Turn runtime spec amended after review: context references with a
context resolver, recovery that re-issues interrupted model calls and
continues after interrupted sync tools, turn_failed.code with
model-call-limit, default maxModelCalls 20.
New session layer spec: append-only session JSONL, turn-file-first
write ordering, in-memory write-through index with startup scan,
reject-while-busy sendMessage, dedicated ask-human endpoint, deferred
v2 shapes for queueing/steering/grants/compaction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(meetings): inline meeting prep under next meeting
Resolve a meeting's attendees against the knowledge base and render each
attendee's existing person.md inline beneath the next upcoming meeting in
the Meetings view. Deterministic (email-exact match, then unambiguous
name/alias), no LLM in the hot path.
- core: resolveMeetingPrep() + meeting-prep:resolve IPC
- renderer: notes-first rows with expandable person.md; unmatched attendees
collapse into a 'no notes yet' row with a Create note action that hands
off to the Copilot
- prep re-resolves when a People note changes
* feat(meetings): per-meeting prep toggle
Add a Prep toggle to every eligible meeting row, not just the next one.
The next meeting still auto-expands; other meetings resolve their attendee
notes lazily when their toggle is opened. All-day and solo events get no
toggle.
* feat(meetings): org matching, cached index, and field-bleed fix
#2 Matching: resolve the meeting's external companies from attendee email
domains (and matched people's Organization field), excluding the user's own
domain, and surface those org notes in a Companies section. Normalize display
names (strip '(via ...)' suffixes) before name/alias matching.
#3 Perf: meeting prep reads a cached knowledge index instead of rescanning the
whole knowledge dir on every resolve; invalidated whenever a file under
knowledge/ changes (wired to the workspace watcher).
Fix: extractField in the knowledge index consumed newlines after the label, so
an empty field (e.g. **Role:**) bled the next line's value. Restrict it to
spaces/tabs so empty fields resolve to undefined. Also strip the folder prefix
from link targets for display (Organizations/Rowboat Labs -> Rowboat Labs).
* feat(meetings): proactive prep notes generated 6h ahead
Generate a meeting prep note ~6h before each meeting and surface its brief
in the prep card.
- Generator (meeting_prep_brief.ts): assembles roster + 'last time' recap
(extracts the prior instance's Action items) + agenda, ordered by meeting
type (recurring -> recap first, one-off -> agenda first), then one model
call for a 'what matters' brief. Writes knowledge/Meetings/prep/<slug>-<date>.md
with eventId/recurringEventId frontmatter. Reuses the summarizeMeeting LLM
path (configured model, useCase: meeting_prep).
- Scheduler (meeting_prep_scheduler.ts): calendar-aware tick (5m), generates
prep within the 6h lead window, state file dedupes, self-heals on changes.
- Card: shows the brief (bulleted, compact) above a People list whose rows
link out to each person's note instead of rendering it inline. Generated
prep notes are kept out of the past-notes table.
* chore(meetings): poll prep scheduler every 15m instead of 5m
- Inbox load: add an mtime-keyed in-memory cache to listInboxPage so it no
longer re-reads and JSON.parses every cached thread (bodies included) on
every call — only files whose mtime changed are re-parsed. Speeds up the
Important→Everything-else handoff, live reloads during sync, and re-opening
the inbox tab.
- Add Ctrl+Tab / Ctrl+Shift+Tab to cycle to the next/previous tab (wraps
around), alongside the existing Cmd+Shift+] / [ shortcuts.
- Improve bar: make the tone preset buttons square (rounded-md), left-aligned,
with a bit of vertical gap so wrapped rows aren't cramped.
- Inbox (gmail-shell): map the bespoke --gm-* palette onto the app design
tokens (background/foreground/border/accent/primary), drop the unloaded
"Inter" font override so it inherits the app font, and remove the redundant
".light .gmail-shell" block so theming follows :root/.dark.
- Composer: rebuilt on the shadcn Button/Input/Dialog primitives with app
tokens; bring the AI describe-and-write bar and tone presets into reply mode.
- Auto-generated reply block (editor.css): retokenize the Gmail-blue pill
buttons and compose surface onto app tokens; inherit the editor font.
- Add a Gmail-style "n" shortcut to open the composer from the inbox.
The app sets no application menu, so it relied on Electron's default menu
for the zoom accelerators — and on Linux that menu bar is suppressed by the
frameless `hiddenInset` title bar, leaving the shortcuts unhandled. Wire
them directly via `before-input-event`: Cmd/Ctrl + (+/=) zooms in,
(-/_) zooms out, and 0 resets, clamped to zoom levels -3..3.
`event.preventDefault()` stops the keystroke from leaking into the editor.
* feat(onboarding): add Code Mode setup step
Adds a new onboarding step (3, between Connect and Done; Completion moves
to 4) shown in both Rowboat and BYOK paths. It informs the user up front
that Code Mode needs Claude Code and/or Codex installed and signed in
locally (claude login / codex login), then offers a master 'Enable code
mode' toggle that reveals per-agent toggles.
Downloads do NOT block onboarding: on Continue, selected-but-not-yet-
installed engines are provisioned fire-and-forget in the main process,
and their live % surfaces in Settings -> Code Mode. Approval policy is
left at the default ('ask'); users adjust it later in Settings. The step
is skippable like Connect Accounts.
To make an onboarding-started download show its progress in Settings, the
module-level provisioning store (startProvisioning/useProvisioning) is
lifted out of settings-dialog.tsx into lib/code-mode-provisioning.ts and
shared by both.
* feat(onboarding): simplify Code Mode agent rows and refine copy
- Show just the agent name (Claude Code / Codex) with a toggle and a
green check when ready; drop the per-row download/sign-in clutter
- Value-led description: use your existing Claude Code or Codex
subscription inside Rowboat, signed in locally via the terminal
* fix(onboarding): BYOK saved entire model catalog as assistant models
testAndSaveActiveProvider persisted the provider's full fetched catalog
into config.models. That field is the user's curated assistant-model
list (rendered as one row per entry in Settings > Models), so every
available model showed up as a separate assistant-model dropdown. Seed
it with just the selected model; users add more from Settings.
* feat(byok): decouple chat model picker from config.models
BYOK config.models was doing double duty: it both seeded the in-chat
model picker AND was rendered as a per-entry dropdown stack in Settings.
That coupling forced a tradeoff between a clean Settings UI and full
model choice in chat, and baked a stale catalog snapshot into config.
Decouple them so BYOK matches the signed-in experience:
- Chat picker (signed-out) now lists the full live provider catalog from
models:list, filtered to providers the user has a key/baseURL for, with
the saved default model leading. No longer limited to config.models, so
newly released models appear without re-saving.
- Settings assistant model collapses to a single default-model dropdown
(removed the .map() stack + add/remove). config.models is now just the
one default; save/load/delete logic is unchanged.
- Remove the circular background and pulsing ring behind the completion
checkmark; place the tick inline to the right of the "You're All Set!"
heading
- Increase spacing between the step indicator and step content
* fix(onboarding): center step connector line with circles
The connector line was vertically centered against the circle+label
column, landing below the circle's center. Absolutely position the
label below the circle so the row height equals just the circle and
items-center aligns the line to the circle's center.
* feat(onboarding): refresh welcome layout & indicator spacing
- Place logo inline to the right of the Welcome heading
- Move the tagline badge below the heading with more breathing room
- Add space between the step indicator and step content
The compose modal/card hardcoded light-mode CSS variables unconditionally
(introduced in 851c3be6 "gmail contacts should use light mode"), overriding
the theme variables inherited from .gmail-shell. As a result the reply/compose
box rendered as a white card with dark text even in dark mode.
Split the single hardcoded block into theme-aware blocks following the existing
.gmail-shell convention: dark values as the unprefixed default, light values
under `.light`. Light mode is unchanged; dark mode now gets a proper dark card.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: suppress notification spam on app re-open
- Add background_task notification category (default ON) so users can
toggle off background-task pings via Settings → Notifications
- Route notify-user builtin through notifyIfEnabled gate so the
category toggle takes effect
- Add 60s startup grace period: background-task notifications fired
within 60s of launch are suppressed, killing the reopen flood where
all queued agents complete at once
- Suppress new_email notifications for emails older than 5 min so
Gmail's startup backlog replay doesn't surface day-old mail
Fixes both issues reported by Ramnique.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: skip automatic chat_completion ping for background task agents
Background task runs were triggering "Response ready / Your agent
finished responding" on every completion. Skip the automatic
chat_completion notification when finalState.runUseCase ===
'background_task_agent' — background tasks notify explicitly via
notify-user when they have something worth surfacing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: correct notification deeplinks and skip internal agent pings
- runtime.ts: also skip chat_completion ping for knowledge_sync
useCase (agent_notes_agent was opening notes view on click)
- sync_gmail.ts: new_email notification now links to specific
email thread (rowboat://open?type=email&threadId=...)
- App.tsx: add email case to parseDeepLink, parse threadId param,
wire threadId through applyViewState, update viewStatesEqual
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: distinguish background-agent notifications from auto knowledge-sync
Per team clarification, two background types are handled differently:
- knowledge_sync (auto knowledge-graph generation): never notifies;
skips the generic chat_completion ping entirely.
- background_task_agent (user-configured agents): notifies via its
own notify-user path, gated behind the toggleable "Background
agents" category, deep-linking to the background-tasks page.
- runtime.ts: skip the generic chat_completion completion ping for
both knowledge_sync and background_task_agent (the latter notifies
via notify-user, so the generic ping would duplicate it).
- builtin-tools.ts: notify-user branches on getCurrentUseCase() —
background agents route through notifyIfEnabled('background_task')
with a bg-tasks deeplink default; chat agents notify directly.
- App.tsx: add the bg-tasks deeplink target (ViewState, parseDeepLink,
applyViewState, currentViewState).
- settings-dialog.tsx: rename the category label to "Background agents".
- runner.ts: wrap the background-task run in withUseCase so tools see
the correct use-case context.
* fix: route coding-session notifications through notifyIfEnabled
Code-mode status-tracker was calling notificationService.notify() directly,
bypassing the user's notification-category toggles. Routed both calls
through notifyIfEnabled() with correct categories:
- needs-you state → agent_permission
- idle after 30s → chat_completion
Removed now-redundant container/INotificationService plumbing.
* fix: fall back to persisted run useCase when ALS context is missing in notify-user
AsyncLocalStorage does not propagate across the background-task agent's
async generator — getCurrentUseCase() returns undefined inside notify-user,
causing the background_task_agent branch to be skipped and the Background
agents toggle to be ignored.
Fix: load persisted useCase from run record via fetchRun(ctx.runId) when
ALS is falsy. Lazy dynamic import() avoids the known module-init cycle.
Background-agent notifications now correctly respect the toggle and only
fire when the app is in the background, with deep link to the bg-tasks page.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>