Old clients pinned AgentModeIcon to a closed enum and rendered
undefined for unknown values, crashing the route in production
Hermes with no stack trace. Loosen the type to a plain string,
let the client skip rendering when an icon is unregistered, and
add the customModeIcons capability so the daemon downgrades
non-legacy icons to ShieldCheck for clients that pre-date the
open-string contract.
* Add Pi extension-launch plumbing for in-process tree primitives
* Add Rewind controls for agent sessions
* Preserve SDK checkpoint env var so Claude file rewind works
The SDK injects CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=true when file checkpointing is enabled. The earlier parent-env stripping from c4782ec71c was too broad and removed that child flag along with real parent-session markers.
Keep stripping parent markers, but preserve the checkpoint flag so the SDK rewind path can run directly. With that boundary fixed, the JSONL file-rewind fallback is no longer needed.
* Update OpenCode history replay tests for rewind state
* Show Rewind actions in an adaptive sheet
* Fix rewind semantics across providers
* Refactor Pi IDs through extension capture
* Restore user message to composer after rewind
* Fix Claude rewind tool result mapping
* Fix Claude rewind session switch roundtrip
* Fix rewind and optimistic message regressions
* Tighten rewind test discipline
* Fix OpenCode optimistic message reconciliation
* Reconcile optimistic user messages by marker
* Reorder user message actions
* Unify provider user message contract
* Add Claude rewind flow Playwright contract
* Add Codex rewind flow Playwright contract
* Add OpenCode rewind flow Playwright contract
* Add Pi rewind flow Playwright contract
* Remove real-provider rewind Playwright contracts
* Gate real-provider rewind specs from CI Playwright
* Run Pi through its RPC interface
Replace the embedded Pi runtime packages with a process-backed runtime that starts pi --mode rpc, keeps the runtime port thin, and leaves provider behavior in the Paseo session and mapper modules.
Add focused adapter and mapper tests plus real Pi E2E coverage for the installed binary path.
* Keep Pi provider code in one module
* Add Pi session imports
* Enable Paseo MCP tools in Pi agents
Route injected MCP servers through Pi's MCP adapter and make structured MCP results visible to model-only clients.
Normalize Pi MCP proxy calls so Paseo timelines show the underlying tool names.
* Fix Pi import symlink tests on Windows
Create directory links explicitly in symlink cwd fixtures so Windows server tests exercise the intended path-equivalence behavior.
* Use native realpath for import cwd matching
Resolve import cwd matchers with native realpath so Windows symlink and junction sessions compare against the same path shape used by persisted provider data.
* Fallback realpath for Windows import matching
Use plain realpath when native realpath cannot resolve a cwd so symlink-equivalent persisted sessions still match on Windows runners.
* Add daemon system prompt support for Pi
* Fix Windows realpath import matching
* Remove Pi RPC migration plan doc
* Add rename for workspaces, terminals, and agent tabs
Surfaces rename via the sidebar workspace kebab (git branch rename with
client-side slugify), the terminal tab context menu (stops OSC 2 auto-title
overrides), and the agent tab context menu (locks the title against the
metadata generator). A shared RenameModal wraps AdaptiveModalSheet and
replaces the inline rename on the host page.
Auto-title races are closed from both sides: AgentManager gains
setGeneratedTitleIfUnset with an atomic per-agent write queue, and
TerminalSession replaces lockedTitle with a titleMode discriminated union
so user rename flips to manual and disposes the OSC subscription.
New WebSocket messages are additive: rename_terminal_request/response and
checkout_rename_branch_request/response (branch rename uses the
CheckoutError family). A new @getpaseo/server/utils/branch-slug subpath
export shares slugify + validateBranchSlug between server and app.
* Tighten rename feature and restore lost rebase wiring
Unslop pass on the rename commit plus two rebase artifacts:
- session.ts: collapse handleRenameTerminalRequest to a respond helper
with early returns; restore workspaceGitWatchTargets.set() in
syncWorkspaceGitObserver (lost during rebase onto main, which broke
onBranchChanged firing on branch rename).
- terminal.ts: remove DA1 query handler accidentally re-introduced by the
rebase (main intentionally removed it); restore conditional
onTitleChange registration under titleMode === "auto".
- rename-modal.tsx: drop unconfident optional-call on setNativeProps and
the unknown-cast HTMLInputElement narrowing dance.
- sidebar-workspace-list.tsx: remove unused branch field from rename
result; flatten validateRenameSlug into early returns.
- host-page.tsx: drop ?? "" fallback on a typed-string field.
* Fix typecheck after rebase: pass parsed config to getScriptConfigs
main refactored getScriptConfigs to take the parsed paseo.json config
instead of a repo path. spawnWorktreeScripts (added in the rename
feature) was still passing repoRoot. Read and parse the config first,
matching how spawnWorkspaceScript already does it.
* Resolve lint regressions and restore lost rebase fixes
Post-rebase cleanup: drop the await on syncWorkspaceGitObservers so
fetch_workspaces emits the response before any cold registration-
triggered git work fires (workspace.id is already on the descriptor —
no registry lookup needed). Restore the DA1 CSI handler that answers
\x1b[?62;4;22c on the daemon-side xterm so foreground apps like nvim
get a reply on stdin. Extract WorkspaceTabRenameModal/useWorkspaceTabRename
to drop WorkspaceScreenContent below the cyclomatic-complexity ceiling.
Switch session test internals to handleMessage so we exercise the public
dispatcher and avoid casting through any. Convert literal 'type' aliases
to interfaces and stop spawning callbacks inline so the codebase passes
the post-rebase oxlint rules.
* Fix typecheck after rebase: wire workspaceGitService and worker setTitle
Bootstrap was missing workspaceGitService when constructing
CreatePaseoWorktreeWorkflowDependencies after main's worktree workflow
refactor. Worker terminal manager needed setTitle on the session and
setTerminalTitle on the manager to satisfy the rename additions to
TerminalSession/TerminalManager interfaces.
* Format session.test.ts after rebase
* Remove stray auto-spawn of workspace scripts after bootstrap
The rebase brought in a spawnWorktreeScripts helper and a call inside
runWorktreeSetupInBackground that auto-started every configured workspace
script after worktree setup completed. main never auto-started scripts —
this regressed the workspace-setup-streaming Playwright test, which
expects the "web" script to be idle so the user can click Run.
Drops the helper, the call, and the workspaceGitService dep that only
existed to feed it.
* Fix checkout branch rename tests
* Fix sidebar checkout action store import
* Skip POSIX terminal tests on Windows
* Unslop the rename-entities feature
Six audit findings closed and ~450 net lines trimmed from the branch:
- Remove the duplicate "rename-branch" union member in
GitMutationRefreshReason.
- Fold dispatchStashMessage back into handleSessionMessage; the split was a
feature-first artifact of adding checkout_rename_branch_request, with no
documented rationale.
- Drop the protected beforeGeneratedTitleIfUnsetWrite test seam from
AgentStorage; rewrite the race test to exercise real Promise.all
concurrency against the existing per-agent write queue.
- Reshape useWorkspaceTabRename so the hook returns state and handlers
only; promote WorkspaceTabRenameModal to an exported component the
consumer renders directly.
- Rename RenameModal to AdaptiveRenameModal so it reads as a generic
primitive next to AdaptiveModalSheet, and update host-page,
sidebar-workspace-list, and the workspace tab rename hook to import it
by its new name.
- Trim duplicate matrix coverage and Zod self-tests across
rename-modal.test.tsx, terminal.test.ts, session.test.ts,
agent-storage.test.ts, messages.rename-entities.test.ts and the three
rename e2e specs; introduce packages/app/e2e/helpers/rename.ts to share
setup across the e2e specs without merging coverage.
Behavior of the rename feature is unchanged; targeted vitest, branch-wide
typecheck, and lint all green.
* Restore agentMetadataMocks dropped during rebase
5e6aeb2d removed the agentMetadataMocks hoisted definition and its
vi.mock wiring when it deleted the import describe block. The block
was kept (it belongs to main's import feature) but the mock support
was left behind.
* Fix terminal-manager tests using hardcoded /tmp on Windows
setTerminalTitle tests used cwd: "/tmp" which is not a valid directory
on Windows, causing node-pty error code 267 (ERROR_DIRECTORY). Use
realpathSync(tmpdir()) like the rest of the test file.
* Fix typecheck and lint after rebase onto main
AdaptiveModalSheet moved from a `title` string prop to a structured
`header: SheetHeader`; update AdaptiveRenameModal to memoize and pass
a SheetHeader. invalidateCheckoutGitQueriesForClient moved from
git/actions-store to git/query-keys. useWorkspaceTerminals now owns
the terminal query, so workspace-screen pulls queryKey from the hook
and re-declares queryClient via useQueryClient.
* Update agent metadata test mock to match setGeneratedTitleIfUnset rename
The branch renamed AgentManager.setTitle to setGeneratedTitleIfUnset
for the rename feature; the generateTitlePromptWithConfig helper still
mocked the old method, so eight prompt-byte tests crashed with
"setGeneratedTitleIfUnset is not a function" on both ubuntu and
windows server-tests jobs. Sister mocks in the same file were already
on the new name.
* Fix rename modal showing empty input by using controlled TextInput
AdaptiveTextInput (introduced on main by 29ce6653f) is intentionally
uncontrolled: it drops the `value` prop and seeds the native input
once via `initialValue`. AdaptiveRenameModal still passed `value=
draft` from the pre-rebase shape, so every rename modal opened with
an empty textbox and a disabled Save button. Three playwright e2e
specs (settings-host-page, sidebar-workspace-rename, workspace-agent-
tab-rename) failed for this reason.
Switch the rename modal to a plain controlled TextInput so the input
seeds with the current label and the slug transform (used by sidebar
workspace rename) reflects live in the textbox as the user types.
The unit test's old AdaptiveTextInput mock is replaced with a
react-native mock providing a controlled TextInput shim that captures
the same onChangeText/onSubmitEditing handlers.
* Revert "Fix rename modal showing empty input by using controlled TextInput"
This reverts commit 6283deae842b8a5fed63cb8bf06c78029f98386e.
* Seed rename modal input via AdaptiveTextInput's initialValue + resetKey
AdaptiveTextInput is intentionally uncontrolled — it drops `value` and
seeds the native input once with `initialValue`. The rename modal was
passing `value={draft}` from the pre-rebase shape, so every rename
modal opened with an empty textbox (three playwright specs failing).
Pass `initialValue={draft}` and bump `resetKey` only when the
transform rewrote what the user typed. That seeds the native input
with the current label on open, lets the user keep typing inside
text without cursor jumps (no remount when transform is a no-op),
and remounts the native input with the slug when the transform
diverges so live slugification keeps working in sidebar workspace
rename. Keeping AdaptiveTextInput preserves the BottomSheetTextInput
swap on mobile so the keyboard stays above the sheet — using a plain
TextInput would break that.
The unit test mock is updated to read `initialValue`/`resetKey` so it
mirrors production behavior instead of pretending the input is
controlled.
* Slugify branch rename at submit only, drop live transform
The rename modal's transform prop remounted the native input every
time the slug diverged from what the user typed, which lost focus
mid-edit on every uppercase letter or space in the sidebar workspace
rename. Live-rewriting what the user typed was also surprising — the
expectation is that you type a name, the daemon stores a slug.
Drop the transform prop from AdaptiveRenameModal entirely. Sidebar
workspace rename now slugifies once in handleSubmitRename before
calling renameBranch, and validateRenameSlug runs validateBranchSlug
against the slugified value so the user sees inline errors. The
playwright spec drops the live-slug assertion; it still verifies the
post-submit branch on disk and the rename request payload.
* Rename new rename RPCs to dotted convention
docs/rpc-namespacing.md says new RPCs use dotted names with the
direction as the final segment, and explicitly bans new flat snake_
case names. This PR introduced two flat ones; rename them in place
before the protocol ships:
- checkout_rename_branch_request → checkout.rename_branch.request
- checkout_rename_branch_response → checkout.rename_branch.response
- rename_terminal_request → terminal.rename.request
- rename_terminal_response → terminal.rename.response
Touched: the Zod literals in messages.ts, the discriminated union
entries, the session dispatcher and its tests, the daemon client
wrappers and their tests, the terminal session controller, and the
message-parsing rename-entities test. No callers exist outside the
server package — app and CLI go through the daemon-client wrappers,
which now emit the dotted names.
* Match rename modal Save button and input focus to app conventions
Save uses Button variant=default so it gets the same accent
background + white text as every other primary action in the app
(open-project, settings host save, pair-link confirm, etc).
The input previously fell back to the browser's blue focus outline
on web because the rename modal never set outlineStyle: none — every
other AdaptiveTextInput call site that wants accent feedback already
does this. Kill the browser outline, track focus state, and switch
borderColor to accent while focused.
* Theme the focus-visible outline color via AdaptiveTextInput
public/index.html paints a 2px :focus-visible outline on every web
element with a hard-coded #20744a (Paseo green). On the Claude theme,
the rename modal's input got that green ring instead of the theme's
brown accent — visible mismatch against every other accent-colored
control (primary buttons, etc).
Add an outlineColor entry to AdaptiveTextInput's stylesheet sourced
from theme.colors.accent. Unistyles' Babel plugin tracks the read and
updates the native ShadowTree on theme switch — no React re-render,
no useUnistyles() call (forbidden on this hot path per docs/unistyles.md).
The inline outline-color on the rendered DOM input overrides the
selector-level color from index.html; outline-width/style/offset
still come from the global rule.
Consumer style merges in after, so existing callers that pass
outlineColor (message-input, review/surface, question-form-card)
still win.
Also drop the half-baked isFocused/focusedBorderStyle local state
I added to rename-modal earlier — the AdaptiveTextInput fix removes
the need for it.
* Drop useUnistyles() from rename modal
The rename modal only read theme.colors.foregroundMuted to pass as
the TextInput's placeholderTextColor prop. The rest already went
through StyleSheet.create((theme) => ...). Per docs/unistyles.md the
hook is forbidden when an alternative exists — and this is exactly
the alternative the rest of the codebase uses (project-settings-screen,
add-host-modal, pair-link-modal, command-center): put the muted color
in a tiny StyleSheet entry and read .color off it for the prop.
* Default placeholderTextColor in AdaptiveTextInput
placeholderTextColor was being duplicated at every AdaptiveTextInput
call site (add-host-modal, command-center, pair-link-modal,
project-picker-modal, provider-diagnostic-sheet, combobox, the
sheet's own search inputs, etc.) — all passing the same
theme.colors.foregroundMuted. The shared input should own this.
Move it into AdaptiveTextInput as a default sourced from the same
StyleSheet.create(theme => ...) block as the accent outline. Consumers
still override via the prop if they need a different color. Strip
the redundant placeholderTextColor and local placeholderColor style
entry from rename-modal; other call sites can be cleaned up in a
follow-up.
* Update rename e2e specs to use dotted RPC type strings
Earlier commit (5d5624943) renamed the new rename RPCs to the dotted
convention but missed the two e2e specs that capture the WebSocket
frames by type. captureWsSessionFrames matched the old flat names, so
renameRequests / renameFrames stayed empty even though the rename
went through end to end — the sidebar updated and the branch was
renamed on disk. The toBeGreaterThan(0) assertion fired and the test
failed in playwright CI.
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.
* feat(server): project-level prompts for metadata generation
Add `metadataGeneration` config to `paseo.json` so projects can customize the
four LLM-generated metadata strings — agent title, worktree branch name, commit
message, and pull request title+body. Each entry takes an `instructions` string
that gets injected as a `<user-instructions>` block between the default rules
and the load-bearing JSON format contract, with wrapper text saying user
instructions override defaults. Missing/empty/invalid config keeps every prompt
byte-identical to today.
* refactor(server): centralize project metadata prompt building
Three near-identical readers and four gate-then-wrap blocks collapsed
into one buildMetadataPrompt helper. wrapWithUserInstructions tightened
to require a non-empty string; its misleading empty fallback is gone.
* feat(app): edit metadata generation prompts in project settings
Adds four textareas (agent title, branch name, commit message, pull
request) under project settings, modeled on the existing setup/teardown
pattern. Round-trips through paseo.json preserving unknown sibling
fields at both the metadataGeneration and entry level.
* Restrict provider import fan-out to enabled, non-derived allowlist
Move provider eligibility from a session-level post-filter to the agent
manager's fan-out so disabled providers are never spawned (fixes the
Gemini-disabled hang) and derived providers (like a custom Z.AI extending
Claude) don't duplicate their base provider's sessions.
Add an IMPORTABLE_PROVIDERS allowlist (claude, codex, opencode); ACP-based
providers are excluded from import discovery entirely.
* Filter Codex persisted threads by cwd before hydrating turns
Codex's listPersistedAgents previously called thread/read includeTurns on
every thread returned by thread/list, even when the import sheet was
scoped to one cwd. With many recent threads outside the scope, that fan
of reads dominated the import-sheet open latency.
Add an optional cwd hint to ListPersistedAgentsOptions, thread it through
the agent manager, and have Codex pre-filter the cheap thread/list rows by
cwd before scheduling the per-thread hydration. Widen the thread/list
window when filtering since most rows will be from other cwds. Other
providers ignore the hint.
* Fan out import session fetch per enabled provider
Sheet now reads the providers snapshot already warmed by the workspace
screen, filters to enabled importable providers (claude/codex/opencode),
and issues one fetchRecentProviderSessions request per provider via
useQueries with limit 15. Results are merged, deduped by handle, and
sorted by last activity.
Falls back to a single unfiltered request when the daemon does not
expose a provider snapshot (older servers). Per-provider failures show
inline so a single broken provider does not hide the rest.
* Add provider filter badges to import sheet
Show "All" plus one badge per enabled importable provider above the
session list when more than one is available. Single-select narrows the
already-merged list — no refetch. Each row now shows the provider's
icon next to its label so multiple providers stay visually distinct.
Selection resets when the sheet closes.
* Distinguish already-imported empty state in import sheet
Track how many descriptors were dropped because their handle was
already imported, surface the count back to the client as an optional
`filteredAlreadyImportedCount`, and switch the sheet copy to
"All recent sessions are already imported." when the merged list is
empty solely due to that filter.
* Cover already-imported provider session filtering
* List persisted OpenCode sessions for import
* Revalidate import sessions when sheet reopens
* Add agent persistSession plumbing
* Drop redundant visibility-transition refetch and test boilerplate
The import sheet's queries are gated on `enabled: visible`, and react-query
already refetches stale data when `enabled` flips on, so the manual
`wasVisibleRef` + `invalidateQueries` effect was redundant. Collapse the
two `selectedProvider` reset effects into one.
Drop `expect(...).toBeTruthy()` after `findByText` / `getByText` — those
helpers throw on miss, so the assertion was noise.
Drop `| null` from `buildOpenCodePersistedAgentDescriptor`'s return type
(it never returns null) and the dead `Boolean(descriptor)` filter at the
caller.
* Add provider-opaque session import path
Introduces fetchRecentProviderSessions on the daemon client plus an
importAgent overload accepting providerId/providerHandleId, and a
projection helper that renders persisted descriptors as provider-opaque
recent-session payloads with prompt previews.
* Unslop the import-session feature
Centralizes the importable-providers list, normalizes import_agent_request
shape at one boundary, and removes the per-provider fan-out fallback in
the import sheet. Old daemons without features.providersSnapshot now show
"Update host" instead of being served a degraded path.
Replaces internal-helper unit tests on the OpenCode and Codex providers
with boundary tests through listPersistedAgents; makes storageRoot
injectable on the OpenCode provider so the boundary test can target a
temp directory.
Splits the CLAUDE.md back-compat rule into two contracts: protocol stays
backward-compatible (always); features may require new daemons (no
fallback paths). Tags the two new compat shims with COMPAT(name) and
target removal dates.
* Extract importable-session listing into its own module
Move listImportableProviderSessions out of session.ts and drop the
unused generic listPersistedAgents from AgentManager.
* feat(checkout): merge PR action with real-GitHub e2e test
Adds a `checkoutPrMerge` server RPC backed by `gh pr merge` (squash, merge,
or rebase) and surfaces three new actions in the git-actions menu —
"Squash and merge", "Create a merge commit", "Rebase and merge" — with
squash taking the primary slot when a PR is open and mergeable. Also
renames the existing local merge action to "Merge locally" to disambiguate
it from the new GitHub-side merge.
Adds an end-to-end test that creates a real temporary GitHub repo, opens a
PR via the daemon, polls until mergeable, calls `checkoutPrMerge`, and
verifies the merge landed on `main` — no mocks. The test pre-flights the
`delete_repo` scope so it skips cleanly on tokens that can't tear down,
and fails loud if cleanup ever errors so we don't leak repos.
PR status gains an optional `mergeable` field (`MERGEABLE | CONFLICTING |
UNKNOWN`) populated from `gh pr view --json mergeable`. The schema is
backward compatible — old payloads without the field still parse and
default to `UNKNOWN` via `z.catch`.
* Update PR status snapshot expectations
* refactor: collect git/checkout/PR feature into packages/app/src/git/
Move 21 git/checkout/PR files out of components/, hooks/, stores/, screens/,
utils/ flat-peer slop into a single packages/app/src/git/ module with
shrunk filenames (git-diff-pane.tsx -> diff-pane.tsx, use-git-actions.ts ->
use-actions.ts, etc).
Dedupe: drop the duplicate useGitActionHandlers from diff-pane.tsx — the
shared useGitActions hook is the single source. Drop the
buildGitActionsForPane wrapper. Collapse the merge-PR triple fan-out
(mergePrSquash/Merge/RebaseStatus) into one record keyed by
CheckoutPrMergeMethod. Streamline server-side single-use helpers
(dispatchStash/PullRequestMessage adapters, GitHubPullRequestMergeError
wrapper, legacy worktree test helpers).
Schema-additive only: messages.ts exports CheckoutPrMergeMethod and
PullRequestMergeable types; daemon-client.ts uses the typed alias.
Backward-compatible.
Net delta: 43 files, +219/-1212.
* fix(app): use GitHub icon for all merge-PR menu actions
* test: namespace temp GitHub repos under `paseotmp-` and centralize naming
Old prefixes (`paseo-checkout-ship-`, `paseo-e2e-`, `paseo-checkout-pr-merge-`)
collided with the `paseo-*` namespace of real repos, making bulk cleanup
unsafe. Switch every test that creates real GitHub repos to a single
unmistakable `paseotmp-` prefix, owned by one shared helper per test
boundary so no caller can name a repo outside the namespace.
- New `packages/server/src/server/test-utils/temp-github-repo.ts` exports
`TEMP_GITHUB_REPO_PREFIX` + `createTempGithubRepoName(category)`; both
server e2e tests (checkout-ship, checkout-pr-merge) consume it.
- `packages/app/e2e/helpers/github-fixtures.ts` locks the prefix internally
and now requires callers to pass a `category` instead of an arbitrary
`prefix`. Updated pr-pane and composer-attachments specs accordingly.
* Surface paseo subagents in a collapsible section above the composer
Subagents no longer auto-open as tabs. Each parent agent's pane shows
a compact tab attached to the top of the composer, listing its direct
children with name and status; clicking a row opens or focuses the
subagent's tab. The section is provider-neutral so Claude Code Task
and Codex subagent sources can feed the same UI later. Reconciliation
now separates pruning (`activeAgentIds`) from auto-opening
(`autoOpenAgentIds`) so manually opened subagent tabs survive.
* Tighten subagents composer code from review
Collapse duplicate helper in workspace reconciliation, swap a hand-rolled
shallow comparator for fast-deep-equal (matching repo convention), drop
an unused React default import and a single-use formatCount helper, and
extract a shared PaneOverrides type in the agent-panel test.
* Fix oxlint failures in subagents code
Switch type aliases to interfaces, hoist inline JSX styles/handlers in
SubagentsSection to satisfy react-perf rules, and lift the test
makeAgent helpers onto AGENT_DEFAULTS so their cyclomatic complexity
drops below the configured cap.
* Add missing unistyles mock exports after rebase
After rebasing onto main, SubagentsSection now imports both
useUnistyles and (transitively, via WorkspaceTabIcon) withUnistyles.
Two test mocks needed updating to expose them.
Test drift fix: the implementation contract is correct; the local
react-native-unistyles mocks were stale.
* Tuck subagents tab under the composer
Subagents tab now grows visually from beneath the composer's rounded
top edge: the surface overlaps the composer by the corner radius so
the rounded curve nests into the tab. Composer's top padding is
removed so the tab sits flush against the input box with no
click-dead gutter. The tab is full-width in both states, the header
label drops the medium weight to follow design-system §3 (content
text inside a surface is normal), and hover covers the entire
visible tab when collapsed but stays scoped to the header row when
expanded.
* Decouple subagent close-tab from archive
Closing a subagent's tab no longer archives the agent — the tab is
removed from the per-client layout, the agent stays in the parent's
track until explicitly archived. Track rows gain an archive button (X)
to make the lifecycle gesture explicit. AgentManager.archiveAgent now
cascades to children carrying the paseo.parent-agent-id label so
subagent fleets don't outlive their orchestrator.
Trade-offs documented in docs/agent-lifecycle.md, including the known
limitation that handoff agents launched via the same MCP path get
cascade-archived alongside true subagents until a richer relation
model lands.
* Stabilize subagent archive button hover
Apply the sidebar workspace row pattern to fix two issues with the
new archive button: the row no longer drops hover when the pointer
moves onto the button, and the button slot reserves layout space
when invisible so adding/removing it doesn't shift the row.
The wrapper View tracks hover via onPointerEnter/onPointerLeave
(workspace rows do the same), and the slot stays mounted with
opacity 0 + pointerEvents="none" when the button is hidden. Also
swap the X icon for the Archive icon to match the gesture's intent.
* Match design language for subagent archive button
Hover state now changes the icon color from foregroundMuted to
foreground, matching the project-row trailing-action pattern in
sidebar-workspace-list.tsx. Drops the surface3 background hover
that didn't match the design language.
Adds a tooltip ("Archive subagent") on the trigger using the
canonical Tooltip + TooltipTrigger asChild pattern. Disabled when
the slot is hidden so it never appears unbidden.
* Slot subagents into one module + harden cascade
Reshape the subagents-in-composer feature so it lives behind one
module and integrates at a few explicit sites instead of being
smeared across generic files.
- Extract `PARENT_AGENT_ID_LABEL` to `packages/server/src/shared/agent-labels.ts`. Sweeps 16 literal sites across server, client, CLI, and tests.
- `packages/app/src/subagents/` is now a deep module: `index.ts` is the only React entry (`SubagentsSection`, `useArchiveSubagent`, `useSubagentsForParent`, `selectSubagentsForParent`, `resolveCloseAgentTabPolicy`, `shouldAutoOpenAgentTab`); `policies.ts` is a second designed entry for non-RN data consumers. Internal files renamed (`section.tsx`, `select.ts`, `close-tab-policy.ts`, `auto-open-tab-policy.ts`, `use-archive-subagent.ts`).
- Move the 22-line subagent archive flow out of `agent-panel.tsx` into `useArchiveSubagent`. Agent-panel becomes a one-line consumer.
- Move the close-tab subagent branch out of `workspace-screen.tsx` into `resolveCloseAgentTabPolicy` (discriminated union).
- Move the auto-open exclusion rule out of `workspace-agent-visibility.ts` into `shouldAutoOpenAgentTab`.
- Theme chevrons in `section.tsx` via `withUnistyles`; drop the forbidden `useUnistyles()` call from the hot path.
Server-side cascade hardening:
- Single write path: extract `markRecordArchived` and route both `archiveAgent` and the off-memory cascade branch through it. Off-memory cascaded children now notify subscribers (previously silent).
- Cascade no longer swallows child failures with `try/catch warn`. Failures propagate.
- Cascade test extended from 1 weak assertion to 5 contract tests: full archive shape (archivedAt, normalized lastStatus, requiresAttention=false), running child runtime stop, off-memory branch, subscriber notification for in-memory and off-memory children, partial-failure surfacing.
Net diff vs the start of this PR: -1037 / +444 lines. The feature now slots into the repo at four explicit sites: `agent-panel.tsx` (3 imports + a hook + the section), `workspace-screen.tsx` (one policy import), `workspace-agent-visibility.ts` (one predicate import), and `agent-manager.ts` (the cascade plus the shared label constant).
* fix(relay): use TLS for any port-443 relay endpoint
`shouldUseTlsForDefaultHostedRelay` only returned true for
`relay.paseo.sh:443`. Self-hosted relays on port 443 connected via
`ws://` instead of `wss://`, causing HTTP 400 from the TLS server.
Port-based detection: port 443 → wss, everything else → ws.
* Test port-based hosted relay TLS detection
Cover the new behavior: any port-443 endpoint (hosted Paseo or
self-hosted) and IPv6 hosts return true, non-443 ports and malformed
input return false.
---------
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
* schedule: add `paseo schedule update` to edit schedules in place
Editing a schedule today requires delete+recreate, losing run history
and the schedule id. This adds an additive RPC and CLI command that
patches name, prompt, cadence, new-agent target fields, max-runs, and
expires-in without touching runs or in-flight executions. nextRunAt is
recomputed only when the cadence actually changes.
* schedule(cli): share cadence flag parser between create and update
Both paths were turning --every/--cron into a ScheduleCadence with
near-identical code. Extract `parseCadenceFromFlags` so the literals
and the exclusivity check live in one place; create wraps it to
require a value, update lets it stay optional.
Interval schedules used to wait the full interval before their first run, which
made `--every 1d` feel broken for fresh schedules. Now `--every` fires
immediately on creation; `--cron` keeps waiting for the next slot. `--no-run-now`
and `--run-now` override the defaults, and the parser rejects redundant combos.
Adds `paseo schedule run-once <id>` to manually trigger a single run without
advancing cadence or recomputing completion. The new `runOnCreate` field and
`schedule/run-once` RPC are additive on the WebSocket schema.
Update CHANGELOG.md in place per beta policy and add a compat note on the
`sub_agent.actions` field flagging it as drop-on-cleanup once clients
<= 0.1.65-beta.3 are out of the field.
With capability negotiation in place, the consumer-side tolerance pattern is unnecessary. Drop the dead readDeclaredClientCapabilities helper, inline the parsing in ClientCapabilities.fromHello, delete a tautological assertion in the relay reconnect test, and document the compatibility rules in architecture.md.
* feat: add browser pane element picker
Port the browser pane from PR #198 and route picked elements through workspace attachments so the context follows every agent composer in a workspace.
Co-authored-by: Jason Kneen <jason.kneen@bouncingfish.com>
* docs: document electron platform overlays
* fix: harden browser pane navigation
* fix: polish browser tab interactions
* fix: route browser pane focus shortcuts
---------
Co-authored-by: Jason Kneen <jason.kneen@bouncingfish.com>
The MCP get_agent_activity tool sliced the raw timeline before passing
to the curator, so `limit: 5` could land mid-message and return a
trailing fragment of streamed assistant text. Now goes through the
existing projection pipeline so the limit counts whole projected
entries — assistant deltas merged, tool calls collapsed by callId,
reasoning chunks merged.
Same bug fixed in the wait_for_agent timeout's "Recent activity"
preview, which used the same raw slice.
Consolidates the merge logic: activity-curator no longer ships its own
collapseTimeline, it delegates to projectTimelineRows. One source of
truth for "what is a complete message".
Drops the meaningless `provider` parameter that was threaded through
the projection module — merge logic never branched on it. The wire
schema keeps `provider` per entry; session stamps it once at the emit
boundary. Adds `reasoning_merge` to the projection kind enum (additive
wire change).
Replace base64-in-JSON file transfers with binary WebSocket frames to
unblock the JS thread when assistant markdown messages contain images.
Receive cost (JSON.parse over multi-MB base64) and persist cost (base64
re-encode to disk) both removed.
- Split daemon-client `exploreFileSystem` into `listDirectory` + `readFile`;
`readFile` returns `{ bytes, mime, size, modifiedAt }`. Encoding stays
private to the seam.
- New `packages/server/src/shared/binary-frames/` module hosts terminal
frames (moved from `terminal-stream-protocol.ts`) and new file-transfer
opcodes (`FileBegin` / `FileChunk` / `FileEnd`) with request-id correlation.
- Add optional `acceptBinary` to `FileExplorerRequestSchema`. New daemons
emit binary frames when set; legacy JSON path remains for old clients.
Compat fallback lives only inside `readFile`.
- New `persistAttachmentFromBytes` writes bytes directly: native via
`expo-file-system` `File.write(Uint8Array)`, web via Blob -> IndexedDB,
desktop via new `write_attachment_bytes` IPC. `persistAttachmentFromBase64`
removed.
Map OpenCode `task` tool events into the existing sub_agent timeline shape,
enabling live activity tracking for OpenCode subagent sessions.
- Add `childSessionId` (optional) to sub_agent schema
- Make `actions` optional with default [] for backward compatibility
- Parse task tool input/output into sub_agent detail (tool-call-detail-parser)
- Track child session linkage and buffer/flush child tool parts
- Render sub_agent actions and session IDs in the app component
- Add glob tool detail mapping
- Include backward-compat schema test
Co-authored-by: Ryan Swift <ryan@mlh.io>
Replace top-level `attachments` and `nameContext` on `create_paseo_worktree_request` with `firstAgentContext: { prompt?, attachments? }`, the single signal driving auto-rename. Drop the `worktreeSlug || …` short-circuits so a random placeholder slug no longer blocks renaming, drop the attachment-as-PR fallback in favor of explicit `githubPrNumber`, and reshape the MCP `create_worktree` tool around a `target` discriminated union (`branch-off` / `checkout-branch` / `checkout-pr`) with concise field descriptions so callers stop misreading `prompt` as a name field.
Five set_agent_*_response and update_agent_response payloads were
pixel-identical. Share one named schema so the concept is explicit and
new agent-action responses don't copy-paste the same four fields.
Wire format and inferred types are unchanged.
* 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.
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.