* 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
* 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
* 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 }.
* fix(app): keep New Workspace on the active project
New Workspace preselection was falling back to stale remembered host state when multiple hosts existed. Pass durable active-project context from workspace entry points and centralize initial host selection so stale offline hosts cannot steal the form while startup restore remains last-workspace only.
* fix(app): address New Workspace review feedback
* fix(app): stabilize New Workspace host selection
* fix(app): preserve New Workspace host fallback
* test(app): loosen stale-host project assertion
* fix(app): prefer reachable new workspace hosts
* fix(app): avoid stale offline workspace preselect
* fix(app): prefer online project hosts
* fix(client): wait longer for session responses
* fix(client): default RPC waits to sixty seconds
* fix(app): preserve detached stream scroll on delayed history
Code drift: longer client RPC waits let delayed timeline responses arrive after a user scroll-away, so web stream anchoring must not reattach on transient scroll-top resets.
Restore the 15s connect deadline and leave app initialization slack above the default 60s session RPC wait.
* fix(client): keep helper waits within caller deadlines
Review fix: the 60s default session RPC wait leaked into wait previews and waitForAgentUpsert helper fetches. Bound those helper RPCs to the caller deadline or a short best-effort preview timeout, and allow small scroll ranges to reattach at bottom.
* fix(client): respect caller timeout budgets
* fix(cli): keep diagnostic probes responsive
* Refactor daemon client request options
* Preserve daemon client legacy overloads
* fix(app): keep slash commands visible after New Workspace
Repeated returns from the app-wide New Workspace route were using dismissTo with the workspace leaf URL. That updated the root URL without popping the nested host stack, so hidden duplicate workspace deck entries could remain mounted and steal composer popover measurements.
Dispatch a root-stack POP_TO into the host workspace route instead, with a Playwright regression for the slash-command popover.
* test(app): tighten workspace autocomplete regression
Derive the expected deck count from the seeded workspaces and make the duplicate-deck assertion hard. Keep the Expo Router pop hint documented because removing it reproduces the hidden deck entries.
* fix(opencode): prevent indexing the entire home directory
Paseo launches opencode serve with cwd=os.homedir() and refreshes the
global provider snapshot with directory=/Users/admin. OpenCode treats
that as a workspace and starts location services + bigram indexing for
the entire home tree, causing ~466% CPU and ~4GB RAM usage.
- Use a neutral scratch directory as the opencode serve cwd.
- Use a separate scratch directory for global provider catalog refresh
so model/mode discovery no longer triggers home directory indexing.
Fixes high CPU/RAM when Paseo starts opencode with no explicit project.
* fix(opencode): use realpath-aware matcher for home detection in catalog refresh
Switch the home-directory check in fetchCatalog from a string-based
path.resolve() comparison to createRealpathAwarePathMatcher, so we
catch macOS /private/var/... aliases, symlinks, trailing separators,
and Windows casing — consistent with the rest of opencode-agent.ts.
Also:
- Hoist the matcher to module scope so each fetchCatalog call doesn't
rebuild it (the matcher runs realpathSync twice on construction).
- Log a debug line when we rewrite the cwd to the scratch path, so
it's easy to diagnose missing per-directory config in catalog scope.
- Update opencode-agent.test.ts to expect the scratch directory when
cwd === os.homedir(), with a comment pointing to the rationale.
* fix(opencode): isolate helper server home
* fix(opencode): pass semantic global catalog scope
* fix(opencode): release catalog acquisition on home resolution failure
---------
Co-authored-by: rex-chang <rex-chang@users.noreply.github.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
* Add Docker images and agent Docker Mods
Ship official container images that run the Paseo daemon headless. One
Dockerfile parametrized by BASE_IMAGE covers Debian 12/13, Ubuntu 22.04/24.04
and Alpine; it bundles Node 22, the npm-published server + CLI, a vendored
s6-overlay as PID 1, and a small Docker Mods loader.
Agents are chosen at runtime via DOCKER_MODS (pipe-separated mod images). Each
mod is a FROM scratch image carrying only an install hook that runs
`npm install -g <agent-cli>`; the loader pulls the layers from the registry,
extracts them, and runs the hook before the daemon starts, so any requested
agent is on PATH when Paseo probes provider availability.
- docker/base: Dockerfile, install scripts, s6 services, mods loader
- docker/mods/*: claude-code, codex, copilot, opencode, pi
- docker/docker-compose.example.yml + docker/README.md
- .github/workflows/docker.yml: multi-arch (amd64/arm64) buildx matrix,
publishes to ghcr.io/getpaseo on version tags
- docs/docker.md + CLAUDE.md docs index row
* feat(docker): print pairing QR and link on daemon startup
Add an s6 oneshot service that waits for the daemon to listen, then runs
`paseo daemon pair` so the pairing QR code and link surface in the container
logs. Best-effort: never blocks boot, skips gracefully when relay is disabled.
Opt out with PASEO_PAIRING_QR=0.
* build(docker): add Arch image support
* ci(docker): build Arch without Buildx
* docs(docker): document paseo env contract
* feat(docker): add opt-in sudo mode
* docs(docker): link env references
* fix(docker): create agent config dirs
* fix(docker): default home to /home/paseo
* docs(docker): document agent auth setup
* docs(docker): document relay port setup
* fix(docker): install Node from tarball
* docs: add Docker quick start
* docs(docker): remove legacy home example
* docs(docker): set container hostname
* fix(docker): prepare opencode storage
* fix(docker): allow paseo login shell
* fix docker opencode permissions
* ci(docker): use Node 24 actions
* fix(docker): install bzip2 runtime tools
* fix(docker): update Pi mod package
* fix(docker): quiet default daemon logs
* fix(docker): split home from state
Docker images now keep HOME at /home/paseo and store Paseo daemon state under /home/paseo/.paseo by default.
Existing volumes can keep the old layout by setting PASEO_HOME=/home/paseo.
* fix(docker): keep mods out of paseo home
* ci(docker): skip alpine arm64 builds
* fix(docker): address review findings
* fix(docker): verify s6 overlay downloads
* fix(docker): honor custom healthcheck port
* fix(docker): fail on mod extraction errors
* feat(docker): add official container image
Ship a focused daemon image with the bundled web UI enabled and document extending it with agent CLIs.
* ci(docker): publish images only on stable releases
* fix(docker): check daemon health over HTTP
---------
Co-authored-by: Herbrant <cdavide98carnemolla@gmail.com>
Documents the daemon-served web UI from #1635: enabling it, reverse
proxy, TLS, tunnels, and the auth/exposure model. Adds a community
projects page for community-built self-hosting tooling.
* Serve the web client from the daemon
Keep the bundled browser UI opt-in and exclude it from desktop packaging so desktop builds do not ship a duplicate renderer.
* Escape daemon web UI bootstrap hint
* Fix bundled web UI dist path
Resolve OpenAI voice credentials and endpoints from voice-specific config before broader OpenAI fallbacks, and use the same REST STT provider for dictation and voice mode.
* refactor(agent): decouple Paseo tools from MCP
Keep MCP as the fallback adapter while exposing the same runtime catalog for providers that can register tools directly.
* fix(protocol): keep native tool support server-internal
* fix(i18n): sync Japanese shortcut labels
* fix(agent): preserve schedule provider error
* 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
Native state restore can resume at the host route instead of the root index, so host home now resolves the remembered workspace itself. Keep shared route policy outside src/app so Expo Router does not register it as a page.
* 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
#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.
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.
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.
* 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>
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.
Derive app keyboard offsets from one provider-owned shared value so chat content, composer surfaces, and floating panels share the same policy.
Treat progress-zero keyboard samples as closed, even if Android reports a stale nonzero height.
* Restore changelog entries for betas
Beta users check release notes, so each beta now updates an in-place
CHANGELOG.md entry that promotion overwrites with the final stable entry
— no -beta.N heading is left behind. The release machinery already
parses beta headings and syncs them; this restores the policy in the
docs and the release-beta skill, reversing the docs side of 28415b354.
* Spell out in-place beta overwrite in the promotion checklist
The stable checklist said only "update CHANGELOG.md", so a promotion
followed by the letter could stack a fresh entry on top of the
-beta.N block — the stale heading the policy forbids. Make the
promotion path overwrite-in-place explicit, matching the beta checklist.
The local/worktree choice was unstored component state and reset on every
remount of the New Workspace screen (e.g. after creating a workspace).
Persist it in the create-form preferences (FormPreferences.isolation).
Also rename the app-layer "backing" to "isolation" to match its UI label.
Isolation is the create-time intent; it stays distinct from workspaceKind,
the git-derived persisted property, and the wire's source.kind.
Multiple workspaces can share one checkout directory. Agent and terminal
status, sidebar tabs, and ownership now attribute strictly by workspace ID,
so a running or blocked agent in one workspace no longer shows up on its
same-directory siblings.
A one-time startup migration backfills workspace IDs onto legacy agents, and
every agent and terminal is created with one thereafter. Directory lookups
remain only for genuinely folder-derived git facts (branch, diff) and the
archive-by-path adapter.
* feat(workspace): bind agents/terminals/status to workspaceId (Model B phase 1)
Model B EX-Phase 1: the binding layer. Records gain an additive
workspaceId that answers "which workspace does this belong to" without
replacing the existing required cwd.
- protocol: add optional workspaceId to agent/terminal records and a
workspaceOwnership capability under server_info.features.*
- server: resolve and persist workspaceId on agent and terminal
creation; carry it through agent-manager, projections, storage,
loading, sessions, and the terminal manager/worker pipeline
- workspace-directory / workspace-registry-model: derive and expose
workspace ownership; bind status to workspaceId
- client: surface workspaceId via daemon-client
- app: filter terminals and agent visibility by workspaceId, thread it
through workspace screen / terminal panel / session store
- tests: cover workspaceId binding across create, agent-manager,
mcp-server, workspace-directory, workspace-registry-model, and
agent-visibility
cwd stays required on every record; workspaceId is purely additive and
back-compatible (optional/defaulted), so old and new peers still parse
each other's messages.
Known follow-ups: none.
* feat(workspace): empty projects + editable titles, fix same-cwd terminal isolation (Model B phase 2)
- Address Phase 1 review fixes and add same-cwd terminal isolation e2e coverage
- Isolate terminal subscriptions per workspaceId so two workspaces sharing one cwd no longer cross-wire terminals (terminal-subscription-key)
- Add editable workspace title, decoupled from the backing directory/branch
- Persist empty projects (no workspaces yet) across daemon restarts
- Add e2e coverage: empty-project-persists (server + app), workspace-same-cwd-isolation, sidebar-workspace-rename
Known follow-ups: none
* feat(workspace): create multiple workspaces per directory — local or worktree (Model B phase 3)
- workspace.create RPC always creates a new workspace record; no directory dedup
- dropped directory dedup in open_project and createPaseoWorktree paths
- non-git directories are first-class: local-checkout workspaces no longer require a repo
- service-proxy collision resolved by defaulting to the owning workspace instead of failing
- new creation UI: choose backing directory (local checkout or worktree) per workspace
- e2e coverage for creating multiple workspaces over the same directory
Known follow-ups:
- COMPAT cwd->workspaceId resolver remains until client floor advances
- prune any remaining workspace==directory assumptions surfaced by usage
* feat(cli): paseo run workspace policy — bare run creates a workspace, --workspace/PASEO_WORKSPACE_ID target (Model B phase 4)
- A workspace is now the explicit home of a `paseo run`: run.ts resolves a
workspace before creating any agent, then stamps the agent with that
workspaceId — no run leans on createAgent's legacy cwd->workspace fallback.
- Precedence: --workspace <id> > $PASEO_WORKSPACE_ID > --worktree > bare run.
--worktree mints its own workspace and overrides the ambient
PASEO_WORKSPACE_ID; --worktree + --workspace is rejected upstream.
- New-workspace-per-bare-run: every bare run mints a fresh local-backed
workspace for its cwd rather than reattaching to an existing one in that dir.
- Env support: $PASEO_WORKSPACE_ID (exported by workspace terminals) targets an
existing workspace, same as --workspace.
- Help text: new --workspace option documents the default (new workspace per
run) and the env fallback; created runs print the workspace id plus a tip.
- e2e: cli-run-workspace-precedence.e2e.test.ts covers bare, distinct-per-cwd,
--workspace attach, and env attach against an isolated daemon.
Known follow-ups:
- createAgent's COMPAT cwd->workspace resolver stays for old clients; the CLI no
longer relies on it but it is not yet removable.
* feat(workspace): archive removes the workspace record, never the directory; last worktree ref offers disk delete (Model B phase 5)
- Archive is now scoped to a single workspace RECORD (by workspaceId), not by cwd/worktree path. It tears down only the agents and terminals owned by the target workspaceId.
- Sibling isolation: a directory can back multiple workspaces, so archiving one workspace no longer destroys a sibling workspace's agents, terminals, or directory.
- Decoupled on-disk worktree deletion into an explicit, last-reference-only option (deleteWorktreeFromDisk). The directory is removed only when the archived workspace was the last active reference to a Paseo-owned worktree; local checkouts are never deleted.
- Unified archive UI with a keep/delete prompt: "Keep on disk" is the default non-destructive choice, "Delete" removes the worktree directory.
- Protocol: new optional, defaulted deleteWorktreeFromDisk field (COMPAT-tagged, back-compat preserved).
- Tests: record-scoped archive e2e (sibling isolation, last-ref disk delete, sibling-keeps-directory) and an app e2e for the keep prompt.
Known follow-ups:
- Surface the keep/delete prompt's "delete" path coverage in app e2e (only the keep path is exercised today).
- COMPAT(worktreeDiskDeletion): drop the optional gate when floor >= v0.1.97.
* feat(workspace): uniform expandable projects + status inbox; merged worktrees cleaned, explicit-id archive (Model B phase 6)
- P5 archive fixes: worktree archive now targets an explicit workspaceId, and auto-archive-on-merge cleans the worktree directory from disk when it is the last reference.
- Dropped non-git project flattening: every project — git or non-git, single- or multi-workspace — renders as the same expandable parent.
- Every project is expandable and every workspace is archivable; each carries its own "+ New workspace" affordance regardless of kind.
- Added a status inbox grouping (Ready for review / Working / Done) where each workspace is bucketed independently by last update.
- The deepest sidebar level is the workspace row: tabs, agents, and terminals never appear in the sidebar.
- Added Model B sidebar e2e coverage proving the expandable-parent, no-leaf, and independent-status-bucketing invariants.
Known follow-ups: the inline header FolderPlus worktree shortcut stays git-only (canCreateWorktreeForProjectKind); non-git projects create workspaces via the "+ New workspace" row.
* fix(protocol): use literal :: separator in terminal subscription key
The committed blob contained a NUL byte as the separator instead of the
:: the comment documents, which broke text diffs and review tooling
(git flagged the file as binary). Functionally the key is an in-memory
Map key only, but source must stay text.
* feat(workspace): shared directory right-sidebar boundary + docs; final consolidation (Model B phase 7)
- Pin and document the directory-backed vs workspace-owned right-sidebar boundary:
same-cwd workspaces share directory-keyed git/PR/file surfaces but never share
workspace-owned drafts, attachments, or file-explorer state. New docs section in
architecture.md plus data-model.md keying convention and two glossary terms.
- Add e2e coverage for the boundary: same-directory-workspaces.spec.ts proves the
right sidebar is shared (a directory change appears in both same-dir workspaces)
while tabs/terminals stay independent per workspace.
- Accumulated follow-up fixes:
- MCP child agents created in a new worktree are stamped with the new worktree's
workspaceId (not the parent's, not unstamped), mirroring the session path so
workspaceId-scoped archive can find and tear them down.
- session-store mergeWorkspaces prunes a stale empty-project descriptor when a
workspace lands in that project, so it stops governing the project's metadata.
- CLI run: reject --worktree alongside an ambient PASEO_WORKSPACE_ID (not just
--workspace), so worktree resolution never races an existing-workspace select.
New run.test.ts pins the three validation outcomes.
- Full-branch reshape outcomes:
- Consolidate the four placeholder server_info.features flags (workspaceOwnership,
workspaceTitles, workspaceProjects, workspaceMultiplicity) into the single
workspaceMultiplicity capability gate.
- Remove the now-dead ensureLocalCheckoutWorkspace and its deps interface; explicit
creation always mints a new same-cwd record via createLocalCheckoutWorkspace.
- Resolve every Model B COMPAT marker from v0.1.X to v0.1.97.
- Drop the obsolete workspaceOwnership feature assertion from the same-cwd
isolation e2e; broaden a sidebar-shortcuts test to cover both project kinds.
- Add workspace-create-errors.e2e.test.ts pinning each early-reject error branch.
- typecheck, lint, format all green.
Known follow-ups: pre-existing COMPAT(rewind) markers still carry v0.1.X placeholders
(out of Model B scope, left untouched).
* test(workspace): de-slop Model B tests per audit-tests (reshape mocks/assertions)
Reshape Model B test changes flagged by audit-tests so they exercise real
behavior and observable state instead of mock scaffolding.
Categories fixed:
- Mocks -> real dependencies + persisted state: create.test.ts and
mcp-server.test.ts dropped hand-built AgentManager/AgentStorage mock objects
and mock.calls[...] assertions, now run the real AgentManager/AgentStorage
with a fake agent client and assert on the stored agent record (workspaceId,
parent label).
- Module-internal spying -> injected seam: acp-agent stopped spying on the
tree-kill module (vi.spyOn(treeKillModule, ...)). Added a ProcessTerminator
injection seam to ACPAgentClient/ACPAgentSession (production defaults to
terminateWithTreeKill); tests inject a typed FakeTerminator and assert on the
recorded children plus observable stream .destroyed state.
- Poking internals -> public API: acp close()/killTerminal/releaseTerminal
tests now create terminals through the public createTerminal API instead of
mutating internals.terminalEntries.
- Reimplemented production logic deleted: cli-run-workspace-precedence dropped
its inline copy of resolveRunWorkspace flag precedence (a fake reimplementation
it then asserted against) and now proves only the daemon behaviors the CLI
builds on; flag precedence stays covered in the CLI's own run.test.ts.
- Sleep -> poll: workspace-same-cwd-isolation replaced a fixed setTimeout with
expect.poll on the observed snapshot state to remove the race.
- Internal protocol-frame assertions removed: sidebar-workspace-rename dropped
the captureWsSessionFrames workspace.title.set.request assertions, relying on
the user-visible rename + reload checks already present.
* fix: green CI — new-workspace status bucket regression + branch-picker flow tests + sdk emptyProjects
Source regression:
- packages/app/src/screens/new-workspace-screen.tsx
- packages/app/src/screens/new-workspace-empty.ts
When a new workspace is created with an initial agent, optimistically merge it
with status "running" (statusEnteredAt now) so it lands in the "Working" bucket
instead of defaulting into the wrong bucket. The empty-workspace path passes
withInitialAgent: false so a bare workspace keeps its descriptor status.
Test-drift (intentional P3 flow change — backing picker now required):
- packages/app/e2e/new-workspace.spec.ts
- packages/app/e2e/helpers/new-workspace.ts
The reshaped creation flow requires choosing a backing ("New worktree") before
the branch / starting-ref picker is reachable. Branch-picker specs now call
selectWorkspaceBacking(page, "worktree") first, and the helper waits for the
worktree control to drop aria-disabled (it stays disabled until the checkout
status query confirms the project is a git repo) before clicking.
Test-drift (protocol field added — sdk emptyProjects):
- packages/client/src/index.test.ts
fetch_workspaces response now carries emptyProjects; the toEqual expectation
includes emptyProjects: [].
Flake hardening:
- packages/app/e2e/same-directory-workspaces.spec.ts
- packages/app/e2e/helpers/seed-client.ts
Out-of-band working-tree writes raced the daemon's debounced filesystem
watcher, so the UI could subscribe before the new file was in the git snapshot.
Force a checkout refresh (same path as the UI's manual refresh) to make the
write authoritative before asserting, removing the timing dependency.
* test(app): expect withInitialAgent:false in empty-workspace create call
The withInitialAgent flag (fix for the new-workspace Done-bucket regression)
added a field to the ensureWorkspace call; the empty-path unit test pinned the
exact args. Test-drift, not behavior — update the expectation.
* feat(app): stack new-workspace creation params (Project / Isolation dropdown / Base) with reserved base row + keyboard avoidance
Restructure the new-workspace creation screen into a vertical stack of
Project / Isolation / Base parameters instead of the previous mixed
layout.
- Isolation/backing is now a dropdown (Local vs New worktree) matching the
app's existing dropdown/combobox primitives, replacing the inline
segmented switcher.
- The Base (starting ref) row now reserves its space even when the backing
is Local and the picker is hidden, so switching backing no longer causes
the form to shift vertically.
- The form avoids the keyboard so the title input and submit stay visible
while typing on native and web.
- Updated the e2e new-workspace helper to drive the dropdown-based
backing selector and the reserved Base row.
* fix(app): simplify new-workspace form — stacked ghost rows above composer, no card/title, project-matched dropdowns
- Render the three rows (formStack) at the top, under the "New workspace"
heading and above the composer input, not in the composer footer.
- Drop the card/surface/border chrome; rows sit on the plain background.
- Stack Project, Isolation (multiplicity only), and Base; remove the title
field — the title is set server-side and create works with no user title.
- Each row is a Label immediately followed by its dropdown control; no
columns, fixed label widths, or reserved horizontal space, with the label
glyph aligned to the heading's text x.
- All three triggers share the ghost badge style of the Project control
(ProjectPickerTrigger / IsolationPickerTrigger / RefPickerTrigger +
Combobox); keep the workspace-create-backing-* test IDs.
- Omit "New worktree" from Isolation entirely when the project is non-git.
- Reserve the Base row height always but render nothing on Local backing,
showing the Base label + ref picker only for New worktree.
- Keep keyboard avoidance on the centered content.
- E2E: drive selectWorkspaceBacking via the Isolation trigger + Combobox and
remove the now-gone workspace-create-title-input usage and title field.
* feat(app): sidebar new-workspace entry points + non-git isolation hidden + project auto-select
- Q1: hide the Isolation control on non-git projects — gate the row on canCreateWorktree (multiplicity && selectedIsGit) so a project with no git checkout never offers a worktree backing choice.
- Q2: reset the stale project preselect across the reused 'new' screen — clear the manual picker choice on route project identity change so each route-driven navigation preselects its own project; align the nav verb to router.navigate.
- Q3: remove the per-project "+ New workspace" sidebar row and add one global "New workspace" entry above Sessions in both mobile and desktop sidebars (testID sidebar-global-new-workspace); creation stays reachable per-project via the existing git new-worktree icon.
- Q4: match the Sessions / New-workspace header button sizing to the workspace rows — SidebarHeaderRow icon md->sm, label fontSize base->sm.
- e2e: new new-workspace-entry.spec covering global entry, project preselect reset across reused screen, and non-git isolation hidden; update sidebar-model-b, empty-project-persists, workspace-multiplicity, and helpers for the removed per-project row.
* fix(app): group New-workspace/Sessions header (no divider, workspace-row sized) + remove banned useUnistyles
- Q5: wrap the New-workspace and Sessions header entries in a single sidebarHeaderGroup that owns one bottom divider, so the two rows sit tight together with no gap and no per-row separator (both mobile and desktop sidebars).
- Add a compact variant to SidebarHeaderRow: workspace-row sized (minHeight 36, surfaceSidebarHover, borderRadius.lg) with horizontal padding that aligns its icon/label with the Workspaces section title and the workspace rows below; the default header variant (settings Back-to-workspace) keeps its sidebar-height row and own separator.
- Remove the banned useUnistyles() from sidebar-header-row.tsx per docs/unistyles.md: theme-reactive icon color now goes through withUnistyles(Icon) + uniProps mappings; static sizing reads ICON_SIZE.
- Taste: new-workspace-screen dedupes the project-icon styles (single projectIcon/projectIconFallback/projectIconFallbackText) and replaces repeated magic numbers with BADGE_HEIGHT and a named fallback-font-size constant.
* fix(app): no layout shift on git<->non-git (reserve Isolation row) + symmetric sidebar divider spacing
- Q6: Isolation row reserves its height and renders an invisible spacer for non-git projects, matching the Base-row pattern, so switching between git and non-git projects keeps a constant form height with no layout shift.
- Q7: sidebar header group splits paddingVertical into paddingTop/paddingBottom so the Sessions-row-to-divider gap equals the divider-to-Workspaces-header gap, centering the divider.
* feat(app): rename Sessions to History (clock icon, Agent history header)
- Sidebar label now reads "History" with a clock icon (was Sessions / MessagesSquare)
- Sessions screen header now reads "Agent history"
- Updated i18n strings across all 6 locales (en, ar, ru, zh-CN, fr, es)
- Route and testIDs unchanged (sidebar-sessions, /sessions)
* fix(app): symmetric sidebar header padding (top == bottom), traffic-light inset preserved
The sidebarHeaderGroup wrapper (New-workspace + History rows) had paddingTop: theme.spacing[1] against paddingBottom: theme.spacing[2]. Equalize paddingTop to theme.spacing[2] so the header group's top padding matches its bottom padding. The divider stays centered since paddingBottom still matches WorkspacesSectionHeader's paddingTop. The desktop window-controls (traffic-light) spacer — paddingTopSpacerStyle plus TitlebarDragRegion — is a separate inset and is left untouched.
* fix(app): hover card shows branch; sidebar copy-branch copies the real branch (not the title)
- Add SidebarWorkspaceEntry.currentBranch, sourced from gitRuntime.currentBranch (normalized; detached HEAD/blank/missing -> null)
- Fix copy-branch smear: handleCopyBranchName copied workspace.name (the title); now copies the real currentBranch (guarded)
- Workspace hover card: add a branch row (GitBranch icon + branch name), shown only when it differs from the title
- Header branch-switcher left untouched; diff-pane re-home decision still pending
* feat(app): branch switcher moves into the git diff panel; header title is static (Model B coherence)
- branch switcher now lives in the diff panel Changes header on desktop+mobile
- workspace header title is a plain static title; branch removed from it
- no new workspace-screen header row
- git diff-stat unchanged, stays where it is
- no duplicate git actions
- unified descriptor name fallbacks via resolveWorkspaceName
- rename-then-switch e2e proves header title and real branch stay independent
* feat: show activity indicators for terminals and their workspaces
A terminal surfaces whether the agent running inside it is working, idle,
or waiting for input as a tab dot, and that state rolls up into the
owning workspace status — so an agent run inside a terminal reads the
same as a native agent. Finishing a turn raises a "Terminal finished"
notification that routes back to the workspace terminal tab.
Activity is reported by the agents, not parsed from terminal output:
Claude and Codex command hooks and an OpenCode plugin post coarse state
to a local daemon endpoint, and a source-agnostic per-terminal tracker
maps it to working/idle/attention. Hook installation is opt-in and off
by default because it edits your real agent config files; enable it under
a host's Terminal settings and Paseo installs the hooks globally, runs
them via `paseo hooks <agent> <event>`, and removes its own hooks when
you opt back out. Installation and reporting are idempotent and
fail-open, so a missing or failing hook never breaks the user's shell.
* Fix terminal activity routing and CI test
Test drift: anchor the provider-leak test to the repo root instead of process.cwd() so it passes under the server workspace CI runner.
Also restore auth-first JSON parsing outside the terminal-activity route and route subdirectory terminal activity to the deepest active workspace.
* Fix workspace resolver regression
Code drift: terminal activity routing needs parent-workspace prefix matching, but agent directory and historical fetches keep exact registered workspace semantics, including archived exact matches.
* Narrow terminal/draft workspace subscriptions to the fields they use
terminal-panel and the draft agent tab subscribed to the whole workspace
descriptor via useWorkspace but read only workspaceDirectory (plus projectKind
in TerminalPanel, plus id in the draft tab), so they re-rendered on any field
change of that workspace (status, diffStat, scripts). Switch them to the
existing useWorkspaceDirectory / useWorkspaceFields hooks so they re-render only
when the fields they actually read change. No behavior change.
* Make TerminalActivity.state forward-compatible
A strict z.enum rejects the whole payload when a newer daemon reports a
terminal activity state this client predates, breaking the protocol's
old-client-parses-new-daemon contract (the activity field has no .catch at
its embedding in messages.ts, so the failure bubbles up). Degrade unknown
states to "idle" (no indicator, no notification) via .catch, matching the
enum-with-catch pattern used elsewhere in the protocol. Add a forward-compat
regression test.
* Make workspace IDs opaque, independent of the filesystem path
Workspace IDs were the resolved checkout/worktree path, so code could
treat an ID as a path: prefix matching, deriving directories from it,
falling back to a path when a lookup missed. IDs are now opaque -
compared only by exact equality and never used as a path. Anything
path-shaped resolves the workspace record first and reads its cwd.
New workspaces get a generated `wks_` ID; existing path-shaped IDs are
read from disk and never regenerated, so there is no migration and no
change to the wire or persisted schemas. Groundwork for running
multiple workspaces in a single directory.
* Keep attachment scope key stable and align refetch test
The attachment scope key keeps `workspace=` instead of `wsid=` so existing
persisted drafts are not orphaned; the rename was cosmetic. The SDK refetch
test no longer asserts a filter, matching refetch fetching one page and
selecting by id client-side.
* Fix live agent updates in directories without a registered workspace
The opaque-ID work removed buildProjectPlacementForCwd's directory
fallback, so an agent running in a folder with no registered workspace
(e.g. a fresh non-git dir) produced a null placement. forwardAgentUpdate
then threw "Workspace not found", the error was swallowed by its catch,
and no agent_update was emitted — live model/thinking switches and status
updates silently stopped. Caught by the live-preferences e2e suite.
The fallback builds a directory-scoped project placement keyed by the
path. That key is a project grouping key (non-git projects group by
path), not a workspace id, so it stays within the opaque-id rule.
* Always run server worktree archive, even without a resolved workspace
Archiving bailed out entirely when the workspace was not found in the
client store, so a race or stale state could make "archive" do nothing
server-side with only a console.warn. The server archive is keyed by
worktreePath, which is always available, so it now runs regardless; only
the optimistic client-side updates (keyed by workspace id) are gated on
the workspace being resolved.
* Expect a directory-scoped placement for unregistered agent dirs
This unit test asserted the no-placement behavior reverted in the live
agent-update fix, which had broken live model/thinking switching. Update
it to expect the directory-scoped placement now emitted for an agent in a
directory with no registered workspace.
* Fix opaque workspace routing in app E2E
* Preserve workspace IDs during partial bootstrap
* Fix archive flows for opaque workspace IDs
* Stop treating opaque workspace IDs as filesystem paths
Workspace IDs are opaque (wks_<hex>), but several call sites still passed
the id where a directory was expected, which broke those flows for opaque
IDs. The branch switcher sent the id as a cwd to branch/stash/checkout git
operations, and server archive/reconcile cleanup keyed git-watch and
subscription teardown by id, leaking that state.
Git and filesystem operations now take the workspace directory; the opaque
id is used only for identity and cache keys. Archive/reconcile cleanup
routes through a single teardownArchivedWorkspace helper that keeps the key
split explicit: runtime store by id, git watch and subscription by cwd.
Path-derived grouping keys are renamed to directory keys, and the helpers
are split into workspace-identity and workspace-directory so the id-vs-path
boundary is obvious.