Claude CLI's built-in slash commands (/voice, /usage, "Unknown command:
...") run client-side with no model turn — they arrive as a result with
output_tokens: 0 and the user-visible text carried in result.result, but
no preceding assistant_message in the stream. Surface that text as an
assistant_message so the turn doesn't end silently.
Also unify Codex skill-command text blocks to always carry the
"$skill-name" prefix in the text payload, matching the no-args path
which already did.
Thread an optional logger through runGitCommand and the CheckoutContext
helpers so workspace-git-service can emit trace-level traces around each
git invocation. Behavior is unchanged when no logger is supplied.
Add a daemon setting `autoArchiveAfterMerge` (default off). When on, the
daemon's existing GitHub poll triggers archive once the PR is merged,
but only if the worktree is clean (not dirty, not ahead of origin).
Skips silently otherwise.
* Add project rename so duplicates get distinguishable names
Two checkouts of the same remote collapse into a single project record
(same projectId), which left users unable to tell duplicates apart in
the UI. customName lives alongside the derived displayName as an
override that reconciliation never touches, so renames persist across
git-remote changes. The rename UI lives in project settings.
Closes#987 (rename half).
* Fix COMPAT(projectCustomName) version to v0.1.76
* Update useProjects shape test for new projectCustomName key
* Use sendCorrelatedSessionRequest for renameProject
Match the pattern used by checkoutPrMerge and other newer RPCs
instead of hand-rolling the select callback.
* Use project.rename.{request,response} dotted RPC names
Match the convention from docs/rpc-namespacing.md (added in 75a6f8277).
Schemas, type exports, daemon client, session handler, and tests all
move to the namespaced names. No back-compat shim needed since the RPC
hasn't shipped.
* fix(server): include OpenCode console subscription providers in model list
Providers with source "api" (e.g. Pi coding agent via OpenCode Go
subscription) appear in providers.all but not in providers.connected,
which only lists env/config providers. Paseo was filtering exclusively
by connected, so subscription-managed providers were silently dropped
and users saw an empty model list or a misleading "set env vars" error.
Fix: treat source === "api" as accessible in both listModels() and the
context-window lookup helper. Also update the error message to mention
the console login option.
chore: add portless as a devDependency so npm run dev works without
a global install.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: remove portless dependency
* test(server): add unit tests for OpenCode api-source provider inclusion
Covers the source="api" logic added in 08e3c6bf: providers managed by the
OpenCode console/subscription must appear in listModels and the context window
lookup even when absent from the connected list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: drop unrelated package-lock.json churn
The lockfile changes were peer metadata additions from a newer npm
version, not actual dependency changes. Restore to match main.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
The reconciler updated project.kind when a directory became a git repo
but never updated the workspace.kind underneath, leaving the sidebar in
a split-brain state: an expandable git project containing a workspace
with no leading icon (workspaceKind stayed "directory" instead of
flipping to "local_checkout" or "worktree").
* Add Auto Review permission mode
* Align Codex Auto Review with auto permissions
* Defer Codex Auto Review to dedicated PR
* fix(app): drop unconditional snapshot refresh on mode selector open
Opening the mode selector was reusing the model selector refresh path, which forced a provider snapshot refresh every time. That made the agent status bar briefly lose its selected provider data and flash empty while the picker opened.
Keep the stale model refresh on the model selector only, and let the mode selector open from the existing snapshot state.
* refactor(claude): rename Claude auto permission mode label to "Auto mode"
Claude Code itself calls this permission setting Auto mode in its CLI/TUI, so match that label in Paseo. The supporting test names and transport eligibility errors now use the same terminology.
---------
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
* Fix relay E2EE reconnect races
* fix(relay): use WebSocket protocol ping for daemon control keepalive
The daemon's control socket pinged the relay every 10s with a JSON
`{type:"ping"}` message. Every JSON message wakes a hibernated Cloudflare
Durable Object via `webSocketMessage`, so an idle daemon was billing
relay DO CPU 24/7 just to echo `pong`.
Switch to `socket.ping()` (RFC 6455 protocol ping frames). Cloudflare's
runtime auto-responds at the edge without waking hibernated DOs, so an
idle daemon-to-relay connection now costs zero DO CPU.
Liveness still works: the `ws` library emits a `pong` event on the
daemon when the edge replies, which refreshes `controlLastSeenAt`.
`markControlReady` stays tied to DO-originated messages (sync,
connected, disconnected), so we still detect a broken DO at connect.
The DO keeps its JSON ping handler tagged `COMPAT(relay-json-ping)` so
old daemons (< v0.1.76) continue to work.
* test(relay): assert protocol-ping pongs arrive in e2e idle test
Adds a `relay_control_pong_received` debug log to the daemon's pong
handler, then asserts in the existing idle-relay e2e test that:
- at least one pong arrived during the 12s idle wait (proving the
Cloudflare edge auto-responds to RFC 6455 protocol pings)
- no stale terminations fired (proving the keepalive path is healthy)
If the daemon ever regresses to JSON-level keepalive pings (which wake
the hibernated relay Durable Object and incur CPU billing), this test
will catch it: the `pong` event would never fire because the DO's reply
would arrive via `message`, not as a protocol pong frame.
The test's capturing logger is bumped to `debug` level so the assertion
can read the new debug log; production daemons stay at `info`.
Confirmed locally against wrangler-hosted relay: 2 pongs / 0 stale per
12s window.
* test(relay): catch dual-ping regression via DO-side legacy ping log
The existing idle-test assertion catches "revert to JSON ping only" via
the missing pong event, but misses the dual-ping regression where
someone adds `socket.send(JSON.stringify({type:"ping"}))` alongside
`socket.ping()`. Both paths would work, the pong assertion still
passes, and DO CPU billing silently returns.
Add a console.log in the DO's handleControlKeepalive (which only runs
when a JSON ping reaches the DO), then assert in the idle e2e test
that no such log appeared during the 12s idle window.
Verified the assertion catches the regression: temporarily injected a
`socket.send(JSON.stringify({type:"ping"}))` next to socket.ping(),
test failed with `expected 1 to be 0` and the relay logged
`legacy_json_ping_received`. Removed the injection.
Log noise during the COMPAT window doubles as useful telemetry: it
counts how many old daemons are still pinging, which informs the
removal date for the COMPAT shim.
OpenCode 1.14.42+ closes the /event SSE stream cleanly right after
server.connected, breaking the entire turn lifecycle: prompts get
queued and run, but Paseo never sees session.idle / message deltas /
tool calls / questions. Every turn either fails with a generic stream
EOF or hangs waiting for events that never arrive.
Switch the post-EOF recovery to the canonical messages REST endpoint
(via the upgraded SDK) and poll incrementally so tool calls and
clarifying questions surface live during the SSE gap. Bound the wait
with a completion cap and a separate liveness cap so silent rejections
fail fast instead of hanging until the cap. Cap session.abort similarly
so explicit cancels land within seconds.
The recovery path is gated on the for-await loop exiting without a
terminal event, so healthy turns never enter it - if upstream restores
SSE delivery, this code becomes dead and is mechanically removable via
the COMPAT(opencodeEofRecovery) and COMPAT(opencodeSlowAbort) tags.
Also tighten the type of projectSettingsRoute on WorktreeSetupCalloutPolicy
so router.navigate accepts it (was widened to string, broke app
typecheck on main).
Refs: getpaseo/paseo#861, anomalyco/opencode#26697,
anomalyco/opencode#26635