Files
paseo/CLAUDE.md
Mohamed Boudra c097678cdf Surface paseo subagents in a collapsible section above the composer (#532)
* 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).
2026-05-09 15:39:13 +08:00

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 server
  • packages/app — Mobile + web client (Expo)
  • packages/cli — Docker-style CLI (paseo run/ls/logs/wait)
  • packages/relay — E2E encrypted relay for remote access
  • packages/desktop — Electron desktop wrapper
  • packages/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 test for 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>&1 then 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.
  • 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 dist declarations 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 format before 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.tsx
    • npm 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. stringenum, 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?"

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 if statements. When a module has fundamentally different implementations per platform, use .web.ts / .native.ts file extensions instead of runtime if (isWeb) branches. Metro resolves the correct file at build time — the unused platform code is never bundled. Reserve if (isWeb) for small, inline checks (a single line or a few props). If you find yourself writing a large if (isWeb) { ... } else { ... } block, split into separate files instead.
    hooks/
      use-audio-recorder.web.ts    ← uses Web Audio API
      use-audio-recorder.native.ts ← uses expo-audio
    
    Import as @/hooks/use-audio-recorder — Metro picks the right file automatically.
  • Use .electron.ts / .electron.tsx for Electron-only web modules. Electron is still the Metro web platform, but desktop dev/build sets PASEO_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 as webviewTag, desktop preload APIs, or the Electron bridge. Keep plain browser web in .web.*, and keep native fallbacks in the base file or .native.*.
    components/
      browser-pane.electron.tsx ← Electron <webview> implementation
      browser-pane.web.tsx      ← plain web fallback
      browser-pane.tsx          ← native fallback
    
    Import as @/components/browser-pane — Electron desktop gets the .electron.tsx file, browser web gets .web.tsx, and native gets the native/base implementation.
  • NEVER use raw DOM APIs without isWeb guard. DOM APIs crash native. Casting a RN ref to HTMLElement is 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/onHoverOut on Pressable does 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), use isHovered || isNative || isCompact so 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/isNative from @/constants/platform. Never write const isWeb = Platform.OS === "web" locally.

Debugging

Find the complete daemon logs and traces in the $PASEO_HOME/daemon.log