Generic app-copilot bridge (no app-specific wiring anywhere):
- app-read-data builtin: read (or list) an installed app's data/ files —
the agent-maintained JSON the app renders. Same path confinement as
app-set-data; 50k char cap with truncation flag; parsed JSON returned
when possible. Attached via the app-navigation skill.
- read-view view=apps: installed apps with name/description/dataFiles/
agentSlugs, navigating the user's screen to the Apps view (same
show-while-telling contract as email/bg-tasks). Renderer routes the
new view value.
- copilot instructions embed the installed-apps list (name +
description per app) with routing guidance: prefer an app's fresh
data over external calls, answer from it, open-app to surface it.
The apps:list handler fingerprints the app set and invalidates the
instructions cache when it changes (installs, deletes, copilot-
created folders all flow through that poll).
- app-navigation skill: apps section + a worked example framing the
pattern generically (match apps by their own descriptions, never a
hardcoded topic map).
Verified against real data: app-read-data returns pr-dashboard's live
10-PR data.json, escape paths rejected, key-order golden test updated
and passing.
Findings from a fresh post-refactor review. The turn-runtime bridges
imported convertFromMessages and getToolPermissionMetadata from
agents/runtime.ts — the legacy engine file — making it undeletable;
both move to neutral modules (agents/message-encoding.ts,
agents/permission-metadata.ts) that both engines now share. The
abort registry, used by the live tool registry, moves out of the
legacy runs/ cluster into turns/. Dead surface goes: the zero-consumer
CopilotInstructions, skillCatalog, RunLogger, and MappedToolCall
exports are deleted, mapAgentTool/StreamStepMessageBuilder become
module-private, and the mid-file import scar from the composer
extraction is hoisted.
Three review fixes ride along: ITurnEventBus gains
subscribe/subscribeAll so consumers stop resolving the concrete hub;
skill carry-forward becomes its own registry trait instead of
overloading workspaceContext; and connection checks (slack/composio/
code-mode/google) collapse into one shared connections.ts consumed by
both skill availability and the copilot prompt blocks — plus stale
doc pointers (VIDEO_MODE.md to moved prompt text, CLAUDE.md's dangling
AGENTS.md reference).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Previously catalog readers (listOnboardingModels, getChatModelIds)
fetched models.dev inline on a 24h TTL, while the reasoning-capability
paths read the cache only — so a signed-in install could run forever
with no cache and the effort chip never appeared, and catalog calls
could stall on a slow third-party fetch.
Now main calls startModelsDevRefresh() once at boot: refresh if the
cache is missing or older than 24h, then every 24h while running, with
a 10s fetch timeout, errors logged and swallowed (existing cache stays
in use). Every consumer reads the on-disk cache only. Catalog-shaped
readers (models:list, gateway annotation) await the warm-up's first
attempt so a fresh install's first list sees the fetched data; the
turn-start capability gate never waits. A missing cache is an empty
catalog, not an error — chat is unaffected either way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a Brain chip next to the model picker (Auto / Fast / Balanced /
Thorough) in both the full-screen and side-pane composer. Selection is
per-tab, next-turn intent like the model picker (same ref pattern), but
unlike model it is never frozen on a run — it applies turn by turn,
riding sendConfig.reasoningEffort into turn_created.config.
The chip renders only when the effective model (run-frozen, picked, or
app default) is known-reasoning per the models:list capability flag,
and a stale selection resets when switching to a non-reasoning model.
Turns that ran at a non-auto effort show the level beside the existing
token-usage affordance, read retroactively from the persisted turn
config; reasoning tokens were already displayed there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The side-pane copilot renders messages through its own markup in
chat-sidebar.tsx, which never got the hover copy button added to the
full-screen chat renderer. Maximizing the pane stays on the same code
path, so it was missing there too.
- Hover kebab menu on sidebar chat rows with Pin / Rename / Delete
- Rename: inline edit persisted via the existing sessions:setTitle;
custom titles survive since auto-titling only fills empty titles
- Pin: up to 3 chats float to the top of the list (localStorage)
- Delete: confirm dialog, sessions:delete, closes any open tab
- Dialog shell: sectioned sidebar nav (Configure / App), larger header
without divider, more padding
- Connections & Mobile: real brand logos (Google, Fireflies, Slack,
WhatsApp, Telegram), no icon tiles, no separator lines
- Models: provider logos on the your-own-providers cards
- Code Mode: Anthropic/OpenAI marks for Claude Code and Codex
- Help: GitHub and Discord logos, tile-free rows
- Dialog overlay: frosted backdrop blur
- Chat empty state: smaller type scale in the side-pane copilot,
compact prop on connections card
- Background tasks: stop button sized to match the Updating pill,
wraps under it in narrow panes, aligned with the state toggle
- Sidebar: Code above Meetings
- Brain: page + graph background matches other tabs, darker light-mode
borders for Base rows and Files containers, bolder section labels
- Top-align icons with titles on two-line nav rows; consistent ink
color for Apps/Background agents/Workspaces (sublabels stay muted)
- Quick actions: labeled New chat button, secondary actions as
compact ghost icons
- Chat empty state: icon-free heading, single suggestion list with
leading icons and hover arrows, aligned to the composer column
- User message bubbles: rounded-2xl with flattened top-right corner,
hover-only copy button under the bubble's bottom-right
- Sidebar: hairline gap between menu items so adjacent active/hover
highlights don't merge
The follower's join protocol was already unit-tested, but the hooks
grew stateful behavior of their own: useTurn's reset-on-turnId-change
vs keep-while-disabled, the snapshotFailed signal and feed-event
recovery, and useAgentRunTranscript's legacy runs:fetch fallback and
loading/error derivation. Tests drive the real turn-feed singleton
through a stubbed window.ipc preload surface.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the turn-spine unification. Chat's SessionChatStore now
consumes turns:events like every other surface, joining live turns by
file offset (drop covered, append contiguous, refetch on gap) instead
of blind-appending session-bus events. Text/reasoning deltas cross IPC
only for turns a window subscribed to (turns:subscribe/unsubscribe, a
per-webContents registry that also survives window teardown), so
headless pipeline chatter never reaches windows that aren't watching.
The channels bridge settles turns off the turn event bus instead of
filtering the session broadcast. With no turn-event consumers left,
SessionsImpl stops forwarding entirely (it drains its execution streams
and keeps outcome/index handling) and sessions:events shrinks to
index-changed entries — one channel for turn events, one for session
metadata.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Background-task and live-note transcript views move from fetch-once to
the shared useAgentRunTranscript hook (useTurn over the turns:events
spine, with a one-shot legacy runs:fetch fallback for pre-migration run
ids), so an in-flight run's transcript now streams live. The headless
runner drains its execution stream — delivery rides the turn event bus,
and an unconsumed HotStream buffered every durable event until settle.
Deletes the caller-less runHeadlessTurn duplicate (HeadlessAgentRunner
is the implementation; session-design.md now says so) and the orphaned
web-search skill file that was never registered.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TurnRuntime now publishes every turn's events (durable ones tagged with
their 1-based file offset, deltas without) to an injected TurnEventHub,
regardless of who started the turn. Main forwards durable events to all
windows on one turns:events channel; the renderer's new useTurn(turnId)
hook joins live turns gap/duplicate-free via the offset protocol. The
sub-agent card drops its 1s polling for push updates, and the nine
per-channel window fan-out loops collapse into one broadcastToWindows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The collapsed card read just "Sub-agent:" — zero information, and the
expanded view showed the task twice (the task chip plus the child
transcript's opening user message, which IS the task).
The title is now "<Humanized name>: <task>" ("London weather: Find the
current weather…"), truncated by the header with the full text on
hover; "Agent" when the model gave no name. The redundant task chip is
gone — the child transcript's first message carries the task.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the agent-as-tool capability deferred in turn-runtime-design §29.2:
a `spawn-agent` builtin that runs a sub-agent in its own standalone
headless turn and returns its final answer to the parent. Multiple
spawn calls in one assistant batch run concurrently (previous commit).
- RequestedAgent is now a union: by-id (unchanged shape) | inline
{name, instructions, model?, tools?}. Inline definitions persist
verbatim in turn_created and resolve to the same immutable snapshot.
- Agent resolution splits by variant: DispatchingAgentResolver narrows
the union once; InlineAgentResolver materializes inline specs
(builtin catalog validation, headless default profile when tools are
omitted); RealAgentResolver keeps the by-id path byte-identical. The
builtin→ToolDescriptor conversion is extracted to a shared helper.
- The spawn handler (RealToolRegistry branch → runSpawnedAgent) runs
the child via HeadlessAgentRunner on the parent's model by default,
clamps the model-call budget at 20, cascades the parent's abort
signal, and records {kind:"subagent", childTurnId} as durable tool
progress — the only parent→child link; no parentTurnId is added to
the schema. Task-level failures return as conversational isError
results, never terminal.
- Depth is capped at 1: both resolvers strip spawn-agent from children
(inline always; by-id via the new subagent composition flag) and the
handler refuses child-shaped parents outright.
- Renderer: spawn-agent calls render as a SubAgentBlock — a collapsed
status card that expands to the child's live transcript
(CompactConversation over sessions:getTurn, polled at 1s while
running; standalone child turns don't reach the session bus).
- The BuiltinTools entry gives copilot (and other catalog-attached
agents) the tool automatically; its execute is the degraded legacy
path only, since the turn runtime intercepts builtin:spawn-agent.
Schema note: RequestedAgent widened under schemaVersion 1 (pre-release)
— requires wiping ~/.rowboat/storage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a Caffeinate switch (Settings → Mobile channels) that uses
Electron's powerSaveBlocker ('prevent-app-suspension', equivalent to
caffeinate -i) to stop the machine from idle-sleeping so the
WhatsApp/Telegram bridge stays connected. While active, an amber
coffee icon in the titlebar shows the state and can be clicked to
turn it off; a power:caffeinateChanged push keeps the titlebar
indicator and the settings switch in sync.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- catalog is ranked by GitHub star count (per-repo lookup with a 10-min
cache; unauthenticated works, the publish token is used when present)
- star button on each catalog card stars/unstars the app's repo as the
signed-in user (public_repo scope covers it), optimistic with revert
and a sign-in hint when signed out
- the info panel now offers Delete for local apps (apps:delete already
existed and cleans up app-owned agents; only installed apps had a
removal button before)
Show a generic animated Working status whenever an active turn can be stopped, and reserve the Thinking shimmer for model reasoning_start/reasoning_end windows.
Keep human-wait state separate so permission and ask-human controls remain interactive, and add renderer state and component coverage for the new behavior.
Removes the placeholder comment added in #684 to trigger the
path-filtered 'test' check; doubles as the trigger for this PR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a harmless comment under apps/x/ so the path-filtered required
'test' check runs on this otherwise README-only PR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cards for apps already installed from the catalog show a green
Installed badge (matched via the install record's registry name) that
opens the app, instead of a second Install button.
Two races around the post-install prompt:
- AppsView kept a stale selectedFolder after the open app was
uninstalled; on reinstall the 4s apps:list poll matched it again and
instantly swapped in the app frame, unmounting the catalog and the
enable-agents dialog mid-choice. Clear the selection when its app
disappears from the list.
- Bundled agents materialized on the NEXT apps:list poll, not at
install — so the dialog's bg-task:get/patch hit a task that didn't
exist yet if the user acted within ~4s. Install handlers now call
syncAppAgents synchronously.
The enable dialog now lists the user's available models (models:list),
preselecting the host-pinned default from materialization. Turning the
agents on patches the chosen model/provider onto the task along with
active — so installers can route a bundled agent to a specific model
at the moment they opt in, without digging into bg-tasks.
Bundled agents install disabled (§8.3), but nothing told the installer
they exist — the app opened empty with the refresher buried in
bg-tasks. After an install that materialized agents, offer to turn
them on: 'Turn on & run now' activates each task and fires a first run
so the app opens with data; 'Not now' keeps them off.
The detail panel's Publish update button opened the same dialog as
first publish, which hardcoded apps:publish — so updating an already
published app always failed with name_taken from the registry check.
The dialog now takes a published flag: it runs apps:publishUpdate with
a version-bump picker (patch/minor/major), shows a simple progress
state (the update path emits no step events), and links the new
release on success.
GitHub rate-limits the device-code token endpoint: a poll arriving even
slightly under the flow interval returns slow_down and permanently raises
the required interval. The renderer polled on a fixed 5s timer (exactly
the limit), so one jittery request tripped slow_down and every poll after
it was 'too fast' — GitHub answered slow_down forever and the dialog sat
on 'Waiting for authorization' even after the user approved.
Core now tracks lastTokenPollAt and skips the request until the flow's
current interval (base +1s margin) has elapsed, so slow_down bumps take
effect and the flow recovers. The renderer timer is just a heartbeat (2s).
- start the apps server before createWindow instead of after the full
service-init chain: the Apps view was reachable ~10s before port 3210
was listening, so every app iframe opened to connection-refused
- retry EADDRINUSE binds (quick relaunch races left the server disabled
for the whole session with no retry)
- app frame: auto-retry the iframe instead of a dead manual Retry, and
surface when the apps server itself is down (new apps:serverStatus)
- log render-process-gone/child-process-gone reasons; renderer helper
crashes were previously invisible in packaged builds
- toolbar (local apps): 'Publish' button; turns into a green 'Published'
badge-check once published (click opens the detail panel)
- detail Source section: clickable repo + release links for published apps
Verified the wire calls are correct (both endpoints behave with this client
id from a plain node run), so instrument the runtime path instead of
guessing:
- log every poll outcome ([GitHubAuth] lines) at the token exchange, the
identity fetch, and the IPC handler
- identity failures now retry at most 3 polls then fail loudly with the HTTP
status — never silently degrade to 'pending' forever
- explicit User-Agent on all GitHub calls (defensive)
GitHub's OAuth endpoints take form-encoded params; the JSON token request
produced an unrecognized error that fell through to 'pending' forever, so the
dialog never advanced after the user authorized.
- form-encode device/code and access_token requests
- unknown token-endpoint errors now fail loudly instead of spinning;
incorrect_device_code maps to expired (restart offered)
- issued-token is remembered so a transient identity-fetch failure retries on
the next poll instead of burning the consumed device code
- dialog catches hard poll failures and surfaces them
- env: GITHUB_OAUTH_CLIENT_ID (device flow enabled on the Rowboat OAuth app;
overridable via ROWBOAT_GITHUB_CLIENT_ID)
- packager (§4.4): allowlist-only .rowboat-app ZIP (yazl), sorted entries,
symlink skip, sha256
- github-auth (§10): device-code start/poll, identity fetch, token stored
0600 with safeStorage encryption injected from main (core stays
electron-free); githubAuth:* IPC + external open of the verification page
- registry client (§9.2): unauthenticated tarball index with 5-min cache and
stale fallback, raw-record resolve, substring search, quota-free
latestManifest via release-asset redirect with name-mismatch guard
- registry repo contents (docs/apps-registry): record JSON schema +
validate-and-merge Action implementing §9.3 checks 1-7 with rejected:<code>
comments and per-name concurrency
- fix: host-api copilot-run adapted to dev's ModelSelection {provider,model}
(this is the same fix dev needs for Ramnique's packaging break)
- pnpm 11: blockExoticSubdeps=false (electron-forge has a git subdep)