Route app, MCP, and agent worktree creation through one workflow boundary. Move branch auto-naming into workspace creation and keep agent metadata title-only.
* Add inline git diff review comments
Adds a review draft store keyed per diff target, inline gutter + thread
rendering in the diff pane, and a generated review attachment that flows
through the composer and agent providers.
* Strip generated review attachments before saving to draft store
Drafts persist only UserComposerAttachment (no review kind), so filter the
auto-generated review attachment out when seeding the draft or restoring
after a failed auto-submit.
* Fix inline review typecheck and test harness drift
* Unslop inline review feature
- Fix stale-closure bug in composer queueMessage: clear drafts for the
actually-queued attachments, not the latest props
- Drop dead useEffect mirroring initialBody in inline review editor
(parent already remounts via key)
- Restore typed AgentAttachment signature on renderPromptAttachmentAsText;
delete schema redispatch helper and drop null-checks across 5 providers
- Inline single-use review prompt formatters and replace marker switch
with const map
- Drop normalizeComment field-copy redundancy after type guard
- Remove __reviewDraftStoreTestUtils export; test migration via
rehydrate() public path
- Refactor findTarget to return the target line directly (no non-null
assertion at call site)
- Drop redundant isGeneratedReviewAttachment helper; inline kind check
* Update review pill to withUnistyles after rebase
Main's unistyles refactor stripped useUnistyles in favor of withUnistyles +
StyleSheet. Convert ReviewAttachmentPill to use ThemedCircleDot + ICON_SIZE so
the icon picks up the foreground-muted mapping like the other pills.
* Fix lint and typecheck after rebase onto main
Resolve drift after main's withUnistyles refactor:
- Composer complexity: dedup voice?.isVoiceSwitching to drop to 20
- Move per-comment Pressable handlers in git-diff-inline-review into
CommentRow subcomponent so callbacks are stable
- Hoist style/onPress factories to module level (iconButton,
iconButtonDestructive, ghostButton)
- Memoize style arrays in git-diff-pane review wrappers
- Replace inline {} placeholders with useMemo styles
- review-draft-store: extract applyCommentUpdates to avoid map-spread,
include input.key in useMemo deps
- Test fixtures: hoist EMPTY_COMMENTS, add buildReviewActions helper
* Drop GitDiffPane default param to keep complexity at 20
After the rebase, main's `enabled = true` default param combined with the
inline review additions pushed cyclomatic complexity to 21. Drop the
default and resolve `enabled !== false` at the call site (comparison
doesn't count toward complexity).
* Reshape inline review behind @/review module
Drop the `generated: true` attachment flag and consolidate inline-review
logic behind a deep `@/review` module so composer and git-diff-pane stop
hand-composing review primitives. Workspace-scoped review snapshots are
now derived at submit/queue time rather than persisted in the draft
store. Composer review references drop from 51 to 4; git-diff-pane drops
~108 lines and now consumes a controller hook + slot components.
* Fix optimistic review attachments
Surfaces the daemon's existing resume-from-persistence capability through a
new top-level CLI command so users can pull existing Claude Code, Codex, or
OpenCode sessions into Paseo without losing history. Adds a new additive
`import_agent_request` WebSocket message; no existing schemas change.
Refs #611, #237, #492; partially addresses #268.
* refactor(server): validate ACP mode/model selection and subscribe to live state
Match Zed's selection validity model: resolve the requested mode/model
against availableModes/configOptions before issuing the ACP RPC, log a
warning instead of throwing on unknown values, and adopt the
currentValue echoed back in setSessionConfigOption responses. Subscribe
to current_mode_update / config_option_update so AgentManager re-emits
state when the agent changes selection unprompted.
Also drops the Copilot Autopilot auto-approve shim to mirror Zed's pure
pass-through requestPermission path.
* refactor(server): emit ACP session config drift via namespaced events
Replace the parallel subscribeToSessionState callback channel with three
payload-bearing variants on the existing AgentStreamEvent union
(mode_changed, model_changed, thinking_option_changed). Manager updates
its record from the event payload and emits state — no callbacks back
into the session. Variants suppressed before websocket dispatch so the
wire schema is unchanged.
The OpenCode adapter sent the prompt before its SSE event subscription
was active. Since /event does not replay, terminal session.error/idle
events for invalid mode or unsupported model could land before the
subscription, leaving the agent stuck in running forever.
Await subscription readiness before promptAsync, and surface synchronous
or rejected prompt errors as turn_failed. Also force-drop sockets in
daemon shutdown so test harnesses don't hang on lingering WS upgrades.
Adds a real-opencode e2e regression test for the invalid-mode case and a
unit test for the synchronous-throw path.
* docs: rename to lowercase + drop leftover plans
Rename docs in docs/ to lowercase kebab-case for consistency and update
all references in CLAUDE.md, CONTRIBUTING.md, CHANGELOG.md,
packages/server/CLAUDE.md, and inter-doc links.
Drop two leftover design plan docs:
- docs/ATTACHMENT_BASED_REVIEW_CONTEXT_PLAN.md
- docs/plan-approval-normalization.md
* docs: drop stale uppercase entries from case-insensitive rename
* feat(website): power /docs from public-docs/ markdown tree
Move website docs out of TSX route components and into a root-level
public-docs/ directory of plain markdown files with frontmatter
(title, description, nav, order).
- Add packages/website/src/docs.ts loader using import.meta.glob with
?raw to compile the markdown into the bundle at build time.
- Replace the 9 hand-written docs/*.tsx routes with a single $.tsx
catch-all that renders any slug via react-markdown.
- Drive the docs sidebar nav from frontmatter order/nav.
- Auto-discover docs routes in vite.config.ts so the sitemap stays in
sync without manual edits.
* fix(website): bind dev server to 0.0.0.0 so port collisions trigger fallback
`host: "127.0.0.1"` (or unset) lets macOS coexist with another process
holding an IPv6 dual-stack `*:8082` socket, so Vite never sees
EADDRINUSE and silently binds alongside it. Forcing IPv4 wildcard
makes the conflict real, and Vite's default `strictPort: false`
falls through to the next free port.
* fix(website): restore docs page styling after markdown migration
Add a .docs-prose class that mirrors the styling the original
docs/*.tsx components hand-rolled (h1/h2/h3 sizes, paragraph/list
spacing, link colors, code blocks, callout-style blockquotes).
ReactMarkdown was emitting unstyled HTML because the previous
wrapper class only had inline-code rules — headings and code
blocks fell back to user-agent defaults.
Fixes the Claude query restart path that could drop conversation context after thinking effort changes or rewind-driven restarts.
Addresses #439.
Related to #156 and #200.
* test(server): add real-spawn round-trip for % in argv
RED test: pass --format=%(refname)... through spawnProcess to a child
node script that echoes argv[2]. Asserts the child receives the original
string verbatim. Wired into the Windows CI matrix to expose the
escapeWindowsCmdValue %-doubling bug under cmd.exe.
* test(server): use bare 'node' command so cmd.exe path is exercised
process.execPath has both an extension and a path separator on Windows,
so shouldUseWindowsShell returns false and the broken %-escape pipeline
is never invoked. Use the bare command name 'node' instead.
* fix(server): stop doubling % in cmd.exe arg escaping on Windows
cmd.exe only collapses `%%` → `%` inside batch files; on the command
line / via `cmd /c "..."` `%%` stays literal. Doubling `%` in
escapeWindowsCmdValue meant args like `--format=%(refname)` reached git
as `--format=%%(refname)`, which git interprets as the escape sequence
for a literal `%`, so format atoms appeared verbatim and the branch
picker showed `%(refname)%09%(committerdate:unix)` instead of branch
names.
Update unit tests that pinned the broken doubling behavior.
The daemon was dying silently with no log trail. Two compounding causes:
1. uncaughtException/unhandledRejection handlers in index.ts called
process.exit(1) immediately after logger.fatal, but pino is configured
with async streams (sync: false console + rotating-file-stream), so the
fatal entry never flushed. main().catch already worked around this with
a 200ms setTimeout — extract exitAfterPinoFlush() and apply it to the
process-level handlers too.
2. socket.send() in the relay transport adapter and ws.send() in
sendToClient/sendBinaryToClient were unprotected. The ws library throws
synchronously on send-after-close, and the readyState === 1 check has a
race window. With an active foreground turn streaming agent_stream
output, a peer-side disconnect could turn into an uncaughtException —
which (per #1) then exited without logging.
Wrap the three send sites in try/catch and warn-log on failure; let
onclose/onerror drive cleanup. The readyState fast-path stays as an
optimization.
Refs getpaseo/paseo#612
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The empty event-stream mock added in #597 races with the summarize-success
path: consumeEventStream sees the empty stream end immediately and emits a
"stream ended before terminal event" turn_failed which gets buffered before
session.run has assigned its local turnId. The buffered event replays after
startTurn returns, marks settled = true, and rejects completion — but
session.run skips await completion because settled is true, leaving the
rejection unhandled and tripping vitest.
Yield session.idle so the stream resolves the turn the same way real
OpenCode SSE does.
Follow-up to the projection fix. The mechanism/policy split landed projection
on a graceful-degrade path, but I also made the setup, teardown, and terminal
wrappers silently absorb parse failures — which broke the existing contract
that `runWorktreeSetupInBackground` surfaces "Failed to parse paseo.json" to
the user when they invoke setup against a workspace with a malformed config.
Restore the throw, but with the path included and the underlying parse error
attached as `cause` (Pino expands it; UI surfaces the path + detail). Extract
`paseoConfigParseError` so spawnWorkspaceScript and the three wrappers throw
the same shape.
Also update opencode-agent.full-access.test.ts mode-order assertion that the
reorder commit forgot to update.
A malformed paseo.json (e.g. git-conflict markers) in any registered
workspace caused readPaseoConfig to throw, which propagated through
getScriptConfigs -> buildWorkspaceScriptPayloads ->
Session.describeWorkspaceRecord and failed the entire
fetch_workspaces_request with code=fetch_workspaces_failed. The client
showed no workspaces at all.
Reshape: separate loading (mechanism) from policy (per-caller).
- readPaseoConfig now returns a discriminated result
({ ok: true; config } | { ok: false; configPath; error }). Pure load,
no logging, no opinions.
- Workspace projection owns its own policy via readPaseoConfigForProjection
in script-status-projection.ts: warn via pino with configPath +
workspaceDirectory + err, treat workspace as having no scripts, keep the
list alive.
- spawnWorkspaceScript throws with cause so the existing
handleStartWorkspaceScriptRequest catch logs it once with full context.
- The setup/teardown/terminal wrappers retain their pre-existing silent
degradation on missing/broken config.
- getScriptConfigs takes a parsed PaseoConfig | null instead of repoRoot
so the projection can pass the result through without double-reading.
Closes https://github.com/getpaseo/paseo/issues/598
* fix(server): support OpenCode full-access approvals
* fix(opencode): unconditionally auto-approve tool permissions in full-access mode
OpenCode permissions that resolve to allow do not surface prompts in the first place, so checking runtime permission rules before replying made Paseo full-access misleading. Permissions that default to ask, like external_directory and doom_loop, would still surface even in full-access.
Make full-access match OpenCode's own --dangerously-skip-permissions behavior by replying "once" to every tool permission prompt, while still letting reply failures fall back to surfacing the permission to the user.
---------
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
Typing @ in the chat input to reference a file froze the app on huge
workspaces. Two compounding causes:
1. The autocomplete hook fed fileFilterQuery straight into the React
Query key, so every keystroke fired a new directory_suggestions
request and a fresh BFS scan on the daemon. Added a 180 ms debounce
that mirrors the existing pattern in new-workspace-screen so rapid
typing coalesces into one request.
2. searchWorkspaceEntries only ignored node_modules, so dist, build,
target, out, coverage, vendor and __pycache__ each consumed the
5,000-entry scan budget before any real source file was reached.
Extended WORKSPACE_IGNORED_DIRECTORY_NAMES with those names and
added a regression test that proves a deep source file is still
reachable when those directories are full of bait entries.
Refs #599
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Built-in provider clients bypassed mergeModels in listModels(), so
configured profileModels/additionalModels were honored by the catalog
but ignored when AgentManager picked a default for new agents.
Wrap the inner client whenever overrides exist (not only when the
provider id differs) and route listModels through mergeModels.
Fixes#579
The hardcoded OPENAI/ANTHROPIC/OPENROUTER env-var check missed
DEEPSEEK_API_KEY and ~14 other providers the pi-ai SDK supports,
hiding the Pi provider entirely when only those keys were set.
Delegate to ModelRegistry.getAvailable() so every supported provider
is recognized automatically. Diagnostic now lists configured providers
dynamically instead of three hardcoded keys.
When a user archives a git repo and later opens a subfolder of it that is
not its own git project, the resulting workspace is a directory — not a
revived git workspace pointing at the archived parent. To get git features
back the user unarchives the parent (S6/S11).
Provider diagnostics were stringifying Error objects through JSON.stringify,
which yields {} and hides the cause. Spawned children logged stderr to the
daemon log but never attached it to thrown errors, so the diagnostic showed
"Server: Unavailable ({})" with no clue why.
Generalize toDiagnosticErrorMessage to extract message, code, signal, stderr,
stdout, and recursive cause from any Error (including execFile-style errors),
and never return {} or empty. Buffer stderr/stdout in OpenCode startServer
and include them in startup-failure rejections. Stop swallowing errors in
resolveBinaryVersion. Drop the duplicate stringifyUnknownError helpers in
opencode-agent and pi-direct-agent and route every callsite through the
single canonical helper.
Filter archivedAt records out of the prefix-fallback resolver and stop
canonicalizing into archived parents — opening a child of an archived
git ancestor now creates a fresh workspace at the input path instead
of resurfacing the archived parent.
- session.test.ts: drop stale 4th workspaceGitService arg from
createPullRequest assertion (signature changed in ee5a23d9).
- workspace-git-service.test.ts: bump local flushPromises microtask
count to cover the extra await resolveGitHubRemoteForTarget step
added by the GitHub remote resolver.
- provider-windows-launch.test.ts: retry rmSync in afterEach to
ride out Windows file locks from the spawned claude.cmd.
Resolves the case where a remote like `git@github-work:owner/repo.git`
points at github.com via `~/.ssh/config` but Paseo treats it as
non-GitHub.
Adds a centralized GitHub remote resolver that parses standard remotes
directly and resolves SSH aliases via `ssh -G` (with hostname
validation to keep the alias out of ssh's option parser). The resolver
is shared by workspace GitHub feature detection, PR status, PR
creation, and workspace git metadata.
Lets first-time users edit project settings via the UI and have those
scripts apply to their first worktree without first having to commit
paseo.json. Committed configs are unaffected — git worktree add already
checks them out, so the seed is a no-op there.
Allow `OPENAI_REALTIME_URL` env var to override the hardcoded
`wss://api.openai.com/v1/realtime?intent=transcription` endpoint, with
the current value as default. Pairs with the OpenAI SDK's existing
`OPENAI_BASE_URL` support that already makes HTTP audio endpoints
reroutable. Unblocks pointing the realtime session at any OpenAI
Realtime API protocol-compatible backend (Azure OpenAI, Alibaba
Bailian / DashScope, self-hosted compat layers) without further
changes.
Closes#569
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
classifyDirectoryForProjectMembership normalizes input.cwd via
path.resolve, which on Windows turns "/tmp/repo-feature" into
"D:\\tmp\\repo-feature". Mirror existing pattern (deriveWorkspaceId
tests at lines 91/107) and wrap the expected cwd with
normalizeWorkspaceId so the test passes on both POSIX and Windows.
Same root cause as 16cf9edf — checkout-git emits mainRepoRoot directly,
so the helper must initialize it to null or downstream isWorktree
derivation flips to true on undefined !== null.
de428582 made workspace-git-service propagate checkoutStatus.mainRepoRoot
directly. The test helper never set the field, so the runtime snapshot
emitted undefined where tests expected null.
Newer clients were rejecting checkout_status_response and
project-checkout-lite payloads from older daemons that don't yet send
mainRepoRoot. Make the field optional with a null default so the schema
stays forward-compatible.
Placement only ever needed local git data; fetching GitHub PR status
was incidental coupling through getSnapshot, blocking the response
~5s with no feedback in the desktop app. Split it: getCheckout for
placement (local only), getSnapshot for the watch path. classify is
now a pure function over a checkout. The observer emits the full
gitRuntime/githubRuntime via workspace_update when the background
snapshot lands.
Browser xterm.js auto-replies to DA1/DA2/DA3 traveled back over the
websocket and arrived at the PTY after the foreground app had exited,
so the response bytes (\x1b[?62;4;9;22c) landed on the shell prompt as
visible garbage. Move all replies to the daemon's headless xterm where
they reach the PTY synchronously, and suppress every query class on
the browser side. The image addon also registers a {final:"c"} handler
inside the post-mount raf, so re-register suppression after addons
load to stay last in xterm's LIFO dispatch.
Replaces the previous printf-injection test with a real foreground
helper that queries DA1 and asserts the reply lands on stdin, plus a
browser table covering DA1/DA2/DA3 + DSR/CPR/DECRQM.
The Claude SDK delivers Grep tool_result content as a plain string, which
claude-agent.ts wraps as { output: <string> } for unrecognized tools. The
canonical ToolGrepOutputSchema requires structured fields, so parsing fell
through to an unknown detail and the UI rendered only the redundant query.
Adds a Projects settings flow for inspecting and editing per-project
paseo.json (worktree setup/teardown, scripts, terminals) directly from the
app. Reads/writes go through new daemon RPCs with revision-based optimistic
concurrency, and the screen always keeps its header and host picker visible
so a hung host doesn't trap the user.
Lifecycle config now takes either a multiline shell script string or an
array of commands, making it friendlier to edit in a textarea. Schema
parsing moved to Zod so coercion stays in one place.
Adds a per-provider toggle in Settings → Providers, persisted via daemon
config, so users can turn off providers they don't have installed (or
don't want to use) without removing them from the registry.
Disabled providers stay visible everywhere — settings, `paseo provider
ls`, MCP `list_providers`, model/mode lists — marked as disabled rather
than silently filtered. Availability probes are skipped for them, and
attempts to launch an agent or fetch models/modes from a disabled
provider return a clear "Provider '<id>' is disabled" error.
Schema additions (`enabled` on `ProviderSnapshotEntry`, MCP provider
summary) are backward-compatible: optional with a default of true so
older clients continue to parse new daemon messages.
CLI: `paseo provider ls` gains an ENABLED column; JSON output includes
the new field.