* fix(codex): keep parent turns active during sub-agent work
Codex multi-agent activity is thread-scoped. Treating child events as root
activity could end the foreground turn early and splice child output into
parent messages.
Route legacy and current activity shapes through stable child identities and
preserve message boundaries across live and resumed turns.
* fix(codex): preserve sub-agent details across stream chunks
Keep empty activity paths explicit and normalize the synthetic assistant
boundary for every chunk in the active message.
* fix(codex): route legacy child events by thread
* fix(codex): stabilize sub-agent activity lifecycles
* fix(codex): retain child tool activity and terminal state
* Hide keyboard shortcut badges in workspace menu on native
The archive item's shortcut badge (e.g. Ctrl+Shift+⌫) rendered in the
workspace kebab/context menu on mobile, where there's no physical
keyboard to invoke it. Gate the trailing badge behind !isNative in both
kebab menu variants.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(app): hide native shortcuts in every workspace menu
Workspace menus were duplicated across project and status views, so the status path missed the native gate. Centralize the menu so every sidebar view shares the same shortcut policy.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* perf(app): keep large sidebars responsive
Sidebar rows rescanned every agent for each session update, multiplying work with both workspace and agent count. Build workspace activity once and share the derived sidebar model so updates scale with the underlying collections.
* test(app): use valid agent fixture for workspace restore
* refactor(app): keep sidebar model surface focused
* fix(app): keep mobile sidebars synced with store
Gesture-owned skip flags could swallow the next store sync after a no-op or overlapping gesture, leaving React state and Reanimated shared values split. Always reconcile both mobile sidebars from the panel store after gesture animations.
* test(app): keep sidebar sync coverage pure
* fix(app): keep sidebar close transitions idempotent
* fix(app): keep mobile sidebars stable through interruptions
Independent React and worklet state let stale gesture or animation callbacks
overwrite newer panel commands. Use one revisioned target and one transient
motion runtime so outdated callbacks cannot restore an earlier panel.
* fix(pi): avoid raw images for text-only models
Pi text-only models reject image content, and Pi persists the rejected raw image in session history. Materialize those image prompts to local files and send path hints while preserving raw images for vision-capable models.
* fix(pi): require explicit vision support
Default unknown Pi model capabilities to the text-hint path so raw image forwarding only happens for models that declare image input. Also keep materialized image files in a private per-user temp directory with restrictive file modes.
* fix(pi): harden materialized image temp dirs
* test(server): accept per-user image attachment dirs
* fix(server): use private temp dirs for materialized images
* fix(server): refresh missing image temp cache
* test(server): clean image temp dirs
* feat(app): restore compact Changes diff controls
Keep the folder tree behind a view toggle while preserving the flat file list as the default.
Move secondary diff options into the overflow menu so the Changes toolbar stays compact.
* fix(app): mirror whitespace diff option copy
* fix(app): polish diff controls review follow-up
* fix(android): handle denied voice audio focus
Android can deny voice-mode audio focus while another system audio owner, such as an incoming call, is active. Treat that as an interruption so resume does not crash the app and JS stops voice mode coherently.
* fix(voice): keep interruption state consistent
Make native interruption handling payload-aware so iOS resume events do not stop the shared JS runtime, avoid ending voice mode for duck-only Android focus changes, and roll host voice mode back if capture fails after startup enabled it.
* fix(voice): avoid duplicate interruption handling
Keep exactly one Android audio-focus request active at a time and leave JS capture state untouched for non-terminal interruption events.
* fix(android): abandon blocked audio focus requests
When Android denies or blocks voice audio focus, abandon the pending focus request so delayed focus gain cannot arrive after voice mode has already stopped.
* fix(voice): handle interrupted dictation capture
Stop Android resume from replaying after recording restart fails, and propagate native blocked interruptions into dictation so users do not remain in a stale recording state.
Daemon-advertised hostnames still name new/default hosts, but connection refreshes and re-pairing no longer overwrite labels that users already changed.
Drop advertised MCP output schemas (no supported harness forwards them to the model) and remove the redundant wait_for_agent tool. Core agent tool catalog drops ~20.5k -> ~5.6k estimated tokens; ~24.9k -> ~9k with browser tools. Runtime structuredContent is preserved via server-side text serialization. Also corrects the agent-wait timeout guidance for blocking callers.
* feat(settings): consolidate daemon management into the host page
The app-level Daemon section and the localhost host page rendered the
same daemon card in two places. The host page is now the single home:
the Host sidebar group leads with Overview, and a desktop-only "Enable
built-in daemon" option in the host switcher recovers a removed
localhost connection. Old settings/daemon deep links redirect
(COMPAT-tagged).
* fix(settings): hide daemon recovery option when the server id probe fails
Review feedback: !query.isLoading treated an errored IPC probe as
resolved, so the recovery affordance could show without knowing the
local server id. Replace the resolved-boolean hook with a single
discriminated state hook (loading | error | resolved) — errors hide the
affordance and fall back to the settings root in the redirect. Also
collapses the two query subscriptions per caller into one.
* fix(settings): keep daemon recovery reachable when no hosts remain
Review feedback: removing localhost as the only host takes the settings
sidebar down its no-host branch, which never mounts the host picker —
so the "Enable built-in daemon" option was unreachable in the most
common recovery case. The sidebar now owns the single hook call, passes
it to the picker, and renders the same action as a sidebar row next to
"Add host" when no hosts exist.
* test(e2e): update settings host header to Overview
Test drift: the host section's sidebar/header label was intentionally
renamed to "Overview"; the spec still asserted the old "Host" title.
Also assert the retired app-level Daemon row stays absent.
* feat(schedules): per-run workspaces, isolation and archive controls
Scheduled agents never appeared in the sidebar: each schedule reused one
stamped workspace and archived the run's agent on completion. Now every run
mints its own workspace — the entity the sidebar shows — with per-schedule
controls for isolation (local or worktree) and whether to archive that
workspace when the run finishes.
The schedule form is rebuilt on a non-React form model (open/commands/close)
to end the flicker, edit-into-create contamination, and multi-host hydration
bugs that came from effect-choreographed shared state. That lands the reusable
foundations it needed: a form kit over a single control-geometry owner, a
data-access taxonomy (replica/snapshot/fetch) with real load-state semantics,
and lint/boundary guardrails so new screens inherit the paved road.
Also fixes desktop combobox popovers truncating options when the trigger is
narrower than the content. New schedule protocol fields are optional on both
create and update, so old and new clients/daemons stay compatible.
* test(app): complete mock themes for control-geometry tokens
switch and terminal-profile-edit-modal now style through
createControlGeometry, which reads theme.borderWidth[1], theme.opacity[50],
and theme.colors.borderAccent. The hand-rolled mock themes in these two
suites lacked those tokens, so the eager StyleSheet.create mock threw at
import (collection error), not on any assertion. Add the missing tokens.
* fix(schedules): address review findings on run cleanup, edit isolation, load error, sheet dismiss, and preference hydration
- Server: archive the run's workspace even when agent creation fails, so a
misconfigured provider no longer orphans a sidebar workspace/worktree.
- Edit form: keep a stored worktree isolation while worktree eligibility is
still resolving, instead of silently downgrading a saved schedule to local.
- Schedules screen: show the load error/retry UI when every host fails, rather
than spinning forever behind the loading branch.
- Form sheet: fire the parent onClose on native gesture/backdrop dismiss so the
sheet closes instead of re-opening.
- Form model: apply late-loading saved preferences to untouched create-mode
fields (never to edited schedules or user-modified fields).
* fix(schedules): review batch — reconnect resubscribe, worktree prune, Android dismiss, cadence and provider edit safety
- Push-router: re-send terminal and checkout-diff subscriptions after a
reconnect; the old per-hook effect resubscribed on isConnected and the
extracted router did not, so daemon restarts silently stopped terminals_changed
pushes (root cause of the terminal-activity e2e failures).
- Server: pass the run's source repo root when archiving worktree runs so the
worktree is unregistered (git worktree remove/prune), not just deleted; also
archive the workspace when agent creation fails even with archive-off (an
agentless workspace has nothing to inspect).
- Sheet: RN Modal onDismiss is iOS-only — notify dismiss once-per-close on the
Android native path too; replace the JSDOM component test with a real
Playwright dismiss spec and inline the trivial dismiss decision.
- Form model: editing an interval schedule no longer rewrites its cadence to
cron unless the cadence UI is touched (90-minute intervals have no cron
equivalent); switching projects clears stale provider/model state and gates
submit until the new host's snapshot resolves.
* chore(lint): drop custom lint wrapper and boundary script, plain oxlint
The import bans stay in .oxlintrc.json (oxlint-native no-restricted-imports).
The receiver-sensitive checks the custom script enforced are not worth a
parallel lint pipeline; if they come back it will be as oxlint rules.
* fix(schedules): crash-safe run cleanup, capability-gated run options, resilient mutations
- Persist the run's workspace/agent ids on the running-run record so a daemon
crash mid-run can be recovered: restart archives the interrupted run's
workspace under the same policy as normal cleanup (agentless always archives,
otherwise archiveOnFinish is respected).
- Hide Archive on finish (and omit archiveOnFinish/isolation from payloads) on
hosts without workspaceMultiplicity — an old daemon ignores the fields, so
offering the switch there would lie.
- Optimistic pause/resume/delete no longer throw when the schedules cache holds
a still-connecting entry alongside loaded data.
- Mode field is gated on provider mode options, not a selected model, so
model-less providers can pick their advertised modes.
* fix(diagnostics): include desktop app logs
Settings diagnostics now collect the Electron main-process log separately from daemon.log so desktop startup and environment-probe failures are included in app reports.
* refactor(desktop): share log tail reader
* fix(diagnostics): surface desktop app log read errors
* refactor(diagnostics): tighten desktop report API
* fix(desktop): retry login shell env resolution
Finder-launched desktop sessions can inherit launchd's minimal PATH if the login shell probe times out or never prints its marker. Retry once with a non-interactive login shell so profile PATH still has a chance to apply.
* fix(desktop): cap login shell probe retry budget
* fix(desktop): repair csh login env retry
* test(desktop): pin login shell env retry platform
* refactor(desktop): use object params in shell env helpers
* fix(worktree): preserve PATH for lifecycle scripts
Lifecycle command strings were running through login shells, which could rewrite PATH before setup commands saw the daemon environment. Use the shared stable script-shell helper without login startup files, and route related project-authored command strings through it.
* fix(worktree): ignore Bash startup env hook
Project command strings should not source shell startup hooks before they see Paseo's supplied environment. Strip BASH_ENV from lifecycle, loop verify, and ACP string-command shells and pin the behavior in setup tests.
* fix(worktree): resolve Bash through PATH
* fix(worktree): preserve Windows cmd command strings
* test(acp): expect Windows cmd terminal strings
* feat(browser): generalize browser automation hosts
Rename the browser automation client capability to a host-neutral payload, advertise supported commands, and route tab-scoped commands by browserId affinity so future browser hosts can register without another protocol shape change.
* fix(browser): harden browser host registration
Reject unusable browser host capabilities, clean up pending requests when a host registration is replaced, and defer list_tabs affinity updates until every host in the aggregation succeeds.
* fix(browser): preserve host tab affinity across reconnects
Keep stranded tab ownership tied to the disconnected host so the same host can reclaim open tabs after reconnect, and tolerate future command names in browser_host capability payloads while retaining only known commands.
* fix(docker): build images from source
Collapse the npm-install and source-build image paths into one Dockerfile so every Docker image is built from the checked-out workspace packages. Include procps in the runtime image because provider process cleanup depends on ps through tree-kill on Linux.
* fix(docker): include native speech packages
* fix(docker): cover source build inputs
* fix(docker): ignore local agent artifacts
* fix(browser): stop new MCP browser tabs stealing focus
browser_new_tab used openTabFocused, switching the workspace UI to
the new tab and interrupting whatever the user was looking at.
Automation commands don't need visual focus to work: webviews stay
resident and paintable regardless, so open the tab in the background
via openTabInBackground instead.
* fix(browser): stop background MCP tabs hijacking Reload
openBrowserTabForRequest still called setWorkspaceActiveBrowser for
tabs opened in the background, so the desktop Reload/Force Reload
menu (which targets getMostRecentWorkspaceActivePaseoBrowserWebContents)
could reload an agent's hidden tab instead of whatever the user was
looking at. Automation commands never needed this: every tab-scoped
MCP tool takes an explicit browserId, and the existing
useSyncWorkspaceActiveBrowser hook already keeps "active browser" in
sync with real UI focus. Drop the redundant call.
* fix(providers): show Claude Ultra Code by capability
Expose Ultra Code for xhigh-capable Claude models and keep first-party Claude model metadata in one manifest so future model updates do not need scattered feature lists.
* fix(providers): preserve Claude fast mode aliases
Normalize versioned Claude model IDs through the manifest before checking fast-mode support so dated Opus IDs keep the same feature surface as their base model.
* fix(providers): keep Claude fast mode first-party
Anchor Claude model normalization so custom settings models that merely contain first-party IDs do not inherit manifest-only fast mode.
* fix(providers): preserve Claude runtime aliases
Keep Fast Mode capability checks strict to manifest IDs while allowing provider-form Claude runtime model strings to resolve known model metadata.
Resident nodes restored from the parking host could carry stale offscreen styles after hot reload, so reapply the permanent parking path before returning them.
Delete the renderer capture prep handshake and keep screenshots on the serialized invalidate/retry path. The capture harness now defaults to the production P1 attach-off parking check.
Element annotation is now free-text: dropped the Fix/Change/Question/
Approve intent pills and the intent field, so the user just says what
they want. The card uses the shared Button and is titled "Annotate
element".
Toolbar: the element picker and DevTools are no longer dev-gated and
work in prod; DevTools uses a wrench icon, and the old clipboard "grab"
is now a Camera "Screenshot element" tool. Each toolbar button
highlights only its own mode, and the copy toast reflects whether a
screenshot image was actually captured.
Browser tools were registering union output schemas that the MCP SDK could not validate at call time. Use raw object output shapes and cover the real tools/call boundary.
Move the New schedule button out of the screen header (the only screen
using MenuHeader rightContent) into the content filter row, and match the
History screen's full-width layout instead of the 720px centered column.
Tighten the shared sm SegmentedControl and Button vertical padding so the
status switcher and button sit flush with the host-filter pill; this is a
global sm change and also affects settings and the schedule form sheet.
* feat(schedules): polish schedules list and editing
Adds host filtering, status grouping, and target resolution to the
schedules screen. Extracts a shared HostFilter from sessions. Improves
schedule editing with project targets and model hydration. Handles
missing targets server-side instead of retrying forever.
* test(mcp): update schedule mocks for createOrReplace
* fix(schedules): only show host name when multiple hosts exist
The Anthropic usage API returns resets_at: null when a window has no
active session. The schema only allowed string/undefined, so the parse
threw and the whole Claude provider surfaced as an error in the quota
panel. The consuming code already handled null; only the schema was
stricter than the API.
* fix(app): keep composer autocomplete visible after route hops
Workspace callers now express only the target workspace. The navigation helper chooses whether to pop to a mounted host route or fall back, and active-workspace tracking ignores stale hidden route params while app-wide routes are foregrounded.
* test(app): address workspace navigation review
* fix(app): parse decoded legacy workspace routes
* fix(app): ignore decoded legacy tab routes
* test(app): use canonical offline workspace route
* fix(app): preserve new workspace drafts across archive
The New Workspace project picker was still tied to the last active workspace. Archiving that workspace could make the picker fall back to another project, changing the draft key and clearing the composer.
* refactor(app): extract new workspace project selection
Keep the archive-draft fix out of the screen effect by moving the project selection transition into a small pure model with focused coverage.
* test(app): seed host in archive draft regression
Make the new Playwright regression self-contained by seeding the local host before opening the app shell.
* refactor(app): home new workspace project picker state
* fix(app): reset project picker on capability hydration
* fix(app): preserve manual project picks during hydration
* fix(app): keep project selection through archive gaps
* fix(app): narrow archive-gap project preservation
* fix(app): refresh new workspace project selection
* feat(app): open a project with Cmd+O
Cmd+O now opens the project picker (was Cmd+Shift+O). New worktree,
which previously used Cmd+O, no longer has a keyboard shortcut; its
sidebar button still creates one.
* test(app): assert old Cmd+Shift+O open-project binding is unbound
Locks in the rebind to Cmd+O so re-adding a Cmd+Shift+O binding would fail CI.
* fix(app): keep Open project override id stable and forward Cmd+O in desktop browser
- Binding ids for Open project keep their original names so existing user
shortcut overrides (keyed by binding id) survive the Cmd+Shift+O -> Cmd+O rebind.
- Forward "o" from focused browser webviews in the desktop app so Cmd/Ctrl+O
reaches the renderer and opens the project picker there too.
* feat: add Schedules screen to manage scheduled agents
Adds a Schedules section in the sidebar header (below Sessions) for viewing
and managing new-agent schedules: recurring jobs that spawn an agent on a
cron or interval cadence.
- Borderless table showing provider icon, model, cadence, next run, and
status, with row actions to edit, pause/resume, run now, and delete.
- Create/edit sheet that reuses the agent provider/model/mode/working-
directory pickers, plus an interval/cron cadence editor with presets,
validation, and a UTC preview.
- Responsive: hover-revealed row actions on desktop, an always-visible
action menu on mobile.
- Data layer over the existing schedule RPCs (list/create/update/pause/
resume/delete/run-once) with optimistic pause, resume, and delete.
Scope is limited to new-agent schedules for now.
* Make the schedule form controls visually consistent
Merge the provider and model pickers into the single nested selector the
draft screen uses, render it as a full-width field, mute the placeholder,
proportion the cadence segmented controls, unify the cron preset chips,
and route the sheet scroll through the themed scrollbar.
* Address schedules review feedback
* Translate schedule hook client fallback errors
* Make schedules global and project scoped
* Stabilize project settings transport E2E
* Reset schedule model on cross-host project change
* Reject malformed cron step expressions
* Tighten schedules project query freshness
* refactor(schedules): clean up review feedback
* test(schedules): extract fake host setup
* refactor(schedules): share cron expression parsing
Use one structural cron parser for app previews and daemon cadence validation so the accepted grammar cannot drift between surfaces.
Remove the unused host-scoped schedules redirect while keeping the global schedules route.
* Add opt-in browser tools for desktop tabs
Adds the daemon opt-in, desktop tab routing, MCP tools, and real browser automation surfaces for Paseo desktop browser tabs.
* Fix browser tools CI expectations
* Address browser tools review findings
* Restrict browser file automation paths
* Fix browser upload test on Windows
* Harden browser navigation inputs
* Make browser tools create usable tabs
* Update browser MCP empty-state test
* Fail browser tab creation when registration times out
* Fix browser screenshots for agents
* Hide disabled browser tools from agents
* Address browser tools architecture review
* Replace browser tools review tests
* Wrap browser tab registration errors
* Mock Expo Router in app unit tests
* Handle invalid browser automation requests
* Return browser failure on desktop disconnect
* Update browser disconnect websocket test
* Relax browser timeout polling test
* Handle invalid browser responses
* Return browser failure when send fails
* Remove local diagnostics and fixture paths
* Fix dev service home fallback
* Use worktree home for dev services
* Use managed daemon in desktop dev
* fix(browser): keep agent tabs addressable
Track agent-active browser targets separately from human-focused tabs and keep resident webviews alive for automation. Browser tool visibility now comes from registration while the broker reports disabled execution.
* refactor(browser): register tools through catalog
Move browser tool registration onto the shared Paseo tool catalog so the MCP server remains only the transport adapter.
* fix(settings): translate browser tools host error
* ci(playwright): shard app e2e in CI
Run the app Playwright suite across isolated CI shards and keep restarted E2E daemons on the same speech-disabled setup path as global setup.
* test(app): share disabled speech e2e env
* fix(app): place turn footers after trailing tools
Completed turn footers now render on the last visible item before the next user message while still using the latest assistant message for footer content and timing.
* test(app): cover split turn footer placement
* feat(voice): configure OpenAI STT and TTS endpoints separately
Replace providers.openai.voice (a single apiKey/baseUrl shared by
speech-to-text and text-to-speech) with independent providers.openai.stt
and providers.openai.tts, each carrying its own apiKey/baseUrl. STT and
TTS now resolve fully independently, so they can point at different
OpenAI-compatible endpoints. The env equivalents OPENAI_VOICE_API_KEY /
OPENAI_VOICE_BASE_URL split into OPENAI_STT_* and OPENAI_TTS_*.
No backcompat: the voice key is removed and no longer read. Each feature
still falls back to providers.openai.apiKey/baseUrl, then OPENAI_API_KEY/
OPENAI_BASE_URL. Composer dictation resolves from the STT endpoint.
* fix(voice): keep daemon bootable and respect global OpenAI key
Two issues from review of the STT/TTS endpoint split:
- A config from an older release that still sets providers.openai.voice
crashed daemon startup, because the strict schema rejects the now-unknown
key. Strip it before parsing (alongside the existing local.autoDownload
strip) so the daemon boots; the value is discarded, not migrated.
- An empty endpoint env var (e.g. a copied .env.example leaving
OPENAI_STT_API_KEY= blank) shadowed the OPENAI_API_KEY fallback, so
speech was reported as missing credentials despite a configured global
key. firstDefined now skips empty/whitespace strings.
* fix(voice): isolate STT and TTS option parsing per endpoint
An STT-only OpenAI setup could be broken by a stale or invalid TTS env
var (e.g. a leftover TTS_VOICE/TTS_MODEL), because the single resolution
schema validated both endpoints' option groups before the per-endpoint
gate. Split into endpoint-key, STT-option, and TTS-option schemas and
parse each option group only when that endpoint has credentials, so an
unused endpoint's bad env can no longer take down the configured one.
* fix(voice): tag voice-config shim and update direct-daemon test callers
- Mark the providers.openai.voice strip with a COMPAT(openaiVoiceConfig)
comment + removal date so it shows up in the back-compat cleanup
inventory, per repo convention.
- Update the tests that build the daemon directly with a resolved OpenAI
config (bootstrap smoke + the real-API voice/daemon e2e suites) to the
new { stt, tts } shape; the old top-level { apiKey } is no longer read,
and these files are excluded from typecheck so the break was silent.
* fix(voice): update voice-roundtrip debug script to new OpenAI config shape
Last direct daemon caller still passing the removed top-level
openai: { apiKey }; the debug script lives outside tsconfig.scripts.json
so the stale shape wasn't caught by typecheck. Use { stt, tts }.
* feat(app): show host in command center and multi-select host filter
Command-center agent results now show which host the workspace lives on,
shown only when more than one host is connected (matching the sidebar).
The sidebar display-preferences filter becomes multi-select — pick any
number of hosts instead of one-or-all — and its host rows now carry a
status dot on the left like the other host pickers. Persisted filter
state migrates from the old single host to a host list.
* test(app): extract host-filter and command-center e2e DSL helpers
Move the mechanical menu/search interactions out of the new spec bodies
into focused helpers so the tests read as user intent.
* fix(app): drop the icon from the host filter "All hosts" item
* fix(app): make multi-host e2e seeding order-independent
The offline-host seed relied on Playwright running the test's init script
after the fixture's registry reset — an ordering Playwright does not
guarantee. Write the full registry and set the fixture's disable-once flag,
then reload, so the multi-host seed survives regardless of script order.
Also tag the pre-v2 hostFilter migration reader with COMPAT so the cleanup
sweep can find it.
* fix(desktop): report found app updates during manual checks
Manual checks could reuse cached update state while the renderer discarded pending update details. Keep found update versions visible while downloads prepare, and reserve install affordances for ready updates.
* test(desktop): cover manual update retry after errors
* fix(desktop): keep ready updates ready on recheck
The old marker only ever tagged the local host as "Local" based on its
server id. Each row now shows the connection actually in use, inline
after the host name — the TCP or relay endpoint (default :443/:80
stripped), or "Local" for socket/pipe transports.