* Fix OMP slash command discovery
Keep Pi on get_commands while allowing the OMP-backed provider to use get_available_commands. Both providers reject the other command without echoing an id, so the adapter needs an explicit command-list RPC setting instead of a timeout-based fallback.
* Address OMP command test review
* Fix OpenCode helper server ownership
Own OpenCode helper generations as soon as spawn returns so startup timeout and shutdown clean up through the manager/reaper path. Collapse the OpenCode runtime wrapper so the provider talks to the server manager directly.
* Protect dedicated OpenCode server startup
* fix(server): align provider catalog diagnostics
* fix(server): preserve catalog profile models
* refactor: unify provider catalog discovery under AgentClient.fetchCatalog
Remove listModels/listModes from AgentClient and fetchModels/fetchModes
from ProviderDefinition. All provider runtime discovery now flows through
a single fetchCatalog(options) => ProviderCatalog API.
ProviderSnapshotManager.listModels/listModes remain as cached snapshot
conveniences only. Provider implementations (acp, codex, opencode, pi,
claude, mock) updated accordingly; agent-manager default model resolution
now calls fetchCatalog.
Reshape step toward issue pi-model-list-empty.
* refactor: remove remaining provider listModels/listModes runtime API residue
Migrate remaining AgentClient/provider-client implementations and tests to
fetchCatalog. Remove obsolete ListModelsOptions/ListModesOptions interfaces.
Update ProviderSnapshotManager.getProviderDiagnostic to materialize clients
via ensureClient(provider, definition) so diagnostics self-heal the settings
sheet instead of failing when providerClients[provider] is absent.
Allowed to remain: ProviderSnapshotManager.listModels/listModes as cached
snapshot readers; protocol/client legacy list_provider_models names; unrelated
local helper in create-agent-mode.
* test(server): repair test clients after fetchCatalog refactor
- Restore TestAgentClient.fetchCatalog with proper model list and resumeSession.
- Restore NativeArchiveRecordingClient and EnvProbeAgentClient removed during refactor.
- Fix ResumeCaptureClient.fetchCatalog and resumeSession.
- Fix stream-coalescing TestAgentClient.fetchCatalog shape and isAvailable.
- Mock accessible OpenCode provider in full-access mode tests so fetchCatalog does not throw.
* refactor(app): update stable discovered models ref directly during render
* refactor(server): move checkout write handlers into CheckoutSession
The checkout read side moved into session/checkout/checkout-session.ts in
#1644; the 17 inline write/PR/stash handlers stayed on the Session god-object.
This carves them into the same subsystem behind an expanded CheckoutSessionHost,
so dispatchCheckoutMessage becomes pure delegation and session.ts drops ~800
lines.
Moved into CheckoutSession (verbatim, only this.X -> this.host.X rewiring):
switch/rename branch, commit, merge, merge-from-base, pull, push, PR
create/merge, github auto-merge/check-details, PR status/timeline, github
search, stash save/pop/list, plus resolveCurrentPullRequest and the
PR-timeline helpers.
CheckoutSessionHost gains the Session-owned, non-checkout collaborators these
handlers orchestrate (notifyGitMutation, emitWorkspaceUpdateForCwd,
handleWorkspaceGitBranchSnapshot, renameCurrentBranch, checkoutExistingBranch,
and the LLM commit/PR-text generators); paseoHome/worktreesRoot join the
options bag. GitMutationRefreshReason moves to utils/checkout-git.ts so both
the shell and the subsystem share one canonical type.
session.test.ts checkout tests now drive the handlers through the public
handleMessage boundary instead of reaching into private methods; new mock-free
fake-host tests cover the moved handlers directly in checkout-session.test.ts.
Behavior-preserving: full session.test.ts + checkout-session.test.ts green,
typecheck/lint/format clean.
* refactor(server): extract chat/schedule/loop handlers into ChatScheduleLoopSession
Move the 7 chat/*, 9 schedule/*, and 5 loop/* request handlers, the three
rpc-error emitters (kept separate), and toScheduleSummary out of the Session
god-object into session/chat/chat-schedule-loop-session.ts, mirroring the shipped
CheckoutSession/VoiceSession deep-module-with-narrow-Host pattern. The least-coupled
remaining domain: stateless request/response over chatService/scheduleService/
loopService, reaching the shell only through a narrow ChatScheduleLoopSessionHost
(emit + agent-roster reads + the mention-fanout send). session.ts drops ~590 lines.
The two dispatch switches collapse into one: schedule/* was previously reached only
via the chat dispatcher's default fall-through arm — now all 21 types are explicit
delegation cases, removing that fragility.
Behavior-preserving: a new routing test drives the real Session.handleMessage for all
21 types (guards against a silently-dropped case), green before and after the move; a
collocated subsystem test covers the mention-fanout send seam, the fanout-limit error
code, the self->agent target remap, and the toScheduleSummary runs-stripping.
* refactor(server): extract provider catalog handlers into ProviderCatalogSession
Move the 8 provider-catalog handlers (model/mode/feature listing, providers
snapshot pull/refresh, diagnostic, usage) plus emitProviderDisabledResponse,
getProviderSnapshotEntryForRead, buildDraftAgentSessionConfig, the two mode-icon
downgrade helpers, and the providers_snapshot_update PUSH wiring out of session.ts
into session/provider/provider-catalog-session.ts behind a narrow
ProviderCatalogSessionHost seam — the same deep-module pattern as ChatScheduleLoopSession,
CheckoutSession, and VoiceSession.
The PUSH (start) and every PULL gate provider visibility and downgrade mode icons
through the SAME injected predicates (isProviderVisibleToClient + supportsCustomModeIcons),
both reading appVersion/clientCapabilities live — the COMPAT invariant the shell could
only enforce by code proximity before. dispatchProviderMessage collapses to delegation;
start()/dispose() wire into subscribeToOptionalManagers/cleanup at the same ordinal.
session.ts drops ~372 lines. Behavior-preservation gate (provider routing through the
real Session.handleMessage) green before and after; full session.test.ts 115/115; new
collocated subsystem test covers the PUSH/PULL parity seam, the disabled-provider path,
and the usage/feature error envelopes.
* refactor(server): extract workspace file-access handlers into WorkspaceFilesSession
Move the file-explorer, file-upload, file-transfer-frame, project-icon and
file-download-token handlers out of session.ts into
session/files/workspace-files-session.ts behind a narrow WorkspaceFilesSessionHost
seam (emit/emitBinary/hasBinaryChannel), matching the deep-module-with-Host shape
used by CheckoutSession/ChatScheduleLoopSession/ProviderCatalogSession.
The new module owns the FileUploadStore and is injected the daemon's
DownloadTokenStore; the shell's dispatch cases and the file_transfer binary route
collapse to delegation. The five handlers touch no workspace-git observer,
registry or subscription state, so this is a clean self-contained slice of the
workspace domain. session.ts drops 240 lines (7316 -> 7076).
Behavior-preservation tests (file explorer list/read/binary, download token
success + empty-cwd, project icon, upload round-trip) added to session.test.ts and
run green against the pre- and post-refactor Session; a collocated
workspace-files-session.test.ts exercises the module via a fake host with real
upload/download stores (no module mocks).
* refactor(server): extract agent-config setters into AgentConfigSession
Move the four agent-config setters (set_agent_mode/model/feature/thinking)
out of the session god-object into session/agent-config/, collapsing their
four near-identical try/log/emit envelopes into one applyConfigChange helper
behind a narrow { emit } host plus an AgentConfigOperations port.
Behavior-preserving: the dispatch cases delegate unchanged, setAgentModeCommand
still routes the mode path, and model/feature still emit no notice. Adds the
previously-absent failure-envelope coverage: 8 gate tests in session.test.ts
(success + forced failure per setter, asserting the *_response envelope and the
activity_log-before-response ordering) green before and after the carve, plus a
collocated agent-config-session.test.ts (typed fake operations + fake host, no
module mocks). session.ts drops ~173 lines.
* refactor(server): extract project-config handlers into ProjectConfigSession
Move read/write_project_config handlers, their failure emitters, and the
root-resolution helpers (resolveKnownProjectRootForConfig, canonicalizeConfigRoot,
stripTrailingPathSeparators) out of session.ts into session/project-config/, behind
a narrow { emit } host plus a projectRegistry port — matching the established
session/<domain>/ pattern. Also corrects a misfiling: project-config was dispatched
inside dispatchAgentConfigMessage despite being its own domain.
session.ts drops 155 lines (6903 -> 6748). The handleMessage-driven gate in
session.test.ts (project config RPC authorization) is green before and after; new
collocated project-config-session.test.ts covers the subsystem with a fake host and
fake registry over a real filesystem (no module mocks).
* refactor(server): extract daemon status/pairing handlers into DaemonSession
Move the two daemon.* RPC handlers (daemon.get_status, daemon.get_pairing_offer)
out of session.ts into session/daemon/daemon-session.ts behind a narrow
{ emit } host + injected reads (paseoHome, serverId, daemonVersion,
daemonRuntimeConfig, listProviderAvailability). These were misfiled inside
dispatchAgentConfigMessage despite being daemon- not agent-scoped; the slice
also removes the now-dead serverId/daemonVersion/daemonRuntimeConfig fields and
the getPidLockInfo/generateLocalPairingOffer imports from the god-file, and
collapses the inline daemonRuntimeConfig type into a named DaemonRuntimeConfig.
The daemon-config get/set handlers stay inline: daemonConfigStore is a
listener-bearing, cross-domain hub (read by structured-generation,
websocket-server, bootstrap, archive-if-safe), so moving it would double-own it.
session.ts: 6748 -> 6662 lines. Gate: new session.test.ts daemon round-trips
(status success + listing-rejects fallback, pairing relay-disabled) pass before
and after; new collocated daemon-session.test.ts (fake host + injected reads
over a real temp dir, no module mocks) 4/4. typecheck/lint/format clean.
#1640 extracted the voice subsystem to server/voice/ (a peer of session.ts);
#1644 extracted the checkout read side to session/checkout/. The two carves
landed in inconsistent homes, so there was no single place to find "what has
session.ts been carved into."
Relocate server/voice/ -> server/session/voice/ so all session subsystems share
one home alongside session/checkout/. Pure relocation: git-tracked renames with
import-depth bumped one level (../ -> ../../); the only external touch point is
the import path in session.ts. Behavior preserved verbatim — the 17 voice unit
tests pass unchanged before and after.
Also record the actual shipped layout and the next carve (the checkout mutation
handlers still inline in session.ts -> session/checkout/checkout-session.ts) in
the decomposition plan, and fix its now-stale voice path.
Extract the checkout read & live-stream side (status, branch validate/suggest, manual refresh, diff + status subscriptions) out of session.ts into session/checkout/checkout-session.ts behind a narrow CheckoutSessionHost seam and a CheckoutDiffSubscriber port. session.ts -234 net lines; 12 unit tests via injected fakes (no module mocks); 6 session tests moved onto the public handleMessage path. Also fixes a pre-existing tilde-resolution bug in the refresh handler (TDD).
* refactor(server): extract voice mode subsystem into VoiceSession
session.ts was a 10.5k-line god object and the most-churned file in the
repo. ~1,100 of those lines were an entire voice/audio subsystem — the
STT/TTS/dictation managers, the barge-in audio-buffering state machine,
voice-turn orchestration, and the MCP voice bridge — interleaved field by
field and method by method with workspace/git/agent/provider concerns.
Move the whole subsystem into a new VoiceSession deep module. Session now
holds one `voiceSession` field, constructs it once, and delegates the
voice/dictation/abort message types, the permission auto-allow gate, and
cleanup to it. VoiceSession owns all 18 voice fields and ~25 methods and
reaches back into the agent run only through a narrow VoiceSessionHost
seam (emit, loadAgent, reloadAgentSession, sendSpokenInput,
interruptAgentIfRunning, hasActiveAgentRun).
Behavior is preserved (verbatim method moves); deleting voice-session.ts
now removes the feature mechanically. The voice unit tests drive the
VoiceSession boundary instead of reaching into Session internals, so
future voice tests can construct a VoiceSession with a fake host.
session.ts: 10,468 -> 9,272 lines.
* refactor(server): move VoiceSession into voice/ and test it at the boundary
Address Greptile review on #1640:
- Move voice-session.ts into the existing voice/ subdirectory, alongside
voice-turn-controller.ts, instead of adding another peer to the 30+ file
server/ directory. The directory now carries the domain.
- Add voice/voice-session.test.ts beside the module: it constructs a
VoiceSession with a fake VoiceSessionHost and drives it through the public
API (handleSetVoiceMode, then turn-detection/STT events), asserting on the
host seam (sendSpokenInput) and emitted messages. No Session, no private
field/method access.
- Remove the voice tests from session.test.ts that reached through Session's
private voiceSession field into VoiceSession internals.
Same three behaviors are covered (streaming final -> agent, finalization
timeout empty path, low-confidence filtering); driving handleSetVoiceMode
additionally exercises the enable path the old tests bypassed.
Commit 25252d1b8 added AgentManager.unarchiveSnapshot, now called from
the shared unarchiveAgentState helper. The hand-rolled manager doubles in
the session-workspaces and import-sessions suites were not updated, so the
refresh/import RPC paths hit an unstubbed method and failed. Add the
method to those doubles; no production change.
Unarchive provider storage before clearing Paseo's archived flag so failed native restores keep the snapshot archived. Keep initial history sync errors visible instead of falling back to the loading state.
Task-level usage reports processed work, not the parent session's active context. Prefer current context data and parent stream usage before falling back to result totals.
Record provider-owned helper processes (currently the OpenCode `serve` helper) in a daemon ledger and reconcile it in the background on startup: terminate validated leftovers, drop dead or PID-reused records without killing anything, and keep a record whose process can't be inspected for the next reconcile. Termination stops as soon as the process exits instead of always escalating to SIGKILL.
Expose the workspace id from worktree creation and let agent creation attach to an existing workspace, so callers can split worktree setup from agent launch without reusing the caller workspace.
Keep worktreeSlug as the worktree path slug and add branchName for branch-off targets.
Resolve exact suffix path queries before falling back to broad search, and keep broad hidden-directory traversal limited to known workspace config dirs.
Require relationship and workspace on create_agent, and reuse the same worktree target union for create_worktree. Agent-scoped prompt follow-ups now default to background finish notifications so callers can continue without polling.
When the Paseo daemon is launched as a macOS GUI app (via launchd), its
working directory is `/`. The `opencode serve` process inherited that cwd,
causing opencode's fff_search to scan and watch the entire filesystem,
leading to 300%+ CPU and 1GB+ RAM usage.
Fix: pass `cwd: os.homedir()` when spawning `opencode serve` so it never
inherits `/`. The per-session working directory is unaffected — it is
passed separately via the OpenCode SDK's `session.create({ directory })`.
Fixes#1617
* feat: live provider quota panel (Claude + Codex)
Adds Claude and Codex plan usage to the context window percentage circle tooltip, querying their respective APIs directly using existing CLI auth tokens.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address review feedback - restore quota trigger, guard NaN date
- Re-add triggerFetch() on agent idle/error in agent status subscription
- Guard formatResetsAtLabel against NaN from malformed API date strings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: resolve composer conflict and stub subscribe in websocket tests
* feat(quota): add cursor, copilot, zai, grok, and kimi quota UI and improve type safety
* feat(quota): make Claude and Codex pluggable, and map additional quota/credits metrics
* security(quota): fix shell injection vulnerability by migrating exec to execFile
* revert: restore original scripts/dev.ps1 and packages/desktop/scripts/dev.ps1
* Fix i18n resource test expectation
* Fix quota tooltip empty-provider state
* Preserve zero values in Grok quota
* Fix empty quota fetch state
* Ignore quota frames in relay reconnect tests
* Persist refreshed Codex tokens to source auth file
* fix(quota): read Claude OAuth credentials from the macOS login Keychain
On macOS, Claude Code stores its OAuth credential in the login Keychain
(generic-password item, service "Claude Code-credentials"), not in
~/.claude/.credentials.json — that file usually does not exist there, so the
Claude provider's existsSync check failed and macOS users silently got no
Claude quota. Read it via the macOS `security` CLI (its ACL only trusts
/usr/bin/security to decrypt; a native Keychain read would prompt), and stay
read-only there since Claude Code owns the refresh/persist. Linux and Windows
keep using ~/.claude/.credentials.json unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix quota keychain timeout and local host reconciliation
* Fix terminal notification test quota stub
* Ignore quota frames in terminal notification tests
* fix(quota): bypass Claude token refresh on macOS Keychain-backed logins
* fix(dev): update local dev daemon port to 6768 on Windows scripts/dev.ps1
* fix(dev): bypass Unix dev-daemon.sh on Windows in dev.ps1
* fix(client): handle and dispatch provider_quota event in DaemonClient
* fix(desktop): fix PowerShell quote stripping in desktop dev.ps1 config seeding
* fix(desktop): execute config update via temp file to avoid PowerShell quoting bugs
* fix(desktop): fix concurrently command invocation on Windows in dev.ps1
* fix(desktop): resolve path escaping and environment setting syntax errors in dev.ps1 for Windows
* Add on-demand provider usage views
* Fix Cursor usage billing dates
* Move provider usage renderer coverage to e2e
* Use provider manifest for quota fetchers
* Add provider usage fetch timeouts
* Reshape provider usage fetchers
---------
Co-authored-by: ABorakati <ABorakati@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
Co-authored-by: lumingjun <lumingjun@bytedance.com>
* fix(opencode): trust discovered modes instead of always injecting defaults
When OpenCode reports its available agents, respect that list exactly
rather than seeding from DEFAULT_MODES first. This means users who have
intentionally disabled modes (e.g. Plan) in their OpenCode config will
no longer see those modes surfaced in Paseo.
DEFAULT_MODES is still used as a fallback when discovery returns nothing
(e.g. OpenCode unreachable or no agents configured).
* test(opencode): update mode discovery expectations
* test(opencode): expect discovered custom modes only
* fix: daemon warns instead of crashing on missing OpenAI speech credentials
validateOpenAiCredentialRequirements was throwing an error when
OpenAI speech providers were configured without credentials,
causing the daemon to crash before it could start serving health
checks. Changed to log a warning instead — the daemon starts
successfully and reports speech services as unavailable.
Closes#1367
* chore: address Greptile review feedback
- Remove duplicate warning in initializeOpenAiSpeechServices when
all credentials are missing (already warned by validate)
- Strengthen test assertions: check getListenTarget() after start
- Wrap daemon start/stop in try/finally for proper cleanup on failure
Remove projects through a daemon RPC so the sidebar can delete the project
record after archiving its active workspaces. Reopening a removed directory
recreates the missing project record so local projects keep their derived
basename, and History skips orphaned archived workspace rows.
Fixes#1583.
Archiving a Paseo-owned worktree deletes its directory, so reopening its
agent previously dead-ended even after the workspace record came back —
there was no folder to return to.
Restore now recreates the worktree at its original path from the kept
branch (the branch is never deleted on archive), re-points the same
workspace record, and clears archivedAt — preserving the workspaceId so
the agent stays linked. The directory is recreated before the un-archive
so the reconciler can't re-archive it for a missing directory. A deleted
or already-checked-out branch surfaces a typed failure and leaves the
workspace archived.
Gated behind a worktreeRestore capability flag; older hosts get an
explicit "update your host" state instead of a misleading failure.
Reopening an archived agent previously dead-ended at "workspace not
found": the agent record was un-archived but its workspace stayed
archived and was never sent to the client.
The daemon now un-archives the owning workspace alongside the agent when
its backing directory still exists, and the app auto-fires the restore
ahead of the missing-workspace gate so the user lands back in the
workspace rather than a dead end.
Worktrees whose directory was deleted on archive are not yet restorable
(recreating them from their branch is a follow-up); the workspace record
now persists its base branch as groundwork for that.
Metadata prompts (workspace title + branch, commit message, PR) mixed the
functional contract with style guidance in one block, and project-owner
instructions from paseo.json were appended after the defaults with an
"override where they conflict" notice — so custom wording competed with the
built-in style instead of replacing it.
Each prompt is now a non-overridable contract (what to produce, the JSON
shape, correctness/safety rules) plus style slots that paseo.json
instructions replace wholesale. Title and branch style are now separate
keys (metadataGeneration.title vs .branchName); previously branchName
instructions also shaped the title.
Auto-generated titles were full sentences that overflowed the sidebar.
The titling prompt now targets ~4 words, drops generic "do" verbs (Fix,
Add, Diagnose...), and keeps only specific operation verbs, with good and
bad examples.
The agent-facing MCP server (/mcp/agents) ran in stateful session mode,
keeping a StreamableHTTPServerTransport plus its full Server and Zod tool
schemas alive per agent connection, keyed by session id. The map was only
pruned on the SDK's onclose/onsessionclosed callbacks, which fire only on a
clean DELETE. Agents that are killed or exit without one never get reaped, so
dead sessions accumulate for the life of the daemon (observed ~77MB across 132
stale transports in a heap snapshot).
Listing and calling tools needs no cross-request state, so each request now
builds a fresh server + transport and tears both down when the response closes.
Per-request cost is ~0.6-0.9ms and ~760KB of short-lived allocation; nothing is
retained between requests. GET and DELETE return 405, which the MCP client
tolerates (it never receives a session id).