* 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).
13 KiB
CLAUDE.md
Paseo is a mobile app for monitoring and controlling your local AI coding agents from anywhere. Your dev environment, in your pocket. Connects directly to your actual development environment — your code stays on your machine.
Supported agents: Claude Code, Codex, and OpenCode.
Repository map
This is an npm workspace monorepo:
packages/server— Daemon: agent lifecycle, WebSocket API, MCP serverpackages/app— Mobile + web client (Expo)packages/cli— Docker-style CLI (paseo run/ls/logs/wait)packages/relay— E2E encrypted relay for remote accesspackages/desktop— Electron desktop wrapperpackages/website— Marketing site (paseo.sh)
Docs
docs/ is the source of truth for system-level and process-level knowledge. "The docs", "check the docs", or "check the X docs" always mean this directory — not the web. Look here before fetching anything online; the docs capture gotchas and conventions you cannot derive from the code or external sources.
At the start of non-trivial work, list docs/ and skim anything relevant to the task. When you learn something meta worth preserving — a gotcha, a convention, a workflow, a piece of system context that will outlive the current task — update an existing doc or propose a new one. Code-level facts belong in inline comments next to the code; system, process, and gotcha-level facts belong in docs/.
| Doc | What's in it |
|---|---|
| docs/product.md | What Paseo is, who it's for, where it's going |
| docs/architecture.md | System design, package layering, WebSocket protocol, agent lifecycle, data flow |
| docs/agent-lifecycle.md | Agent states, parent/child relationships, archive semantics, tabs vs archive, subagents track |
| docs/data-model.md | File-based JSON persistence, Zod schemas, atomic writes, no migrations |
| docs/glossary.md | Authoritative terminology — UI label wins, no synonyms |
| docs/coding-standards.md | Type hygiene, error handling, state design, React patterns, file organization |
| docs/design.md | Theme tokens — colors, fonts, spacing, radii, icons |
| docs/unistyles.md | Unistyles gotchas — useUnistyles() is forbidden, alternatives in order |
| docs/file-icons.md | Material icon theme integration for the file explorer |
| docs/providers.md | Adding a new agent provider end-to-end |
| docs/custom-providers.md | Custom provider config: Z.AI, Alibaba/Qwen, ACP agents, profiles, custom binaries |
| docs/development.md | Dev server, build sync gotchas, CLI reference, agent state, Playwright MCP |
| docs/testing.md | TDD workflow, determinism, real dependencies over mocks, test organization |
| docs/mobile-testing.md | Maestro and mobile test workflows |
| docs/ad-hoc-daemon-testing.md | Isolated in-process daemon test harness |
| docs/android.md | App variants, local/cloud builds, EAS workflows |
| docs/release.md | Release playbook, draft releases, completion checklist |
| SECURITY.md | Relay threat model, E2E encryption, DNS rebinding, agent auth |
Quick start
npm run dev # Start daemon + Expo in Tmux
npm run cli -- ls -a -g # List all agents
npm run cli -- daemon status # Check daemon status
npm run typecheck # Always run after changes
npm run lint # Always run after changes
npm run format # Auto-format with Biome
npm run format:check # Check formatting without writing
See docs/development.md for full setup, build sync requirements, and debugging.
Critical rules
- NEVER restart the main Paseo daemon on port 6767 without permission — it manages all running agents. If you're an agent, restarting it kills your own process.
- NEVER assume a timeout means the service needs restarting — timeouts can be transient.
- NEVER add auth checks to tests — agent providers handle their own auth.
- NEVER run the full test suite locally. The test suites are heavy and will freeze the machine, especially if multiple agents run them in parallel. Rules:
- Run only the specific test file you changed:
npx vitest run <file> --bail=1 - Never run
npm run testfor an entire workspace unless explicitly asked. - If you must run a broad suite, pipe output to a file and read it afterward:
npx vitest run <file> --bail=1 > /tmp/test-output.txt 2>&1then read the file. - Never re-run a test suite that another agent already ran and reported green — trust the result.
- For full suite verification, push to CI and check GitHub Actions instead.
- Run only the specific test file you changed:
- Always run typecheck and lint after every change.
- Build workspace packages before diagnosing cross-package type errors. This repo consumes generated declarations across workspaces. If typecheck fails in a package that depends on another workspace (especially CLI depending on server/daemon types), rebuild the owning package first so
distdeclarations are current:npm run build:daemon— rebuild highlight, relay, server, and CLI when daemon/server/CLI types may be stale.- Do not patch inferred callback parameters or add local duplicate types just to silence stale declaration errors.
- Run
npm run formatbefore committing. This repo uses Biome for formatting. Do not manually fix formatting — let the formatter handle it. - Always use npm scripts for linting and formatting. Do not run tools directly with
npx eslint,npx oxfmt,npx oxlint, or package-local binaries. For targeted checks, pass file paths through the npm script:npm run lint -- packages/app/src/components/message.tsxnpm run format:files -- CLAUDE.md packages/app/src/components/message.tsx
- NEVER make breaking changes to WebSocket or message schemas. The primary compatibility path is old mobile app clients talking to newly updated daemons. Users update desktop and daemon first, then keep running the old app for a while. Every schema change MUST be backward-compatible for old clients against new daemons:
- New fields: always
.optional()with a sensible default or.transform()fallback. - Never change a field from optional to required.
- Never remove a field — deprecate it (keep accepting it, stop sending it).
- Never narrow a field's type (e.g.
string→enum,nullable→ non-null). - Test with: "does a 6-month-old client still parse this?" and "does a 6-month-old daemon still send something this client accepts?"
- New fields: always
Platform gating
The app runs on iOS, Android, web (browser), and web (Electron desktop). Code is cross-platform by default. Gate only when you must. Import gates from @/constants/platform.
The four gates
| Gate | Type | When to use |
|---|---|---|
isWeb |
constant | DOM APIs — document, window, <div>, addEventListener, ResizeObserver. This is the exception, not the default. |
isNative |
constant | Native-only APIs — Haptics, StatusBar.currentHeight, push tokens, camera/scanner, expo-av. |
getIsElectron() |
cached fn | Desktop wrapper features — file dialogs, titlebar drag region, daemon management, app updates, dock badges. |
useIsCompactFormFactor() |
hook | Layout decisions — sidebar overlay vs pinned, modal vs full screen, single-panel vs split. From @/constants/layout. |
Decision matrix
| I need to... | Use |
|---|---|
Access DOM (document, window, <div>, addEventListener) |
if (isWeb) |
| Use a native-only API (Haptics, push tokens, camera) | if (isNative) |
| Use an Electron bridge (file dialog, titlebar, updates) | if (getIsElectron()) |
| Switch layout between phone and tablet/desktop | useIsCompactFormFactor() |
| Show something on hover, always-visible on native | isHovered || isNative || isCompact (hover only works on web) |
| Gate to iOS or Android specifically | Platform.OS === "ios" / Platform.OS === "android" (rare, keep inline) |
Rules
- Default is cross-platform. Don't gate unless you have a specific reason.
- Prefer Metro file extensions over
ifstatements. When a module has fundamentally different implementations per platform, use.web.ts/.native.tsfile extensions instead of runtimeif (isWeb)branches. Metro resolves the correct file at build time — the unused platform code is never bundled. Reserveif (isWeb)for small, inline checks (a single line or a few props). If you find yourself writing a largeif (isWeb) { ... } else { ... }block, split into separate files instead.Import ashooks/ use-audio-recorder.web.ts ← uses Web Audio API use-audio-recorder.native.ts ← uses expo-audio@/hooks/use-audio-recorder— Metro picks the right file automatically. - Use
.electron.ts/.electron.tsxfor Electron-only web modules. Electron is still the Metrowebplatform, but desktop dev/build setsPASEO_WEB_PLATFORM=electron, so Metro first looks for.electron.*files and falls back to normal.web.*files. Use this when the implementation depends on Electron-only behavior such aswebviewTag, desktop preload APIs, or the Electron bridge. Keep plain browser web in.web.*, and keep native fallbacks in the base file or.native.*.Import ascomponents/ browser-pane.electron.tsx ← Electron <webview> implementation browser-pane.web.tsx ← plain web fallback browser-pane.tsx ← native fallback@/components/browser-pane— Electron desktop gets the.electron.tsxfile, browser web gets.web.tsx, and native gets the native/base implementation. - NEVER use raw DOM APIs without
isWebguard. DOM APIs crash native. Casting a RN ref toHTMLElementis a red flag — ensure the block is web-only. - NEVER use
onPointerEnter/onPointerLeave. They don't fire on native iOS. - Hover only works on web. React Native's
onHoverIn/onHoverOutonPressabledoes NOT fire on native iOS/iPad — the underlying W3C pointer events are behind disabled experimental flags. For hover-to-show UI (kebab menus, action buttons), useisHovered || isNative || isCompactso the controls are always visible on native and hover-to-show on web. - Don't use Platform.OS as a proxy for layout capabilities. Use breakpoints for layout decisions, not platform checks.
- Import
isWeb/isNativefrom@/constants/platform. Never writeconst isWeb = Platform.OS === "web"locally.
Debugging
Find the complete daemon logs and traces in the $PASEO_HOME/daemon.log