Commit graph

870 commits

Author SHA1 Message Date
Ramnique Singh
5ba7918bb7
Merge pull request #753 from rowboatlabs/feat/chatgpt-oauth
Some checks failed
rowboat / apps/x Vitest suites (push) Has been cancelled
rowboat / apps/x Electron package smoke test (push) Has been cancelled
feat: use your ChatGPT subscription — sign in with ChatGPT + Codex subscription models in the composer
2026-07-17 17:27:55 +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
2575238a01
fix(x): don't re-apply stale assistant email search on re-entering email view (#762)
The assistant's read-view email search stored its query in
emailInitialSearchQuery and never cleared it, so every later visit to the
email section remounted EmailView with the old query re-injected into the
search box — even after the user had cleared it. Clear the stored query on
any email navigation that doesn't carry an explicit search (sidebar,
openEmailView) so deep-linked searches apply exactly once.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 16:03:52 +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
Prakhar Pandey
63ed6e2dd6 fix: auto-detect text direction for RTL/BiDi chat content
Refs #749
2026-07-16 14:18:44 +05:30
arkml
b43dfaf14f
Merge pull request #755 from rowboatlabs/feat/browser-screenshare-notifications-extensions
feat(x): browser screen-share picker, web notifications, Chrome extensions
2026-07-16 09:50:27 +05:30
Arjun
781b16d881 feat(x): browser screen-share picker, web notifications, Chrome extensions
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>
2026-07-16 09:18:40 +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
Gagan
786382c840 fix(x): meeting popup window is the card — kills the grey backing slab
macOS NSPanel windows don't honor transparent: true (nor a #00000000
backgroundColor); the transparent margins around the card rendered as an
opaque grey slab. Drop transparency entirely: size the window to the
card (376×48), paint the card color on the window, use native rounded
corners + shadow. The hover × slides in over the card's left edge
instead of overhanging a corner.
2026-07-14 13:33:29 +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
Prakhar Pandey
7ddc82f25d feat: remove model-name entry from onboarding to match connect-only settings
Onboarding still asked for a model name for openrouter/aigateway/ollama/
openai-compatible. It now mirrors the settings dialog: entering a key (or
base URL) is enough and the model is resolved silently from the fetched
list (same precedence), with openai-compatible keeping a visible Model
field as its escape hatch. Save payload shape and multi-provider flow
unchanged.
2026-07-14 13:10:52 +05:30
Prakhar Pandey
e74ce4e9a5 fix: pin picker search input to top and connect providers in a single click
Bug 1: the composer picker search input lived inside the scrolling dropdown
body as a sticky element, so container padding and Radix's open-focus
scroll left it slightly below the top. Moved it out of the scroll area into
a fixed header with the model list in an inner scroller, so it's flush at
the top and always visible.

Bug 2: Connect gated on resolvedModel, which is empty while the debounced
key-change fetch is still in flight right after pasting a key, so the first
click was a no-op. Connect now gates on credentials only, and the save
handler resolves the model on demand (one fetch, same silent precedence)
when it hasn't loaded yet. Save payload, models:test gate, and per-function
fields unchanged.
2026-07-14 12:38:00 +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
9ee332e21b style(x): tray recording waveform + popup polish
- animated sine-wave indicator (braille dot bars, two per cell, 300ms
  frames) beside the tray icon while a meeting records
- popup: hover-anywhere reveals the close button (the drag region was
  swallowing mouse events), leaner card (48px, 76px window), sits 44px
  below the menu bar
2026-07-14 01:27:32 +05:30
Gagan
f2d6bbbff3 style(x): refine meeting-detected popup
- leaner: 400×84 window, 56px card, type stepped down (14/13px)
- close button only appears on hover
- Rowboat sail sits directly on the button background (no chip box),
  slightly larger
- dropped the dashed/solid left rule; symmetric margins and padding
- browser preview mode (vite only, no Electron): renders sample data at
  the real window size on a backdrop — #meeting-detected, plus
  ?variant=calendar for the calendar-linked look
2026-07-14 01:06:46 +05:30
Gagan
3419cf8ef0 fix(x): don't overwrite same-titled meeting notes within a day
Note filenames derive from the meeting title, so every ad-hoc detected
meeting (titled "Meeting") — and any recurring calendar event repeating
its summary — resolved to the same file and each recording clobbered the
previous one's notes. Suffix with the start timestamp when the path
already exists.
2026-07-14 00:51:11 +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
9bf286bfea feat: add provider Disconnect with automatic default reassignment
Each connected provider gets an explicit Disconnect in Settings. When the
disconnected provider was the default, another connected provider is
silently promoted (defaultSelection's provider preferred) through the
existing models:saveConfig path, and the toast names the new default.
Disconnecting the last provider clears the top-level config entirely so
llm:getDefaultModel degrades to the signed-in gateway default or a clean
no-config rejection and the composer shows its connect hint instead of a
phantom default. A defaultSelection pointing at the removed flavor is
dropped in the same write.
2026-07-12 23:10:08 +05:30
Prakhar Pandey
c9ebbd7600 feat: add search filter to model picker and live fallback for catalog providers
The picker dropdown gets a sticky search input filtering model ids across
all groups (headers hide at zero matches; loading/error rows stay visible
as status; a No-models-match row appears when nothing matches anywhere).
openai/anthropic/google groups fall back to a live models:listForProvider
fetch when the models.dev catalog is unavailable (signed-in mode or empty
cache), so they always show the provider's full list.
2026-07-12 23:10:08 +05:30
Prakhar Pandey
367071916a feat: aggregate model picker in composer grouped by connected provider
The composer picker lists every model across all connected providers:
models.dev catalog groups for openai/anthropic/google (plus the rowboat
gateway when signed in) and live per-provider fetches via
models:listForProvider for openrouter/aigateway/ollama/openai-compatible,
each group loading and failing independently with inline Retry. Picking a
model persists as the app default (models:updateConfig defaultSelection)
so background agents and new tabs follow the last pick; per-tab override
behavior is unchanged. Empty state points to Settings; the dropdown
scrolls for large lists and OpenRouter ids containing slashes are handled.
2026-07-12 18:50:49 +05:30
Prakhar Pandey
5377d7837a feat: make Settings > Models connect-only
Settings no longer exposes model selection. Each provider connects with
just its API key (or base URL for local providers); the live fetch now
drives a connection status line (Connected - N models) and the model the
config schema still requires is resolved silently at save (saved >
preferred > first fetched). Escape hatches: openai-compatible keeps a
visible Model field, and fetch failures offer manual entry so offline
saves still work. Provider cards show a Connected badge per configured
flavor. Save payload, models:test gate, and the four per-function fields
unchanged. Model selection moves to the chat composer next.
2026-07-12 14:14:55 +05:30
Prakhar Pandey
75805e2d67 feat: fetch provider models live from API key in settings
Settings > Models is now key-first, matching the onboarding flow: entering
an API key (or base URL for local providers) fetches the provider's live
model list via the existing models:listForProvider IPC, shown in a
dropdown. Free-text model entry survives only as the fetch-failure
fallback and the openai-compatible custom-model escape hatch. Unkeyed
providers no longer show auto-seeded models. Also fixes the models[] wipe:
editing the primary model previously discarded every saved model after
the first.
2026-07-12 13:36:16 +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
Prakhar Pandey
7ec93ba28f fix: watch skill roots before they exist so first install loads without restart 2026-07-11 14:04:31 +05:30
Ramnique Singh
054f807925 fix(x): record builtin error envelopes as isError in durable results
Builtins signal failure by returning an envelope ({error}, {success:
false, error|message}, composio's {successful: false}) instead of
throwing, and the tool-registry bridge marked every non-throwing result
isError: false — so the durable log recorded most builtin failures as
successes, misleading the reducer's record, the renderer's error state,
telemetry, and the tools_extended gate. The spec's ToolResultData.isError
channel was effectively populated only by throws.

The bridge now detects the envelope at its single chokepoint and sets
isError accordingly. Model-facing bytes are unchanged (encoding ignores
isError). Deliberately not an error: executeCommand's success:false for
a plain non-zero exit with no error/message — a failing grep is a
result, not a tool failure. Composio success passthrough (error: null)
stays non-error via the truthy-string check. The authoring convention is
documented on the catalog schema.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 12:31:47 +05:30
Ramnique Singh
49cb64d8ba fix(x): cancellation no longer requires live dependency materialization
advance() resolved the context chain, agent snapshot, live model, and
every snapshotted tool before applying ANY input — including cancel,
which uses none of them. A permanently broken environment (provider
removed from models.json, builtin renamed by an update, context
predecessor unreadable) therefore rejected every advance forever: the
turn could not be cancelled, stopTurn's fallback failed, and the
session rejected sendMessage with TurnNotSettledError permanently — a
bricked session with valid bytes on disk.

Materialization is now a thunk the invocation awaits only for work that
continues the turn: run() short-circuits a cancel input straight to the
cancel path (same commit ritual, same synthetic results, byte-identical
events for healthy turns), while every other input still materializes
first, preserving §15.3's reject-before-touching-the-file ordering.
applyInput narrows to non-cancel inputs.

Tests: cancel succeeds against a broken tool registry (with the
non-cancel input asserted to reject file-unchanged), a broken model
registry, and an unreadable context chain; plus a healthy-cancel
regression pinning the exact event sequence and outcome. All three
broken-environment tests fail against the old code. Design doc §15.3,
§18, §22, and §26.5 updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:59:42 +05:30
arkml
30def374af
Merge pull request #707 from rowboatlabs/fix/outlook-quoted-replies
fix(x): collapse quoted replies in Outlook-authored email
2026-07-11 10:52:36 +05:30
Arjun
0e3918f32a fix(x): stop clipping the last line of short email bodies
A message body whose first block has a top margin (a bare <p>, most
Outlook mail) rendered with its final line cut off. The margin collapses
through <body>, so body.scrollHeight under-reports by the margin height
while the content's real bottom sits lower — and that short height is
what we set on the iframe. Measured in Electron: a two-line reply
reported 63px against a 73px content bottom, losing 10px. display:
flow-root on <body> contains the margin (the existing rule only reset the
*last* child's bottom margin). Pre-existing; not caused by quote
collapsing.

Also restore <body> attributes when re-serializing a prepared body.
prepareEmailHtml joined head.innerHTML + body.innerHTML, dropping the
<body> tag — and with it the bgcolor/style that 431 of 584 local
messages rely on for their background (the host parser merges a nested
<body>'s attributes onto its own). Serialize documentElement.innerHTML
instead so the tag and its attributes survive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 10:35:24 +05:30
Ramnique Singh
46ad5aa3b6 fix(x): never record a repository failure as a tool's own error result
executeSyncTool's success-result append sat inside the try whose catch
appends an error tool result. A transient repository failure (disk
pressure, EMFILE) on that append was therefore durably recorded as the
TOOL having failed — after its side effect already happened — inviting
the model to retry the side effect on the next call (double-sent email,
duplicate issue). This violated §21.4: repository failures must reject
the execution, not masquerade as tool outcomes.

The try now covers execution and result parsing only (a tool returning
garbage is still a tool error). The success append happens outside: a
failure there propagates as an infrastructure error, the log keeps the
invocation without a result, and the existing §23 recovery closes it
honestly on re-advance ("outcome is unknown and it was not retried")
without re-executing.

Tests: a repo fake failing exactly the success append — asserts no
false error is persisted, the outcome rejects, recovery yields the
indeterminate result without re-execution, and a committed sibling
result survives untouched with correct model-facing ordering; plus a
parse-failure case pinning garbage returns as tool errors. Both repo
tests fail against the old code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 10:30:08 +05:30
Ramnique Singh
8a41cd480a fix(x): hide grant-scope buttons where no grant persistence exists
The permission card offered "Allow for Session" and "Always Allow" on the
turns path (full-screen chat and side-pane), but the sessions layer never
persists grants — the scope rode respondToPermission metadata that nothing
consumes, so both buttons behaved exactly like "Approve" once while
telling the user otherwise. Grant persistence itself is deferred until
there is a sound notion of grant keys (executeCommand parsing is not
AST-based), and auto-permission mode covers the fatigue meanwhile.

The dropdown now renders only when the caller wires the scope handlers.
The legacy code-mode chat keeps them (runs:authorizePermission does
persist grants there); the turns-path call sites stop passing them and
drop the dead scope plumbing from their handlers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:55:07 +05:30
Ramnique Singh
ac7679ec16 fix(x): fail-closed tool permissions with per-tool catalog declarations
The permission checker returned {required: false} for every non-builtin
toolId and for any builtin outside the executeCommand/file-tools switch,
so composio-execute-tool (email sends, GitHub/Jira writes), executeMcpTool,
and mcp:* attachments on user agents executed with no permission check —
even in manual mode — and never reached the auto-permission classifier.
The extension contract was inverted: a new side-effecting tool shipped
ungated unless someone remembered to extend a switch in another module.

Every builtin now declares its permission policy in the catalog itself
(required field, compile-enforced): "none", "prompt", "command-allowlist",
"file-boundary", "composio-execute", or "mcp-execute". The checker reads
the declaration and FAILS CLOSED: undeclared builtins, mcp:* attachments,
and unknown toolId families require permission. Composio and MCP requests
carry family-specific payloads (new ToolPermissionMetadata kinds, rendered
by the permission card; generic fallback for everything else). All
previously-ungated builtins keep today's behavior via explicit "none"
declarations, except addMcpServer which now prompts; a catalog test pins
the audited gated set so policy changes stay intentional.

Auto-permission flows (background tasks, live notes, channels) route the
newly gated calls through the existing classifier per the §9.3 matrix;
defer still denies without a human. No grant persistence yet — every
gated call prompts (or classifies) each time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 23:07:11 +05:30
Ramnique Singh
e22d3b4796 fix(x): make stopTurn reliable under concurrent advances on one turn
The sessions layer tracked live advances in a map keyed by bare turnId
with overwrite-on-set and unconditional delete-on-settle. A turn can
legally have several live advances at once — a running invocation plus
external-input invocations queued on the turn lock (e.g. a permission
response while an async result is pending). The overwrite made stopTurn
abort the wrong controller and await an outcome that couldn't settle;
the unconditional delete untracked a sibling advance, so a later
stopTurn missed the abort path, fell back to a cancel input, and could
reject with TurnInputError if the turn had completed meanwhile — a
user-visible stop failure. Durable state was never affected.

Track a set of advances per turn: each advance removes only its own
entry on settle, and stopTurn aborts every live controller and awaits
all outcomes. A cancel input that loses the race with a concurrent
settle now counts as a successful stop (the turn is already terminal)
instead of rejecting.

Adds four tests: concurrent invocations both aborted, an earlier settle
not untracking a later advance, the settle-race cancel treated as
success, and non-terminal rejections still surfacing. Updates
session-design.md §9.3 and §13.5 accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 22:46:06 +05:30
Ramnique Singh
21c93913f4 fix(x): validate turn event batches before they become durable
The commit ritual persisted first and reduced second, so an illegal
append (e.g. a misbehaving tool reporting progress after its terminal
result) became durable before the reducer rejected it — permanently
corrupting the turn file and, through context references, blocking
every later turn in the session. Reduce the batch against the in-memory
history first: illegal batches now reject in memory for their caller
only, the file stays legal, and a failed commit no longer poisons
this.events for subsequent appends in the same invocation.

Adds a test that fires a stashed reportProgress after the tool's result
is durable (while a sibling keeps the invocation alive) and asserts the
late append rejects, nothing illegal reaches the file/stream/bus, and
the turn completes and stays re-advanceable. Updates the design doc
(§4.5, §24) to the reduce → persist → stream order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 22:32:33 +05:30