Commit graph

486 commits

Author SHA1 Message Date
Ramnique Singh
5c330de3fa
Merge pull request #791 from rowboatlabs/model-selection
Some checks failed
rowboat / apps/x Vitest suites (push) Has been cancelled
rowboat / apps/x Electron package smoke test (push) Has been cancelled
One model-selection experience: providers own catalogs, one Assistant model, split-view picker
2026-07-24 17:45:18 +05:30
Ramnique Singh
72e71e0ab5 feat(x): apply per-task model recommendations at initial selection
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>
2026-07-24 17:41:31 +05:30
Ramnique Singh
d9317f3ca1 feat(x): PostHog instrumentation for provider/model selection
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>
2026-07-24 17:41:31 +05:30
Ramnique Singh
c695ddf1c1 feat(x): one model-selection experience — unified settings, provider lifecycle, split-view picker
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>
2026-07-24 17:41:31 +05:30
Ramnique Singh
f39daa9f5d refactor(x)!: models.json v2 — providers carry credentials, models live in assistantModel/taskModels
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>
2026-07-24 17:14:30 +05:30
Harshvardhan Vatsa
15d484980d
fix(x): show chat timestamps in the user's local timezone (#779)
* 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>
2026-07-24 16:26:04 +05:30
Ramnique Singh
b0000a10d9 feat(x): rowboat config IPC + model recommendations plumbing
- 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>
2026-07-24 13:36:20 +05:30
Ramnique Singh
5729e21d77 refactor(x): unify model listing behind one catalog pipeline
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>
2026-07-24 12:42:40 +05:30
gagan
f0e8e51a0e
Merge pull request #786 from rowboatlabs/feat/chat-auto-titles
Auto-name chats from the first user message
2026-07-24 00:13:30 +05:30
Gagan
f22fff9d72 feat(x): chat titles on gemini-3.5-flash-lite
Now that the gateway allowlist includes it. Verified against staging.
2026-07-24 00:09:44 +05:30
Gagan
1f308267c2 feat(x): gateway chat titles on gemini-3.5-flash with low thinking
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.
2026-07-23 16:30:53 +05:30
Gagan
3c3ae780e7 fix(x): gateway chat titles use 3.1-flash-lite
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.
2026-07-23 16:27:50 +05:30
Gagan
aa235d774c feat(x): chat titles must be 2-5 words; few-shot the title prompt
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.
2026-07-23 16:24:44 +05:30
Gagan
81c07b3a3f feat(x): auto-name chats from the first user message
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.
2026-07-23 16:13:27 +05:30
Ramnique Singh
c7b9ca5a61
Merge branch 'main' into feat/configurable-model-call-limit 2026-07-22 10:53:12 +05:30
Ramnique Singh
e659f0bdde Log background agent failure reasons 2026-07-22 10:48:36 +05:30
Gagan
7bf735d6c9 test(x): cover chart fence parsing; document y arrays in doc-collab
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.
2026-07-22 01:40:51 +05:30
Gagan
099a47ac36 fix(x): chart fences with alias fields no longer hang on the placeholder
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.
2026-07-22 01:38:08 +05:30
Gagan
5acd825903 feat(x): inline charts in chat via a charts skill
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.
2026-07-22 01:30:36 +05:30
Ramnique Singh
1de932c7ba bump max model calls to 50 2026-07-22 00:07:24 +05:30
Gagan
7b05bf80cb refactor(x): drop limits DI, read setting at default site; async fs
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.
2026-07-21 20:58:53 +05:30
Gagan
7cb9c478ac feat(x): drop limit wrap-up special-casing, raise default limit to 50
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.
2026-07-21 16:51:36 +05:30
Gagan
f270f39e35 feat(x): graceful wrap-up when a chat turn hits its model-call limit
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.
2026-07-21 15:50:04 +05:30
Gagan
dd04738015 feat(x): raise model-call limit cap to 500, polish limit stepper UI
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.
2026-07-21 15:29:11 +05:30
Gagan
eece1af90e feat(x): configurable model-call limits by workflow (#768)
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.
2026-07-21 15:25:21 +05:30
arkml
033cc6d351
feat(x): first-time-action credit rewards (#754)
* 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>
2026-07-20 14:59:25 +05:30
Ramnique Singh
6c5324eeac feat: ChatGPT subscription (codex) models — provider, model list, composer wiring
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>
2026-07-17 17:23:57 +05:30
Prakhar Pandey
a1db0e395a feat: Sign in with ChatGPT — OAuth sign-in, token storage, and settings UI
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.
2026-07-17 17:23:46 +05:30
arkml
826bce90ad
feat(x): apps catalog cards, sidebar app pins, empty-state catalog landing (#759)
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>
2026-07-16 18:43:10 +05:30
Harshvardhan Vatsa
70ddf19489
feat(x): client auto-update with restart card and inline release notes (#744)
* 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>
2026-07-16 17:59:03 +05:30
Arjun
1c895c7db6 feat(x): stamp platform=desktop on all analytics events
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>
2026-07-15 07:50:39 +05:30
Arjun
86589dbe50 feat(x): PostHog usage analytics across all feature surfaces
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>
2026-07-14 22:44:25 +05:30
gagan
f68f546223
Merge pull request #748 from rowboatlabs/feat/granola-parity
Granola-parity meeting capture: resident app, ambient detection, auto-stop + notes redirect
2026-07-14 14:34:42 +05:30
gagan
5f71ff2000
Merge pull request #746 from rowboatlabs/fix/workspace-watcher-scope
fix(workspace): scope the workspace watcher to whitelisted roots (EMFILE crash)
2026-07-14 13:48:31 +05:30
Gagan
827bd4df5e feat(x): popup countdown line, arrival chime, faster call-end
- 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
2026-07-14 13:42:03 +05:30
PRAKHAR PANDEY
1e8e36ebfb
Merge pull request #738 from rowboatlabs/feat/settings-models-picker
feat: connect-only model settings with dynamic composer model picker
2026-07-14 13:27:46 +05:30
arkml
7cfc2200ba
Merge pull request #750 from rowboatlabs/email_improvements
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)
2026-07-14 11:54:44 +05:30
Arjun
8436e0e245 allow labels and drafts to be steered by the user in the app 2026-07-14 11:40:17 +05:30
Arjun
b2158f320f add email improvements 2026-07-14 10:02:22 +05:30
Gagan
95f232cca9 feat(x): auto-stop on call end + redirect to finished notes (Granola parity phase 3)
- 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
2026-07-14 00:37:10 +05:30
Prakhar Pandey
1a8367ada4 fix: gate OpenRouter connection on the authenticated /models/user endpoint
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).
2026-07-13 23:07:01 +05:30
Gagan
66865b4f6b feat(x): ambient meeting detection + take-notes popup (Granola parity phase 2)
- 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
2026-07-13 21:50:39 +05:30
Gagan
9b0776596d feat(x): resident app — launch at login + menu bar tray (Granola parity phase 1)
- 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
2026-07-13 21:02:20 +05:30
Gagan
aa47871e47 fix(watchers): ignore .git/node_modules in apps watcher, degrade watcher fd errors
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.
2026-07-13 18:16:12 +05:30
Gagan
5bf743f93d fix(workspace): pre-create watched roots so late-appearing dirs emit events
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.
2026-07-13 17:45:51 +05:30
Gagan
4db72e1fe4 fix(workspace): whitelist watched roots to stop EMFILE crash
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).
2026-07-13 17:41:01 +05:30
Ramnique Singh
cc2daf1874 chore(x): migrate to AI SDK 7
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>
2026-07-13 16:22:36 +05:30
Ramnique Singh
10525c58b8
Merge pull request #743 from rowboatlabs/fix/turn-lock-straggler-append
fix(x): hold the per-turn lock until straggler sync-tool appends land
2026-07-13 11:32:38 +05:30
Ramnique Singh
1dc042adc8 fix(x): hold the per-turn lock until straggler sync-tool appends land
A sync-tool batch runs under Promise.all, which rejects the moment one
tool's result append fails (a transient fs fault, or errorMessage()
throwing on a nullish thrown value). That rejection propagated out
through withLock and freed the per-turn lock while sibling tools were
still executing. A straggler's later append then ran with no lock held
and validated against a stale in-memory history rather than the file —
so if the caller re-advanced in between (writing the recovery
indeterminate result for the not-yet-settled tool), the straggler
appended a second tool_result for the same call. Duplicate results make
reduceTurn throw on every future read, permanently corrupting the turn
file and, through context references, every session behind it.

- executeAllowedTools now awaits Promise.allSettled and rethrows the
  first fault only after every tool has finished appending, so a fault
  never orphans a sibling.
- advance() drains the append queue (settleAppends) before withLock
  releases the turn, closing the fire-and-forget reportProgress path and
  any future append-path rejection.
- errorMessage() uses optional chaining so a `throw null`/`throw
  undefined` yields a normal isError tool result instead of a TypeError
  that escapes as an infrastructure rejection (the same fault that
  triggers the lock-escape mid-batch).

Adds two regression tests (both fail without the fix): a nullish throw
records an isError result and completes the turn; a faulted batch does
not settle until the slow sibling's append is durable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 11:17:42 +05:30
PRAKHAR PANDEY
2e45bc0213
Merge pull request #734 from prakhar1605/fix/skills-watcher-cold-start
Some checks failed
rowboat / apps/x Vitest suites (push) Has been cancelled
rowboat / apps/x Electron package smoke test (push) Has been cancelled
fix: skills load without restart on first install
2026-07-11 21:46:05 +05:30