Files
paseo/CLAUDE.md
nllptrx a8ebd390fa feat(forge): pluggable forge abstraction + GitLab and Gitea/Forgejo/Codeberg (#1913)
* refactor(forge): forge-neutral foundation (GitHub-only)

Decouple git-hosting from GitHub behind a neutral abstraction (issue #1616), GitHub-only for now; existing GitHub behaviour is unchanged.

- Forge manifest, neutral ForgeService contract, forge registry + resolver, and a client forge-module registry.
- GitHub code renamed to the neutral shape; PR/Issue attachment wording preserved.
- forge.search.response enums parse tolerantly (unknown kind/auth state degrade instead of breaking the client).
- createPullRequest reports typed CLI/auth errors instead of a generic message.
- forge-resolver host/remote caches are LRU-bounded.
- Forge host trust is explicit: only a known cloud host or a CLI-authenticated host is ever talked to; an unauthenticated GitHub Enterprise host fails resolution instead of routing to github.com.
- Docs: forge-providers guide, glossary and i18n forge-copy conventions, architecture and rpc-namespacing terminology.
- Vitest React Native mocks (unistyles, svg, linking, lucide) consolidated into shared aliased test-stubs.

* feat(forge): GitLab adapter, forge-aware UI, pipelines and approvals

GitLab adapter over the glab CLI on the neutral contracts: MR status, forge-aware UI, pipeline tree, and N-of-M approvals.

- threadIsResolved is part of the neutral timeline item.
- Pipeline load failures show an error instead of an empty section.
- Manual pipeline jobs render as pending.
- Fork/detached MR head pipelines are fetched by MR iid (glab ci get --merge-request).

* feat(forge): Gitea family adapter (Gitea, Forgejo, Codeberg)

One adapter over the tea CLI serving Gitea, Forgejo, and Codeberg on the neutral contracts.

- CI status aggregates commit statuses and Actions runs together.
- Gitea's terminal "warning" state maps to failure on server and client.
- Gitea Actions check details are reachable from the PR pane by workflowRunId.

* refactor(forge): localize compatibility handling

* test(forge): expect normalized GitLab facts

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-17 15:03:26 +08:00

19 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/forms.md Form architecture — non-React form model, form kit, load-state gating; the schedule form is the golden example
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/expo-router.md Expo Router route ownership, startup restore, and native blank-screen gotchas
docs/file-icons.md Material icon theme integration for the file explorer
docs/providers.md Adding a new agent provider end-to-end
docs/forge-providers.md Adding a git forge: registry/manifest, drop-in checklist, self-host/GHES, the two facts tiers
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/protocol-validation.md zod-aot generated inbound WebSocket validation, patched compiler regressions, schema-purity rules
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/mobile-panels.md Compact left/center/right panel ownership, worklet motion, gesture revisions, and Fabric constraints
docs/ad-hoc-daemon-testing.md Isolated in-process daemon test harness
docs/browser-capture-harness.md Real-Electron browser screenshot harness and compositor-surface gotcha
docs/android.md App variants, local/cloud builds, EAS workflows
docs/docker.md Running the daemon and bundled web UI in Docker, volumes, agent images, security
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.

  • Before changing app routes, startup routing, remembered workspace restore, or active workspace selection, read docs/expo-router.md.

  • 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.
      • 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?"
      • Wire schemas are pure structural declarations. Do not add .transform(), .catch(), or .preprocess() to WebSocket message schemas; put normalization in an explicit post-validation pass.
      • Plain z.union() is forbidden when every branch has a shared literal tag. Use z.discriminatedUnion() unless generated-code regression tests prove that specific shape is miscompiled.
      • .default() is acceptable on primitive leaves only. Never put defaults on item schemas for large arrays or big inbound containers.
    • 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