* feat: direct TCP URI with SSL toggle and optional password auth
Replaces the heuristic-driven direct-connection model with a user-controlled
one. The Add Host dialog now exposes structured Host, Port, "Use SSL", and
masked Password fields that compose the canonical
`tcp://host:port?ssl=true&password=xxx` URI used as the storage form.
The daemon gains optional shared-secret auth: `Authorization: Bearer <pw>` on
HTTP and `Sec-WebSocket-Protocol: paseo.bearer.<pw>` on the WS upgrade
(browser WebSocket can't set custom headers). Configured via config.json
`auth.password` or `PASEO_PASSWORD` env. Off by default — old clients keep
working unchanged.
The `port === 443` heuristic for ws/wss is gone; the explicit `useTls` flag
drives scheme selection at every call site.
* fix: stabilize direct tcp auth ci checks
* fix: restore fetch stub in bootstrap smoke test
* fix(app): collapse Advanced section in add-host modal
* feat(server): hash daemon password in config
* fix: update lockfile signatures and Nix hash
* Improve direct TCP auth failures
* Fix workspace cwd updates after rebase
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Pass `--host https://app.paseo.sh/#offer=...` (or `PASEO_HOST=...`) to
any CLI command and the connection routes through the relay with E2EE,
the same way the mobile and desktop apps connect. Same `paseo daemon
pair` link the app already accepts — no new pairing flow.
Closes#181
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.
The app service was passing the proxy URL (daemon.<branch>.<slug>.localhost:6767)
which routes through the main daemon. Switch to the worktree daemon's own
ephemeral port so the worktree app talks to the worktree daemon without
a main-daemon hop.
Refs #637.
EXPO_PUBLIC_LOCAL_DAEMON is a developer directive that the app must show
the configured host even when disconnected, so the user can see and
diagnose connection failures. The previous bootstrap conflated this with
the default "auto-add localhost:6767 if a daemon answers" behavior, and
the silent inner catch hid every failure mode.
- Split bootstrapLocalhost into bootstrapDefaultLocalhost (probe-then-
upsert, unchanged) and bootstrapConfiguredOverride (unconditional
upsert with placeholder serverId local:<endpoint>).
- Reconcile the placeholder to the daemon's real serverId in place when
the probe receives server-info; rekey controllers and listeners.
- Replace the silent catch with console.warn carrying endpoint and error.
- On every override boot, purge any host owning the override connection
under a non-matching serverId so a fresh dev daemon (rotated keypair)
no longer leaves the registry pointing at a defunct host.
- Prune cross-endpoint placeholders when EXPO_PUBLIC_LOCAL_DAEMON changes.
Refs #637.
* 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.
Cross-arch packaging (e.g. arm64 build on an x64 Windows runner)
can't smoke the unpacked binary because the host can't spawn it.
Skip the smoke instead of crashing with spawn UNKNOWN.
Combines `git pull` followed by `git push` behind a single action in the
git actions split-button. Push is skipped if pull fails, and errors are
surfaced through the same toast pathway as the existing pull and push
actions.
Multi-arch Windows build in one electron-builder pass, with the website
self-healing across the rename. Asset names go from
`Paseo-Setup-${version}.exe` to `Paseo-Setup-${version}-{x64,arm64}.exe`.
The website now reads the actual installer filename off the latest
GitHub release, so legacy single-arch releases keep rendering a single
"Download" pill while dual-arch releases promote to "Intel / x64" +
"ARM64" automatically. No homepage code change needed when ARM ships.
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>
`mapfile` is bash 4+, but macOS GitHub runners ship bash 3.2, so the
upload step exited 127 on both arm64 and x64 jobs. Replaced with a
portable while-read loop.
Add release notes for the 0.1.65-beta.1 beta covering the atomic
manifest fix for the Apple Silicon Rosetta race (#555), the Linux
process-name fix (#602), assistant image loading states, and the
diff gutter alignment fix.
The macOS publish matrix had each arch run electron-builder with
--publish always, so each runner uploaded its own latest-mac.yml to the
GitHub release the moment it finished. The slower arch clobbered the
faster one, leaving a window (typically 1-3 minutes) where the live
manifest contained only one arch's files[]. Apple Silicon clients
polling during that window could install the x64 build, ending up
under Rosetta.
Also closed the related window where finalize-rollout stamped
releaseDate and rolloutHours after the merged manifest was already
public — auto-updater treats missing rollout fields as "admit", so 100%
of stable users could grab a release before staged rollout kicked in.
Now every build runs --publish never. Each platform job uploads its
non-yml artifacts directly via gh release upload --clobber, and stages
its manifest as an Actions artifact. The renamed finalize-rollout job
downloads all manifest artifacts, merges mac arm64+x64, stamps rollout
metadata, validates, and uploads every manifest in one pass. Public
manifests are never visible in an unmerged or unstamped state.
Refs #555.
Render an activity indicator while image dimensions resolve and an
"Image unavailable" fallback if loading fails, replacing the previous
behavior where the surface stayed blank. Reserves a fixed minimum
height in both states so message layout doesn't shift.
The recently added numberOfLines={1} on the gutter <Text> wraps the
content in display:-webkit-box; overflow:hidden on web, where a plain
ASCII space collapses to zero height. That shifted every line number up
by one row, leaving "1" next to the hunk marker. Use a non-breaking
space so the cell keeps its line height.
The Linux wrapper renamed the executable to Paseo.bin and exec'd it with
--no-sandbox, which surfaced as Paseo.bin in the dock and process list on
Ubuntu. Drop the wrapper and the chrome-sandbox deletion entirely. .deb/.rpm
now ship chrome-sandbox SUID 4755 (electron-builder 26 default postinst,
matches VS Code) and run Paseo directly with the Chromium sandbox on.
AppImage cannot carry SUID, so --no-sandbox is now injected at runtime only
when process.env.APPIMAGE is set.
Fixes#602
Re #381
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.
Add 0.1.64 release notes and document the instant-admit release path
(release:patch + immediate desktop-rollout.yml dispatch) so future
fast-rollout releases follow a single canonical flow.
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>