Commit graph

2137 commits

Author SHA1 Message Date
Ramnique Singh
5c330de3fa
Merge pull request #791 from rowboatlabs/model-selection
Some checks are pending
rowboat / apps/x Vitest suites (push) Waiting to run
rowboat / apps/x Electron package smoke test (push) Waiting to run
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
f8f5c8614f
Merge pull request #790 from rowboatlabs/fix/billing-error-notice
fix(x): show inline billing error notice instead of blank turn
2026-07-24 16:00:33 +05:30
hrsvrn
cc5819b469 fix(x): show inline billing error notice instead of blank turn
Billing errors (out of credits, subscription required/inactive) were
rendered as null in the transcript, so if the upgrade dialog was
dismissed or missed the turn ended silently blank. Render an inline
BillingErrorNotice in both the main chat and the chat sidebar instead.

Also watch the sessions-runtime conversation (not just legacy state)
for billing failures, and track handled error ids in a Set so a
previously seen error doesn't block newer ones from popping the dialog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 14:23:29 +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
95618a364f
feat(x): sign Windows builds with Azure Trusted Signing in CI (#783)
* feat(x): sign Windows builds with Azure Trusted Signing in CI

Wire windowsSign (packager + Squirrel maker) through Azure Trusted
Signing's signtool dlib. Signing activates only when the CI env vars
are present; local and mac/linux builds are unaffected. The Windows
job stages the dlib, metadata.json, and a modern SDK signtool under
C:\azsign (space-free paths — @electron/windows-sign splits
signWithParams on spaces).

Requires repo secrets: AZURE_TENANT_ID, AZURE_CLIENT_ID,
AZURE_CLIENT_SECRET, AZURE_ENDPOINT, AZURE_CODE_SIGNING_NAME,
AZURE_CERT_PROFILE_NAME.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): use renamed Microsoft.ArtifactSigning.Client NuGet package

Microsoft renamed Trusted Signing to Artifact Signing and delisted the
old Microsoft.Trusted.Signing.Client package, which broke the setup
step. The dlib inside kept its Azure.CodeSigning.Dlib.dll filename;
locate it by search instead of a hardcoded path so future package
reshuffles fail loudly rather than at a stale path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): don't run bare signtool in the signing setup step

signtool with no arguments prints usage and exits 1, and the Actions
pwsh wrapper propagates the last native exit code as the step result,
failing the job after an otherwise successful setup. Log the signtool
version from file metadata instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(x): stage only the current platform's node-pty prebuilds

Every build shipped all platforms' prebuilt binaries. Windows code
signing walks every .node file in the app and signtool hard-fails on
the Mach-O darwin pty.node ("file format cannot be signed"). Filtering
to the host platform fixes signing and drops dead weight from all
installers. The Linux CI-compiled prebuild and the node-gyp self-heal
path both still stage correctly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 14:12:39 +05:30
PRAKHAR PANDEY
873187805f
Merge pull request #781 from prakhar1605/fix/onboarding-ux-batch
fix(x): add Sign in with ChatGPT to onboarding LLM setup
2026-07-22 23:23:55 +05:30
Prakhar Pandey
ca399561da fix(x): add Sign in with ChatGPT to onboarding LLM setup
Onboarding's LLM step predates ChatGPT-subscription support, so the option
was reachable in the composer and Settings but missing from first-run
setup. Mirrors Settings exactly: when the OpenAI provider card is selected,
the ChatGPT subscription sign-in block renders directly under the OpenAI
API-key field (gated on llmProvider === "openai", same as
settings-dialog.tsx), reusing the existing useChatGPT hook / chatgpt:* IPC
— a nested block, NOT a standalone provider card. ChatGPT never enters the
LlmProviderFlavor union, so the BYOK config/save machinery is untouched.
Signing in counts as a connected provider (chatgpt.status.signedIn) so
Continue unlocks with no API key, and the block shows the same three-state
UI (Sign In / Waiting / Connected + Sign Out) as Settings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 23:09:11 +05:30
Harshvardhan Vatsa
aad414065b
Merge pull request #780 from rowboatlabs/fix/settings-dialog-tab-scroll
fix(x): default settings dialog tabs to scrollable
2026-07-22 21:12:18 +05:30
hrsvrn
b893700da6 fix(x): default settings dialog tabs to scrollable
Invert the tab scroll logic in the settings dialog content area:
instead of listing every tab that should scroll, default to
overflow-y-auto and only opt out the JSON editor tabs (which render
a self-scrolling full-height textarea) and note-tagging (which
manages its own scroll region). New tabs are no longer clipped by
the fixed-height dialog unless explicitly opted out.
2026-07-22 20:29:47 +05:30
PRAKHAR PANDEY
40e450f0ae
feat: explicit Assistant model picker for BYOK providers (#778)
* feat(x): ModelSelector liveCredentials — scoped live group from unsaved creds

A provider being configured right now has no store group (models.json
not saved yet) and openrouter/aigateway/ollama/openai-compatible have
no static catalog either. liveCredentials synthesizes the scoped live
group from the form's typed credentials, winning over a saved group
whose stored key may be stale. Same 'some credential present' bar as
the store; useProviderModels' debounce + cache prevent fetch spray.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(x): explicit Assistant model picker on the BYOK provider card

The four category fields said 'Same as assistant' while the assistant
model itself was invisible (silently auto-resolved at connect). Adds an
Assistant model ModelSelector above them: scoped to the card's flavor,
live-fetching with the CURRENT typed credentials (liveCredentials, so
unsaved keys work), allowCustom for arbitrary ids. The Auto sentinel
keeps today's silent resolve and shows what it would pick right now
('Auto (currently gpt-5.4)') once the live list settles. An explicit
pick writes through setPrimaryModel into models[0] (models[1..]
preserved) and connect uses it verbatim — no silent swap; Auto follows
exactly the old resolve-then-save flow including the on-demand fetch.
Replaces the openai-compatible-only free-text Model field (customModel
state + unconfirmed-model sync effect deleted): typing the id in the
picker's search covers the no-/models servers, for every provider.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 16:36:07 +05:30
Harshvardhan Vatsa
d589325c3c
fix(x): make meeting system-audio capture work on Linux (#757)
* fix(x): make meeting system-audio capture work on Linux (#722)

Meeting transcription failed on Linux because the system-audio side of
the recorder was unreliable there. Two real problems, neither of which
was the one proposed in the issue:

- desktopCapturer.getSources() goes through the Wayland screencast
  portal, which can block on a system dialog or hang outright. Audio-only
  display-media requests (meeting transcription discards the video track)
  now answer with the requesting frame as the mandatory video source and
  Chromium's PulseAudio loopback for audio - no portal involved.

- Chromium captures loopback audio at the default sink's monitor-source
  volume. Desktop tools sometimes leave that volume near zero, which
  turns the capture into digital silence with no error anywhere. Before
  capture starts, ensureLinuxMonitorVolume() raises it back to 100%
  (raise-only, best-effort via pactl).

The issue's proposed fix - enumerating PipeWire/Pulse monitor sources
via enumerateDevices() and capturing with getUserMedia - is not viable:
Chromium filters monitor sources out of input-device enumeration on
Linux (audio_manager_pulse.cc), so they never appear. Verified against
Electron 39 / Chromium 142 that audio:'loopback' works on Linux through
the Pulse layer (PipeWire's pipewire-pulse or classic PulseAudio).

On Linux the loopback track mirrors the output device rather than the
meeting app, so "ended" never fires when the meeting closes; auto-stop
relies on the existing silence detector. Capture failures now surface a
toast pointing at PipeWire/PulseAudio instead of silently going idle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(x): restore Linux system-audio capture lost in main merge

The merge of main (5ad7ddb1) resolved the conflict in main.ts by taking
main's refactored configureAppDisplayMediaHandler wholesale, dropping
ensureLinuxMonitorVolume() and the Linux branch of the display-media
handler while keeping the now-unused execFile/promisify imports.

Re-apply both into the new handler structure: audio-requesting display
media on Linux is answered with the requesting frame + 'loopback' (no
Wayland portal), after raising a near-zero default-sink monitor volume
back to 100% via pactl (raise-only, best-effort).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 14:53:48 +05:30
PRAKHAR PANDEY
8a782358bc
fix(x): settings Models tab tracks Rowboat sign-out live (#775)
Signing out with the dialog open left the Models tab on the stale
signed-in section: the dialog snapshotted oauth:getState once per open
into local state and nothing updated it. Main DOES broadcast sign-out —
disconnectProvider emits { provider, success: false } on the
oauth:didConnect channel — and the useModels store already refetches on
that channel, so the fix is to derive the dialog's rowboatConnected
from useModels().isRowboatConnected (single source of truth). The
section now swaps live on sign-out AND sign-in; the composer's rowboat
group already followed the store. Documents the channel's
fires-on-disconnect-too behavior at the subscription site and covers
the sign-out flip in the store tests.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 14:05:07 +05:30
PRAKHAR PANDEY
2274369383
Feat/model selector (#770)
* feat(x): add useModels hook — shared model-catalog store

One module-level store behind useSyncExternalStore: N mounted consumers
share one snapshot and one in-flight fetch (models:list + models.json +
oauth state + llm:getDefaultModel). Re-fetches on models-config-changed,
oauth:didConnect and chatgpt:statusChanged; newest-wins seq guard ports
the composer's epoch race protection. Extracted verbatim from the chat
composer's loadModelConfig for the ModelSelector consolidation (Phase 1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(x): add ModelSelector extracted from the chat composer

Controlled picker ({provider, model} value + onChange, persistence stays
with the caller) rendering the composer's exact UI: provider-grouped
catalog + live groups via useProviderModels, search filter, standalone
default row, locked-model display, and the reasoning-effort control
(shown only for catalog-flagged reasoning models, '' reported up when
the model loses reasoning support). Catalog comes from useModels().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(x): migrate chat composer to useModels + ModelSelector

Deletes the composer's inline copies of the catalog loader (oauth state,
llm:getDefaultModel, models:list, models.json parsing, event listeners)
and the picker/effort dropdown JSX in favor of the shared hook and
component. Behavior preserved: picks persist globally via
models:updateConfig({defaultSelection}) + models-config-changed dispatch,
locked-model display, per-turn unpersisted reasoning effort, and the
tab-activation re-fetch. SelectedModel/ReasoningEffortLevel exports stay
for existing consumers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(x): ModelSelector settings modes — sentinel, field trigger, provider scoping, custom ids

defaultOption pins a top entry that selects null ('Rowboat default' /
'Same as assistant'); variant='field' renders a SelectTrigger-style
full-width box; providerFilter scopes to one provider's group, falling
back to the raw models:list catalog (newly exposed on the useModels
snapshot as catalogByProvider) for providers mid-setup; allowCustom
offers a Use-"<text>" row when the search matches nothing. onChange
widens to ModelRef | null — the composer guards the (unreachable
without defaultOption) null and is otherwise untouched. Still zero
persistence inside the component.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(x): signed-in Settings Models section on ModelSelector

The four hybrid dropdowns (Assistant / Knowledge graph / Background
agents / Permission checks) become field-variant ModelSelectors with a
'Rowboat default' sentinel, sourcing the shared store's groups — BYOK
providers without a static catalog (e.g. OpenRouter) now live-fetch
their full list inside the open dropdown instead of showing only the
saved model (user-reported bug). State is ModelRef | null; the
provider::model hybridKey round-trip, options collector, and local
providerDisplayNames map are gone. Persistence unchanged: nothing
writes until Save's single models:updateConfig + models-config-changed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(x): BYOK per-provider category fields on ModelSelector

Knowledge graph / Meeting notes / Track block / Auto-permission become
provider-scoped field-variant ModelSelectors (providerFilter=active
card, allowCustom, 'Same as assistant' sentinel), replacing the static
catalog Select + free-text Input pair. Values still persist as bare
model-id strings inside providers[flavor] through the existing save
path — the ModelRef ↔ string adapter lives at the call site. Deletes
the section's own models:list loader (modelsCatalog/modelsLoading/
showModelInput/LlmModelOption); scoped pickers fall back to the store's
catalogByProvider for providers mid-setup, and configured providers now
live-fetch full lists (OpenRouter bug fix on the BYOK side too).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(x): assert defaultModel refreshes on models-config-changed

Covers the settings-Save → event → store refetch leg that updates a
fresh composer tab's trigger label.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(x): ModelSelector inheritance mode + un-scoped custom ids

inheritDefault?: {label} is defaultOption with placeholder styling —
one sentinel code path, the trigger renders the label muted when
nothing overrides. allowCustom no longer requires providerFilter: an
un-scoped custom entry splits 'provider/model' on the first slash
(OpenRouter-style ids must be typed provider-qualified) and pairs
slash-less text with the global default's provider, matching how the
runtime resolves a provider-less override. Adds modelOverrideToRef /
refToModelOverride adapters for the two-optional-strings persistence
shape shared by BackgroundTask and LiveNote.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(x): bg-task model/provider override on ModelSelector

The Advanced section's two free-text Inputs become one inheritance-mode
picker ('(global default)' sentinel, allowCustom for arbitrary ids).
Same draft state and bg-task:patch save path; a task with model set but
no provider round-trips untouched via the shared override adapters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(x): live-note model/provider override on ModelSelector

Same inheritance-mode picker as bg-tasks (the section was a copy-paste
of it), same shared adapters, unchanged live-note:set save path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(x): EnableAgentsDialog model picker on ModelSelector

Replaces the flat models:list-flattening <select> with an
inheritance-mode picker pre-seeded from the pinned agent's bg-task
model/provider. '(default)' (null) still enables with an active-only
patch so agents keep their pinned model; a picked model still patches
each bundled agent once. Drops the dialog's private catalog fetch —
the shared store already has the data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(x): ModelSelector staticOptions mode for caller-supplied lists

When staticOptions is set the picker renders ONLY those rows (+ the
defaultOption sentinel) — no catalog groups, no live fetches. Entries
are opaque engine ids carried in ref.model with provider '', so
call-site adapters stay one-liners ('default' ↔ null, id ↔ string).
Rows whose label differs from their id show the id as secondary text,
disambiguating Claude's colliding 'Opus' labels (alias + concrete id).
Adds toSelectorOptions() in code-agent-options to split an engine list
into sentinel label + rows, and a triggerTitle prop for header tooltips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(x): new-session dialog on ModelSelector

Deletes loadModelOptions() — the admitted composer-mirror duplicate —
in favor of the shared store: the Rowboat-mode picker (still rendered
only for rowboat mode, gated on configured providers) is a field
ModelSelector whose null sentinel keeps 'default → omit model/provider'
create-payload semantics. The agent-model select becomes a
staticOptions picker fed by toSelectorOptions(fetchCodeAgentOptions),
preserving per-agent reload/reset and the 'default → omit agentModel'
payload rule. The effort select stays a plain Select on purpose — a
caller-supplied axis, not model selection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(x): code session header agent-model picker on ModelSelector

The header's model DropdownMenu becomes a pill ModelSelector with
staticOptions (search + Opus id disambiguation for free); the effort
dropdown stays untouched. codeSession:update payload semantics
unchanged — the sentinel still sends the literal 'default'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(x): delete dead onboarding-modal.tsx

Nothing imports it — App.tsx renders OnboardingModal from
components/onboarding/index.tsx (the steps-based flow). Its rich
four-category model picker was superseded by the silent auto-resolve
in use-onboarding-state; the only other mention was a stale comment in
useConnectors, updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 10:59:35 +05:30
Ramnique Singh
711aff58f2
Merge pull request #769 from rowboatlabs/feat/configurable-model-call-limit
feat(x): configurable model-call limits, default raised to 50
2026-07-22 10:57:14 +05:30
Ramnique Singh
c7b9ca5a61
Merge branch 'main' into feat/configurable-model-call-limit 2026-07-22 10:53:12 +05:30
Ramnique Singh
f10bfdf9f1
Merge pull request #774 from rowboatlabs/bg_agent_run_failed
Log background agent failure reasons
2026-07-22 10:51:55 +05:30
Ramnique Singh
e659f0bdde Log background agent failure reasons 2026-07-22 10:48:36 +05:30
gagan
358e819a15
Merge pull request #773 from rowboatlabs/feat/inline-charts-skill
feat(x): inline interactive charts in chat via a charts skill
2026-07-22 01:46:42 +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
efa6a0523c
Merge pull request #772 from rowboatlabs/max-calls-50
Some checks failed
rowboat / apps/x Vitest suites (push) Has been cancelled
rowboat / apps/x Electron package smoke test (push) Has been cancelled
bump max model calls to 50
2026-07-22 00:09:31 +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
Harshvardhan Vatsa
ed97392557
Merge pull request #767 from rowboatlabs/feat/update-card-scrollable-notes
feat(x): scrollable release-notes box on the update card, notes on Windows too
2026-07-21 16:34:38 +05:30
Gagan
180ae10e2b fix(x): wrap-up call answers substantively, not with a status report
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.
2026-07-21 16:32:18 +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
47453fc333 fix(x): nudge stepper placeholder up a bit more (4px) 2026-07-21 15:50:04 +05:30
Gagan
34501ee2cf fix(x): optically center the 'Same as above' placeholder in the chat stepper 2026-07-21 15:37:21 +05:30
Gagan
af19cad43b fix(x): chat model-call field opens as 'Same as above' unless it differs
A chat override equal to the global limit is behaviorally identical to
no override, so persist it as absent and collapse it on load. Stepping
the empty chat field (which lands on the global value) no longer pins a
redundant override that makes the field open with a number forever.
2026-07-21 15:33:35 +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
Harshvardhan Vatsa
60165979f0 feat(x): scrollable release-notes box on the update card, notes on Windows too
The notes pane was a bare max-h clip: nothing signalled that more content
sat below the fold, and Squirrel.Windows never supplies release notes at
all, so Windows users only ever saw the static fallback line. Squirrel.Mac
is no better in the edit-after-publish window — it snapshots the release
body at download time, which is how v0.7.7 shipped a card showing two
paragraphs of a ten-item release.

- Render the notes in a bordered, inset scroll box (max-h + overflow-y)
  so the frame and scrollbar make the overflow visible on both platforms.
- Backfill the notes from the GitHub release body after update-downloaded:
  gives Windows notes for the first time and replaces macOS's stale
  snapshot with the current body. On fetch failure the snapshot (or the
  fallback line) stands.
- Strip the release body's leading "What's new" heading, which duplicated
  the card's own label.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 17:51:14 +05:30
arkml
63a99e5fe4
Fix typos in README.md (#766)
Corrected typos in the README regarding the browser and app features.
2026-07-20 17:20:58 +05:30
gagan
789301402f
Merge pull request #765 from rowboatlabs/fix/first-app-built-credit
fix(x): grant first_app_built credit for copilot-built apps
2026-07-20 16:27:18 +05:30
Gagan
a2971a08f9 fix(x): grant first_app_built credit for copilot-built apps
The reward only fired in the apps:create IPC handler, which no UI path
invokes — the copilot builds apps by writing the folder directly, so the
credit never triggered. Fire it from apps:list instead, off observed
state: any valid non-installed (kind=local) app counts as built.
Catalog installs (.rowboat-install.json present) don't count.
2026-07-20 16:15:41 +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