Commit graph

2124 commits

Author SHA1 Message Date
Ramnique Singh
4e4a91583f 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>
2026-07-23 10:49:58 +05:30
Ramnique Singh
9eb6a81b98 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>
2026-07-23 10:42:30 +05:30
Ramnique Singh
df72bb2b29 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>
2026-07-23 10:38:15 +05:30
Ramnique Singh
71c5034e10 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>
2026-07-23 10:33:35 +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
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
arkml
7b032537bc
Merge pull request #756 from rowboatlabs/fix/rtl-bidi-rendering
fix: auto-detect text direction for RTL/BiDi chat content
2026-07-16 16:37:58 +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
arkml
ab03a2d619
Merge pull request #752 from rowboatlabs/feat/posthog-usage-analytics
Some checks failed
rowboat / apps/x Vitest suites (push) Has been cancelled
rowboat / apps/x Electron package smoke test (push) Has been cancelled
feat(x): PostHog usage analytics across all feature surfaces
2026-07-15 11:07:10 +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