Files
paseo/CLAUDE.md
Mohamed Boudra 0d76654ba0 Show activity indicators for terminals and their workspaces (#1507)
* 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.
2026-06-14 23:29:14 +08:00

17 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, GitHub Copilot, OpenCode, and Pi.

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/hover.md Hover — the canonical pattern (plain View + onPointerEnter/Leave, separate inner Pressable) and the three ways agents break it
docs/unistyles.md Unistyles gotchas — useUnistyles() is forbidden, alternatives in order
docs/floating-panels.md Anchored popovers — Portal/Modal escape for Android, lifecycle gates, keyboard-shared-value, status-bar offset, the flash
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/service-proxy.md Service proxy: exposing workspace scripts at public URLs, DNS setup, reverse proxy config
docs/development.md Dev server, build sync gotchas, CLI reference, agent state, Playwright MCP
docs/rpc-namespacing.md WebSocket RPC naming convention — dotted namespaces and .request/.response pairs
docs/terminal-performance.md Terminal latency pipeline, coalescing/backpressure invariants, benchmark + perf spec usage
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
docs/terminal-activity.md Terminal activity indicators — source-agnostic tracker, agent hook reporting, adding a new hook provider
SECURITY.md Relay threat model, E2E encryption, DNS rebinding, agent auth

Quick start

npm run dev                          # Start the dev daemon
npm run dev:app                      # Start Expo against the dev daemon
npm run dev:desktop                  # Start Electron desktop dev
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

Repo dev commands use checkout-local state by default. In this checkout, PASEO_HOME resolves to .dev/paseo-home, and npm run cli -- ... targets that same dev home automatically. The packaged desktop app and production-style daemon keep using ~/.paseo on port 6767.

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, rebuild the owning stack first so dist declarations are current:

    • npm run build:client — rebuild protocol and client declarations.
    • npm run build:server — rebuild highlight, relay, protocol, client, server, and CLI when 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
  • The protocol stays backward-compatible. Features don't have to. Two separate contracts:

    • Protocol contract (always): schema changes must not break parsing in either direction. An old client must still parse messages from a new daemon; a new daemon must still parse messages from an old client.
      • New fields: .optional() with a sensible default or .transform() fallback.
      • Never flip optional → required, remove fields, or narrow types (stringenum, nullable → non-null).
      • Removed fields stay accepted (we stop sending them, not stop reading them).
      • Test with: "does a 6-month-old client still parse this?" and "does a 6-month-old daemon still send something this client accepts?"
    • Feature contract (per-feature): a new feature may require a new daemon capability. The client detects whether the capability is present and either runs the feature or shows "Update the host to use this." That's it.
      • No fallback paths. Don't write a degraded version of a new feature that runs on old daemons. Don't fan out across legacy RPCs to simulate a missing capability. The user upgrades or doesn't get the feature.
      • No defensive branches scattered through the feature. Capability detection happens in one place; downstream code reads a clean shape.
      • Capability flags live in server_info.features.* with a single // COMPAT(featureName): added in v0.1.X, drop the gate when floor >= v0.1.X comment marking the cleanup site.
      • Existing functionality keeps working across versions — that's the protocol contract doing its job. New-feature degradation is not the goal.
      • New RPCs use dotted namespaces with direction suffixes. Follow docs/rpc-namespacing.md: domain.provider.operation.request pairs with domain.provider.operation.response. Existing flat RPC names will migrate over time; don't add new ones.
  • All back-compat shims are tagged and dated for cleanup. Every shim that exists for old-client/old-daemon support carries a COMPAT(name) comment with the version it was added in and a target removal date (typically 6 months out). One grep — rg "COMPAT\(" — should produce the full list of cleanup work. Don't bury back-compat in untagged ??-fallbacks or optional-chain tunnels — that's how it stops being deletable.

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