The backend's modelRecommendations entries are now { assistantModel,
taskModels? } (rowboatx-backend#18), mirroring models.json v2 vocabulary.
When a provider connect/sign-in seeds the assistant (and ONLY then), its
per-task recommendations become visible taskModels overrides — each
validated against the live list and skipped when equal to the chosen
assistant (only differences are written, same principle as the migration).
This restores pre-v2 economics for signed-in task models without hidden
defaults: fresh Rowboat sign-ins, new devices, and sign-out→sign-in round
trips all land with the lite-tier task models the app used to hardcode —
server-controlled, updateable without a release. Saved choices are still
never touched: no seeding happens when an assistant already exists.
- selection logic moves to @x/shared/initial-selection (core re-exports;
the renderer connect flow uses the same implementation)
- RowboatApiConfig accepts both the legacy flat shape and the nested one
(normalizeModelRecommendation), so backend deploy order and rollback
are non-events
- llm_initial_model_selected gains task_overrides_seeded
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Privacy rules encoded in one place (analytics/model-providers.ts): only
provider FLAVORS leave the app — never instance ids (future-proofing for
user-named instances), never apiKey/headers, never baseURL (local endpoints
can carry internal hostnames). Model ids allowed.
- llm_usage.provider now reports the flavor: captureLlmUsage maps the
instance id centrally (lazy, cached, falls back to the raw value — which
today equals the flavor, so history stays continuous)
- llm_provider_connected / _disconnected {flavor}: one event family across
all surfaces — BYOK at the repo choke point (new entries only; key
rotation is not a connect), rowboat at OAuth sign-in/out, codex at
ChatGPT sign-in/out
- person properties synced on every launch and after provider/assistant
changes: llm_provider_flavors (sorted, auth-derived included),
llm_provider_count, assistant_model + assistant_model_flavor
- llm_initial_model_selected {flavor, model, recommended, source}:
recommendation hit-rate for the initial-selection flow (connect /
onboarding / sign_in)
- models_config_migrated {had_assistant, materialized_overrides,
provider_count}: one-shot rollout health for the v2 migration
- ANALYTICS.md updated (llm_usage provider row, new lifecycle section,
person-properties table)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Task slots (config + runtime):
- taskModels gains backgroundTask and subagent; getBackgroundTaskAgentModel
resolves its own override instead of mirroring live-notes (migration
materializes the old mirror rule so effective models are unchanged)
- spawn-agent precedence: explicit per-spawn args > configured subagent
override > parent turn's model
- repo.setProvider rejects auth-derived flavors (rowboat/codex) and merges
over existing entries so key rotation keeps contextLength/reasoningEffort
Models settings, rebuilt:
- ModelSelectionSection: ONE Assistant model picker (with an Unavailable
badge when the saved model drops off its provider's live list) + all
seven tasks defaulting to "Same as Assistant" with resolve subtext and a
clear-override link. Replaces both the signed-in and BYOK screens; the
"Auto (recommended)" sentinel and both hardcoded preferredDefaults maps
are gone — connect-time resolution uses backend recommendations
- ProvidersSection replaces the old ModelSettings card grid: status cards
(Connected · N models / error + Retry), an add-provider dialog
(choose → creds → loading → first-vs-more result states → error with
manual-model fallback), and a manage dialog (masked key + Replace,
endpoint, Refresh models, Used-by list, disconnect with consequences
spelled out). Rowboat and ChatGPT are cards in the same list
- connecting a provider seeds the assistant ONLY when none is configured —
never replaces a saved choice
- models:getConfig IPC serves selections + credential-free provider
metadata; the probe machinery (use-provider-models, liveCredentials)
is deleted — the add flow validates credentials click-driven
Onboarding:
- screen 2 is the same ProvidersSection (onboarding variant) for BOTH
paths: "Add more providers" after sign-in, "Connect a model provider"
otherwise; Continue gates on an assistant model existing. The tile grid,
per-provider key forms, and ~350 lines of duplicate state are deleted;
one step sequence replaces the two path-dependent indicators
Picker:
- split-view browse (Popover + cmdk Command): providers left with counts
and error dots, selected provider's models right, ←/→ switches provider,
↑/↓ navigates, typing collapses to a flat cross-provider list that
matches provider NAMES as well as model ids (searching "rowboat" now
works). Scoped/static pickers stay flat; public API unchanged
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The v1 config fossilized three schema generations: a top-level
provider/model pair (single-provider era), a providers map duplicating
credentials AND caching model lists (pre-dynamic-listing era), plus
defaultSelection and flat category overrides (hybrid era) — while several
effective models existed only as hidden curated branches in code.
v2 is the intended shape:
{ version: 2,
providers: { <instance-id>: { flavor, apiKey?, baseURL?, … } },
assistantModel: { provider, model },
taskModels: { knowledgeGraph?, meetingNotes?, liveNoteAgent?,
autoPermissionDecision?, chatTitle? },
deferBackgroundTasks? }
- one-time boot migration (core/models/migrate.ts, invoked from
ensureConfig): evaluates the v1 resolution rules — including the curated
signed-in defaults — one last time and writes the answers explicitly, so
the simplified resolvers produce identical effective models; task
overrides are written only where the old effective model differs from
inherit-from-assistant; curated ids survive solely as frozen literals in
the migration
- defaults.ts: getDefaultModelAndProvider reads assistantModel, period;
getCategoryModel/getChatTitleModel are "override else assistant"; all
SIGNED_IN_* constants and the repo's gpt-5.4 bootstrap deleted
- rowboat sign-in = connecting a provider: with no saved assistant, the
initial model is picked via selectInitialModel (backend recommendation
if the gateway lists it, else first listed) and saved; sign-out clears
rowboat-referencing selections (same dangling-ref cleanup as removing
any provider). A saved assistant is never replaced — signing in no
longer hijacks a BYOK selection
- IPC: models:saveConfig replaced by models:setProvider/removeProvider;
models:updateConfig speaks v2 keys; models:test takes {provider, model}
- renderer call sites (settings dialog, onboarding, composer) translated;
the settings dialog's delete-provider path collapses from ~100 lines of
top-level-pair juggling to removeProvider + best-effort promotion
- migration dry-run verified against a real-world signed-in config
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(x): show chat timestamps in the user's local timezone
The per-message "Current date and time" context was built with toISOString(), giving the model a UTC time frame with no zone. It then quoted email Date: headers (sent in +0000) in UTC — an email shown as 1:06 PM IST in the inbox was described as 7:36 AM in chat.
Use local wall-clock via toLocaleString with an explicit timeZoneName and the IANA zone (matching what the legacy engine already did) so the model converts offset-bearing timestamps to local time.
* fix(x): convert email timestamps to local time for the assistant
The "now" fix gave the model a local time frame, but email dates still reached it as raw RFC 2822 Date: headers (marketing mail is sent in +0000), so it kept quoting them in UTC — a 1:06 PM IST email described as 7:36 AM in chat.
Add formatEmailDateLocal() and apply it at the three model-facing serialization points: the read-view email tool, the thread-summary line, and the gmail-sync knowledge markdown (**Date:**). The structured date field on snapshots/messages is left raw, since the renderer parses it with new Date() and formats it client-side.
* fix(x): generalize local-time formatting for model-facing timestamps
Move formatEmailDateLocal out of sync_gmail into a shared
formatTimestampForModel() util — email is just the first source of
external timestamps; Slack/calendar serialization can now reuse the
same one-line call instead of growing per-source formatters. Also add
a system-prompt instruction to always express times in the user's
local timezone, as a fallback for raw dates inside email bodies, web
content, and third-party tool output that pre-conversion can't reach.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- RowboatApiConfig gains optional modelRecommendations (one model per
provider FLAVOR, native id format) matching the merged backend payload;
optional so older API deployments and failed fetches never break parsing
- new rowboat:getConfig IPC serves the unauthenticated /v1/config bootstrap
independent of sign-in (signed-out BYOK users need recommendations when
connecting a provider); account:getRowboat is back to pure account state
(signedIn/accessToken) — config is no longer a property of the account
- renderer useRowboatConfig() hook: one shared store for all config
consumers (appUrl dialogs/sidebar/settings), plus imperative
fetchRowboatConfig() for event-time consumers (voice, meeting) that must
await the value when starting a session
- selectInitialModel(flavor, models, recommendations): pure spec-ordered
initial pick (recommendation if listed, else first, else null) — runs
only when a provider is first connected, never over a saved choice
Inert plumbing: nothing calls selectInitialModel yet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One function in core (getModelCatalog) now answers "which providers are
connected and what models does each offer", treating every provider the
same way — Rowboat gateway, ChatGPT subscription (codex), BYOK keys, and
local endpoints. Each entry carries { id, flavor, status, savedModel,
models }: id is the provider *instance* (what ModelRef.provider joins on),
flavor is the provider *type* (display naming, listing mechanics) — one
instance per flavor today, so id === flavor key, but a future multi-key
setup stays additive.
- models:list reshaped to serve the catalog plus the effective default;
req gains optional refreshProvider (Retry / Refresh models)
- main-side list cache keyed on a credential fingerprint (auto-invalidates
on key change; failures retry after 30s) — the gateway is no longer hit
on every snapshot rebuild
- useModels collapses from 4 IPC calls to 1: no more renderer-side flavor
sets, sign-in awareness, or raw models.json reads
- ModelSelector drops the live-group fetch machinery; groups render
uniformly with an inline error row + Retry. The only renderer-side fetch
left is probing typed-but-unsaved credentials (settings forms)
- channels bridge lists from the same catalog, so WhatsApp/Telegram
pickers now match the desktop picker
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3.5-flash reasons by default and starved the 50-token output cap
(finishReason: length, 44/50 tokens on thinking). Map reasoning effort
to low via the existing per-provider translation, raise the cap, and
keep only the first output line.
The gateway allowlist has no google/gemini-3.5-flash-lite (only
3.1-flash-lite and full 3.5-flash), so signed-in title calls 403'd
'Model not allowed' and silently kept the placeholder.
Flash-lite under-complied with a bare word-count rule (returned
one-word titles). Few-shot examples rein it in; a code guard rejects
any remaining single-word output so the placeholder stays instead.
A tiny background LLM call names new chats in 3-5 words, replacing the
truncated-first-message placeholder once it lands. Manual renames always
win; failures silently keep the placeholder.
Model resolution (getChatTitleModel): chatTitleModel override in
models.json, else the curated gateway flash-lite when the assistant
default routes through the gateway, else the assistant's own provider -
so signed-in users who switched their default to BYOK get titles on
that provider instead of a dead gateway call.
parseChartSource extracted as a pure function with tests for the three
render states (streaming / invalid / parsed), the label/value alias
mapping, and multi-series y arrays. doc-collab's chart block reference
now mentions the array form of y.
Live testing showed models emitting pie configs with label/value
instead of x/y, and long-format line data with a series column —
schema-invalid fences sat on 'Preparing chart…' forever. The renderer
now maps the label/value aliases before validating, distinguishes
complete-but-invalid JSON (explicit 'config invalid' state) from
still-streaming JSON (placeholder), and the skill spells out the exact
field list, pie's x/y meaning, and wide-vs-long data format with a
wrong/right example.
The chat's markdown fence override now renders ```chart blocks
(the same ChartBlockSchema notes use) as interactive recharts inline —
the mermaid pattern. A new ChartRenderer handles line/bar/pie with
multi-series support, light/dark theming, and a quiet placeholder
while the fence body is still streaming.
ChartBlockSchema.y widens to string | string[] for multi-series
charts (notes chart block updated to map series too), with a
chartSeries() helper in shared.
A new 'charts' skill teaches the model the fence format, form
selection (line/bar/pie, no mixed scales), and data honesty rules;
loaded on demand like any skill.
Address review: remove ITurnLimitsResolver, its bridge and container
registration. createTurn now defaults an omitted maxModelCalls straight
from the turn-limits config (lazy import, built-in fallback), with no
chat/headless distinction in the runtime — the chat UI passes its
optional chat override explicitly via the existing sessions:sendMessage
maxModelCalls field. turn_limits.ts now uses fs/promises throughout.
Per review: no special handling in the agent loop when the model-call
limit is hit — the turn fails as before. Instead the default limit
rises from 20 to 50 (global, and chat inherits it) so the limit rarely
triggers in practice; users can tune it in Settings > Advanced. The
friendly limit-failure message in the chat error bubble stays, since it
points users at the setting.
The budget notice now tells the model to answer the user's request with
the content it already gathered before noting what's unfinished, and
the fallback limit-failure bubble drops the 'work is saved' phrasing.
Interactive turns no longer die with a raw 'Model call limit of N
reached' error. The final budgeted model call is composed as a wrap-up:
the durable request records wrapUp: true, and the composer strips tools
and appends a notice telling the model to answer with what it has,
state what's unfinished, and mention the limit is configurable in
Settings -> Advanced. The turn then completes normally with that answer.
Headless turns (and a wrap-up call that still emits tool calls) keep
the hard model-call-limit failure so automation retains the
machine-readable outcome; sub-agents already surface partial results.
If a hard limit failure still reaches the chat UI, the error bubble now
explains the limit and points at the setting instead of the raw error.
Validation range is now 1-500. The Advanced tab's native number inputs
are replaced with a segmented minus/value/plus stepper (custom buttons,
digits-only typing, range clamping, immediate save on step). The chat
override gets a smaller placeholder, steps from the effective global
limit when empty, and has a clear button to fall back to the global
limit.
Replace the hardcoded 20-call turn budget with user settings: a global
model-call limit that every turn inherits (headless/knowledge work,
scheduled agents, sub-agents) plus an optional chat-only override for
interactive turns. Settings live in config/turn_limits.json (1-100,
default 20) and are edited from a new Advanced tab in Settings.
The limit is resolved at turn creation via an ITurnLimitsResolver
injected into TurnRuntime, keyed on humanAvailable; explicit per-call
overrides still win, and the resolved value is persisted in
turn_created.config.maxModelCalls as before, so historical and
in-flight turns are unaffected by settings changes. Sub-agents now
default to and are capped by the global limit instead of the constant.
* feat(x): first-time-action credit rewards
Reward signed-in free-tier users the first time they connect Gmail, send
an email, take meeting notes, set up a background agent, or build an app.
- core credits service: POST /v1/billing/credit-activations, once per
(user, code); local claimed cache in config/credit_activations.json
keyed by user; 409 reconciles claimed state; other failures retry on
the next occurrence
- triggers at the action success sites in main/core (google oauth
success, gmail:sendReply, meeting:summarize, bg-task create sites,
apps:create); grants broadcast to windows via credits:didActivate
- eligibility: signed-in free tier only (paid starter/pro excluded);
whole feature behind the PostHog credit-rewards flag (default ON,
flag acts as kill switch; ROWBOAT_CREDITS env override for dev)
- UI: sidebar 'Earn $X in credits' pill with checklist popover
(dismissible, rows navigate to each action), settings Account section
with earned state + bonus balance, confetti celebration on grant
- shared: credits catalog/types, credits:getState IPC, BillingInfo
gains store bucket; decodeJwtPayload moved to auth/jwt.ts for reuse
Backend catalog codes deployed separately (rowboatx-backend).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(x): fetch reward catalog from the API, hardcode only activity codes
Review feedback: names, descriptions, and credit amounts are backend-owned
and subject to change; the app now reads them from GET /v1/config
'creditActivations' instead of a hardcoded catalog. Only the activity codes
remain in the app — they anchor the trigger call sites and per-code UI
(icons, navigation).
- shared: drop CREDIT_ACTIVITIES; add CreditActivationCatalogEntrySchema;
RowboatApiConfig gains optional creditActivations
- core: getCreditsState joins the fetched catalog (unknown codes dropped,
API order preserved) with local claimed flags; celebration title comes
from the catalog with the code as fallback
- renderer: description now optional; settings section hides while the
API serves no catalog
Requires the API to serve creditActivations: [{code, displayName,
description?, credits}] on /v1/config; until then the rewards UI stays
hidden and activations still work.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(x): invite-a-friend referral rewards
Surfaces the backend's referral codes (GET /v1/referral, POST
/v1/referral/claims) in the app. The inviter's permanent code appears as
an "Invite friends" item in the sidebar rewards popover and the settings
rewards list, with copy button and claims progress ($5 per friend who
joins, up to 3). The invited side redeems via a self-gating "Have an
invite code?" entry in onboarding's completion step and in settings;
a successful claim fires the existing celebration and refreshes the
balance. Backend error copy (unknown code, own code, already claimed,
cap reached, account too old) is shown inline verbatim.
The one-lifetime-claim state is cached per user in the local claimed
store (pseudo-code referral_claimed), reconciled when the backend
answers "already claimed" from another install. Referral status is
cached for 60s and busted after a claim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Adds the model-call half of "Use your ChatGPT subscription": a new
credential-less "codex" provider flavor (like "rowboat") that runs calls
against the Codex backend using the OAuth session from chatgpt-auth.
- core/models/codex.ts: AI SDK OpenAI provider in Responses mode. A
wrapLanguageModel middleware injects providerOptions.openai.store=false
on every call so the SDK emits full self-contained items (text +
reasoning with encrypted_content) instead of item_reference entries the
stateless backend 404s. A wire-level fetch wrapper adds auth
(bearer + chatgpt-account-id) and Cloudflare headers (originator), and
backstops the contract: store:false, stream forced with SSE aggregation
for non-streaming callers, max_output_tokens dropped, encrypted-
reasoning include, friendly usage-limit errors
- live model discovery via /codex/models, gated by client_version
(mirrored from the local codex CLI's models_cache.json so the catalog
matches what `codex` shows); visibility "hide" utility models filtered;
display names carried through; hardcoded fallback list for offline
- createProvider/resolveProviderConfig/mapReasoningEffort codex cases
- models:list merges the "OpenAI Codex" catalog while signed in with
ChatGPT; chatgpt:statusChanged push event on sign-in/out
- composer picker shows the group gated on the session and refreshes on
status changes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Lets users connect their ChatGPT Plus/Pro subscription instead of pasting
an API key. Sign-in half only: the codex model client consumes the session
via getChatGPTAccessToken().
- PKCE OAuth against auth.openai.com using the official Codex CLI public
client id (constants verified against the openai/codex sources); system
browser + loopback callback server on the pre-registered fixed port
127.0.0.1:1455, state validated, stale callbacks rejected
- token store (core): single config/chatgpt-auth.json holding non-secret
display fields (email, accountId, expiresAt) plus the token material
encrypted via the safeStorage cipher bridge (plaintext-with-marker
fallback when no keychain, matching the GitHub token path); no token
value ever written in the clear or logged
- getChatGPTAccessToken(): single-flight refresh within 5 min of expiry;
refresh rejection (400/401) clears to a clean signed-out state and
throws typed ChatGPTAuthRequiredError; 5xx/network kept as transient
- sign-out with best-effort token revocation
- IPC chatgpt:getStatus/signIn/cancelSignIn/signOut — raw tokens never
cross to the renderer; real cancellation settles the in-flight attempt,
frees the port, and a retry starts a fresh attempt (new PKCE/state)
- settings UI: ChatGPT Subscription section on the OpenAI card
(sign in / waiting + cancel / connected as {email} + sign out) via a
useChatGPT hook
Vitest covers the token store, single-flight refresh, and
transient-vs-terminal refresh handling.
Apps section improvements:
- Landing with no apps of your own now falls through to the Catalog tab
with a banner pointing at installing from the catalog or building your
own (links into the copilot app-builder flow).
- Catalog renders as a card grid matching the My Apps visual language
(shared card-theme helpers) instead of one-line rows, with star
button, INSTALLED badge, and Install/Open actions on each card.
- Apps can be pinned to the nav sidebar: right-click an app card to
add/remove; pinned rows sit under the Apps nav item, click opens the
app, right-click removes. Persisted in localStorage (x:pinned-apps).
- My Apps shows a hint that right-click pins an app to the sidebar.
- Uninstall/delete unpins the app, and the Apps view prunes pins for
apps that no longer exist (only off an authoritative server list).
GitHub stars rate-limit backoff (packages/core/src/apps/stars.ts):
once GitHub reports the budget spent (403/429 + x-ratelimit-remaining:
0), gate all star fetches until the advertised reset, serving stale
cached counts instead of burning requests on guaranteed 403s.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(x): client auto-update with non-interrupting UX
Auto-update via update.electronjs.org (Squirrel), replacing the native
update dialog with a state machine pushed to the renderer:
- Non-modal "restart to update" toast, deferred while a call/turn is
active and retracted if one starts; 24h "Later" snooze persisted in
main so it survives window reloads
- Offline detection: network errors get a soft `offline` state instead
of a red failure, and don't emit update_failed analytics
- Update-waiting badges: macOS dock badge, Windows taskbar overlay icon
- macOS move-to-Applications prompt parented to the main window, with a
failure fallback dialog and focus re-check after a manual drag
- Settings > Help: version, manual check with accurate transient
"You're up to date" feedback, per-state messaging
- "Updated to vX" card on first launch after an upgrade (downgrades
restamp silently); "What's new" links to release notes
- Toaster: follow app dark mode (fixes unreadable description text) and
restyle to theme tokens
- Window state persistence so restart-to-update feels lossless
- Tests for version stamping and upgrade comparison
* feat(x): replace update toast with Zed-style titlebar chip
The restart-to-update prompt moves from a sonner toast to a persistent,
non-interrupting titlebar indicator: a spinner while an update downloads,
then a 'Restart to update' chip once staged. Clicking restarts into the
new version; the x snoozes the chip for 24h via the existing persisted
snooze. Busy-deferral is dropped - the chip never interrupts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(x): bottom-left update card with inline release notes
Reframe the restart prompt per PR feedback: by the time the user sees it,
Squirrel has already installed the update — the prompt only asks for a
restart (Chrome-style), and being loud about what shipped matters more
than being unobtrusive during early adoption.
- Replace the titlebar chip with a bottom-left "Update available" card
showing the new version, an inline "What's new" section rendered from
the GitHub release notes, and Later / Release notes / Restart now
- Release notes come from Squirrel.Mac's update feed (update.electronjs.org
passes the release body through); Squirrel.Windows only reports the
release name, so missing notes are backfilled from the GitHub API
- Drop the 24h snooze machinery — Later/× just dismiss for the session
- Drop offline detection (soft `offline` state) — separate PR later
- Drop the macOS move-to-Applications prompt/move button — separate PR
later; Settings still explains why updates are unavailable outside
/Applications
- Drop window state persistence
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(x): aggregate release notes across skipped versions, persistent up-to-date status
- Updater aggregates release bodies for every version between the running
app and the update target, with a commit-log fallback when no release in
range has notes
- CI fills empty release bodies with GitHub's auto-generated notes
- Settings shows a persistent 'You're up to date' line with last-checked
time instead of a transient confirmation
* fix(x): restore updater:quitAndInstall lines dropped in merge
The conflict resolution in 67d6a542 truncated the updater:quitAndInstall
entry in both the shared IPC schema and the main-process handler, leaving
an unclosed brace that broke the shared package build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(x): simplify release notes handling per review feedback
Release notes are a process concern, not the updater's: drop the
CI job that auto-filled empty release bodies and the in-app GitHub
API backfill (backfillReleaseNotes + commitLogFallback). The update
card now adapts instead — it renders the notes Squirrel supplies,
or a static "Bug fixes and improvements." line when empty.
Also document that updateElectronApp() configures the same
autoUpdater singleton our listeners observe (no race), and that
gen-install-loading.sh is a manual one-off tool whose committed
GIF feeds Squirrel's loadingGif.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(x): drop update-electron-app, drive autoUpdater directly
With notifyUser: false the package reduced to setFeedURL + an
immediate checkForUpdates() + a 10-minute unconditional timer, plus
guards we already have (isPackaged, platform, app-ready). Inline
those three lines instead and remove the dependency.
The interval now runs through the existing guarded checkForUpdates()
(no-op unless idle/error), so ticks no longer emit Squirrel.Mac
"check already in progress" errors or make Squirrel.Windows
re-download an already-staged update.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Three capability upgrades to the embedded browser pane:
- Screen share: getDisplayMedia() in the browser partition now shows a
Chrome-style source picker (screens/windows with thumbnails, optional
system audio for screens) instead of silently capturing the primary
screen. Requests are denied when the pane is hidden. The app's own
session keeps its auto-loopback handler for meeting transcription.
- Notifications: the browser partition grants the notifications
permission so sites (WhatsApp Web, Gmail, ...) can post native OS
notifications while loaded. Background Web Push remains unavailable.
- Extensions: electron-chrome-extensions wired to the browser session.
Unpacked extensions load from ~/.rowboat/extensions/<name>/; tab
lifecycle is mirrored into chrome.tabs, and a <browser-action-list>
toolbar hosts action icons/popups next to the address bar. NOTE: the
library is GPL-3.0/Patron dual-licensed — licensing decision required
before shipping in a release build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Registered as a posthog-js super property in the renderer and merged
into every posthog-node capture/identify in main, plus set as a person
property. Segments desktop traffic from the legacy web dashboard's
anonymous autocapture if it ever shares the PostHog project. Desktop
app only — the web app is legacy and intentionally untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds feature-level instrumentation so dashboards can answer "how
important is X and what do people do inside it":
- view_opened fired centrally from the currentViewState effect — one
event per section visit (email/meetings/bg-tasks/apps/...), plus
one-shot has_used_* person properties for cohorts
- Email: thread opened, compose opened, sent (mode/attachments/
ai_assisted), AI draft generated, archived, trashed, marked unread,
importance/category corrections, bulk category archive, search,
instructions saved, manual sync, send failures
- Meetings: recording started/stopped (duration), popup action
(captured in main — popup window has no PostHog), note opened,
summarize failures
- Calls: call_ended with duration (call_started already existed)
- Background agents: created (manual/coding/copilot), updated,
toggled, run clicked, stopped, deleted; run completed/failed with
trigger captured in the core runner for a true failure rate
- Live notes: saved, toggled, run, stopped, deleted, edit-with-copilot
- Code mode: session created (direct vs rowboat, agent) and
direct-drive messages (direct mode emits no llm_usage otherwise)
- Billing: paywall shown / upgrade clicked by error kind
- Search: opened + result selected; Settings: opened + tab changed
- Notes: created; edited (deduped to one event per note per session)
- Apps: app_rolled_back (rest were already captured in main)
All changes are additive — existing events, identity flow, and person
properties are untouched. ANALYTICS.md updated with the full catalog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 2px progress line drains toward auto-dismiss (now 30s); hovering the
popup freezes both the line and the dismissal. The renderer owns the
countdown; main keeps only a 3-min crash-safety fallback
- soft synthesized arrival chime (WebAudio — no asset, no spawn)
- close button in the top-left corner, hover-revealed
- call-end grace 3s → 1s with a 1s detector poll: notes flow starts
within ~2s of hang-up
feat(x): unified email classifier with labels, triage sections, and user instructions
Replace the two disjoint email classification systems (inline importance
classifier + batch labeling agent) with a single LLM pass that emits
importance, category, knowledge verdict, summary, and a pre-drafted reply.
Classification:
- classify_thread.ts now returns {importance, category, knowledge}; the
category enum and prompt are built at call time from a label registry
(7 built-ins named after Superhuman's vocabulary — Direct, Calendar,
News, Marketing, Pitch, Notification, Receipt — plus up to 12 custom)
- Users create labels in plain text via the new "Email agent instructions"
dialog; an extraction pass syncs them into config/email_labels.json.
Instructions are also injected into every classify/draft call and the
composer's Write-with-AI bar (seeded: Investor / Candidate / Customer)
- Per-thread category corrections are sticky and feed few-shot learning
(email_category_feedback.ts), same pattern as importance corrections
Knowledge graph:
- Delete the labeling agent pipeline (label_emails, labeling_agent,
labeling_state); admission is now the stamped `knowledge: extract|skip`
frontmatter, with legacy noise-tag fallback and a budgeted sweep that
backfills unclassified markdown (build_graph holds unstamped files)
- Guardrails: user-participated threads always extract; user importance
flips never touch the knowledge verdict; classify failures stamp nothing
Inbox UI:
- Sections become Needs you / Waiting on them / Everything else; waiting
state is derived per render (user sent last, others participated), never
stored, so it can't go stale
- Category chips on rows (Direct stays chipless), filter pills with counts,
one-click bulk archive per category, "Reply ready" indicator
- Fix: Everything else section could never load/render under sustained
sync writes (fetch was chained on hasReachedEnd, which live reloads reset)
- mic-monitor now resolves each mic owner's bundle ID (CoreAudio) and
executable path (libproc) natively — the main process never shells out
during capture (child-process spawns fail with EBADF while Electron
holds a capture session; this bit the ps-based first cut)
- detector matches owners by bundle-ID prefix (com.google.chrome,
us.zoom, com.tinyspeck.slackmacgap, …) with executable-name fallback,
and recognizes Rowboat's own capture (com.rowboat / dev electron)
- call-end detection: once a meeting app has been seen on the mic during
a recording, its absence for 3s means the call ended → auto-stop →
summarize — same flow as a manual stop. Arms only when a meeting app
was actually observed, so in-person recordings never auto-stop
- post-summary redirect: Rowboat foregrounds itself on the finished note
(app:focusMainWindow now steals focus); the new meeting_notes_ready
notification stays as a background-only fallback with a toggle in
Settings > Notifications
OpenRouter's public /api/v1/models returned the full catalog even with an
invalid key, so Settings showed Connected while the model call failed with
Missing Authentication header. With a key present we now fetch the
account-scoped /models/user with the Bearer token, so a bad key surfaces
as an error instead of a false Connected; no key keeps the public preview.
Other providers untouched. Note: unlike the rest of this PR, this touches
core (models.ts).
- mic-monitor Swift helper: polls CoreAudio for mic-in-use, with owning
PIDs on macOS 14.4+ (process-object API) so calls attribute to the app
that actually holds the mic (Chrome vs Zoom vs Slack); no TCC permission
needed. Compiled best-effort by bundle.mjs; exits when stdin closes
- core meetings/detector: 5s mic debounce, PID→process attribution with
running-app fallback, Granola labels (Huddle/Call/Meeting detected),
calendar merge (event start −15min through end), once per mic session
(30s idle reset), suppressed while Rowboat itself captures (recording
or assistant voice call), gated by a meeting_detection notification
category (Settings > Notifications)
- popup: lean Granola-style bar (title + platform + Take notes pill,
floating ×) in a transparent non-activating panel at screen-saver level
with fullscreen-auxiliary behavior — floats over fullscreen meetings on
the cursor's display; activation policy restored immediately so the
Dock icon survives and Take Notes can foreground Rowboat
- Take Notes routes into the existing take-meeting-notes flow with a
synthetic or matched calendar event (source: detected); never records
without a click
- tray icon bumped to 18pt
- Register as an OS login item on first packaged run (default on); the OS
registry stays the source of truth afterward, with a toggle in
Settings > Appearance > System
- Menu bar tray with template icon (derived from the app glyph, embedded
as base64): Open Rowboat / Start-Stop meeting notes / Quit
- App keeps running with no windows while the tray exists; launches at
login start hidden (menu bar only), Dock/tray reveals the window
- Tray menu reflects live recording state via meeting:setRecordingState;
tray-initiated toggles survive window-loading races through a pending
command drained on renderer mount (same pattern as deep links)
- GRANOLA_PARITY.md: research doc — how Granola works, current-state
audit, gap analysis, phased parity plan
The apps-dir watcher recursed into anything an installed app ships;
ignore .git and node_modules segments like the workspace watcher does.
(The skills watcher already bounds itself with depth: 1.)
fs.watch failures (EMFILE/ENOSPC) escape chokidar's error handler as
uncaught exceptions from Node watcher internals and killed the main
process with the native error dialog. Catch them at the process level
and log instead — watching is degradable; other uncaught exceptions
keep the default dialog behavior.
chokidar v4 silently skips a watched directory that doesn't exist at
watch time, so e.g. calendar_sync/ created by the first sync after app
start would never emit events until restart. Create the dir roots up
front; for the file root only its parent dir is needed.
The workspace watcher watched all of ~/.rowboat recursively. Chokidar v4
(no fsevents) holds one OS watch handle per file, and internal state dirs
(runs-archive, storage/turns, engines, ...) grow unboundedly with usage —
~20k open fds on a 1.1G workdir, crashing the app with EMFILE once the
fd limit is hit.
Only watch the roots whose change events actually have consumers:
knowledge, bases, inbox_lists, gmail_sync, calendar_sync, bg-tasks and
config/agent-schedule.json — with .git ignored (knowledge/ is a git repo).
Bump apps/x from AI SDK v5 to v7 (single hop through the 6-0 and 7-0
migration guides). All deps upgraded via pnpm --filter:
ai@7, @ai-sdk/{anthropic@4,google@4,openai@4,openai-compatible@3,provider@4},
@openrouter/ai-sdk-provider@3, ollama-ai-provider-v2@4.
Changes (per the official migration guides):
- Provider spec bumped V2->V4: ProviderV2 -> ProviderV4 (models.ts, gateway.ts)
- streamText: result.fullStream -> result.stream (+ StreamTextInvoker seam,
test fakes); stepCountIs -> isStepCount
- system -> instructions on SDK generateText/streamText/generateObject calls
(left our own generateObjectSafe.system param untouched)
- allowSystemInMessages: true on calls passing stored messages (v7 rejects
system-role messages in the array by default; convertFromMessages emits them)
- image content part {type:'image'} -> {type:'file'} (message-encoding.ts)
- tool() no longer accepts a `name` key (removed; tools keyed by ToolSet key)
- usage token relocation: cachedInputTokens -> inputTokenDetails.cacheReadTokens,
reasoningTokens -> outputTokenDetails.reasoningTokens (mapUsage rewrite);
renderer decoupled from LanguageModelUsage to a local flat UsageSummary
- providerMetadata relay cast to shared ProviderOptions (spec loosened)
No persisted-data changes: @x/shared is AI-SDK-free, so old chats
list/view/continue unchanged. Full typecheck clean; 637 tests pass
(shared 107, core 447, renderer 83); main.cjs bundles ESM-only ai into CJS.
Pending live check (needs creds): OpenAI strictJsonSchema now defaults true
in v7 and may reject zod schemas for openai-family flavors at runtime.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>