New bridge commands: "model" lists the available models (same catalog as
the desktop picker), "model N" / "model <name>" sets a per-sender
override passed as agent.overrides.model on every turn, and
"model default" resets to the app default. In-memory scope: resets on
app restart, never sticks a session to a bad model.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a transport-agnostic messaging bridge in @x/core that drives the
session/turn runtime from your phone: WhatsApp links as a companion
device via Baileys QR pairing (self-chat is the command channel),
Telegram long-polls the user's own bot token. Commands: list, resume N,
new, status, stop; anything else runs a turn in the current session and
replies with the final assistant text. Ask-human questions are relayed
and answerable from the phone.
- packages/core/src/channels/: bridge (commands, settle watcher,
ask-human relay), WhatsApp + Telegram transports, config repo, service
(lifecycle, status fan-out, QR rendering, current-transport reply
routing, dynamic baileys import)
- Settings → Mobile tab: enable toggles, QR pairing, bot token,
sender allowlists
- Strict sender authorization: WhatsApp self-chat + allowlisted numbers
(LID-aware), Telegram allowlisted chat IDs only
- Telegram offset persisted across restarts; generation-guarded
WhatsApp socket lifecycle; vitest coverage for the bridge
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Remove the "Mark as read" toggle for Everything else end-to-end (UI, IPC,
config, sync auto-read, markSectionRead) — it could bulk-mark backlog and
archived mail read on the server during cache-recovery syncs
- Sending an edited draft now reuses the draft's stored In-Reply-To/References
instead of self-referencing its own Message-ID (external clients thread
correctly); standalone drafts send as fresh messages
- Keep draft autosaves out of the sync pipeline: skip DRAFT history messages
(no more md/knowledge-event leaks, phantom "New email" notifications, or
per-autosave LLM reclassification) and mirror the draft body onto the cached
snapshot surgically instead of waking the sync loop
- Only recreate a Gmail draft when update fails with 404/410 — transient
errors no longer pile up duplicates
- Emptying a draft and closing now deletes it from Gmail (matches Gmail)
- Forward keydown out of message iframes so j/k/e/r etc. keep working after
clicking into an email body; restore expanded quotes across theme reloads
- Composer footer wraps on narrow panes: formatting toolbar + Discard move as
one unit instead of overflowing into each other
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Welcome step's "Use Next or your arrow keys" hint stays in the bubble
but is dropped from the voice clip (its tail kept synthesizing with
gibberish). Steps gain an optional voiceText override, honored by both
the clip generator and the live-TTS fallback; the generator also takes
step ids as args to re-roll individual clips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bundle the 11 tour narration clips as renderer assets so the tour never
calls ElevenLabs (works offline and signed-out). useVoiceTTS gains
speakUrl() which plays a ready URL through the same queue/analyser path,
keeping lip-sync and cancellation intact. Clips are regenerated with
scripts/generate-tour-audio.mjs, which parses TOUR_STEPS from
product-tour.tsx and synthesizes via @x/core. Default TTS voice is now
s3TPKV1kjDlVtZbl4Ksh.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
List: j/k/arrow cursor with focus ring, Enter/o open, e archive, # trash,
u read toggle, c/n compose, / search, g+i / g+d view switch, ? help overlay,
layered Escape (suggestions > link bar > composer > thread > search).
Thread: r / a / f open reply, reply-all (with fallback), forward.
Composer: Cmd/Ctrl+Enter send (commits half-typed recipients), Cmd/Ctrl+
Shift+C/B reveal+focus Cc/Bcc, Esc closes with draft autosave.
Perf: content-visibility row virtualization, pointer-events suppression
while scrolling, memoized ThreadRow so cursor moves and page appends only
re-render affected rows.
Animations: 160ms row slide-out on archive/trash/draft-delete with snap-back
on failure, fast thread-detail and inline-composer open transitions, 140ms
ease-out animated scrolling for cursor-follow and thread-open (respects
prefers-reduced-motion).
Also fixes stale search-result rows on archive/read actions and Escape
closing the whole compose modal while a suggestion menu or link bar was open.
Co-Authored-By: Claude Fable 5 <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>