Recasts the old "track blocks" as "tracks" — directives stored in a
note's frontmatter rather than inline YAML fences and HTML-comment
target regions. The motivation is UX: the inline anatomy made notes
feel like config, leaked into the editing surface, and competed with
the writing flow. Frontmatter is invisible to the body editor, so
moving directives there reclaims the body as just markdown the user
wrote.
The runtime agent now edits the note body freely via standard
workspace tools rather than rewriting a constrained target region.
Each track's instruction names an H2 section to own; the agent
finds or creates that section, updates only its content, and
self-heals position on subsequent runs.
Triggers are now a unified array per track. cron / window / once /
event in any combination, including multi-trigger setups (the
flagship example: a priorities track that rebuilds at three
day-windows and reacts to incoming gmail / calendar events).
window is forgiving — fires once per day anywhere inside its
band — so users opening the app late in the morning still get the
morning run.
The chip-in-editor is gone. Tracks are managed from a right-side
sidebar opened by a Radio-icon button at the top-right of the
editor toolbar. Cmd+K is no longer a Copilot entry point — search-
only — pending a more intuitive invocation surface later.
Today.md ships as the flagship demo of what tracks can do, with a
versioned migration system so future template updates roll out
cleanly to existing users (existing body preserved, old version
backed up).
Copilot is tuned to listen for any signal that the user wants
something dynamic — not just the literal word "track". Strong
phrasings get acted on directly; one-off questions about decaying
information are answered first and then offered as a track. New or
edited tracks run once by default so the user immediately sees
content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: restyle email block with Gmail-style layout and avatar
* style: apply Google Sans/Roboto font to email block
* feat: add Gmail inbox-style multi-email block with accordion rows
* style: fix sender name casing, weight, and email display in expanded view
* feat: emails inbox block with container layout, two-line rows, Gmail title style
Signed-in users can now connect Gmail and Calendar directly through
Rowboat instead of going through Composio. Cleaner connection, no
third-party in the data path.
How it works:
- Click "Connect Google" anywhere it appears (sidebar, onboarding,
settings) and the system browser opens to a Rowboat-hosted page.
Authorize Google there and the app picks up the connection
automatically — no client id or secret to paste.
- Token refresh happens through Rowboat's backend, so Google
credentials never need to live on the user's machine.
- Disconnect cleanly revokes access on Google's side too.
Migration for existing Composio users:
- A one-time modal explains that we've moved off Composio and asks the
user to reconnect Google directly.
- Their old Composio Gmail / Calendar connections are disconnected
automatically when the modal first appears.
- All previously-synced emails and calendar events are preserved on
disk — the new connection picks up where Composio left off rather
than re-downloading the last week from scratch.
- "I'll do this later" dismisses the modal permanently; the user can
still reconnect anytime via the connectors UI. (Sync stops in the
meantime; nothing is deleted.)
Other coverage:
- BYOK mode (users who paste their own Google client id + secret) is
unchanged — same modal, same local OAuth flow, same behavior.
- Composio integrations for non-Google services (Slack, Linear, etc.)
are unaffected. Only the Gmail and Calendar paths moved.
- The "Connect Google" button label and connection state now apply
uniformly to Gmail + Calendar (one OAuth grant covers both).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds INotificationService with an Electron implementation, plus a deep-link
dispatcher (rowboat://) for routing notification clicks back into the app.
Notifications:
- New `notify-user` skill + builtin tool. Title, message, optional primary
link, optional secondary actions. Supports https:// (opens in browser) and
rowboat:// (opens in app) targets.
- ElectronNotificationService holds strong refs to active Notification
instances so click handlers survive GC (otherwise macOS click silently
no-ops).
- Calendar meeting notifier fires 1-min warnings with "take notes" /
"join + take notes" actions backed by deep links.
Deep links (rowboat://):
- forge.config.cjs declares the protocol; main.ts wires single-instance
lock, setAsDefaultProtocolClient, open-url (mac), second-instance (win/
linux), and first-launch argv extraction.
- New deeplink.ts dispatcher with dispatchUrl(url): main-handled actions
(rowboat://action?type=...) vs renderer navigation (rowboat://open?...)
via app:openUrl IPC. Includes pending-URL buffering for first-launch
delivery before the renderer is ready.
- Renderer parseDeepLink supports file / chat / graph / task /
suggested-topics targets.
- New app:consumePendingDeepLink IPC for renderer one-time drain on mount.
Refactor: extractConferenceLink moved out of calendar-block.tsx into
shared lib/calendar-event.ts (used by both the block and the take-notes
deep-link handler)
Previously identify() only fired during the OAuth completion flow, so
existing installs (signed in before analytics shipped) and every cold
start of v0.3.4+ would emit main-process events under the anonymous
installation_id until the user happened to re-sign-in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consecutive plain tool calls are now grouped into a single collapsible
row instead of rendering as individual items.
- Header shows the currently-executing tool name live with a vertical
ticker animation, then switches to "Ran N tools" on completion
- Expanding the group reveals each tool call individually collapsible
- Tool calls with pending permission requests render individually
- Special cards (web search, composio connect, app actions) excluded
Captures per-LLM-call token usage tagged by feature (copilot chat,
track block, meeting note, knowledge sync), plus sign-in / sign-out
and identity. Renderer and main share one PostHog identity so events
from either process resolve to the same user.
See apps/x/ANALYTICS.md for the event catalog, person properties,
use-case taxonomy, and how to add new events.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Track block YAML gains optional `model` and `provider` fields. When set,
the track runner passes them through to `createRun` so this specific
track runs on the chosen model/provider; when unset the global default
flows through (`getTrackBlockModel()` + the resolved provider).
The track skill picks up the new fields automatically via the embedded
`z.toJSONSchema(TrackBlockSchema)` and adds an explicit "Do Not Set"
section: copilot leaves them omitted unless the user named a specific
model or provider for the track. Common bad reasons ("might be faster",
"in case it matters", complex instruction) are called out so the
defaults stay the path of least resistance.
Track modal Details tab shows the values when set, in the same
conditional `<dt>/<dd>` style as the lastRun fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bring back per-category model selection that 5c4aa772 dropped, plus add a
new track-block category. Each is a BYOK-only override on `LlmModelConfig`
(`knowledgeGraphModel`, `meetingNotesModel`, `trackBlockModel`); signed-in
users always get the curated gateway default and never hit the on-disk
config.
Three helpers in core/models/defaults.ts — `getKgModel`,
`getTrackBlockModel`, `getMeetingNotesModel` — each check `isSignedIn`
first (fast path) and fall through to `cfg.<field> ?? cfg.model` for BYOK.
The model is now picked at the invocation site rather than via runtime
agent-name branching: each top-level `createRun` for a polling KG agent
or a track-block update passes `model: await getXxxModel()`. The `model:`
declarations on the affected agent YAMLs are dropped — they were dead
code under the per-call override. Standalone (non-run) callers
`track/routing` and `summarize_meeting` use the helpers inline.
Settings dialog and the two onboarding flows surface the two new fields
("Meeting Notes Model", "Track Block Model") next to the existing
"Knowledge Graph Model"; `repo.setConfig` persists all three per-provider.
Note: the signed-in `RowboatModelSettings` panel still has its
now-defunct kg selector; that's a UI cleanup for a later pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The model dropdown was broken in two ways: it wrote to ~/.rowboat/config/models.json
(the BYOK creds file, stamped with a fake `flavor: 'openrouter'` to satisfy zod
when signed in), and the runtime ignored that write entirely for signed-in users
because `streamAgent` hard-coded `gpt-5.4`. Model selection was also globally
scoped, so every chat shared one brain.
This change moves model + provider out of the global config and onto the run
itself, resolved once at runs:create and frozen for the run's lifetime.
## Resolution
`runsCore.createRun` resolves per-field, falling through:
run.model = opts.model ?? agent.model ?? defaults.model
run.provider = opts.provider ?? agent.provider ?? defaults.provider
A new `core/models/defaults.ts` is the only place in the codebase that branches
on signed-in state. `getDefaultModelAndProvider()` returns name strings;
`resolveProviderConfig(name)` does the name → full LlmProvider lookup at
runtime. `createProvider` learns about `flavor: 'rowboat'` so the gateway is
just another flavor.
`provider` is stored as a name (e.g. `"rowboat"`, `"openai"`), not a full
LlmProvider object. API keys never get written into the JSONL log; rotating a
key in models.json applies to existing runs without re-creation. Cost: deleting
a provider from settings breaks runs that referenced it (clear error surfaced
via `resolveProviderConfig`).
## Runtime
`streamAgent` no longer resolves anything — it reads `state.runModel` /
`state.runProvider`, looks up the provider config, instantiates. Subflows
inherit the parent run's pair, so KG / inline-task subagents run on whatever
the main run resolved to at creation. The `knowledgeGraphAgents` array,
`isKgAgent`, and the per-agent default constants are gone.
KG / inline-task / pre-built agents declare their preferred model in YAML
frontmatter (claude-haiku-4.5 / claude-sonnet-4.6) — used at resolution time
when those agents are themselves the top-level agent of a run (background
triggers, scheduled tasks, etc.).
## Standalone callers
Non-run LLM call sites (summarize_meeting, track/routing, builtin-tools
parseFile) and `agent-schedule/runner` were branching on signed-in
independently. They all route through `getDefaultModelAndProvider` +
`resolveProviderConfig` + `createProvider` now; `agent-schedule/runner`
switched from raw `runsRepo.create` to `runsCore.createRun` so resolution
applies to scheduled-agent runs too.
## UI
`chat-input-with-mentions` stops calling `models:saveConfig`. The dropdown
notifies the parent via `onSelectedModelChange` ({provider, model} as names);
App.tsx stashes selection per-tab and passes it to the next `runs:create`.
When a run already exists, the input fetches it and renders a static label —
model can't change mid-run.
## Legacy runs
A lenient zod schema in `repo.ts` (`StartEvent.extend(...optional)` plus
`RunEvent.or(LegacyStartEvent)`) parses pre-existing runs. `repo.fetch` fills
missing model/provider from current defaults and returns the strict canonical
`Run` type. No file-rewriting migration; no impact on the canonical schema in
`@x/shared`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>