diff --git a/.github/workflows/deploy-app.yml b/.github/workflows/deploy-app.yml index 5a8b47a70..7a180d5d7 100644 --- a/.github/workflows/deploy-app.yml +++ b/.github/workflows/deploy-app.yml @@ -3,10 +3,10 @@ name: Deploy App on: push: tags: - - 'v*' - - '!v*-beta.*' - - 'app-v*' - - '!app-v*-beta.*' + - "v*" + - "!v*-beta.*" + - "app-v*" + - "!app-v*-beta.*" workflow_dispatch: jobs: @@ -18,10 +18,10 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '22' - cache: 'npm' - registry-url: 'https://npm.pkg.github.com' - scope: '@boudra' + node-version: "22" + cache: "npm" + registry-url: "https://npm.pkg.github.com" + scope: "@boudra" - name: Install dependencies run: npm install --workspace=@getpaseo/app --include-workspace-root diff --git a/.github/workflows/deploy-relay.yml b/.github/workflows/deploy-relay.yml index e064721cf..4ab27253d 100644 --- a/.github/workflows/deploy-relay.yml +++ b/.github/workflows/deploy-relay.yml @@ -4,8 +4,8 @@ on: push: branches: [main] paths: - - 'packages/relay/**' - - '.github/workflows/deploy-relay.yml' + - "packages/relay/**" + - ".github/workflows/deploy-relay.yml" workflow_dispatch: jobs: @@ -17,8 +17,8 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '22' - cache: 'npm' + node-version: "22" + cache: "npm" - name: Install dependencies run: npm install --workspace=@getpaseo/relay --include-workspace-root @@ -30,4 +30,3 @@ jobs: run: cd packages/relay && npx wrangler deploy env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml index 3a02c59e0..af286bb79 100644 --- a/.github/workflows/deploy-website.yml +++ b/.github/workflows/deploy-website.yml @@ -4,12 +4,12 @@ on: push: branches: [main] paths: - - 'CHANGELOG.md' - - 'packages/website/**' - - 'package.json' - - 'package-lock.json' - - 'patches/**' - - '.github/workflows/deploy-website.yml' + - "CHANGELOG.md" + - "packages/website/**" + - "package.json" + - "package-lock.json" + - "patches/**" + - ".github/workflows/deploy-website.yml" release: types: [published] workflow_dispatch: @@ -24,8 +24,8 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '22' - cache: 'npm' + node-version: "22" + cache: "npm" - name: Install dependencies run: npm install --workspace=@getpaseo/website --include-workspace-root diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 3d724f30f..f6379d2fc 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -3,21 +3,21 @@ name: Desktop Release on: push: tags: - - 'v*' - - 'desktop-v*' - - 'desktop-macos-v*' - - 'desktop-linux-v*' - - 'desktop-windows-v*' + - "v*" + - "desktop-v*" + - "desktop-macos-v*" + - "desktop-linux-v*" + - "desktop-windows-v*" workflow_dispatch: inputs: tag: - description: 'Existing tag to build (e.g. v0.1.0)' + description: "Existing tag to build (e.g. v0.1.0)" required: true type: string platform: - description: 'Optional desktop platform to build.' + description: "Optional desktop platform to build." required: false - default: 'all' + default: "all" type: choice options: - all @@ -31,8 +31,8 @@ concurrency: env: SOURCE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} - DESKTOP_WORKSPACE: '@getpaseo/desktop' - DESKTOP_PACKAGE_PATH: 'packages/desktop' + DESKTOP_WORKSPACE: "@getpaseo/desktop" + DESKTOP_PACKAGE_PATH: "packages/desktop" jobs: create-release: @@ -100,11 +100,11 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '22' - cache: 'npm' + node-version: "22" + cache: "npm" cache-dependency-path: package-lock.json - registry-url: 'https://npm.pkg.github.com' - scope: '@boudra' + registry-url: "https://npm.pkg.github.com" + scope: "@boudra" - name: Install JS dependencies run: npm ci @@ -278,11 +278,11 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '22' - cache: 'npm' + node-version: "22" + cache: "npm" cache-dependency-path: package-lock.json - registry-url: 'https://npm.pkg.github.com' - scope: '@boudra' + registry-url: "https://npm.pkg.github.com" + scope: "@boudra" - name: Install JS dependencies run: npm ci @@ -346,11 +346,11 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '22' - cache: 'npm' + node-version: "22" + cache: "npm" cache-dependency-path: package-lock.json - registry-url: 'https://npm.pkg.github.com' - scope: '@boudra' + registry-url: "https://npm.pkg.github.com" + scope: "@boudra" - name: Install JS dependencies run: npm ci diff --git a/.github/workflows/fix-nix-hash.yml b/.github/workflows/fix-nix-hash.yml index b5f0f7b1e..880731d3f 100644 --- a/.github/workflows/fix-nix-hash.yml +++ b/.github/workflows/fix-nix-hash.yml @@ -4,12 +4,12 @@ on: push: branches: [main] paths: - - 'package.json' - - 'package-lock.json' + - "package.json" + - "package-lock.json" pull_request: paths: - - 'package.json' - - 'package-lock.json' + - "package.json" + - "package-lock.json" permissions: contents: write @@ -25,8 +25,8 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '22' - cache: 'npm' + node-version: "22" + cache: "npm" - uses: cachix/install-nix-action@v31 with: diff --git a/.github/workflows/nix-build.yml b/.github/workflows/nix-build.yml index 949c67f30..4266debf9 100644 --- a/.github/workflows/nix-build.yml +++ b/.github/workflows/nix-build.yml @@ -4,27 +4,27 @@ on: push: branches: [main] paths: - - 'nix/**' - - 'flake.nix' - - 'flake.lock' - - 'package.json' - - 'package-lock.json' - - 'packages/highlight/**' - - 'packages/server/**' - - 'packages/relay/**' - - 'packages/cli/**' + - "nix/**" + - "flake.nix" + - "flake.lock" + - "package.json" + - "package-lock.json" + - "packages/highlight/**" + - "packages/server/**" + - "packages/relay/**" + - "packages/cli/**" pull_request: branches: [main] paths: - - 'nix/**' - - 'flake.nix' - - 'flake.lock' - - 'package.json' - - 'package-lock.json' - - 'packages/highlight/**' - - 'packages/server/**' - - 'packages/relay/**' - - 'packages/cli/**' + - "nix/**" + - "flake.nix" + - "flake.lock" + - "package.json" + - "package-lock.json" + - "packages/highlight/**" + - "packages/server/**" + - "packages/relay/**" + - "packages/cli/**" permissions: contents: read diff --git a/.github/workflows/server-ci.yml b/.github/workflows/server-ci.yml index f0291f8db..be06c3f55 100644 --- a/.github/workflows/server-ci.yml +++ b/.github/workflows/server-ci.yml @@ -4,17 +4,17 @@ on: push: branches: [main] paths: - - 'packages/server/**' - - 'package.json' - - 'package-lock.json' - - '.github/workflows/server-ci.yml' + - "packages/server/**" + - "package.json" + - "package-lock.json" + - ".github/workflows/server-ci.yml" pull_request: branches: [main] paths: - - 'packages/server/**' - - 'package.json' - - 'package-lock.json' - - '.github/workflows/server-ci.yml' + - "packages/server/**" + - "package.json" + - "package-lock.json" + - ".github/workflows/server-ci.yml" jobs: test: @@ -27,8 +27,8 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'npm' + node-version: "20" + cache: "npm" - name: Fetch origin/main (worktree tests) run: git fetch --no-tags origin main:refs/remotes/origin/main diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a84b09d..c1449085a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,15 @@ ## 0.1.62 - 2026-04-23 ### Added + - Sidebar warning when your app and daemon versions drift apart, with a shortcut to settings. ### Improved + - Workspaces appear in the sidebar immediately on startup instead of waiting for git registration. ### Fixed + - Pull request status resolves correctly for PRs opened from forks. - Installing the paseo CLI from the macOS desktop app now works in packaged builds. - Agents launched from the desktop app no longer inherit Electron-only environment variables. @@ -16,10 +19,12 @@ ## 0.1.61 - 2026-04-23 ### Added + - `additionalModels` option in provider config lets you add or relabel models without replacing the full list — entries merge with runtime-discovered models (ACP) or your static `models` list. See the [Providers docs](https://paseo.sh/docs/providers). - New [Providers docs page](https://paseo.sh/docs/providers) covering first-class providers and every custom provider config pattern in one place. ### Improved + - Pi loads your installed extensions on startup so their models show up in the model picker. - Resizing the explorer sidebar no longer rerenders the rest of the workspace. - Images in assistant messages (both file paths and inline data URLs) persist as local attachments and open in the file pane. @@ -27,6 +32,7 @@ ## 0.1.60 - 2026-04-22 ### Added + - Scripts and services per worktree — define named commands in `paseo.json`, and long-running services get supervised with their own ports and nice proxy URLs like `http://web.my-app.localhost:6767`. See the [worktrees guide](https://paseo.sh/docs/worktrees). - Launch scripts and services for a worktree directly from the workspace header. - New Setup tab in every workspace showing setup, teardown, and script progress live. @@ -41,6 +47,7 @@ - New-workspace picker ranks branches by recency with fast search. ### Improved + - Workspace and tab switching are dramatically faster on desktop and mobile — you can keep many workspaces open in parallel without lag. - Agent streams render more smoothly during heavy tool output. - App startup routes through a stable connection and lands on the right screen without flicker. @@ -55,6 +62,7 @@ - GitHub branch and PR picker loads faster — queries are deferred until the picker opens. ### Fixed + - Composer textarea shrinks back down after sending on web. - New workspace drafts clear after submit instead of sticking around. - Replacing a running agent cleans up the previous one without leaving it behind. @@ -76,18 +84,21 @@ ## 0.1.59 - 2026-04-16 ### Added + - Opus 4.7 in the Claude model picker, with a 1M-context variant. - Extra High reasoning effort for Opus 4.7, between High and Max. ## 0.1.58 - 2026-04-16 ### Added + - Markdown files render as formatted markdown in the file pane. ([#427](https://github.com/getpaseo/paseo/pull/427) by [@aaronflorey](https://github.com/aaronflorey)) - Cmd+L (Ctrl+L on Windows/Linux) focuses the agent message input. - Provider models refresh on a freshness TTL; Settings shows last-updated time and any fetch errors. ([#426](https://github.com/getpaseo/paseo/pull/426)) - `disallowedTools` option in provider config to block specific tools from an agent. ### Improved + - Windows: agents launch reliably from npm `.cmd` shims, paths with spaces, and JSON config args — fixes `spawn EINVAL` startup errors. ([#454](https://github.com/getpaseo/paseo/pull/454)) - OpenCode permission prompts include the requesting tool's context. ([#398](https://github.com/getpaseo/paseo/pull/398) by [@aaronflorey](https://github.com/aaronflorey)) - OpenCode todo and compaction events render in the timeline. ([#429](https://github.com/getpaseo/paseo/pull/429) by [@aaronflorey](https://github.com/aaronflorey)) @@ -97,6 +108,7 @@ - Native scrollbars match the active theme across all web views. ([#399](https://github.com/getpaseo/paseo/pull/399) by [@ethersh](https://github.com/ethersh)) ### Fixed + - Code file previews can be selected and copied on iOS. ([#447](https://github.com/getpaseo/paseo/pull/447) by [@muzhi1991](https://github.com/muzhi1991)) - File preview no longer shows stale content when reopening the same file. ([#411](https://github.com/getpaseo/paseo/pull/411) by [@muzhi1991](https://github.com/muzhi1991)) - File explorer reinitialises when the client reconnects after a page refresh. ([#442](https://github.com/getpaseo/paseo/pull/442) by [@1996fanrui](https://github.com/1996fanrui)) @@ -110,12 +122,14 @@ ## 0.1.56 - 2026-04-14 ### Fixed + - Projects with empty git repositories (no commits yet) no longer crash the app on startup. - A single problematic project can no longer prevent the rest of your workspaces from loading. ## 0.1.55 - 2026-04-14 ### Added + - Provider profiles — define custom providers in your Paseo config that appear alongside built-ins. Override a built-in's binary, env, or models, or create entirely new providers. See the [configuration guide](https://github.com/getpaseo/paseo/blob/main/docs/CUSTOM-PROVIDERS.md). - ACP agent support — add any ACP-compatible agent to Paseo with `extends: "acp"` in your provider config. No code changes needed. - Choose provider and model when creating scheduled agents. @@ -123,12 +137,14 @@ - Cmd+, (Ctrl+, on Windows/Linux) opens settings. ### Improved + - Git operations are dramatically faster — workspace status, PR checks, and branch data all use a shared cached snapshot service instead of shelling out to git on every request. Running 20+ workspaces simultaneously is now smooth. - Windows support — the daemon and CLI run natively on Windows with proper shell quoting, executable resolution, and path handling. - iPad and tablet layouts work correctly across all screen sizes. - IME composition (Chinese, Japanese, Korean input) no longer submits prematurely when pressing Enter. ### Fixed + - Creating a worktree no longer briefly flashes it as a standalone project before placing it under the correct repository. - Worktree creation spinner stays visible throughout the process instead of disappearing on mouse-out. - Workspace navigation updates correctly when switching between workspaces in the same project. @@ -138,13 +154,16 @@ ## 0.1.54 - 2026-04-12 ### Added + - Inline image previews in agent messages — screenshots and images generated by agents render directly in the conversation instead of showing as raw markdown links. ### Improved + - Paseo tools are no longer injected into agents by default — opt in from Settings when you need agent-to-agent orchestration. - Agent provider and mode are now resolved server-side, so CLI commands like `paseo run` use consistent defaults without client-side lookups. ### Fixed + - Shift+Enter now correctly inserts a newline in agent terminal input instead of submitting. - Windows: MCP configuration is no longer mangled when spawning Claude agents. - Branch ahead/behind count no longer errors for branches with no remote tracking branch. @@ -152,6 +171,7 @@ ## 0.1.53 - 2026-04-12 ### Added + - Agents get Paseo tools automatically — every new agent gets access to terminals, schedules, worktrees, and other agents through MCP. Toggle it off in Settings under "Inject Paseo tools". - Git pull — pull remote changes directly from the workspace header. Promoted to the primary action when your branch is behind origin. - Child agent notifications — parent agents are automatically notified when a child agent finishes, errors, or needs permission approval. @@ -160,6 +180,7 @@ - Keyboard shortcut to cycle themes. ### Improved + - Unavailable git actions now explain why in a toast instead of being silently greyed out. - Streaming markdown on mobile renders significantly faster. - Sidebar, branch switcher, and agent panel no longer re-render unnecessarily — noticeable on large workspaces. @@ -167,6 +188,7 @@ - Relay and pairing URLs are stripped from daemon logs. ### Fixed + - Closed agent tabs no longer reappear after reconnecting. - Desktop notification badge counts match across all workspaces. - Host switcher status syncs correctly when switching between hosts. @@ -174,11 +196,13 @@ ## 0.1.52 - 2026-04-10 ### Added + - Theme selector — choose from six themes including Midnight, Claude, and Ghostty dark variants. - Branch switching — switch git branches directly from the workspace header, with automatic stash and restore for uncommitted changes. - Auto-download updates — desktop updates download silently in the background so they're ready to install when you are. ### Fixed + - Layout now responds correctly when resizing the window or rotating a tablet — previously the app could get stuck in mobile layout on a large screen. - Terminal no longer causes massive memory spikes from snapshot thrashing during heavy output. - Typing in the terminal works reliably — special keys, Ctrl combos, and paste are handled natively by the terminal emulator. @@ -191,11 +215,13 @@ ## 0.1.51 - 2026-04-09 ### Added + - Image attachments for OpenCode — attach screenshots and images to OpenCode agent prompts. - WebStorm — added to the "Open in editor" list alongside Cursor, VS Code, and Zed. - Send behavior setting — choose whether pressing Enter while an agent is running interrupts immediately or queues your message. ### Fixed + - Model selector no longer crashes on iPad. - Pairing now uses the correct hostname, fixing connection failures on some network setups. - OpenCode agents show the correct terminal state and refresh models reliably. @@ -207,6 +233,7 @@ ## 0.1.50 - 2026-04-07 ### Added + - Context window meter — see how much of the context window your agent has used, with color thresholds at 70% and 90%. Works with Claude Code, Codex, and OpenCode. - Open in editor — jump from any workspace straight into Cursor, VS Code, Zed, or your file manager. Paseo remembers your choice. - Side-by-side diffs — toggle between unified and split-column diff views, with a whitespace visibility option. @@ -215,6 +242,7 @@ - Background git fetch — ahead/behind counts in the Changes pane stay up to date automatically. ### Improved + - Workspaces load instantly on connect instead of waiting for a full sync. - File explorer and diff pane remember which folders are expanded when you switch tabs. - Closing a workspace tab is now instant. @@ -222,6 +250,7 @@ - Reload agent moved away from the close button to prevent accidental taps. ### Fixed + - Voice mode no longer drifts into false speech detection during long sessions. - Garbled overlapping text on plan cards. - Changes pane could show stale diffs when working with git worktrees. @@ -234,6 +263,7 @@ ## 0.1.49 - 2026-04-07 ### Fixed + - Models and providers now load reliably on first connect instead of requiring a manual refresh. - Model picker only shows models from the agent's own provider, not every provider on the server. - Model lists stay consistent regardless of which screen you open first. @@ -241,17 +271,20 @@ ## 0.1.48 - 2026-04-05 ### Added + - Provider diagnostics — tap a provider in Settings to see binary path, version, model count, and status at a glance. Helps troubleshoot why an agent type isn't available. - Provider snapshot system — daemon now pushes real-time provider availability and model lists to the app, replacing the old poll-based approach. Models and modes update live as providers come online or go offline. - Codex question handling — Codex agents can now ask the user questions mid-session (e.g. "which file?") and receive answers inline, matching the Claude Code question flow. - Reload tab action — right-click a workspace tab to reload its agent list without restarting the app. ### Improved + - Model selector redesigned — grouped by provider with status badges, search, and better touch targets on mobile. - Enter key now submits question card answers and confirms dictation, matching the expected keyboard flow. - Removed noisy agent lifecycle toasts that fired on every state change. ### Fixed + - Desktop app now resolves the user's full login shell environment at startup, fixing tools like `codex`, `node`, `bun`, and `direnv` not being found when Paseo is launched from Finder or Dock. Terminals spawned by Paseo now inherit the same PATH and environment variables as a normal terminal session. Approach adapted from VS Code's battle-tested shell environment resolution. - Input field on running agent screens now correctly receives keyboard focus. - Mobile model selector alignment and sizing. @@ -259,6 +292,7 @@ ## 0.1.47 - 2026-04-05 ### Fixed + - Voice TTS in Electron — sherpa now requests copied buffers and the voice MCP bridge sets `ELECTRON_RUN_AS_NODE`, preventing "external buffers not allowed" crashes. - QR pairing in desktop — CLI JSON output parsing now tolerates Node deprecation warnings in stdout. - STT segment race condition — segment ID and audio buffer are snapshotted before the async transcription call, so rapid commits no longer interleave. @@ -267,6 +301,7 @@ ## 0.1.46 - 2026-04-04 ### Fixed + - Voice activation in packaged builds — Silero VAD model is now copied out of the Electron asar archive so native code can read it. - App version sent in probe client hello so the daemon's version gate no longer hides Pi/Copilot from reconnected sessions. - `worktreeRoot` schema made backward-compatible for old clients and daemons that don't send the field. @@ -275,6 +310,7 @@ ## 0.1.45 - 2026-04-04 ### Added + - Pi (pi.dev) agent provider — connect Pi as a new ACP-based agent type with thinking levels and tool call support. - Copilot agent provider re-enabled after ACP compatibility fixes. - `paseo .` and `paseo ` open the desktop app with the given project, similar to `code .`. @@ -287,11 +323,13 @@ - Setup hint and paseo.sh link on the mobile welcome screen so new App Store users know what to do next. ### Improved + - Desktop startup is faster — existing daemon connections are raced against bootstrap so the app is usable sooner. - Settings sections reordered for better grouping (integrations and daemon together). - Sidebar projects and workspaces now persist across sessions, with a context menu to remove projects. ### Fixed + - Sidebar crash when switching iOS theme (Unistyles/Reanimated interaction). - Silero VAD crash caused by external buffer mode in CircularBuffer. - Bulk close now correctly archives stored agents instead of leaving orphans. @@ -308,6 +346,7 @@ ## 0.1.44 - 2026-04-03 ### Fixed + - Desktop app now stops the daemon cleanly before auto-update restarts. - Disabled claude-acp and copilot providers from the agent registry. - Keyboard focus scope resolution now checks multiple candidates for broader compatibility. @@ -317,16 +356,19 @@ ## 0.1.43 - 2026-04-02 ### Added + - Copilot agent support via ACP base provider — connect GitHub Copilot as a new agent type. - Searchable model favorites — quickly find and pin preferred models. - Slash command support for OpenCode agents. ### Improved + - Refined model selector UX with better mobile sheet behavior. - Workspace status now uses amber alert styling for "needs input" state. - Themed scrollbar on message input for consistent styling. ### Fixed + - Ctrl+C/V copy and paste now works correctly in the terminal on Windows and Linux. - Shell arguments with spaces are now properly quoted on Windows. - Claude models with 1M context support are now correctly reported. @@ -334,11 +376,13 @@ ## 0.1.42 - 2026-04-01 ### Fixed + - Fixed Claude Code failing to launch on Windows when installed to a path with spaces (e.g. `C:\Program Files\...`). ## 0.1.41 - 2026-04-01 ### Fixed + - Fixed agent spawning on Windows — all providers (Claude, Codex, OpenCode) now use shell mode so npm shims and `.cmd` wrappers resolve correctly. - Fixed terminal creation on Windows defaulting to a Unix shell instead of `cmd.exe`. - Fixed path handling across the app to support Windows drive-letter paths (`C:\...`) and UNC paths (`\\...`). @@ -350,18 +394,22 @@ - Removed the 40-item cap on activity timeline output so long agent sessions display their full history. ### Improved + - Improved light mode theming with dedicated workspace background, scrollbar handle colors, and lighter shadows. - Window controls overlay on Windows/Linux reduced from 48px to 29px height for a more compact titlebar. ## 0.1.40 - 2026-04-01 ### Added + - Workspace tabs can now be closed in batches. ### Improved + - Provider model lists are now cached per server and provider, reducing redundant model lookups in the UI. ### Fixed + - OpenCode reasoning content no longer appears duplicated as assistant text. - Daemon no longer crashes when a Codex binary is missing or fails to spawn. - Archive tab now correctly reconciles agent visibility after archiving. @@ -372,61 +420,73 @@ ## 0.1.39 - 2026-03-30 ### Added + - **Terminal management from the CLI** — new `paseo terminal` command group lets you list, create, and interact with workspace terminals without leaving your terminal. - **Material file icons in the explorer** — the file explorer tree now shows language-specific icons (TypeScript, JSON, Markdown, etc.) so you can spot files at a glance. ### Fixed + - Fixed iOS sidebar scroll flicker caused by redundant overflow clipping. - Centralized window controls padding into a shared hook, eliminating layout inconsistencies across platforms. ## 0.1.38 - 2026-03-30 ### Fixed + - Fixed daemon startup race where the app could time out connecting on first launch because the PID file advertised a listen address before the server was ready. - Fixed daemon log rotation losing startup traces — trace-level WebSocket logs no longer include full message payloads. ## 0.1.37 - 2026-03-29 ### Added + - Custom window controls on Windows and Linux — the native titlebar is replaced with overlay controls that match the app's design. - Desktop file logging with electron-log for easier debugging of daemon and app issues. ### Fixed + - Fixed broken PATH propagation and Claude binary resolution on Windows. - Dictation errors now show a visible toast instead of failing silently. ## 0.1.36 - 2026-03-27 ### Fixed + - Fixed Windows drive-letter path handling across the codebase. - Fixed stale Nix hash with automatic lockfile-change detection. ### Added + - Added metrics collection and terminal performance tests. ## 0.1.35 - 2026-03-26 ### Improved + - Faster app startup by redirecting to the welcome screen immediately and showing host connection status inline. - Codex file deletions now display correctly as removed lines in diffs. - OpenCode questions are now surfaced in the permission UI. ### Fixed + - Fixed queued prompt dispatch after idle transition. - Replaced bash-only `mapfile` with a portable `while-read` loop in the chat script. ### Added + - Added support for Nix and NixOS installation. ## 0.1.34 - 2026-03-25 ### Added + - Added `paseo archive` as a top-level alias for `paseo agent archive`. - Added the `PASEO_AGENT_ID` environment variable for Claude and Codex agents. - Added a redesigned command autocomplete with a detail card and dropdown styling. - Linked Android download surfaces to the Google Play Store. ### Improved + - Autonomous turns now complete gracefully on interrupt instead of being canceled. - Thinking/model selection now always resolves to a real option instead of showing a generic Default choice. - Restored per-provider form preferences and removed the Auto model fallback. @@ -435,6 +495,7 @@ - Improved chat transcript readability. ### Fixed + - Fixed `paseo send --no-wait` not taking effect. - Fixed stale abort results contaminating replacement turns after an interrupt. - Fixed Claude interrupt handling and autonomous wake reliability. @@ -451,6 +512,7 @@ ## 0.1.33 - 2026-03-23 ### Fixed + - Fixed the desktop app failing to reopen after closing on macOS — the daemon and agent processes were registering with Launch Services as instances of the main app, blocking subsequent launches. - Fixed dictation not working in the packaged desktop app — the microphone entitlement was missing from the hardened runtime configuration. - Fixed leaked Claude Code child processes when agents were closed — the SDK query stream was not being properly shut down. @@ -459,6 +521,7 @@ ## 0.1.32 - 2026-03-23 ### Added + - Fully rebindable keyboard shortcuts with chord support — all shortcuts are now declarative with proper Cmd (Mac) vs Ctrl (Windows/Linux) separation. - Migrated the desktop app from Tauri to Electron, with macOS notarization, code signing, and Linux Wayland support. - Added line numbers and word-wrap toggle to file previews. @@ -468,6 +531,7 @@ - Added status bar tooltips for project and agent status. ### Improved + - Redesigned the mobile tab switcher as a compact header row with quick access to new agents and terminals. - Streamlined workspace creation — worktrees are now created inline with a single action instead of a multi-step flow. - Agent history now streams from disk on reconnect, so you see past messages immediately instead of a blank screen. @@ -482,6 +546,7 @@ - Better error messages from the daemon — RPC errors now include the actual underlying details. ### Fixed + - Fixed user messages appearing as assistant output in the timeline when messages contained structured content blocks. - Fixed archived workspace routing so navigating to an archived session no longer breaks the app. - Fixed Linux AppImage failing to launch on Wayland-only desktops. @@ -490,16 +555,19 @@ ## 0.1.30 - 2026-03-19 ### Added + - Added terminal tabs, split pane controls, and drop previews for workspace layouts. - Added a combined model selector and agent mode visuals across key UI surfaces. - Added Open Graph metadata improvements for richer website sharing previews. ### Improved + - Improved workspace navigation with better active-workspace tracking and keyboard-driven pane interactions. - Improved terminal scrollbar behavior, pane focus handling, and status bar/message input spacing. - Improved project picker path display and general workspace UI polish. ### Fixed + - Fixed agent startup reliability by tightening PATH resolution and surfacing missing provider binaries in status. - Fixed workspace route syncing, drag hit areas, and git diff panel header styling regressions. - Fixed website mobile horizontal scrolling and ensured the workspace audio module builds during EAS installs. @@ -507,30 +575,36 @@ ## 0.1.28 - 2026-03-15 ### Added + - Added OpenCode build and plan modes. - Added website landing pages for Claude Code, Codex, and OpenCode. ### Improved + - Improved the git action menu for more reliable repository actions. - Improved the mobile settings screen, workspace header actions, and welcome screen presentation. - Updated the website hero copy and added a sponsor callout section. ### Fixed + - Fixed assistant file links so they open the correct workspace files from chat. ## 0.1.27 - 2026-03-13 ### Added + - Added voice runtime with new audio engine architecture for voice interactions. - Added Grep tool support in Claude tool-call mapping. - Added ability to open workspace files directly from agent chat messages. - Added desktop notifications via a custom native bridge. ### Improved + - Improved image picker, markdown rendering, and UI interactions. - Improved shell environment detection using shell-env. ### Fixed + - Fixed platform-specific markdown link rendering. - Fixed Linux AppImage CLI resource paths. - Fixed Codex replacement stream being killed by stale turn notifications. @@ -538,15 +612,18 @@ ## 0.1.26 - 2026-03-12 ### Added + - Added single-instance desktop behavior, Android APK download access, and refreshed splash screen styling. - Added bundled Codex and OpenCode binaries in the server so setup no longer depends on global installs. - Added Windows support with improved cross-platform shell execution. ### Improved + - Improved desktop runtime behavior on Windows by suppressing console windows and defaulting app data to `~/.paseo`. - Added a Discord link to the website navigation. ### Fixed + - Fixed desktop Claude agent startup from the managed runtime and rotated logs correctly on restart. - Fixed the home route to hide browser chrome when appropriate. - Fixed Expo Metro compatibility by updating the `exclusionList` import. @@ -556,61 +633,79 @@ ## 0.1.25 - 2026-03-11 ### Fixed + - Fixed desktop app failing to start the built-in daemon on fresh macOS installs. The DMG was not notarized and code-signing stripped entitlements from the bundled Node runtime, causing Gatekeeper to block execution. - Fixed Linux AppImage build by restoring the AppImage bundle format and stripping CUDA dependencies from onnxruntime. ## 0.1.24 - 2026-03-10 ### Improved + - Improved command center keyboard navigation and new tab shortcut. - Simplified desktop release pipeline for faster and more reliable builds. ## 0.1.21 - 2026-03-10 + ### Improved + - Improved desktop release reliability by fixing the Windows managed-runtime build path during GitHub Actions releases. ### Fixed + - Fixed a desktop release CI failure caused by a Unix-only server build script on Windows runners. - Fixed server CI to build the relay dependency before running tests, restoring relay E2EE test coverage on clean runners. - Fixed a Claude redesign test that depended on the local Claude CLI being installed. ## 0.1.20 - 2026-03-10 + ### Added + - Added workspace sidebar git actions with quick diff stats and archive controls. - Added refreshed website downloads and homepage presentation for desktop installs. ### Improved + - Desktop release packaging now rebuilds and validates the bundled managed runtime during CI, improving installer reliability for macOS users. - Improved desktop and web stream rendering, settings polish, and React 19.1.4 compatibility. ### Fixed + - Fixed Claude interrupt/restart regressions and strengthened managed-daemon smoke coverage for desktop releases. ## 0.1.19 - 2026-03-09 + ### Added + - Added a draft GitHub release flow so maintainers can upload and review desktop and Android release assets before publishing the final release. ## 0.1.18 - 2026-03-06 + ### Added + - Added a desktop `Mod+W` shortcut to close the current tab. ### Improved + - New and newly selected terminals now take focus automatically so you can type immediately. - Kept newly created workspaces and projects in a more stable order in the sidebar. - Improved project naming for GitHub remotes and expanded project icon discovery to Phoenix `priv/static` assets. - Updated the website desktop download link to use the universal macOS DMG. ### Fixed + - Restored automatic agent metadata generation for Claude runs. ## 0.1.17 - 2026-03-06 + ### Added + - New workspace-first navigation model with workspace tabs, file tabs, and sortable tab groups. - Keyboard shortcuts for workspace and tab navigation, with shortcut badges in the sidebar. - Workspace-level archive actions with improved worktree archiving flow and context menu support. - In-chat task notifications rendered as synthetic tool-call events for clearer status tracking. ### Improved + - Desktop builds now ship as a universal macOS binary (Apple Silicon + Intel). - More reliable workspace routing and tab identity handling across refreshes and deep links. - Better sidebar drag-and-drop behavior with explicit drag handles and nested list interactions. @@ -618,6 +713,7 @@ - Stronger provider error surfacing and updated Claude model/runtime handling. ### Fixed + - Fixed orphan workspace runs caused by non-canonical tab routes. - Fixed mobile terminal tab remount/routing restore issues. - Fixed agent metadata title/branch update reliability. @@ -625,7 +721,9 @@ - Fixed reversed edge-wheel scroll behavior in chat/tool stream views. ## 0.1.16 - 2026-02-22 + ### Added + - Update the Paseo desktop app and local daemon directly from Settings. - Microphone and notification permission controls in Settings. - Thinking/reasoning mode — agents can use extended thinking when the provider supports it. @@ -633,6 +731,7 @@ - `paseo wait` now shows a snapshot of recent agent activity while you wait. ### Improved + - Smoother streaming with less UI flicker and scroll jumping during long agent runs. - Faster agent sidebar list rendering. - Archiving an agent now stops it first instead of archiving a half-running session. @@ -640,16 +739,20 @@ - More reliable relay connections. ### Fixed + - Fixed Claude background tasks desyncing the chat. - Fixed duplicate user messages appearing in the timeline. - Fixed a startup crash caused by an OpenCode SDK update. - Fixed spurious "needs attention" notifications from background agent activity. ## 0.1.15 - 2026-02-19 + ### Added + - Added a public changelog page on the website so users can browse release notes. ### Improved + - Redesigned the website get-started experience into a clearer two-step flow. - Simplified website GitHub navigation and changelog headings. - Improved app draft/new-agent UX with clearer working directory placeholder and empty-state messaging. @@ -657,11 +760,14 @@ - Hid empty filter groups in the left sidebar. ### Fixed + - Fixed archived-agent navigation by redirecting archived agent routes to draft. - Fixed duplicate `/rewind` user-message behavior. ## 0.1.14 - 2026-02-19 + ### Added + - Added Claude `/rewind` command support. - Added slash command access in the draft agent composer. - Added `@` workspace file autocomplete in chat prompts. @@ -670,6 +776,7 @@ - Added shared desktop/web overlay scroll handles, including file preview panes. ### Improved + - Improved worktree flow after shipping, including better merged PR detection. - Improved draft workflow by enabling the explorer sidebar immediately after CWD selection. - Improved new worktree-agent defaults by prefilling CWD to the main repository. @@ -679,6 +786,7 @@ - Improved scrollbar visibility, drag interactions, tracking, and animation timing on web/desktop. ### Fixed + - Fixed worktree archive/setup lifecycle issues, including terminal cleanup and archive timing. - Fixed worktree path collisions by hashing CWD for collision-safe worktree roots. - Fixed terminal sizing when switching back to an agent session. @@ -692,25 +800,32 @@ - Fixed daemon version badge visibility in settings when daemon version data is unavailable. ## 0.1.9 - 2026-02-17 + ### Improved + - Unified structured-output generation through a single shared schema-validation and retry pipeline. - Reused provider availability checks for structured generation fallback selection. - Added structured generation waterfall ordering for internal metadata and git text generation: Claude Haiku, then Codex, then OpenCode. ### Fixed + - Fixed CLI `run --output-schema` to use the shared structured-output path instead of ad-hoc JSON parsing. - Fixed `run --output-schema` failures where providers returned empty `lastMessage` by recovering from timeline assistant output. - Fixed internal commit message, pull request text, and agent metadata generation to follow one consistent structured pipeline. ## 0.1.8 - 2026-02-17 + ### Added + - Added a cross-platform confirm dialog flow for daemon restarts. ### Improved + - Simplified local speech bootstrap and daemon startup locking behavior. - Updated website hero copy to emphasize local execution. ### Fixed + - Fixed stuck "send while running" recovery across app and server session handling. - Fixed Claude session identity preservation when reloading existing agents. - Fixed combobox option behavior and related interactions. @@ -718,73 +833,93 @@ - Fixed web tool-detail wheel event routing at scroll edges. ## 0.1.7 - 2026-02-16 + ### Added + - Improved agent workspace flows with better directory suggestions. - Added iOS TestFlight and Android app access request forms on the website. ### Improved + - Unified daemon startup behavior between dev and CLI paths for more predictable local runs. - Improved website app download and update guidance. ### Fixed + - Prevented an initial desktop combobox `0,0` position flash. - Fixed CLI version output issues. - Hardened server runtime loading for local speech dependencies. ## 0.1.6 - 2026-02-16 + ### Notes + - No major visible product changes in this patch release. ## 0.1.5 - 2026-02-16 + ### Added + - Added terminal reattach support and better worktree terminal handling. - Added global keyboard shortcut help in the app. - Added sidebar host filtering and improved agent workflow controls. ### Improved + - Improved worktree setup visibility by streaming setup progress. - Improved terminal streaming reliability and lifecycle handling. - Preserved explorer tab state so context survives navigation better. ## 0.1.4 - 2026-02-14 + ### Added + - Added voice capability status reporting in the client. - Added background local speech model downloads with runtime gating. - Added adaptive dictation finish timing based on server-provided budgets. - Added relay reconnect behavior with grace periods and branch suggestions. ### Improved + - Improved connection selection and agent hydration reliability. - Improved timeline loading with cursor-based fetch behavior. - Improved first-run experience by bootstrapping a default localhost connection. - Improved inline code rendering by auto-linkifying URLs. ### Fixed + - Fixed Linux checkout diff watch behavior to avoid recursive watches. - Fixed stale relay client timer behavior. - Fixed unnecessary git diff header auto-scroll on collapse. ## 0.1.3 - 2026-02-12 + ### Added + - Added CLI onboarding command. - Added CLI `--output-schema` support for structured agent output. - Added CLI agent metadata update support for names and labels. - Added provider availability detection with normalization of legacy default model IDs. ### Improved + - Improved file explorer refresh feedback and unresolved checkout fallback handling. - Added better voice interrupt handling with a speech-start grace period. - Improved CLI defaults to list all non-archived agents by default. - Improved website UX with clearer install CTA and privacy policy access. ### Fixed + - Fixed dev runner entry issues and sherpa TTS initialization behavior. ## 0.1.2 - 2026-02-11 + ### Notes + - No major visible product changes in this patch release. ## 0.1.1 - 2026-02-11 ### Added + - Initial `0.1.x` release line. diff --git a/CLAUDE.md b/CLAUDE.md index 9d897d32f..6cd9ac117 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,17 +17,17 @@ This is an npm workspace monorepo: ## Documentation -| Doc | What's in it | -|---|---| -| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | System design, package layering, WebSocket protocol, agent lifecycle, data flow | -| [docs/CODING_STANDARDS.md](docs/CODING_STANDARDS.md) | Type hygiene, error handling, state design, React patterns, file organization | -| [docs/TESTING.md](docs/TESTING.md) | TDD workflow, determinism, real dependencies over mocks, test organization | -| [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) | Dev server, build sync gotchas, CLI reference, agent state, Playwright MCP | -| [docs/RELEASE.md](docs/RELEASE.md) | Release playbook, draft releases, completion checklist | +| Doc | What's in it | +| ---------------------------------------------------- | --------------------------------------------------------------------------------- | +| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | System design, package layering, WebSocket protocol, agent lifecycle, data flow | +| [docs/CODING_STANDARDS.md](docs/CODING_STANDARDS.md) | Type hygiene, error handling, state design, React patterns, file organization | +| [docs/TESTING.md](docs/TESTING.md) | TDD workflow, determinism, real dependencies over mocks, test organization | +| [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) | Dev server, build sync gotchas, CLI reference, agent state, Playwright MCP | +| [docs/RELEASE.md](docs/RELEASE.md) | Release playbook, draft releases, completion checklist | | [docs/CUSTOM-PROVIDERS.md](docs/CUSTOM-PROVIDERS.md) | Custom provider config: Z.AI, Alibaba/Qwen, ACP agents, profiles, custom binaries | -| [docs/ANDROID.md](docs/ANDROID.md) | App variants, local/cloud builds, EAS workflows | -| [docs/DESIGN.md](docs/DESIGN.md) | How to design features before implementation | -| [SECURITY.md](SECURITY.md) | Relay threat model, E2E encryption, DNS rebinding, agent auth | +| [docs/ANDROID.md](docs/ANDROID.md) | App variants, local/cloud builds, EAS workflows | +| [docs/DESIGN.md](docs/DESIGN.md) | How to design features before implementation | +| [SECURITY.md](SECURITY.md) | Relay threat model, E2E encryption, DNS rebinding, agent auth | ## Quick start @@ -68,23 +68,23 @@ The app runs on iOS, Android, web (browser), and web (Electron desktop). Code is ### The four gates -| Gate | Type | When to use | -|---|---|---| -| `isWeb` | constant | DOM APIs — `document`, `window`, `
`, `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`. | +| Gate | Type | When to use | +| -------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------- | +| `isWeb` | constant | DOM APIs — `document`, `window`, `
`, `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`, `
`, `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) | +| I need to... | Use | +| -------------------------------------------------------------- | ------------------------------------------------------------------------- | +| Access DOM (`document`, `window`, `
`, `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 @@ -104,5 +104,4 @@ The app runs on iOS, Android, web (browser), and web (Electron desktop). Code is ## Debugging - Find the complete daemon logs and traces in the $PASEO_HOME/daemon.log diff --git a/README.md b/README.md index 4daa16f18..b3491f40a 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ paseo This shows a QR code in the terminal. Connect from any client. This path is useful for servers and remote machines. For full setup and configuration, see: + - [Docs](https://paseo.sh/docs) - [Configuration reference](https://paseo.sh/docs/configuration) @@ -116,6 +117,7 @@ Then use them in any agent conversation: ## Development Quick monorepo package map: + - `packages/server`: Paseo daemon (agent process orchestration, WebSocket API, MCP server) - `packages/app`: Expo client (iOS, Android, web) - `packages/cli`: `paseo` CLI for daemon and agent workflows diff --git a/docs/AD-HOC-DAEMON-TESTING.md b/docs/AD-HOC-DAEMON-TESTING.md index 6918bb727..758ee3f6d 100644 --- a/docs/AD-HOC-DAEMON-TESTING.md +++ b/docs/AD-HOC-DAEMON-TESTING.md @@ -58,6 +58,7 @@ await rm(staticDir, { recursive: true, force: true }); ``` Run with: + ```bash npx tsx packages/server/src/server/your-script.ts ``` @@ -107,6 +108,7 @@ const updated = await client.waitForAgentUpsert(agent.id, (s) => s.status === "i The daemon hides non-legacy providers (anything other than claude, codex, opencode) from clients that don't send an `appVersion >= 0.1.45`. The `DaemonClient` sends no version by default, so custom providers like ACP-based ones will be invisible in snapshot responses. Always pass `appVersion`: + ```typescript const client = new DaemonClient({ url: `ws://127.0.0.1:${port}/ws`, diff --git a/docs/ANDROID.md b/docs/ANDROID.md index 2c432b418..bd2f12ef5 100644 --- a/docs/ANDROID.md +++ b/docs/ANDROID.md @@ -4,9 +4,9 @@ Controlled by `APP_VARIANT` in `packages/app/app.config.js` (vanilla Expo, no custom Gradle plugin): -| Variant | App name | Package ID | -|---|---|---| -| `production` | Paseo | `sh.paseo` | +| Variant | App name | Package ID | +| ------------- | ----------- | ---------------- | +| `production` | Paseo | `sh.paseo` | | `development` | Paseo Debug | `sh.paseo.debug` | EAS profiles: `development`, `production`, and `production-apk` in `packages/app/eas.json`. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index fe783703e..91663a96d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -53,17 +53,17 @@ The heart of Paseo. A Node.js process that: **Key modules:** -| Module | Responsibility | -|---|---| -| `bootstrap.ts` | Daemon initialization: HTTP server, WS server, agent manager, storage, relay | -| `websocket-server.ts` | WebSocket connection management, hello/welcome handshake, binary multiplexing | -| `session.ts` | Per-client session state, timeline subscriptions, terminal operations | -| `agent/agent-manager.ts` | Agent lifecycle state machine, timeline tracking, subscriber management | -| `agent/agent-storage.ts` | File-backed JSON persistence at `$PASEO_HOME/agents/` | -| `agent/mcp-server.ts` | MCP server for sub-agent creation, permissions, timeouts | -| `providers/` | Provider adapters: Claude (Agent SDK), Codex (AppServer), OpenCode | -| `relay-transport.ts` | Outbound relay connection with E2E encryption | -| `client/daemon-client.ts` | Client library for connecting to the daemon (used by CLI and app) | +| Module | Responsibility | +| ------------------------- | ----------------------------------------------------------------------------- | +| `bootstrap.ts` | Daemon initialization: HTTP server, WS server, agent manager, storage, relay | +| `websocket-server.ts` | WebSocket connection management, hello/welcome handshake, binary multiplexing | +| `session.ts` | Per-client session state, timeline subscriptions, terminal operations | +| `agent/agent-manager.ts` | Agent lifecycle state machine, timeline tracking, subscriber management | +| `agent/agent-storage.ts` | File-backed JSON persistence at `$PASEO_HOME/agents/` | +| `agent/mcp-server.ts` | MCP server for sub-agent creation, permissions, timeouts | +| `providers/` | Provider adapters: Claude (Agent SDK), Codex (AppServer), OpenCode | +| `relay-transport.ts` | Outbound relay connection with E2E encryption | +| `client/daemon-client.ts` | Client library for connecting to the daemon (used by CLI and app) | ### `packages/app` — Mobile + web client (Expo) @@ -132,6 +132,7 @@ Server → Client: WSWelcomeMessage { clientId, daemonVersion, sessionId, capab **Binary multiplexing:** Terminal I/O and agent streaming share the same connection via `BinaryMuxFrame`: + - Channel 0: control messages - Channel 1: terminal data - 1-byte channel ID + 1-byte flags + variable payload @@ -153,13 +154,14 @@ initializing → idle → running → idle (or error → closed) Each provider implements a common `AgentClient` interface: -| Provider | Wraps | Session format | -|---|---|---| -| Claude | Anthropic Agent SDK | `~/.claude/projects/{cwd}/{session-id}.jsonl` | -| Codex | CodexAppServer | `~/.codex/sessions/{date}/rollout-{ts}-{id}.jsonl` | -| OpenCode | OpenCode CLI | Provider-managed | +| Provider | Wraps | Session format | +| -------- | ------------------- | -------------------------------------------------- | +| Claude | Anthropic Agent SDK | `~/.claude/projects/{cwd}/{session-id}.jsonl` | +| Codex | CodexAppServer | `~/.codex/sessions/{date}/rollout-{ts}-{id}.jsonl` | +| OpenCode | OpenCode CLI | Provider-managed | All providers: + - Handle their own authentication (Paseo does not manage API keys) - Support session resume via persistence handles - Map tool calls to a normalized `ToolCallDetail` type diff --git a/docs/CODING_STANDARDS.md b/docs/CODING_STANDARDS.md index ba06083e6..cda4af781 100644 --- a/docs/CODING_STANDARDS.md +++ b/docs/CODING_STANDARDS.md @@ -40,7 +40,10 @@ No complex inline types in public function signatures. function enqueueJob(input: { userId: string; priority: "low" | "normal" | "high" }) {} // Good -interface EnqueueJobInput { userId: string; priority: "low" | "normal" | "high" } +interface EnqueueJobInput { + userId: string; + priority: "low" | "normal" | "high"; +} function enqueueJob(input: EnqueueJobInput) {} ``` @@ -53,7 +56,11 @@ If a function needs more than one argument, use a single object parameter. function createToolCall(provider: string, toolName: string, payload: unknown) {} // Good: object param -interface CreateToolCallInput { provider: string; toolName: string; payload: unknown } +interface CreateToolCallInput { + provider: string; + toolName: string; + payload: unknown; +} function createToolCall(input: CreateToolCallInput) {} ``` @@ -78,7 +85,11 @@ Use discriminated unions instead of bags of booleans and optionals. ```typescript // Bad -interface FetchState { isLoading: boolean; error?: Error; data?: Data } +interface FetchState { + isLoading: boolean; + error?: Error; + data?: Data; +} // Good type FetchState = @@ -136,7 +147,10 @@ Avoid packing branching, lookup, and transformation into single dense expression // Bad: nested ternaries + inline lookups const billing = shouldUseLegacy(account) ? getLegacy(account) - : buildBilling(account, rates.find((r) => r.region === account.region)); + : buildBilling( + account, + rates.find((r) => r.region === account.region), + ); // Good: named steps, then assemble const rate = rates.find((r) => r.region === account.region); diff --git a/docs/CUSTOM-PROVIDERS.md b/docs/CUSTOM-PROVIDERS.md index 968914f0c..dc24bba61 100644 --- a/docs/CUSTOM-PROVIDERS.md +++ b/docs/CUSTOM-PROVIDERS.md @@ -55,6 +55,7 @@ Use `extends` to create a new provider entry that inherits from a built-in provi ``` Required fields for custom providers: + - `extends` — which built-in provider to inherit from (or `"acp"`) - `label` — display name in the UI @@ -95,12 +96,12 @@ Required fields for custom providers: ### Available models -| Model | Tier | -|---|---| -| `glm-5.1` | Advanced (flagship) | -| `glm-5-turbo` | Advanced | -| `glm-4.7` | Standard | -| `glm-4.5-air` | Lightweight | +| Model | Tier | +| ------------- | ------------------- | +| `glm-5.1` | Advanced (flagship) | +| `glm-5-turbo` | Advanced | +| `glm-4.7` | Standard | +| `glm-4.5-air` | Lightweight | ### Notes @@ -148,10 +149,10 @@ Required fields for custom providers: ### API endpoints -| Mode | Base URL | -|---|---| -| Coding plan (subscription) | `https://coding-intl.dashscope.aliyuncs.com/apps/anthropic` | -| Pay-as-you-go (no subscription) | `https://dashscope-intl.aliyuncs.com/apps/anthropic` | +| Mode | Base URL | +| ------------------------------- | ----------------------------------------------------------- | +| Coding plan (subscription) | `https://coding-intl.dashscope.aliyuncs.com/apps/anthropic` | +| Pay-as-you-go (no subscription) | `https://dashscope-intl.aliyuncs.com/apps/anthropic` | For pay-as-you-go, use `ANTHROPIC_API_KEY` with a standard Model Studio key (`sk-xxxxx`) instead of `ANTHROPIC_AUTH_TOKEN`. @@ -159,13 +160,13 @@ For pay-as-you-go, use `ANTHROPIC_API_KEY` with a standard Model Studio key (`sk **Recommended for coding plan:** -| Model | Notes | -|---|---| -| `qwen3.5-plus` | Vision capable, recommended | -| `qwen3-coder-next` | Optimized for coding | -| `kimi-k2.5` | Vision capable | -| `glm-5` | Zhipu GLM | -| `MiniMax-M2.5` | MiniMax | +| Model | Notes | +| ------------------ | --------------------------- | +| `qwen3.5-plus` | Vision capable, recommended | +| `qwen3-coder-next` | Optimized for coding | +| `kimi-k2.5` | Vision capable | +| `glm-5` | Zhipu GLM | +| `MiniMax-M2.5` | MiniMax | **Additional models (pay-as-you-go):** `qwen3-max`, `qwen3.5-flash`, `qwen3-coder-plus`, `qwen3-coder-flash`, `qwen3-vl-plus`, `qwen3-vl-flash` @@ -221,16 +222,12 @@ You can also combine profiles with model overrides to pin specific models per pr "claude-fast": { "extends": "claude", "label": "Claude (Fast)", - "models": [ - { "id": "claude-sonnet-4-6", "label": "Sonnet 4.6", "isDefault": true } - ] + "models": [{ "id": "claude-sonnet-4-6", "label": "Sonnet 4.6", "isDefault": true }] }, "claude-smart": { "extends": "claude", "label": "Claude (Smart)", - "models": [ - { "id": "claude-opus-4-6", "label": "Opus 4.6", "isDefault": true } - ] + "models": [{ "id": "claude-opus-4-6", "label": "Opus 4.6", "isDefault": true }] } } } @@ -338,6 +335,7 @@ Set `extends: "acp"` and provide a `command`: ``` Required fields for ACP providers: + - `extends: "acp"` - `label` - `command` — the command to spawn the agent process (must support ACP over stdio) @@ -455,9 +453,7 @@ Example: relabel a discovered model without replacing the full list: "extends": "acp", "label": "My Agent", "command": ["my-agent", "--acp"], - "additionalModels": [ - { "id": "provider/model-id", "label": "My Preferred Label" } - ] + "additionalModels": [{ "id": "provider/model-id", "label": "My Preferred Label" }] } } } @@ -472,39 +468,39 @@ When an `additionalModels` entry has the same `id` as a discovered model, it upd Every entry under `agents.providers` accepts these fields: -| Field | Type | Required | Description | -|---|---|---|---| -| `extends` | `string` | Yes (custom only) | Built-in provider ID to inherit from, or `"acp"` | -| `label` | `string` | Yes (custom only) | Display name in the UI | -| `description` | `string` | No | Short description shown in the UI | -| `command` | `string[]` | Yes (ACP only) | Command to spawn the agent process | -| `env` | `Record` | No | Environment variables to set for the agent process | -| `models` | `ProviderProfileModel[]` | No | Static model list (overrides runtime discovery) | -| `additionalModels` | `ProviderProfileModel[]` | No | Static model additions (merged with runtime discovery or `models`) | -| `disallowedTools` | `string[]` | No | Tool names to disable for this provider (e.g. `["WebSearch"]`) | -| `enabled` | `boolean` | No | Set to `false` to hide the provider (default: `true`) | -| `order` | `number` | No | Sort order in the provider list | +| Field | Type | Required | Description | +| ------------------ | ------------------------ | ----------------- | ------------------------------------------------------------------ | +| `extends` | `string` | Yes (custom only) | Built-in provider ID to inherit from, or `"acp"` | +| `label` | `string` | Yes (custom only) | Display name in the UI | +| `description` | `string` | No | Short description shown in the UI | +| `command` | `string[]` | Yes (ACP only) | Command to spawn the agent process | +| `env` | `Record` | No | Environment variables to set for the agent process | +| `models` | `ProviderProfileModel[]` | No | Static model list (overrides runtime discovery) | +| `additionalModels` | `ProviderProfileModel[]` | No | Static model additions (merged with runtime discovery or `models`) | +| `disallowedTools` | `string[]` | No | Tool names to disable for this provider (e.g. `["WebSearch"]`) | +| `enabled` | `boolean` | No | Set to `false` to hide the provider (default: `true`) | +| `order` | `number` | No | Sort order in the provider list | ### Model definition Each entry in the `models` array: -| Field | Type | Required | Description | -|---|---|---|---| -| `id` | `string` | Yes | Model identifier sent to the provider | -| `label` | `string` | Yes | Display name in the UI | -| `description` | `string` | No | Short description | -| `isDefault` | `boolean` | No | Mark as the default model selection | -| `thinkingOptions` | `ThinkingOption[]` | No | Available thinking/reasoning levels | +| Field | Type | Required | Description | +| ----------------- | ------------------ | -------- | ------------------------------------- | +| `id` | `string` | Yes | Model identifier sent to the provider | +| `label` | `string` | Yes | Display name in the UI | +| `description` | `string` | No | Short description | +| `isDefault` | `boolean` | No | Mark as the default model selection | +| `thinkingOptions` | `ThinkingOption[]` | No | Available thinking/reasoning levels | ### Thinking option -| Field | Type | Required | Description | -|---|---|---|---| -| `id` | `string` | Yes | Thinking option identifier | -| `label` | `string` | Yes | Display name | -| `description` | `string` | No | Short description | -| `isDefault` | `boolean` | No | Mark as the default thinking option | +| Field | Type | Required | Description | +| ------------- | --------- | -------- | ----------------------------------- | +| `id` | `string` | Yes | Thinking option identifier | +| `label` | `string` | Yes | Display name | +| `description` | `string` | No | Short description | +| `isDefault` | `boolean` | No | Mark as the default thinking option | ### Gotcha: `extends: "claude"` with third-party endpoints diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md index f839afb51..e089a201d 100644 --- a/docs/DATA_MODEL.md +++ b/docs/DATA_MODEL.md @@ -34,88 +34,88 @@ $PASEO_HOME/ Each agent is stored as a separate JSON file, grouped by project directory. -| Field | Type | Description | -|---|---|---| -| `id` | `string` | UUID, primary key | -| `provider` | `string` | Agent provider (`"claude"`, `"codex"`, `"opencode"`, etc.) | -| `cwd` | `string` | Working directory the agent operates in | -| `createdAt` | `string` (ISO 8601) | Creation timestamp | -| `updatedAt` | `string` (ISO 8601) | Last update timestamp | -| `lastActivityAt` | `string?` (ISO 8601) | Last activity timestamp | -| `lastUserMessageAt` | `string?` (ISO 8601) | Last user message timestamp | -| `title` | `string?` | User-visible title | -| `labels` | `Record` | Key-value labels (default `{}`) | -| `lastStatus` | `AgentStatus` | One of: `"initializing"`, `"idle"`, `"running"`, `"error"`, `"closed"` | -| `lastModeId` | `string?` | Last active mode ID | -| `config` | `SerializableConfig?` | Agent session configuration (see below) | -| `runtimeInfo` | `RuntimeInfo?` | Live runtime state (see below) | -| `features` | `AgentFeature[]?` | Provider-reported features (toggles/selects) | -| `persistence` | `PersistenceHandle?` | Handle for resuming sessions | -| `requiresAttention` | `boolean?` | Whether the agent needs user attention | -| `attentionReason` | `"finished" \| "error" \| "permission"?` | Why attention is needed | -| `attentionTimestamp` | `string?` (ISO 8601) | When attention was flagged | -| `internal` | `boolean?` | Whether this is a system-internal agent (loop workers, etc.) | -| `archivedAt` | `string?` (ISO 8601) | Soft-delete timestamp | +| Field | Type | Description | +| -------------------- | ---------------------------------------- | ---------------------------------------------------------------------- | +| `id` | `string` | UUID, primary key | +| `provider` | `string` | Agent provider (`"claude"`, `"codex"`, `"opencode"`, etc.) | +| `cwd` | `string` | Working directory the agent operates in | +| `createdAt` | `string` (ISO 8601) | Creation timestamp | +| `updatedAt` | `string` (ISO 8601) | Last update timestamp | +| `lastActivityAt` | `string?` (ISO 8601) | Last activity timestamp | +| `lastUserMessageAt` | `string?` (ISO 8601) | Last user message timestamp | +| `title` | `string?` | User-visible title | +| `labels` | `Record` | Key-value labels (default `{}`) | +| `lastStatus` | `AgentStatus` | One of: `"initializing"`, `"idle"`, `"running"`, `"error"`, `"closed"` | +| `lastModeId` | `string?` | Last active mode ID | +| `config` | `SerializableConfig?` | Agent session configuration (see below) | +| `runtimeInfo` | `RuntimeInfo?` | Live runtime state (see below) | +| `features` | `AgentFeature[]?` | Provider-reported features (toggles/selects) | +| `persistence` | `PersistenceHandle?` | Handle for resuming sessions | +| `requiresAttention` | `boolean?` | Whether the agent needs user attention | +| `attentionReason` | `"finished" \| "error" \| "permission"?` | Why attention is needed | +| `attentionTimestamp` | `string?` (ISO 8601) | When attention was flagged | +| `internal` | `boolean?` | Whether this is a system-internal agent (loop workers, etc.) | +| `archivedAt` | `string?` (ISO 8601) | Soft-delete timestamp | ### Nested: SerializableConfig -| Field | Type | Description | -|---|---|---| -| `title` | `string?` | Configured title | -| `modeId` | `string?` | Configured mode | -| `model` | `string?` | Configured model | -| `thinkingOptionId` | `string?` | Thinking/reasoning level | -| `featureValues` | `Record?` | Feature preference overrides | -| `extra` | `Record?` | Provider-specific config | -| `systemPrompt` | `string?` | Custom system prompt | -| `mcpServers` | `Record?` | MCP server configurations | +| Field | Type | Description | +| ------------------ | -------------------------- | ---------------------------- | +| `title` | `string?` | Configured title | +| `modeId` | `string?` | Configured mode | +| `model` | `string?` | Configured model | +| `thinkingOptionId` | `string?` | Thinking/reasoning level | +| `featureValues` | `Record?` | Feature preference overrides | +| `extra` | `Record?` | Provider-specific config | +| `systemPrompt` | `string?` | Custom system prompt | +| `mcpServers` | `Record?` | MCP server configurations | ### Nested: RuntimeInfo -| Field | Type | Description | -|---|---|---| -| `provider` | `string` | Active provider | -| `sessionId` | `string?` | Active session ID | -| `model` | `string?` | Active model | -| `thinkingOptionId` | `string?` | Active thinking option | -| `modeId` | `string?` | Active mode | -| `extra` | `Record?` | Provider-specific runtime data | +| Field | Type | Description | +| ------------------ | -------------------------- | ------------------------------ | +| `provider` | `string` | Active provider | +| `sessionId` | `string?` | Active session ID | +| `model` | `string?` | Active model | +| `thinkingOptionId` | `string?` | Active thinking option | +| `modeId` | `string?` | Active mode | +| `extra` | `Record?` | Provider-specific runtime data | ### Nested: PersistenceHandle -| Field | Type | Description | -|---|---|---| -| `provider` | `string` | Provider that owns the session | -| `sessionId` | `string` | Session ID for resumption | -| `nativeHandle` | `any?` | Provider-specific handle (Codex thread ID, Claude resume token, etc.) | -| `metadata` | `Record?` | Extra metadata | +| Field | Type | Description | +| -------------- | ---------------------- | --------------------------------------------------------------------- | +| `provider` | `string` | Provider that owns the session | +| `sessionId` | `string` | Session ID for resumption | +| `nativeHandle` | `any?` | Provider-specific handle (Codex thread ID, Claude resume token, etc.) | +| `metadata` | `Record?` | Extra metadata | ### Nested: AgentFeature (discriminated union on `type`) **Toggle:** -| Field | Type | -|---|---| -| `type` | `"toggle"` | -| `id` | `string` | -| `label` | `string` | -| `description` | `string?` | -| `tooltip` | `string?` | -| `icon` | `string?` | -| `value` | `boolean` | +| Field | Type | +| ------------- | ---------- | +| `type` | `"toggle"` | +| `id` | `string` | +| `label` | `string` | +| `description` | `string?` | +| `tooltip` | `string?` | +| `icon` | `string?` | +| `value` | `boolean` | **Select:** -| Field | Type | -|---|---| -| `type` | `"select"` | -| `id` | `string` | -| `label` | `string` | -| `description` | `string?` | -| `tooltip` | `string?` | -| `icon` | `string?` | -| `value` | `string?` | -| `options` | `AgentSelectOption[]` | +| Field | Type | +| ------------- | --------------------- | +| `type` | `"select"` | +| `id` | `string` | +| `label` | `string` | +| `description` | `string?` | +| `tooltip` | `string?` | +| `icon` | `string?` | +| `value` | `string?` | +| `options` | `AgentSelectOption[]` | --- @@ -172,22 +172,22 @@ All fields are optional with sensible defaults. One file per schedule. ID is 8 hex characters. -| Field | Type | Description | -|---|---|---| -| `id` | `string` | 8-char hex ID | -| `name` | `string?` | Human-readable name | -| `prompt` | `string` | The prompt to send | -| `cadence` | `ScheduleCadence` | Timing (see below) | -| `target` | `ScheduleTarget` | What to run (see below) | -| `status` | `"active" \| "paused" \| "completed"` | Current state | -| `createdAt` | `string` (ISO 8601) | | -| `updatedAt` | `string` (ISO 8601) | | -| `nextRunAt` | `string?` (ISO 8601) | Next scheduled execution | -| `lastRunAt` | `string?` (ISO 8601) | Last execution time | -| `pausedAt` | `string?` (ISO 8601) | When paused | -| `expiresAt` | `string?` (ISO 8601) | Auto-expire time | -| `maxRuns` | `number?` | Max executions before completing | -| `runs` | `ScheduleRun[]` | Execution history | +| Field | Type | Description | +| ----------- | ------------------------------------- | -------------------------------- | +| `id` | `string` | 8-char hex ID | +| `name` | `string?` | Human-readable name | +| `prompt` | `string` | The prompt to send | +| `cadence` | `ScheduleCadence` | Timing (see below) | +| `target` | `ScheduleTarget` | What to run (see below) | +| `status` | `"active" \| "paused" \| "completed"` | Current state | +| `createdAt` | `string` (ISO 8601) | | +| `updatedAt` | `string` (ISO 8601) | | +| `nextRunAt` | `string?` (ISO 8601) | Next scheduled execution | +| `lastRunAt` | `string?` (ISO 8601) | Last execution time | +| `pausedAt` | `string?` (ISO 8601) | When paused | +| `expiresAt` | `string?` (ISO 8601) | Auto-expire time | +| `maxRuns` | `number?` | Max executions before completing | +| `runs` | `ScheduleRun[]` | Execution history | ### Nested: ScheduleCadence (discriminated union on `type`) @@ -201,16 +201,16 @@ One file per schedule. ID is 8 hex characters. ### Nested: ScheduleRun -| Field | Type | Description | -|---|---|---| -| `id` | `string` | Run ID | -| `scheduledFor` | `string` (ISO 8601) | Intended execution time | -| `startedAt` | `string` (ISO 8601) | | -| `endedAt` | `string?` (ISO 8601) | | -| `status` | `"running" \| "succeeded" \| "failed"` | | -| `agentId` | `string?` (UUID) | Agent used for this run | -| `output` | `string?` | Agent output text | -| `error` | `string?` | Error message if failed | +| Field | Type | Description | +| -------------- | -------------------------------------- | ----------------------- | +| `id` | `string` | Run ID | +| `scheduledFor` | `string` (ISO 8601) | Intended execution time | +| `startedAt` | `string` (ISO 8601) | | +| `endedAt` | `string?` (ISO 8601) | | +| `status` | `"running" \| "succeeded" \| "failed"` | | +| `agentId` | `string?` (UUID) | Agent used for this run | +| `output` | `string?` | Agent output text | +| `error` | `string?` | Error message if failed | --- @@ -229,25 +229,25 @@ Single file containing all rooms and messages. ### ChatRoom -| Field | Type | Description | -|---|---|---| -| `id` | `string` (UUID) | | -| `name` | `string` | Unique room name (case-insensitive) | -| `purpose` | `string?` | Room description | -| `createdAt` | `string` (ISO 8601) | | -| `updatedAt` | `string` (ISO 8601) | Updated on each new message | +| Field | Type | Description | +| ----------- | ------------------- | ----------------------------------- | +| `id` | `string` (UUID) | | +| `name` | `string` | Unique room name (case-insensitive) | +| `purpose` | `string?` | Room description | +| `createdAt` | `string` (ISO 8601) | | +| `updatedAt` | `string` (ISO 8601) | Updated on each new message | ### ChatMessage -| Field | Type | Description | -|---|---|---| -| `id` | `string` (UUID) | | -| `roomId` | `string` | FK to ChatRoom.id | -| `authorAgentId` | `string` | Agent ID of the author | -| `body` | `string` | Message text (supports `@mentions`) | -| `replyToMessageId` | `string?` | FK to another ChatMessage.id | -| `mentionAgentIds` | `string[]` | Extracted `@mention` agent IDs | -| `createdAt` | `string` (ISO 8601) | | +| Field | Type | Description | +| ------------------ | ------------------- | ----------------------------------- | +| `id` | `string` (UUID) | | +| `roomId` | `string` | FK to ChatRoom.id | +| `authorAgentId` | `string` | Agent ID of the author | +| `body` | `string` | Message text (supports `@mentions`) | +| `replyToMessageId` | `string?` | FK to another ChatMessage.id | +| `mentionAgentIds` | `string[]` | Extracted `@mention` agent IDs | +| `createdAt` | `string` (ISO 8601) | | --- @@ -257,84 +257,84 @@ Single file containing all rooms and messages. Single file containing an array of all loop records. -| Field | Type | Description | -|---|---|---| -| `id` | `string` | 8-char UUID prefix | -| `name` | `string?` | Human-readable name | -| `prompt` | `string` | Worker prompt | -| `cwd` | `string` | Working directory | -| `provider` | `string` | Default provider | -| `model` | `string?` | Default model | -| `workerProvider` | `string?` | Override provider for workers | -| `workerModel` | `string?` | Override model for workers | -| `verifierProvider` | `string?` | Override provider for verifiers | -| `verifierModel` | `string?` | Override model for verifiers | -| `verifyPrompt` | `string?` | LLM verification prompt | -| `verifyChecks` | `string[]` | Shell commands to run as checks | -| `archive` | `boolean` | Whether to archive worker agents after use | -| `sleepMs` | `number` | Delay between iterations (ms) | -| `maxIterations` | `number?` | Cap on iterations | -| `maxTimeMs` | `number?` | Total time budget (ms) | -| `status` | `"running" \| "succeeded" \| "failed" \| "stopped"` | | -| `createdAt` | `string` (ISO 8601) | | -| `updatedAt` | `string` (ISO 8601) | | -| `startedAt` | `string` (ISO 8601) | | -| `completedAt` | `string?` (ISO 8601) | | -| `stopRequestedAt` | `string?` (ISO 8601) | | -| `iterations` | `LoopIteration[]` | | -| `logs` | `LoopLogEntry[]` | | -| `nextLogSeq` | `number` | Monotonic log sequence counter | -| `activeIteration` | `number?` | Currently executing iteration index | -| `activeWorkerAgentId` | `string?` | Currently running worker agent | -| `activeVerifierAgentId` | `string?` | Currently running verifier agent | +| Field | Type | Description | +| ----------------------- | --------------------------------------------------- | ------------------------------------------ | +| `id` | `string` | 8-char UUID prefix | +| `name` | `string?` | Human-readable name | +| `prompt` | `string` | Worker prompt | +| `cwd` | `string` | Working directory | +| `provider` | `string` | Default provider | +| `model` | `string?` | Default model | +| `workerProvider` | `string?` | Override provider for workers | +| `workerModel` | `string?` | Override model for workers | +| `verifierProvider` | `string?` | Override provider for verifiers | +| `verifierModel` | `string?` | Override model for verifiers | +| `verifyPrompt` | `string?` | LLM verification prompt | +| `verifyChecks` | `string[]` | Shell commands to run as checks | +| `archive` | `boolean` | Whether to archive worker agents after use | +| `sleepMs` | `number` | Delay between iterations (ms) | +| `maxIterations` | `number?` | Cap on iterations | +| `maxTimeMs` | `number?` | Total time budget (ms) | +| `status` | `"running" \| "succeeded" \| "failed" \| "stopped"` | | +| `createdAt` | `string` (ISO 8601) | | +| `updatedAt` | `string` (ISO 8601) | | +| `startedAt` | `string` (ISO 8601) | | +| `completedAt` | `string?` (ISO 8601) | | +| `stopRequestedAt` | `string?` (ISO 8601) | | +| `iterations` | `LoopIteration[]` | | +| `logs` | `LoopLogEntry[]` | | +| `nextLogSeq` | `number` | Monotonic log sequence counter | +| `activeIteration` | `number?` | Currently executing iteration index | +| `activeWorkerAgentId` | `string?` | Currently running worker agent | +| `activeVerifierAgentId` | `string?` | Currently running verifier agent | ### Nested: LoopIteration -| Field | Type | Description | -|---|---|---| -| `index` | `number` | 1-based iteration index | -| `workerAgentId` | `string?` | Agent ID of the worker | -| `workerStartedAt` | `string` (ISO 8601) | | -| `workerCompletedAt` | `string?` (ISO 8601) | | -| `verifierAgentId` | `string?` | Agent ID of the verifier | -| `status` | `"running" \| "succeeded" \| "failed" \| "stopped"` | | -| `workerOutcome` | `"completed" \| "failed" \| "canceled"?` | | -| `failureReason` | `string?` | | -| `verifyChecks` | `LoopVerifyCheckResult[]` | Shell check results | -| `verifyPrompt` | `LoopVerifyPromptResult?` | LLM verification result | +| Field | Type | Description | +| ------------------- | --------------------------------------------------- | ------------------------ | +| `index` | `number` | 1-based iteration index | +| `workerAgentId` | `string?` | Agent ID of the worker | +| `workerStartedAt` | `string` (ISO 8601) | | +| `workerCompletedAt` | `string?` (ISO 8601) | | +| `verifierAgentId` | `string?` | Agent ID of the verifier | +| `status` | `"running" \| "succeeded" \| "failed" \| "stopped"` | | +| `workerOutcome` | `"completed" \| "failed" \| "canceled"?` | | +| `failureReason` | `string?` | | +| `verifyChecks` | `LoopVerifyCheckResult[]` | Shell check results | +| `verifyPrompt` | `LoopVerifyPromptResult?` | LLM verification result | ### Nested: LoopLogEntry -| Field | Type | -|---|---| -| `seq` | `number` (monotonic) | -| `timestamp` | `string` (ISO 8601) | -| `iteration` | `number?` | -| `source` | `"loop" \| "worker" \| "verifier" \| "verify-check"` | -| `level` | `"info" \| "error"` | -| `text` | `string` | +| Field | Type | +| ----------- | ---------------------------------------------------- | +| `seq` | `number` (monotonic) | +| `timestamp` | `string` (ISO 8601) | +| `iteration` | `number?` | +| `source` | `"loop" \| "worker" \| "verifier" \| "verify-check"` | +| `level` | `"info" \| "error"` | +| `text` | `string` | ### Nested: LoopVerifyCheckResult -| Field | Type | -|---|---| -| `command` | `string` | -| `exitCode` | `number` | -| `passed` | `boolean` | -| `stdout` | `string` | -| `stderr` | `string` | -| `startedAt` | `string` (ISO 8601) | +| Field | Type | +| ------------- | ------------------- | +| `command` | `string` | +| `exitCode` | `number` | +| `passed` | `boolean` | +| `stdout` | `string` | +| `stderr` | `string` | +| `startedAt` | `string` (ISO 8601) | | `completedAt` | `string` (ISO 8601) | ### Nested: LoopVerifyPromptResult -| Field | Type | -|---|---| -| `passed` | `boolean` | -| `reason` | `string` | -| `verifierAgentId` | `string?` | -| `startedAt` | `string` (ISO 8601) | -| `completedAt` | `string` (ISO 8601) | +| Field | Type | +| ----------------- | ------------------- | +| `passed` | `boolean` | +| `reason` | `string` | +| `verifierAgentId` | `string?` | +| `startedAt` | `string` (ISO 8601) | +| `completedAt` | `string` (ISO 8601) | --- @@ -344,15 +344,15 @@ Single file containing an array of all loop records. Array of project records. -| Field | Type | Description | -|---|---|---| -| `projectId` | `string` | Primary key | -| `rootPath` | `string` | Filesystem root of the project | -| `kind` | `"git" \| "non_git"` | | -| `displayName` | `string` | | -| `createdAt` | `string` (ISO 8601) | | -| `updatedAt` | `string` (ISO 8601) | | -| `archivedAt` | `string?` (ISO 8601) | Soft-delete timestamp | +| Field | Type | Description | +| ------------- | -------------------- | ------------------------------ | +| `projectId` | `string` | Primary key | +| `rootPath` | `string` | Filesystem root of the project | +| `kind` | `"git" \| "non_git"` | | +| `displayName` | `string` | | +| `createdAt` | `string` (ISO 8601) | | +| `updatedAt` | `string` (ISO 8601) | | +| `archivedAt` | `string?` (ISO 8601) | Soft-delete timestamp | --- @@ -362,16 +362,16 @@ Array of project records. Array of workspace records. A workspace is a specific working directory within a project. -| Field | Type | Description | -|---|---|---| -| `workspaceId` | `string` | Primary key | -| `projectId` | `string` | FK to Project.projectId | -| `cwd` | `string` | Filesystem path | -| `kind` | `"local_checkout" \| "worktree" \| "directory"` | | -| `displayName` | `string` | | -| `createdAt` | `string` (ISO 8601) | | -| `updatedAt` | `string` (ISO 8601) | | -| `archivedAt` | `string?` (ISO 8601) | Soft-delete timestamp | +| Field | Type | Description | +| ------------- | ----------------------------------------------- | ----------------------- | +| `workspaceId` | `string` | Primary key | +| `projectId` | `string` | FK to Project.projectId | +| `cwd` | `string` | Filesystem path | +| `kind` | `"local_checkout" \| "worktree" \| "directory"` | | +| `displayName` | `string` | | +| `createdAt` | `string` (ISO 8601) | | +| `updatedAt` | `string` (ISO 8601) | | +| `archivedAt` | `string?` (ISO 8601) | Soft-delete timestamp | --- @@ -417,12 +417,12 @@ Stores binary attachment blobs keyed by attachment ID. ### AttachmentMetadata -| Field | Type | Description | -|---|---|---| -| `id` | `string` | Unique attachment ID | -| `mimeType` | `string` | MIME type | -| `storageType` | `string` | Storage backend identifier | -| `storageKey` | `string` | Key within the storage backend | -| `createdAt` | `number` | Epoch ms | -| `fileName` | `string?` | Original filename | -| `byteSize` | `number?` | Size in bytes | +| Field | Type | Description | +| ------------- | --------- | ------------------------------ | +| `id` | `string` | Unique attachment ID | +| `mimeType` | `string` | MIME type | +| `storageType` | `string` | Storage backend identifier | +| `storageKey` | `string` | Key within the storage backend | +| `createdAt` | `number` | Epoch ms | +| `fileName` | `string?` | Original filename | +| `byteSize` | `number?` | Size in bytes | diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index fbb39ea22..1e965751b 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -31,6 +31,7 @@ PASEO_HOME=~/.paseo-blue npm run dev ### Default ports In the main checkout: + - Daemon: `localhost:6767` - Expo app: `localhost:8081` @@ -63,13 +64,13 @@ Pass either a DB directory or a `paseo.sqlite` file to `--db`. The script opens Every `scripts` entry with `"type": "service"` receives these environment variables: -| Variable | Value | -|---|---| -| `PASEO_SERVICE__URL` | Proxied daemon URL for a declared peer service. Prefer this for peer discovery; it survives peer restarts. | +| Variable | Value | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `PASEO_SERVICE__URL` | Proxied daemon URL for a declared peer service. Prefer this for peer discovery; it survives peer restarts. | | `PASEO_SERVICE__PORT` | Raw ephemeral port for a declared peer service. Use only as a bypass escape hatch; it can go stale if that peer restarts. | -| `PASEO_URL` | Self alias for `PASEO_SERVICE__URL`. | -| `PASEO_PORT` | Self alias for `PASEO_SERVICE__PORT`. | -| `HOST` | Bind host for the service process. | +| `PASEO_URL` | Self alias for `PASEO_SERVICE__URL`. | +| `PASEO_PORT` | Self alias for `PASEO_SERVICE__PORT`. | +| `HOST` | Bind host for the service process. | `` is normalized from the script name by uppercasing it, replacing each run of non-`A-Z0-9` characters with `_`, and trimming leading or trailing `_`. For example, `app-server` and `app.server` both normalize to `APP_SERVER`; that collision fails at spawn time with an actionable error. @@ -135,11 +136,13 @@ $PASEO_HOME/agents/{cwd-with-dashes}/{agent-id}.json ``` Find an agent by ID: + ```bash find $PASEO_HOME/agents -name "{agent-id}.json" ``` Find by content: + ```bash rg -l "some title text" $PASEO_HOME/agents/ ``` @@ -149,11 +152,13 @@ rg -l "some title text" $PASEO_HOME/agents/ Get the session ID from the agent JSON (`persistence.sessionId`), then: **Claude:** + ``` ~/.claude/projects/{cwd-with-dashes}/{session-id}.jsonl ``` **Codex:** + ``` ~/.codex/sessions/{YYYY}/{MM}/{DD}/rollout-{timestamp}-{session-id}.jsonl ``` diff --git a/docs/FILE_ICONS.md b/docs/FILE_ICONS.md index 96cedfb13..8c41110ed 100644 --- a/docs/FILE_ICONS.md +++ b/docs/FILE_ICONS.md @@ -35,8 +35,8 @@ cat node_modules/material-icon-theme/icons/ICON_NAME.svg ``` 3. Add two things to `material-file-icons.ts`: - - The SVG string in `SVG_ICONS`: + ```ts "icon_name": `...`, ``` @@ -52,58 +52,58 @@ cat node_modules/material-icon-theme/icons/ICON_NAME.svg 53 unique icons covering these extensions: -| Extension(s) | Icon | -|---|---| -| `ts` | typescript | -| `tsx` | react_ts | -| `js` | javascript | -| `jsx` | react | -| `py` | python | -| `go` | go | -| `rs` | rust | -| `rb` | ruby | -| `java` | java | -| `kt` | kotlin | -| `c` | c | -| `cpp` | cpp | -| `h` | h | -| `hpp` | hpp | -| `cs` | csharp | -| `swift` | swift | -| `dart` | dart | -| `ex`, `exs` | elixir | -| `erl` | erlang | -| `hs` | haskell | -| `clj` | clojure | -| `scala` | scala | -| `ml` | ocaml | -| `r` | r | -| `lua` | lua | -| `zig` | zig | -| `nix` | nix | -| `php` | php | -| `html` | html | -| `css` | css | -| `scss` | sass | -| `less` | less | -| `json` | json | -| `yml`, `yaml` | yaml | -| `xml` | xml | -| `toml` | toml | -| `md`, `markdown` | markdown | -| `sql` | database | -| `graphql`, `gql` | graphql | -| `sh`, `bash` | console | -| `tf` | terraform | -| `hcl` | hcl | -| `vue` | vue | -| `svelte` | svelte | -| `astro` | astro | -| `wasm` | webassembly | -| `svg` | svg | -| `png`, `jpg`, `jpeg`, `gif`, `webp`, `ico` | image | -| `txt` | document | -| `conf`, `cfg`, `ini` | settings | -| `lock` | lock | -| `groovy` | groovy | -| `gradle` | gradle | +| Extension(s) | Icon | +| ------------------------------------------ | ----------- | +| `ts` | typescript | +| `tsx` | react_ts | +| `js` | javascript | +| `jsx` | react | +| `py` | python | +| `go` | go | +| `rs` | rust | +| `rb` | ruby | +| `java` | java | +| `kt` | kotlin | +| `c` | c | +| `cpp` | cpp | +| `h` | h | +| `hpp` | hpp | +| `cs` | csharp | +| `swift` | swift | +| `dart` | dart | +| `ex`, `exs` | elixir | +| `erl` | erlang | +| `hs` | haskell | +| `clj` | clojure | +| `scala` | scala | +| `ml` | ocaml | +| `r` | r | +| `lua` | lua | +| `zig` | zig | +| `nix` | nix | +| `php` | php | +| `html` | html | +| `css` | css | +| `scss` | sass | +| `less` | less | +| `json` | json | +| `yml`, `yaml` | yaml | +| `xml` | xml | +| `toml` | toml | +| `md`, `markdown` | markdown | +| `sql` | database | +| `graphql`, `gql` | graphql | +| `sh`, `bash` | console | +| `tf` | terraform | +| `hcl` | hcl | +| `vue` | vue | +| `svelte` | svelte | +| `astro` | astro | +| `wasm` | webassembly | +| `svg` | svg | +| `png`, `jpg`, `jpeg`, `gif`, `webp`, `ico` | image | +| `txt` | document | +| `conf`, `cfg`, `ini` | settings | +| `lock` | lock | +| `groovy` | groovy | +| `gradle` | gradle | diff --git a/docs/MOBILE_TESTING.md b/docs/MOBILE_TESTING.md index ee99568bf..2e36be7dd 100644 --- a/docs/MOBILE_TESTING.md +++ b/docs/MOBILE_TESTING.md @@ -223,7 +223,7 @@ const styles = StyleSheet.create((theme) => ({ }, })); - +; ``` ```tsx @@ -244,7 +244,7 @@ const { theme } = useUnistyles(); +/>; ``` Regular `View` components can safely use Unistyles dynamic styles — the conflict is specific to `Animated.View`. diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index eae9d40ce..acd4c32fa 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -30,6 +30,7 @@ Each project opens as a workspace. For git projects, the default workspace is th ### Inside a workspace A workspace is a flexible canvas: + - Launch multiple agents side by side in split panes - Open terminals alongside agents - Mix and match providers within the same workspace @@ -39,6 +40,7 @@ A workspace is a flexible canvas: Paseo is a client-server system. The daemon (Node.js) runs on your machine, manages agent processes, and streams output in real time over WebSocket. Clients connect to the daemon — locally or remotely. This architecture means: + - The daemon can run on any machine: laptop, VM, remote server - Multiple clients can connect simultaneously - Agents keep running when you close the app diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index 6de30f82b..7a89667e9 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -58,10 +58,10 @@ type MyProviderClientOptions = { export class MyProviderACPAgentClient extends ACPAgentClient { constructor(options: MyProviderClientOptions) { super({ - provider: "my-provider", // Must match the ID used everywhere else + provider: "my-provider", // Must match the ID used everywhere else logger: options.logger, runtimeSettings: options.runtimeSettings, - defaultCommand: ["my-agent-binary", "--acp"], // CLI command to spawn + defaultCommand: ["my-agent-binary", "--acp"], // CLI command to spawn defaultModes: MY_PROVIDER_MODES, capabilities: MY_PROVIDER_CAPABILITIES, }); @@ -70,7 +70,7 @@ export class MyProviderACPAgentClient extends ACPAgentClient { // Override isAvailable() if the provider needs specific auth/env vars override async isAvailable(): Promise { if (!(await super.isAvailable())) { - return false; // Binary not found + return false; // Binary not found } return Boolean(process.env["MY_PROVIDER_API_KEY"]); } @@ -212,8 +212,8 @@ export const agentConfigs = { provider: "my-provider", model: "default-model-id", modes: { - full: "autonomous", // Mode with no permission prompts - ask: "default", // Mode that requires permission approval + full: "autonomous", // Mode with no permission prompts + ask: "default", // Mode that requires permission approval }, }, } as const satisfies Record; @@ -264,8 +264,15 @@ If your agent does not speak ACP, implement the interfaces from `agent-sdk-types interface AgentClient { readonly provider: AgentProvider; readonly capabilities: AgentCapabilityFlags; - createSession(config: AgentSessionConfig, launchContext?: AgentLaunchContext): Promise; - resumeSession(handle: AgentPersistenceHandle, overrides?: Partial, launchContext?: AgentLaunchContext): Promise; + createSession( + config: AgentSessionConfig, + launchContext?: AgentLaunchContext, + ): Promise; + resumeSession( + handle: AgentPersistenceHandle, + overrides?: Partial, + launchContext?: AgentLaunchContext, + ): Promise; listModels(options?: ListModelsOptions): Promise; isAvailable(): Promise; // Optional: diff --git a/docs/TESTING.md b/docs/TESTING.md index 18d51a3c2..cdd8ef86b 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -79,7 +79,9 @@ const realSender: EmailSender = { send: sendgrid.send }; function createTestEmailSender() { const sent: Array<{ to: string; body: string }> = []; return { - send: async (to: string, body: string) => { sent.push({ to, body }); }, + send: async (to: string, body: string) => { + sent.push({ to, body }); + }, sent, }; } @@ -115,6 +117,7 @@ The test output is the source of truth, not your reading of the code. ## Design for testability If code isn't testable, refactor it. Signs: + - You want to reach for a mock - You can't inject a dependency - You need to test private internals diff --git a/docs/UNISTYLES.md b/docs/UNISTYLES.md index 3d992d50b..b3b0d4ba4 100644 --- a/docs/UNISTYLES.md +++ b/docs/UNISTYLES.md @@ -19,7 +19,7 @@ The important detail: the automatic native path tracks `props.style`. It does no Avoid this pattern when the style depends on the theme: ```tsx - +; const styles = StyleSheet.create((theme) => ({ container: { @@ -39,10 +39,8 @@ Preferred pattern: put themed backgrounds on a normal wrapper view, and keep `co ```tsx - - {children} - - + {children} +; const styles = StyleSheet.create((theme) => ({ container: { @@ -66,10 +64,7 @@ import { StyleSheet, withUnistyles } from "react-native-unistyles"; const ThemedScrollView = withUnistyles(ScrollView); - +; ``` `withUnistyles` extracts dependency metadata from both `style` and `contentContainerStyle`, subscribes to the relevant theme/runtime changes, and re-renders only that wrapped component when needed. Its [auto-mapping behavior for `style` and `contentContainerStyle`](https://www.unistyl.es/v3/references/with-unistyles#auto-mapping-for-style-and-contentcontainerstyle-props) is the reason it fixes themed `ScrollView` content containers. Reach for it when wrapper-view layout would be awkward or when a third-party component needs theme-aware non-`style` props mapped through Unistyles. @@ -80,11 +75,8 @@ The smallest escape hatch is to use `useUnistyles()` and pass an inline value th const { theme } = useUnistyles(); + contentContainerStyle={[styles.contentContainer, { backgroundColor: theme.colors.surface0 }]} +/>; ``` Use this sparingly. It works because React re-renders the prop, but it gives up the main Unistyles native-update path for that value. @@ -98,8 +90,14 @@ The sharp edge: Unistyles hashes styles by value. If `withUnistyles` receives a Concrete regression we hit: `welcome-screen.tsx` had `const ThemedScrollView = withUnistyles(ScrollView)` with `style={{ flex: 1, backgroundColor: theme.colors.surface0 }}`. `agent-panel.tsx` had `root` and `container` styles with the exact same value. All three collided on class `unistyles_j2k2iilhfz`, so the browser stylesheet contained: ```css -.unistyles_j2k2iilhfz { flex: 1 1 0%; background-color: var(--colors-surface0); } -.unistyles_j2k2iilhfz > * { flex: 1 1 0%; background-color: var(--colors-surface0); } +.unistyles_j2k2iilhfz { + flex: 1 1 0%; + background-color: var(--colors-surface0); +} +.unistyles_j2k2iilhfz > * { + flex: 1 1 0%; + background-color: var(--colors-surface0); +} ``` The child-selector rule forced `flex:1` and `background-color: surface0` onto the Composer's outer `Animated.View` (a direct child of `container`), stretching it to fill remaining space and leaving a large empty gap between the composer UI and the bottom of the screen. It also painted a `surface0` band behind the scroll-to-bottom button. The bug only appeared in the browser — Electron skips `WelcomeScreen` after pairing, so the `> *` rule was never injected there. @@ -113,8 +111,10 @@ Symptoms to watch for: Quick confirmation in DevTools console: ```js -[...document.styleSheets].flatMap(s => [...(s.cssRules || [])]) - .map(r => r.cssText).filter(t => t.includes("unistyles") && t.includes("> *")); +[...document.styleSheets] + .flatMap((s) => [...(s.cssRules || [])]) + .map((r) => r.cssText) + .filter((t) => t.includes("unistyles") && t.includes("> *")); ``` Any match beyond benign `r-pointerEvents-* > *` rules from react-native-web is a leak. @@ -130,9 +130,7 @@ We saw this in `AdaptiveModalSheet`: the body text and buttons were dark-theme-c ```tsx const { theme } = useUnistyles(); - - {title} - +{title}; ``` Keep layout and typography in `StyleSheet.create`; move only the stale theme-dependent value through React. If a larger subtree shows the same behavior, consider remounting the sheet on theme changes or moving the themed paint onto a wrapper that is mounted with the visible content. @@ -170,7 +168,7 @@ Use `useUnistyles()` inside the component instead: ```tsx const { theme } = useUnistyles(); - +; ``` Importing `baseColors`, theme-name constants, or `type Theme` is fine when the value is intentionally static or type-only. @@ -233,4 +231,4 @@ The welcome-screen investigation used this approach to prove the white layer was - [GitHub issue #550: ScrollView sticky-header theme updates](https://github.com/jpudysz/react-native-unistyles/issues/550) - [GitHub issue #817: `UnistylesRuntime.themeName` does not re-render](https://github.com/jpudysz/react-native-unistyles/issues/817) - [GitHub issue #1030: `Image.tintColor` and native style update edge case](https://github.com/jpudysz/react-native-unistyles/issues/1030) -- [Local research note: welcome theme split]() +- [Local research note: welcome theme split](/Users/moboudra/.paseo/notes/welcome-theme-split-research.md) diff --git a/packages/app/e2e/helpers/agent-bottom-anchor.ts b/packages/app/e2e/helpers/agent-bottom-anchor.ts index b4c2cc829..c33c99d68 100644 --- a/packages/app/e2e/helpers/agent-bottom-anchor.ts +++ b/packages/app/e2e/helpers/agent-bottom-anchor.ts @@ -92,9 +92,7 @@ type DaemonClientConfig = { }; async function loadDaemonClientConstructor(): Promise< - new ( - config: DaemonClientConfig, - ) => DaemonClientInstance + new (config: DaemonClientConfig) => DaemonClientInstance > { const repoRoot = path.resolve(__dirname, "../../../../"); const moduleUrl = pathToFileURL( diff --git a/packages/app/e2e/helpers/archive-tab.ts b/packages/app/e2e/helpers/archive-tab.ts index 175e42cdb..fb5798ad0 100644 --- a/packages/app/e2e/helpers/archive-tab.ts +++ b/packages/app/e2e/helpers/archive-tab.ts @@ -81,9 +81,7 @@ type ArchiveTabDaemonClientConfig = { }; async function loadDaemonClientConstructor(): Promise< - new ( - config: ArchiveTabDaemonClientConfig, - ) => ArchiveTabDaemonClient + new (config: ArchiveTabDaemonClientConfig) => ArchiveTabDaemonClient > { const repoRoot = path.resolve(__dirname, "../../../../"); const moduleUrl = pathToFileURL( diff --git a/packages/app/e2e/helpers/new-workspace.ts b/packages/app/e2e/helpers/new-workspace.ts index 2f199f100..0471496ea 100644 --- a/packages/app/e2e/helpers/new-workspace.ts +++ b/packages/app/e2e/helpers/new-workspace.ts @@ -49,9 +49,7 @@ function getDaemonWsUrl(): string { } async function loadDaemonClientConstructor(): Promise< - new ( - config: NewWorkspaceDaemonClientConfig, - ) => NewWorkspaceDaemonClient + new (config: NewWorkspaceDaemonClientConfig) => NewWorkspaceDaemonClient > { const repoRoot = path.resolve(__dirname, "../../../../"); const moduleUrl = pathToFileURL( diff --git a/packages/app/e2e/helpers/terminal-perf.ts b/packages/app/e2e/helpers/terminal-perf.ts index 394666551..36de7aa41 100644 --- a/packages/app/e2e/helpers/terminal-perf.ts +++ b/packages/app/e2e/helpers/terminal-perf.ts @@ -63,9 +63,7 @@ type TerminalPerfDaemonClientConfig = { }; async function loadDaemonClientConstructor(): Promise< - new ( - config: TerminalPerfDaemonClientConfig, - ) => TerminalPerfDaemonClient + new (config: TerminalPerfDaemonClientConfig) => TerminalPerfDaemonClient > { const repoRoot = path.resolve(__dirname, "../../../../"); const moduleUrl = pathToFileURL( diff --git a/packages/app/maestro/flows/dev-client.yaml b/packages/app/maestro/flows/dev-client.yaml index 56edaf51b..12e223705 100644 --- a/packages/app/maestro/flows/dev-client.yaml +++ b/packages/app/maestro/flows/dev-client.yaml @@ -24,4 +24,3 @@ appId: sh.paseo commands: - tapOn: point: "50%,20%" - diff --git a/packages/app/maestro/launch-clear.yaml b/packages/app/maestro/launch-clear.yaml index f5f36e0a1..5a7d1029f 100644 --- a/packages/app/maestro/launch-clear.yaml +++ b/packages/app/maestro/launch-clear.yaml @@ -3,4 +3,3 @@ appId: sh.paseo - launchApp: clearState: true - runFlow: flows/dev-client.yaml - diff --git a/packages/app/package.json b/packages/app/package.json index d14f91735..14730ca4f 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,8 +1,8 @@ { "name": "@getpaseo/app", - "main": "index.ts", "version": "0.1.62", "private": true, + "main": "index.ts", "scripts": { "start": "APP_VARIANT=development expo start", "reset-project": "node ./scripts/reset-project.js", diff --git a/packages/app/public/index.html b/packages/app/public/index.html index 00c15da0c..b55e6f172 100644 --- a/packages/app/public/index.html +++ b/packages/app/public/index.html @@ -1,4 +1,4 @@ - + @@ -21,8 +21,9 @@ } /* Prevent white flash before React mounts */ @media (prefers-color-scheme: dark) { - html, body { - background-color: #181B1A; + html, + body { + background-color: #181b1a; } } /* These styles make the root element full-height */ @@ -38,17 +39,17 @@ input, textarea, select, - [role='button'], - [role='link'], - [role='textbox'], - [role='combobox'], - [role='tab'], - [role='switch'], - [role='checkbox'], - [role='slider'], - [role='menuitem'], + [role="button"], + [role="link"], + [role="textbox"], + [role="combobox"], + [role="tab"], + [role="switch"], + [role="checkbox"], + [role="slider"], + [role="menuitem"], [tabindex], - [contenteditable='true'] { + [contenteditable="true"] { -webkit-app-region: no-drag !important; } @@ -59,7 +60,7 @@ outline: none; } *:focus-visible { - outline: 2px solid #20744A; + outline: 2px solid #20744a; outline-offset: 2px; } diff --git a/packages/app/src/attachments/store.ts b/packages/app/src/attachments/store.ts index ce0837766..ffcfc6e50 100644 --- a/packages/app/src/attachments/store.ts +++ b/packages/app/src/attachments/store.ts @@ -7,9 +7,8 @@ let attachmentStorePromise: Promise | null = null; async function createAttachmentStore(): Promise { if (isWeb) { if (isElectronRuntime()) { - const { createDesktopAttachmentStore } = await import( - "../desktop/attachments/desktop-attachment-store" - ); + const { createDesktopAttachmentStore } = + await import("../desktop/attachments/desktop-attachment-store"); return createDesktopAttachmentStore(); } diff --git a/packages/app/src/components/attachment-lightbox.test.tsx b/packages/app/src/components/attachment-lightbox.test.tsx index 15ff85ac6..64f947f87 100644 --- a/packages/app/src/components/attachment-lightbox.test.tsx +++ b/packages/app/src/components/attachment-lightbox.test.tsx @@ -82,13 +82,7 @@ vi.mock("expo-image", () => ({ vi.mock("react-native", async () => { const actual = await vi.importActual>("react-native"); - const Modal = ({ - visible = true, - children, - }: { - visible?: boolean; - children?: React.ReactNode; - }) => + const Modal = ({ visible = true, children }: { visible?: boolean; children?: React.ReactNode }) => visible ? React.createElement("div", { "data-testid": "lightbox-modal" }, children) : null; return { ...actual, Modal }; }); diff --git a/packages/app/src/components/composer.test.tsx b/packages/app/src/components/composer.test.tsx index 315b859c1..5df8bd04a 100644 --- a/packages/app/src/components/composer.test.tsx +++ b/packages/app/src/components/composer.test.tsx @@ -285,13 +285,7 @@ vi.mock("expo-image", () => ({ vi.mock("react-native", async () => { const actual = await vi.importActual>("react-native"); - const Modal = ({ - visible = true, - children, - }: { - visible?: boolean; - children?: React.ReactNode; - }) => + const Modal = ({ visible = true, children }: { visible?: boolean; children?: React.ReactNode }) => visible ? React.createElement("div", { "data-testid": "lightbox-modal" }, children) : null; return { ...actual, Modal }; }); diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index 47711799e..47d56870b 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -1954,8 +1954,8 @@ export function SidebarWorkspaceList({ select: toProjectIconDataUri, enabled: Boolean( getHostRuntimeStore().getClient(request.serverId) && - isHostRuntimeConnected(getHostRuntimeStore().getSnapshot(request.serverId)) && - request.cwd, + isHostRuntimeConnected(getHostRuntimeStore().getSnapshot(request.serverId)) && + request.cwd, ), staleTime: Infinity, gcTime: 1000 * 60 * 60, diff --git a/packages/app/src/components/split-container.tsx b/packages/app/src/components/split-container.tsx index 0befd840d..f05cfa3d2 100644 --- a/packages/app/src/components/split-container.tsx +++ b/packages/app/src/components/split-container.tsx @@ -134,18 +134,17 @@ interface SplitNodeViewProps extends Omit { tabDropPreview: TabDropPreview | null; } -interface SplitPaneViewProps - extends Omit< - SplitNodeViewProps, - | "node" - | "workspaceKey" - | "focusedPaneId" - | "activeDragTabId" - | "showDropZones" - | "dropPreview" - | "onMoveTabToPane" - | "onResizeSplit" - > { +interface SplitPaneViewProps extends Omit< + SplitNodeViewProps, + | "node" + | "workspaceKey" + | "focusedPaneId" + | "activeDragTabId" + | "showDropZones" + | "dropPreview" + | "onMoveTabToPane" + | "onResizeSplit" +> { pane: SplitPane; uiTabs: WorkspaceTab[]; isFocused: boolean; diff --git a/packages/app/src/hooks/use-providers-snapshot.test.ts b/packages/app/src/hooks/use-providers-snapshot.test.ts index de88fa487..cdfdef63f 100644 --- a/packages/app/src/hooks/use-providers-snapshot.test.ts +++ b/packages/app/src/hooks/use-providers-snapshot.test.ts @@ -237,23 +237,24 @@ describe("providers snapshot hook cache scope", () => { it.each([ { name: "missing", entries: [] }, { name: "loading", entries: [codexEntry("loading")] }, - ])("ensures a selected provider snapshot on selector open when it is $name", async ({ - entries, - }) => { - enableProvidersSnapshot(); - mockClient.getProvidersSnapshot - .mockResolvedValueOnce(providersSnapshot(entries)) - .mockResolvedValueOnce(providersSnapshot([codexEntry("ready", [readyCodexModel])])); + ])( + "ensures a selected provider snapshot on selector open when it is $name", + async ({ entries }) => { + enableProvidersSnapshot(); + mockClient.getProvidersSnapshot + .mockResolvedValueOnce(providersSnapshot(entries)) + .mockResolvedValueOnce(providersSnapshot([codexEntry("ready", [readyCodexModel])])); - const { result } = renderProvidersSnapshotHook(); + const { result } = renderProvidersSnapshotHook(); - await waitForSnapshotEntries(result, entries); - await openSelectorForSelectedProvider(result); - await waitForSnapshotReads(2); + await waitForSnapshotEntries(result, entries); + await openSelectorForSelectedProvider(result); + await waitForSnapshotReads(2); - expect(mockClient.getProvidersSnapshot).toHaveBeenLastCalledWith({}); - expect(mockClient.refreshProvidersSnapshot).not.toHaveBeenCalled(); - }); + expect(mockClient.getProvidersSnapshot).toHaveBeenLastCalledWith({}); + expect(mockClient.refreshProvidersSnapshot).not.toHaveBeenCalled(); + }, + ); it("does not ensure a selected provider snapshot on selector open when the provider is ready with no models", async () => { enableProvidersSnapshot(); diff --git a/packages/app/src/keyboard/focus-scope.ts b/packages/app/src/keyboard/focus-scope.ts index cd06538e9..b5fde8bc4 100644 --- a/packages/app/src/keyboard/focus-scope.ts +++ b/packages/app/src/keyboard/focus-scope.ts @@ -51,7 +51,7 @@ export function resolveKeyboardFocusScope(input: { candidates.some((element) => Boolean( element.closest("[data-testid='command-center-panel']") || - element.closest("[data-testid='command-center-input']"), + element.closest("[data-testid='command-center-input']"), ), ) ) { diff --git a/packages/app/src/keyboard/keyboard-shortcuts.test.ts b/packages/app/src/keyboard/keyboard-shortcuts.test.ts index 307d5cf4c..ddcffc0c2 100644 --- a/packages/app/src/keyboard/keyboard-shortcuts.test.ts +++ b/packages/app/src/keyboard/keyboard-shortcuts.test.ts @@ -289,23 +289,19 @@ describe("keyboard-shortcuts", () => { }, ]; - it.each(matchingCases)("$name", ({ - event, - context, - action, - payload, - preventDefault, - stopPropagation, - }) => { - expectShortcutResolution({ - event, - context, - action, - ...(payload !== undefined ? { payload } : {}), - ...(preventDefault !== undefined ? { preventDefault } : {}), - ...(stopPropagation !== undefined ? { stopPropagation } : {}), - }); - }); + it.each(matchingCases)( + "$name", + ({ event, context, action, payload, preventDefault, stopPropagation }) => { + expectShortcutResolution({ + event, + context, + action, + ...(payload !== undefined ? { payload } : {}), + ...(preventDefault !== undefined ? { preventDefault } : {}), + ...(stopPropagation !== undefined ? { stopPropagation } : {}), + }); + }, + ); const nonMatchingCases: NonMatchingShortcutCase[] = [ { diff --git a/packages/app/src/runtime/host-runtime.ts b/packages/app/src/runtime/host-runtime.ts index a026f306a..2beb4c2b0 100644 --- a/packages/app/src/runtime/host-runtime.ts +++ b/packages/app/src/runtime/host-runtime.ts @@ -491,10 +491,7 @@ export class HostRuntimeController { private probeRequestVersion = 0; private probeCycleInFlight: Promise | null = null; - constructor(input: { - host: HostProfile; - deps?: HostRuntimeControllerDeps; - }) { + constructor(input: { host: HostProfile; deps?: HostRuntimeControllerDeps }) { this.host = input.host; this.deps = input.deps ?? createDefaultDeps(); this.connectionMachineState = { @@ -1155,9 +1152,7 @@ export class HostRuntimeStore { private storageLoaded = false; private bootstrapAttempted = false; - constructor(input?: { - deps?: HostRuntimeControllerDeps; - }) { + constructor(input?: { deps?: HostRuntimeControllerDeps }) { this.deps = input?.deps ?? createDefaultDeps(); } diff --git a/packages/app/src/screens/workspace/workspace-draft-agent-tab.tsx b/packages/app/src/screens/workspace/workspace-draft-agent-tab.tsx index 57c7394d5..c003f4f68 100644 --- a/packages/app/src/screens/workspace/workspace-draft-agent-tab.tsx +++ b/packages/app/src/screens/workspace/workspace-draft-agent-tab.tsx @@ -230,11 +230,11 @@ export function WorkspaceDraftAgentTab({ const isReadyForPendingAutoSubmit = Boolean( pendingAutoSubmit && - draftInput.isHydrated && - workspaceDirectory && - client && - !isSubmitting && - !composerState.isModelLoading, + draftInput.isHydrated && + workspaceDirectory && + client && + !isSubmitting && + !composerState.isModelLoading, ); const autoSubmitKeyRef = useRef(null); useEffect(() => { diff --git a/packages/app/src/utils/tool-call-detail-state.ts b/packages/app/src/utils/tool-call-detail-state.ts index e07d448b3..92f0a92f8 100644 --- a/packages/app/src/utils/tool-call-detail-state.ts +++ b/packages/app/src/utils/tool-call-detail-state.ts @@ -36,10 +36,10 @@ export function hasMeaningfulToolCallDetail(detail: ToolCallDetail | undefined): case "search": return Boolean( detail.query.trim().length > 0 || - detail.content || - (detail.filePaths && detail.filePaths.length > 0) || - (detail.webResults && detail.webResults.length > 0) || - (detail.annotations && detail.annotations.length > 0), + detail.content || + (detail.filePaths && detail.filePaths.length > 0) || + (detail.webResults && detail.webResults.length > 0) || + (detail.annotations && detail.annotations.length > 0), ); case "fetch": return Boolean(detail.url || detail.result || detail.codeText); diff --git a/packages/cli/package.json b/packages/cli/package.json index 240e95923..39d6b9e35 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -2,17 +2,17 @@ "name": "@getpaseo/cli", "version": "0.1.62", "description": "Paseo CLI - control your AI coding agents from the command line", - "type": "module", + "bin": { + "paseo": "bin/paseo" + }, "files": [ "bin", "dist" ], + "type": "module", "publishConfig": { "access": "public" }, - "bin": { - "paseo": "bin/paseo" - }, "scripts": { "build": "node -e \"require('node:fs').rmSync('dist',{ recursive: true, force: true })\" && tsc -p tsconfig.json --incremental false", "prepack": "npm run build", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 3d2e1fb6f..2d294a52e 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -3,6 +3,16 @@ "version": "0.1.62", "private": true, "description": "Paseo desktop app (Electron wrapper)", + "homepage": "https://paseo.sh", + "license": "AGPL-3.0-or-later", + "author": { + "name": "Mohamed Boudra", + "email": "hello@moboudra.com" + }, + "repository": { + "type": "git", + "url": "https://github.com/getpaseo/paseo.git" + }, "main": "dist/main.js", "scripts": { "build": "npm --prefix ../.. run build:daemon && npm run build:main && electron-builder --config electron-builder.yml", @@ -27,15 +37,5 @@ "typescript": "5.9.3", "unzip-crx-3": "^0.2.0", "wait-on": "8.0.5" - }, - "homepage": "https://paseo.sh", - "repository": { - "type": "git", - "url": "https://github.com/getpaseo/paseo.git" - }, - "author": { - "name": "Mohamed Boudra", - "email": "hello@moboudra.com" - }, - "license": "AGPL-3.0-or-later" + } } diff --git a/packages/expo-two-way-audio/CODE_OF_CONDUCT.md b/packages/expo-two-way-audio/CODE_OF_CONDUCT.md index 2af98746d..074b8503e 100644 --- a/packages/expo-two-way-audio/CODE_OF_CONDUCT.md +++ b/packages/expo-two-way-audio/CODE_OF_CONDUCT.md @@ -17,23 +17,23 @@ diverse, inclusive, and healthy community. Examples of behavior that contributes to a positive environment for our community include: -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the +- Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: -* The use of sexualized language or imagery, and sexual attention or +- The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within @@ -125,4 +125,4 @@ enforcement ladder](https://github.com/mozilla/diversity). For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. \ No newline at end of file +https://www.contributor-covenant.org/translations. diff --git a/packages/expo-two-way-audio/examples/basic-usage/README.md b/packages/expo-two-way-audio/examples/basic-usage/README.md index 3aba57b9b..cd55472f6 100644 --- a/packages/expo-two-way-audio/examples/basic-usage/README.md +++ b/packages/expo-two-way-audio/examples/basic-usage/README.md @@ -18,18 +18,20 @@ This example showcases the basic functions of the expo-two-way-audio module: In the `examples/basic-usage` directory: 1. Install dependencies: - ```sh - npm install - ``` + + ```sh + npm install + ``` 2. Run the app: - ```sh - # Run iOS - npm run ios - # Run Android - npm run android - ``` + ```sh + # Run iOS + npm run ios + + # Run Android + npm run android + ``` ## Notes diff --git a/packages/expo-two-way-audio/examples/basic-usage/package.json b/packages/expo-two-way-audio/examples/basic-usage/package.json index 336677551..5ba14b147 100644 --- a/packages/expo-two-way-audio/examples/basic-usage/package.json +++ b/packages/expo-two-way-audio/examples/basic-usage/package.json @@ -1,6 +1,7 @@ { "name": "expo-two-way-audio-example", "version": "1.0.0", + "private": true, "main": "expo/AppEntry.js", "scripts": { "start": "expo start", @@ -11,17 +12,16 @@ "dependencies": { "buffer": "^6.0.3", "expo": "^52.0.0", - "react": "18.3.1", - "react-native": "0.76.2", + "expo-build-properties": "~0.13.1", "expo-dev-client": "~5.0.3", - "expo-build-properties": "~0.13.1" + "react": "18.3.1", + "react-native": "0.76.2" }, "devDependencies": { "@babel/core": "^7.20.0", "@types/react": "~18.3.12", "typescript": "^5.1.3" }, - "private": true, "expo": { "autolinking": { "nativeModulesDir": "../.." diff --git a/packages/expo-two-way-audio/examples/flow-api/package.json b/packages/expo-two-way-audio/examples/flow-api/package.json index 80b7fac2f..97d73b031 100644 --- a/packages/expo-two-way-audio/examples/flow-api/package.json +++ b/packages/expo-two-way-audio/examples/flow-api/package.json @@ -1,6 +1,7 @@ { "name": "expo-two-way-audio-flow-example", "version": "1.0.0", + "private": true, "main": "expo/AppEntry.js", "scripts": { "start": "expo start", @@ -25,7 +26,6 @@ "@types/react": "~18.3.12", "typescript": "^5.1.3" }, - "private": true, "expo": { "autolinking": { "nativeModulesDir": "../.." diff --git a/packages/highlight/package.json b/packages/highlight/package.json index 929b85641..d9982486b 100644 --- a/packages/highlight/package.json +++ b/packages/highlight/package.json @@ -1,10 +1,10 @@ { "name": "@getpaseo/highlight", "version": "0.1.62", + "files": [ + "dist" + ], "type": "module", - "publishConfig": { - "access": "public" - }, "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { @@ -14,9 +14,9 @@ "default": "./dist/index.js" } }, - "files": [ - "dist" - ], + "publishConfig": { + "access": "public" + }, "scripts": { "build": "node -e \"require('node:fs').rmSync('dist',{ recursive: true, force: true })\" && tsc -p tsconfig.json --incremental false", "prepack": "npm run build", diff --git a/packages/relay/package.json b/packages/relay/package.json index 619f203f5..d7fd89e67 100644 --- a/packages/relay/package.json +++ b/packages/relay/package.json @@ -2,14 +2,11 @@ "name": "@getpaseo/relay", "version": "0.1.62", "description": "Paseo relay for bridging daemon and client connections", - "type": "module", - "publishConfig": { - "access": "public" - }, - "types": "./dist/index.d.ts", "files": [ "dist" ], + "type": "module", + "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", @@ -30,6 +27,9 @@ "default": "./src/cloudflare-adapter.ts" } }, + "publishConfig": { + "access": "public" + }, "scripts": { "build": "node -e \"require('node:fs').rmSync('dist',{ recursive: true, force: true })\" && tsc -p tsconfig.json --incremental false", "prepack": "npm run build", diff --git a/packages/relay/src/e2e.test.ts b/packages/relay/src/e2e.test.ts index f0293790a..b9ef966e7 100644 --- a/packages/relay/src/e2e.test.ts +++ b/packages/relay/src/e2e.test.ts @@ -199,169 +199,176 @@ async function stopRelayProcess(relayProcess: ChildProcess): Promise { } }, SHUTDOWN_TIMEOUT_MS); - it("full flow: daemon and client exchange encrypted messages through relay", { - timeout: 90_000, - }, async () => { - const serverId = "test-session-" + Date.now(); - const connectionId = "clt_test_" + Date.now() + "_" + Math.random().toString(36).slice(2); + it( + "full flow: daemon and client exchange encrypted messages through relay", + { + timeout: 90_000, + }, + async () => { + const serverId = "test-session-" + Date.now(); + const connectionId = "clt_test_" + Date.now() + "_" + Math.random().toString(36).slice(2); - // === DAEMON SIDE === - // Generate keypair (public key goes in QR) - const daemonKeyPair = await generateKeyPair(); - const daemonPubKeyB64 = await exportPublicKey(daemonKeyPair.publicKey); + // === DAEMON SIDE === + // Generate keypair (public key goes in QR) + const daemonKeyPair = await generateKeyPair(); + const daemonPubKeyB64 = await exportPublicKey(daemonKeyPair.publicKey); - // QR would contain: { serverId, daemonPubKeyB64, relay: { endpoint } } + // QR would contain: { serverId, daemonPubKeyB64, relay: { endpoint } } - // Daemon connects to relay as "server" control role - const daemonControlWs = new WebSocket( - `ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&v=2`, - ); + // Daemon connects to relay as "server" control role + const daemonControlWs = new WebSocket( + `ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&v=2`, + ); - await new Promise((resolve, reject) => { - daemonControlWs.on("open", resolve); - daemonControlWs.on("error", reject); - }); + await new Promise((resolve, reject) => { + daemonControlWs.on("open", resolve); + daemonControlWs.on("error", reject); + }); - // === CLIENT SIDE === - // Client scans QR, gets daemon's public key and session ID - // Client generates own keypair - const clientKeyPair = await generateKeyPair(); - const clientPubKeyB64 = await exportPublicKey(clientKeyPair.publicKey); + // === CLIENT SIDE === + // Client scans QR, gets daemon's public key and session ID + // Client generates own keypair + const clientKeyPair = await generateKeyPair(); + const clientPubKeyB64 = await exportPublicKey(clientKeyPair.publicKey); - // Client imports daemon's public key and derives shared secret - const daemonPubKeyOnClient = await importPublicKey(daemonPubKeyB64); - const clientSharedKey = await deriveSharedKey(clientKeyPair.secretKey, daemonPubKeyOnClient); + // Client imports daemon's public key and derives shared secret + const daemonPubKeyOnClient = await importPublicKey(daemonPubKeyB64); + const clientSharedKey = await deriveSharedKey(clientKeyPair.secretKey, daemonPubKeyOnClient); - const waitForClientSeen = new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error("timed out waiting for connected")), 5000); - const onMessage = (raw: unknown) => { - try { - const text = - typeof raw === "string" - ? raw - : raw && typeof (raw as any).toString === "function" - ? (raw as any).toString() - : ""; - const msg = JSON.parse(text); - if (msg?.type === "connected" && msg.connectionId === connectionId) { - clearTimeout(timeout); - daemonControlWs.off("message", onMessage); - resolve(); - return; + const waitForClientSeen = new Promise((resolve, reject) => { + const timeout = setTimeout( + () => reject(new Error("timed out waiting for connected")), + 5000, + ); + const onMessage = (raw: unknown) => { + try { + const text = + typeof raw === "string" + ? raw + : raw && typeof (raw as any).toString === "function" + ? (raw as any).toString() + : ""; + const msg = JSON.parse(text); + if (msg?.type === "connected" && msg.connectionId === connectionId) { + clearTimeout(timeout); + daemonControlWs.off("message", onMessage); + resolve(); + return; + } + if ( + msg?.type === "sync" && + Array.isArray(msg.connectionIds) && + msg.connectionIds.includes(connectionId) + ) { + clearTimeout(timeout); + daemonControlWs.off("message", onMessage); + resolve(); + } + } catch { + // ignore } - if ( - msg?.type === "sync" && - Array.isArray(msg.connectionIds) && - msg.connectionIds.includes(connectionId) - ) { - clearTimeout(timeout); - daemonControlWs.off("message", onMessage); - resolve(); - } - } catch { - // ignore - } - }; - daemonControlWs.on("message", onMessage); - }); + }; + daemonControlWs.on("message", onMessage); + }); - // Client connects to relay as "client" role (must include connectionId) - const clientWs = new WebSocket( - `ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=client&connectionId=${connectionId}&v=2`, - ); + // Client connects to relay as "client" role (must include connectionId) + const clientWs = new WebSocket( + `ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=client&connectionId=${connectionId}&v=2`, + ); - await new Promise((resolve, reject) => { - clientWs.on("open", resolve); - clientWs.on("error", reject); - }); + await new Promise((resolve, reject) => { + clientWs.on("open", resolve); + clientWs.on("error", reject); + }); - await waitForClientSeen; + await waitForClientSeen; - const daemonWs = new WebSocket( - `ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&connectionId=${connectionId}&v=2`, - ); - await new Promise((resolve, reject) => { - daemonWs.on("open", resolve); - daemonWs.on("error", reject); - }); + const daemonWs = new WebSocket( + `ws://127.0.0.1:${relayPort}/ws?serverId=${serverId}&role=server&connectionId=${connectionId}&v=2`, + ); + await new Promise((resolve, reject) => { + daemonWs.on("open", resolve); + daemonWs.on("error", reject); + }); - // Client sends hello with its public key (this message is NOT encrypted - it's the handshake) - const helloMsg = JSON.stringify({ type: "hello", key: clientPubKeyB64 }); - clientWs.send(helloMsg); + // Client sends hello with its public key (this message is NOT encrypted - it's the handshake) + const helloMsg = JSON.stringify({ type: "hello", key: clientPubKeyB64 }); + clientWs.send(helloMsg); - // === DAEMON RECEIVES HELLO === - const daemonReceivedHello = await new Promise((resolve) => { - daemonWs.once("message", (data) => resolve(data.toString())); - }); + // === DAEMON RECEIVES HELLO === + const daemonReceivedHello = await new Promise((resolve) => { + daemonWs.once("message", (data) => resolve(data.toString())); + }); - const hello = JSON.parse(daemonReceivedHello); - expect(hello.type).toBe("hello"); - expect(hello.key).toBe(clientPubKeyB64); + const hello = JSON.parse(daemonReceivedHello); + expect(hello.type).toBe("hello"); + expect(hello.key).toBe(clientPubKeyB64); - // Daemon imports client's public key and derives shared secret - const clientPubKeyOnDaemon = await importPublicKey(hello.key); - const daemonSharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientPubKeyOnDaemon); + // Daemon imports client's public key and derives shared secret + const clientPubKeyOnDaemon = await importPublicKey(hello.key); + const daemonSharedKey = await deriveSharedKey(daemonKeyPair.secretKey, clientPubKeyOnDaemon); - // === VERIFY BOTH HAVE SAME KEY - Exchange encrypted messages === + // === VERIFY BOTH HAVE SAME KEY - Exchange encrypted messages === - // Daemon sends encrypted "ready" message - const readyPlaintext = JSON.stringify({ type: "ready" }); - const readyCiphertext = await encrypt(daemonSharedKey, readyPlaintext); - daemonWs.send(Buffer.from(readyCiphertext)); + // Daemon sends encrypted "ready" message + const readyPlaintext = JSON.stringify({ type: "ready" }); + const readyCiphertext = await encrypt(daemonSharedKey, readyPlaintext); + daemonWs.send(Buffer.from(readyCiphertext)); - // Client receives and decrypts - const clientReceivedReady = await new Promise((resolve) => { - clientWs.once("message", (data) => resolve(data as Buffer)); - }); - const decryptedReady = await decrypt( - clientSharedKey, - clientReceivedReady.buffer.slice( - clientReceivedReady.byteOffset, - clientReceivedReady.byteOffset + clientReceivedReady.byteLength, - ), - ); - expect(JSON.parse(decryptedReady as string)).toEqual({ type: "ready" }); + // Client receives and decrypts + const clientReceivedReady = await new Promise((resolve) => { + clientWs.once("message", (data) => resolve(data as Buffer)); + }); + const decryptedReady = await decrypt( + clientSharedKey, + clientReceivedReady.buffer.slice( + clientReceivedReady.byteOffset, + clientReceivedReady.byteOffset + clientReceivedReady.byteLength, + ), + ); + expect(JSON.parse(decryptedReady as string)).toEqual({ type: "ready" }); - // Client sends encrypted message - const clientMessage = "Hello from client!"; - const clientCiphertext = await encrypt(clientSharedKey, clientMessage); - clientWs.send(Buffer.from(clientCiphertext)); + // Client sends encrypted message + const clientMessage = "Hello from client!"; + const clientCiphertext = await encrypt(clientSharedKey, clientMessage); + clientWs.send(Buffer.from(clientCiphertext)); - // Daemon receives and decrypts - const daemonReceivedMsg = await new Promise((resolve) => { - daemonWs.once("message", (data) => resolve(data as Buffer)); - }); - const decryptedClientMsg = await decrypt( - daemonSharedKey, - daemonReceivedMsg.buffer.slice( - daemonReceivedMsg.byteOffset, - daemonReceivedMsg.byteOffset + daemonReceivedMsg.byteLength, - ), - ); - expect(decryptedClientMsg).toBe(clientMessage); + // Daemon receives and decrypts + const daemonReceivedMsg = await new Promise((resolve) => { + daemonWs.once("message", (data) => resolve(data as Buffer)); + }); + const decryptedClientMsg = await decrypt( + daemonSharedKey, + daemonReceivedMsg.buffer.slice( + daemonReceivedMsg.byteOffset, + daemonReceivedMsg.byteOffset + daemonReceivedMsg.byteLength, + ), + ); + expect(decryptedClientMsg).toBe(clientMessage); - // Daemon sends encrypted response - const daemonMessage = "Hello from daemon!"; - const daemonCiphertext = await encrypt(daemonSharedKey, daemonMessage); - daemonWs.send(Buffer.from(daemonCiphertext)); + // Daemon sends encrypted response + const daemonMessage = "Hello from daemon!"; + const daemonCiphertext = await encrypt(daemonSharedKey, daemonMessage); + daemonWs.send(Buffer.from(daemonCiphertext)); - // Client receives and decrypts - const clientReceivedMsg = await new Promise((resolve) => { - clientWs.once("message", (data) => resolve(data as Buffer)); - }); - const decryptedDaemonMsg = await decrypt( - clientSharedKey, - clientReceivedMsg.buffer.slice( - clientReceivedMsg.byteOffset, - clientReceivedMsg.byteOffset + clientReceivedMsg.byteLength, - ), - ); - expect(decryptedDaemonMsg).toBe(daemonMessage); + // Client receives and decrypts + const clientReceivedMsg = await new Promise((resolve) => { + clientWs.once("message", (data) => resolve(data as Buffer)); + }); + const decryptedDaemonMsg = await decrypt( + clientSharedKey, + clientReceivedMsg.buffer.slice( + clientReceivedMsg.byteOffset, + clientReceivedMsg.byteOffset + clientReceivedMsg.byteLength, + ), + ); + expect(decryptedDaemonMsg).toBe(daemonMessage); - // Cleanup - daemonWs.close(); - clientWs.close(); - }); + // Cleanup + daemonWs.close(); + clientWs.close(); + }, + ); it("relay only sees opaque bytes after handshake", { timeout: 90_000 }, async () => { const serverId = "opaque-test-" + Date.now(); diff --git a/packages/relay/src/e2ee.ts b/packages/relay/src/e2ee.ts index e213f6c67..f538ba60d 100644 --- a/packages/relay/src/e2ee.ts +++ b/packages/relay/src/e2ee.ts @@ -1,8 +1,4 @@ -export { - createClientChannel, - createDaemonChannel, - EncryptedChannel, -} from "./encrypted-channel.js"; +export { createClientChannel, createDaemonChannel, EncryptedChannel } from "./encrypted-channel.js"; export type { Transport, EncryptedChannelEvents } from "./encrypted-channel.js"; export { diff --git a/packages/relay/src/index.ts b/packages/relay/src/index.ts index c17bb678f..79b575c71 100644 --- a/packages/relay/src/index.ts +++ b/packages/relay/src/index.ts @@ -1,7 +1,4 @@ -export type { - ConnectionRole, - RelaySessionAttachment, -} from "./types.js"; +export type { ConnectionRole, RelaySessionAttachment } from "./types.js"; export { generateKeyPair, @@ -12,9 +9,5 @@ export { decrypt, } from "./crypto.js"; -export { - createClientChannel, - createDaemonChannel, - EncryptedChannel, -} from "./encrypted-channel.js"; +export { createClientChannel, createDaemonChannel, EncryptedChannel } from "./encrypted-channel.js"; export type { Transport, EncryptedChannelEvents } from "./encrypted-channel.js"; diff --git a/packages/relay/wrangler.toml b/packages/relay/wrangler.toml index 04c7df8fe..9cabd8790 100644 --- a/packages/relay/wrangler.toml +++ b/packages/relay/wrangler.toml @@ -3,9 +3,7 @@ account_id = "10ed39a1dbf316e30abd0c409bed40d6" main = "src/cloudflare-adapter.ts" compatibility_date = "2024-12-01" -routes = [ - { pattern = "relay.paseo.sh", custom_domain = true } -] +routes = [{ pattern = "relay.paseo.sh", custom_domain = true }] [observability] enabled = true diff --git a/packages/server/CLAUDE.md b/packages/server/CLAUDE.md index 3f433621e..3d829b93f 100644 --- a/packages/server/CLAUDE.md +++ b/packages/server/CLAUDE.md @@ -11,6 +11,7 @@ Paseo is a mobile + CLI app for monitoring and controlling local AI coding agent ## Build / Lint / Test Commands ### Root (monorepo) + ```bash npm run dev # Start daemon + Expo in Tmux npm run build:daemon # Build: highlight + relay + server + cli @@ -20,6 +21,7 @@ npm run format # Format with Biome (in-place) ``` ### Server package (`packages/server`) + ```bash npm run dev # Start dev daemon (tsx watch) npm run build # Build lib + scripts to dist/ @@ -45,6 +47,7 @@ npm run test:ui # Vitest UI at localhost:51204 ``` ### Other useful commands + ```bash npm run build --workspace=@getpaseo/relay # Rebuild relay before daemon npm run build --workspace=@getpaseo/server # Rebuild server @@ -58,13 +61,22 @@ npm run cli -- daemon status # Check daemon status ## Code Style ### Biome (formatting only, no linting) + ```json -{ "indentStyle": "space", "indentWidth": 2, "lineWidth": 100, "quoteStyle": "double", "trailingCommas": "all", "semicolons": "always" } +{ + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100, + "quoteStyle": "double", + "trailingCommas": "all", + "semicolons": "always" +} ``` ### TypeScript + - **Fully strict** — no `any`, no implicit `any` -- **`interface`** over `type`** when possible +- **`interface`** over `type`\*\* when possible - **`function` declarations** over arrow function assignments - **Named types** — no complex inline types in public signatures - **Object parameters** — use single object param when >1 argument @@ -72,15 +84,18 @@ npm run cli -- daemon status # Check daemon status - `noUnusedLocals: true`, `noUnusedParameters: true`, `noFallthroughCasesInSwitch: true` ### Imports + - Use path alias `@server/*` in server package (maps to `./src/`) - No barrel `index.ts` re-exports — they create unnecessary indirection ### Naming + - Files: `kebab-case.ts` named after the main export (`create-tool-call.ts`) - Tests: collocated with implementation (`thing.test.ts`) - No prefixes like `RpcX`, `DbX`, `UiX` — keep one canonical type per concept ### Error Handling + - **Fail explicitly** — throw instead of silently returning defaults - **Typed domain errors** — extend `Error` with structured metadata @@ -97,10 +112,16 @@ class TimeoutError extends Error { ``` ### State Design + Discriminated unions over bags of booleans/optionals: + ```typescript // Bad -interface FetchState { isLoading: boolean; error?: Error; data?: Data } +interface FetchState { + isLoading: boolean; + error?: Error; + data?: Data; +} // Good type FetchState = @@ -168,10 +189,10 @@ npm run db:query -- "SELECT * FROM agent_timeline_rows..." ## Relevant Docs -| File | What it covers | -|---|---| -| [../CLAUDE.md](../CLAUDE.md) | Repository overview, critical rules, quick start | -| [../docs/ARCHITECTURE.md](../docs/ARCHITECTURE.md) | System design, WebSocket protocol, data flow | -| [../docs/CODING_STANDARDS.md](../docs/CODING_STANDARDS.md) | Type hygiene, error handling, React patterns | -| [../docs/TESTING.md](../docs/TESTING.md) | TDD workflow, determinism, real deps over mocks | -| [../SECURITY.md](../SECURITY.md) | Relay threat model, E2E encryption | +| File | What it covers | +| ---------------------------------------------------------- | ------------------------------------------------ | +| [../CLAUDE.md](../CLAUDE.md) | Repository overview, critical rules, quick start | +| [../docs/ARCHITECTURE.md](../docs/ARCHITECTURE.md) | System design, WebSocket protocol, data flow | +| [../docs/CODING_STANDARDS.md](../docs/CODING_STANDARDS.md) | Type hygiene, error handling, React patterns | +| [../docs/TESTING.md](../docs/TESTING.md) | TDD workflow, determinism, real deps over mocks | +| [../SECURITY.md](../SECURITY.md) | Relay threat model, E2E encryption | diff --git a/packages/server/README.md b/packages/server/README.md index c0ca73ce0..cbdceb073 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -49,15 +49,18 @@ npm start ## Project Status **✅ Completed** (Phases 1-2): + - Package setup and configuration - Express server with WebSocket - React UI with Vite - WebSocket client with ping/pong testing **⏳ In Progress** (Phase 3): + - Terminal control (tmux integration) **📋 Planned** (Phases 4-9): + - LLM integration (OpenAI GPT-4) - Agent orchestrator - Speech-to-Text (Deepgram) @@ -95,6 +98,7 @@ PASEO_HOME=~/.paseo-blue PASEO_LISTEN=127.0.0.1:7777 npm run dev ## Testing Currently manual testing via: + 1. Start servers: `npm run dev` 2. Open http://localhost:5173 3. Test WebSocket connection (green status indicator) diff --git a/packages/server/package.json b/packages/server/package.json index 2474877ad..474513285 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -2,11 +2,6 @@ "name": "@getpaseo/server", "version": "0.1.62", "description": "Paseo backend server", - "type": "module", - "publishConfig": { - "access": "public" - }, - "types": "./dist/server/server/exports.d.ts", "files": [ "dist/server", "dist/src", @@ -15,6 +10,8 @@ "README.md", ".env.example" ], + "type": "module", + "types": "./dist/server/server/exports.d.ts", "exports": { ".": { "types": "./dist/server/server/exports.d.ts", @@ -27,6 +24,9 @@ "default": "./dist/server/utils/tool-call-parsers.js" } }, + "publishConfig": { + "access": "public" + }, "scripts": { "dev": "cross-env NODE_ENV=development node --import tsx scripts/dev-runner.ts", "dev:tsx": "cross-env NODE_ENV=development tsx watch --ignore '**/*.timestamp-*' src/server/index.ts", diff --git a/packages/server/src/client/daemon-client.ts b/packages/server/src/client/daemon-client.ts index d77508031..3fa079ee7 100644 --- a/packages/server/src/client/daemon-client.ts +++ b/packages/server/src/client/daemon-client.ts @@ -232,11 +232,10 @@ export interface CreateAgentRequestOptions extends AgentConfigOverrides { labels?: Record; } -export interface CreatePaseoWorktreeInput - extends Pick< - CreatePaseoWorktreeRequest, - "cwd" | "worktreeSlug" | "attachments" | "refName" | "action" | "githubPrNumber" - > {} +export interface CreatePaseoWorktreeInput extends Pick< + CreatePaseoWorktreeRequest, + "cwd" | "worktreeSlug" | "attachments" | "refName" | "action" | "githubPrNumber" +> {} type CheckoutStatusPayload = CheckoutStatusResponse["payload"]; type SubscribeCheckoutDiffPayload = Extract< diff --git a/packages/server/src/server/agent/agent-response-loop.ts b/packages/server/src/server/agent/agent-response-loop.ts index d99b2c115..f7ac9615f 100644 --- a/packages/server/src/server/agent/agent-response-loop.ts +++ b/packages/server/src/server/agent/agent-response-loop.ts @@ -130,9 +130,7 @@ function buildZodValidator(schema: z.ZodTypeAny, schemaName: string): SchemaV function buildJsonSchemaValidator(schema: JsonSchema): SchemaValidator { const AjvConstructor = Ajv as unknown as { - new ( - options?: AjvOptions, - ): { + new (options?: AjvOptions): { compile: (input: JsonSchema) => ((value: unknown) => boolean) & { errors?: ErrorObject[] | null; }; diff --git a/packages/server/src/server/agent/mcp-server.ts b/packages/server/src/server/agent/mcp-server.ts index 49f9c1269..5fe6e060e 100644 --- a/packages/server/src/server/agent/mcp-server.ts +++ b/packages/server/src/server/agent/mcp-server.ts @@ -1203,9 +1203,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom ? ( await Promise.all( terminalManager.listDirectories().map(async (directory) => - ( - await terminalManager.getTerminals(directory) - ).map((terminal) => ({ + (await terminalManager.getTerminals(directory)).map((terminal) => ({ id: terminal.id, name: terminal.name, cwd: terminal.cwd, diff --git a/packages/server/src/server/agent/orchestrator.ts b/packages/server/src/server/agent/orchestrator.ts index a72a719d0..dfadc0ee8 100644 --- a/packages/server/src/server/agent/orchestrator.ts +++ b/packages/server/src/server/agent/orchestrator.ts @@ -8,4 +8,3 @@ * This file is kept as a stub to document the migration. * It can be deleted once the refactoring is confirmed working. */ - diff --git a/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts b/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts index 4f8f25a27..441f24026 100644 --- a/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts +++ b/packages/server/src/server/agent/providers/claude/sidechain-tracker.ts @@ -49,9 +49,7 @@ export class ClaudeSidechainTracker { private readonly activeSidechains = new Map(); private readonly getToolInput: (toolUseId: string) => AgentMetadata | null | undefined; - constructor(input: { - getToolInput: (toolUseId: string) => AgentMetadata | null | undefined; - }) { + constructor(input: { getToolInput: (toolUseId: string) => AgentMetadata | null | undefined }) { this.getToolInput = input.getToolInput; } diff --git a/packages/server/src/server/agent/providers/mock-load-test-agent.ts b/packages/server/src/server/agent/providers/mock-load-test-agent.ts index e82ade211..fc3b92cad 100644 --- a/packages/server/src/server/agent/providers/mock-load-test-agent.ts +++ b/packages/server/src/server/agent/providers/mock-load-test-agent.ts @@ -291,11 +291,7 @@ export class MockLoadTestAgentSession implements AgentSession { private modeId: string | null; private modelId: string | null; - constructor(options: { - config: AgentSessionConfig; - sessionId: string; - logger?: Logger; - }) { + constructor(options: { config: AgentSessionConfig; sessionId: string; logger?: Logger }) { this.id = options.sessionId; this.logger = options.logger; this.modeId = options.config.modeId ?? MOCK_LOAD_TEST_MODE_ID; diff --git a/packages/server/src/server/agent/providers/provider-windows-launch.test.ts b/packages/server/src/server/agent/providers/provider-windows-launch.test.ts index c099b2561..a56c1d7d9 100644 --- a/packages/server/src/server/agent/providers/provider-windows-launch.test.ts +++ b/packages/server/src/server/agent/providers/provider-windows-launch.test.ts @@ -192,26 +192,23 @@ describe.runIf(process.platform === "win32")("Windows provider launch parity", ( }); }); - test.each( - providerLaunchCases, - )("$provider launches a cmd shim from a path with spaces through spawnProcess", async ({ - binaryName, - args, - shell, - }) => { - const fixture = makeFixture(binaryName, args); + test.each(providerLaunchCases)( + "$provider launches a cmd shim from a path with spaces through spawnProcess", + async ({ binaryName, args, shell }) => { + const fixture = makeFixture(binaryName, args); - const result = await runProviderFixture({ - command: fixture.shim, - args: fixture.expectedArgs, - expectedArgs: fixture.expectedArgs, - shell, - }); + const result = await runProviderFixture({ + command: fixture.shim, + args: fixture.expectedArgs, + expectedArgs: fixture.expectedArgs, + shell, + }); - expect(result.error).toBeNull(); - expect(result.code).toBe(0); - expect(result.signal).toBeNull(); - expect(result.stderr).toBe(""); - expect(result.stdout.trim()).toBe("ARGV_OK"); - }); + expect(result.error).toBeNull(); + expect(result.code).toBe(0); + expect(result.signal).toBeNull(); + expect(result.stderr).toBe(""); + expect(result.stdout.trim()).toBe("ARGV_OK"); + }, + ); }); diff --git a/packages/server/src/server/daemon-e2e/live-preferences.e2e.test.ts b/packages/server/src/server/daemon-e2e/live-preferences.e2e.test.ts index 57c1ad1b1..0ecc3cb43 100644 --- a/packages/server/src/server/daemon-e2e/live-preferences.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/live-preferences.e2e.test.ts @@ -78,52 +78,51 @@ describe("daemon E2E", () => { await ctx.cleanup(); }, 60000); - describe.each([ - "claude", - "codex", - "opencode", - ] as const)("live model switching (%s)", (provider) => { - const shouldRun = - provider === "claude" || - (provider === "codex" && hasCodex) || - (provider === "opencode" && hasOpenCode); + describe.each(["claude", "codex", "opencode"] as const)( + "live model switching (%s)", + (provider) => { + const shouldRun = + provider === "claude" || + (provider === "codex" && hasCodex) || + (provider === "opencode" && hasOpenCode); - test.runIf(shouldRun)( - "updates agent model without restarting", - async () => { - const cwd = tmpCwd(); - try { - const modelList = await ctx.client.listProviderModels(provider); - if (!modelList.models || modelList.models.length === 0) { - throw new Error(`No models returned for provider ${provider}`); + test.runIf(shouldRun)( + "updates agent model without restarting", + async () => { + const cwd = tmpCwd(); + try { + const modelList = await ctx.client.listProviderModels(provider); + if (!modelList.models || modelList.models.length === 0) { + throw new Error(`No models returned for provider ${provider}`); + } + const [modelA, modelB] = pickTwoDistinctModels(modelList.models); + + const agent = await ctx.client.createAgent({ + provider, + cwd, + title: `Model Switch (${provider})`, + model: modelA, + }); + + const startIndex = messages.length; + await ctx.client.setAgentModel(agent.id, modelB); + + const updated = await waitForAgentUpdate( + messages, + startIndex, + (a) => a.id === agent.id && a.model === modelB, + { timeoutMs: 20000 }, + ); + + expect(updated.model).toBe(modelB); + } finally { + rmSync(cwd, { recursive: true, force: true }); } - const [modelA, modelB] = pickTwoDistinctModels(modelList.models); - - const agent = await ctx.client.createAgent({ - provider, - cwd, - title: `Model Switch (${provider})`, - model: modelA, - }); - - const startIndex = messages.length; - await ctx.client.setAgentModel(agent.id, modelB); - - const updated = await waitForAgentUpdate( - messages, - startIndex, - (a) => a.id === agent.id && a.model === modelB, - { timeoutMs: 20000 }, - ); - - expect(updated.model).toBe(modelB); - } finally { - rmSync(cwd, { recursive: true, force: true }); - } - }, - 180000, - ); - }); + }, + 180000, + ); + }, + ); test("live thinking switching works for Claude (off -> on)", async () => { const cwd = tmpCwd(); diff --git a/packages/server/src/server/exports.ts b/packages/server/src/server/exports.ts index 48ffff923..ee3d0fcb8 100644 --- a/packages/server/src/server/exports.ts +++ b/packages/server/src/server/exports.ts @@ -35,11 +35,7 @@ export { type ProviderOverride, type ProviderProfileModel, } from "./agent/provider-launch-config.js"; -export { - findExecutable, - quoteWindowsArgument, - quoteWindowsCommand, -} from "../utils/executable.js"; +export { findExecutable, quoteWindowsArgument, quoteWindowsCommand } from "../utils/executable.js"; export { execCommand, spawnProcess } from "../utils/spawn.js"; // Provider manifest (source of truth for provider definitions) diff --git a/packages/server/src/server/session.workspaces.test.ts b/packages/server/src/server/session.workspaces.test.ts index 631627257..688fbfb12 100644 --- a/packages/server/src/server/session.workspaces.test.ts +++ b/packages/server/src/server/session.workspaces.test.ts @@ -1776,9 +1776,9 @@ describe("workspace aggregation", () => { rmSync(tempDir, { recursive: true, force: true }); } - const response = emitted.find((message) => message.type === "create_paseo_worktree_response") as - | { type: "create_paseo_worktree_response"; payload: any } - | undefined; + const response = emitted.find( + (message) => message.type === "create_paseo_worktree_response", + ) as { type: "create_paseo_worktree_response"; payload: any } | undefined; expect(response?.payload.error).toBeNull(); expect(response?.payload.workspace).toMatchObject({ diff --git a/packages/server/src/server/test-utils/index.ts b/packages/server/src/server/test-utils/index.ts index eeff30370..56832c7e9 100644 --- a/packages/server/src/server/test-utils/index.ts +++ b/packages/server/src/server/test-utils/index.ts @@ -7,8 +7,5 @@ export { type DaemonEvent, type DaemonEventHandler, } from "./daemon-client.js"; -export { - createDaemonTestContext, - type DaemonTestContext, -} from "./daemon-test-context.js"; +export { createDaemonTestContext, type DaemonTestContext } from "./daemon-test-context.js"; export { useTempClaudeConfigDir } from "./claude-config.js"; diff --git a/packages/server/src/shared/messages.pull-request-timeline.test.ts b/packages/server/src/shared/messages.pull-request-timeline.test.ts index 804f4ea2f..4d8460ad8 100644 --- a/packages/server/src/shared/messages.pull-request-timeline.test.ts +++ b/packages/server/src/shared/messages.pull-request-timeline.test.ts @@ -172,20 +172,19 @@ describe("pull request timeline message schemas", () => { expect(parsed.payload.error).toEqual({ kind: "unknown", message: "x" }); }); - test.each([ - "rate_limited", - "", - undefined, - ])("normalizes malformed error kind %j to unknown", (kind) => { - const parsed = PullRequestTimelineResponseSchema.parse({ - type: "pull_request_timeline_response", - payload: { - error: { kind }, - }, - }); + test.each(["rate_limited", "", undefined])( + "normalizes malformed error kind %j to unknown", + (kind) => { + const parsed = PullRequestTimelineResponseSchema.parse({ + type: "pull_request_timeline_response", + payload: { + error: { kind }, + }, + }); - expect(parsed.payload.error).toEqual({ kind: "unknown", message: "" }); - }); + expect(parsed.payload.error).toEqual({ kind: "unknown", message: "" }); + }, + ); }); describe("checkout PR status compatibility", () => { diff --git a/packages/website/wrangler.toml b/packages/website/wrangler.toml index ee8aa4236..8732226d5 100644 --- a/packages/website/wrangler.toml +++ b/packages/website/wrangler.toml @@ -6,7 +6,7 @@ main = "@tanstack/react-start/server-entry" routes = [ { pattern = "paseo.sh", custom_domain = true }, - { pattern = "www.paseo.sh", custom_domain = true } + { pattern = "www.paseo.sh", custom_domain = true }, ] [observability] diff --git a/skills/paseo-chat/SKILL.md b/skills/paseo-chat/SKILL.md index 764ef54b3..24178cbb4 100644 --- a/skills/paseo-chat/SKILL.md +++ b/skills/paseo-chat/SKILL.md @@ -19,6 +19,7 @@ Load the **Paseo skill** first if you need CLI guidance for launching or messagi ## Rules When using chat: + - create a room with `paseo chat create` if you need a new room - inspect available rooms with `paseo chat ls` and `paseo chat inspect` - post with `paseo chat post` @@ -27,6 +28,7 @@ When using chat: - check chat often while working Mentions are active: + - write mentions inline in the message body as `@` to notify a specific agent immediately - use `@everyone` to notify all non-archived, non-internal agents - notifications are sent to the target agent without blocking the chat post @@ -99,10 +101,12 @@ paseo chat wait issue-456 --timeout 60s ## Defaults When creating a room: + - choose a short slug: `issue-456`, `pr-143-review`, `relay-cleanup` - give it a clear purpose When using a room: + - read only a bounded window before acting - post updates when they would help another agent or your future self - use `--reply-to` when responding to a specific message @@ -112,6 +116,7 @@ When using a room: - your own agent ID is available via `$PASEO_AGENT_ID` Typical things to post: + - status updates - blockers - handoffs diff --git a/skills/paseo-handoff/SKILL.md b/skills/paseo-handoff/SKILL.md index 7919335eb..c25838d4c 100644 --- a/skills/paseo-handoff/SKILL.md +++ b/skills/paseo-handoff/SKILL.md @@ -30,13 +30,13 @@ Parse `$ARGUMENTS` to determine: ### Provider Resolution -| User says | --provider | Mode | -|---|---|---| -| *(nothing)* | `codex/gpt-5.4` | `full-access` | -| `codex` | `codex/gpt-5.4` | `full-access` | -| `claude` | `claude/opus` | `bypass` | -| `opus` | `claude/opus` | `bypass` | -| `sonnet` | `claude/sonnet` | `bypass` | +| User says | --provider | Mode | +| ----------- | --------------- | ------------- | +| _(nothing)_ | `codex/gpt-5.4` | `full-access` | +| `codex` | `codex/gpt-5.4` | `full-access` | +| `claude` | `claude/opus` | `bypass` | +| `opus` | `claude/opus` | `bypass` | +| `sonnet` | `claude/sonnet` | `bypass` | Default is **Codex** with `gpt-5.4`. diff --git a/skills/paseo-loop/SKILL.md b/skills/paseo-loop/SKILL.md index d383c79b1..7e75fd9b1 100644 --- a/skills/paseo-loop/SKILL.md +++ b/skills/paseo-loop/SKILL.md @@ -108,6 +108,7 @@ paseo loop stop # Stop a running loop ### Worker prompt The worker prompt must be: + - self-contained - concrete about commands, files, branches, tests, PRs, or systems to inspect - explicit about what counts as progress this iteration @@ -115,6 +116,7 @@ The worker prompt must be: ### Verifier prompt The verifier prompt should: + - check facts, not offer fixes - cite commands, outputs, or file evidence - be specific about what "done" means diff --git a/skills/paseo-orchestrate/SKILL.md b/skills/paseo-orchestrate/SKILL.md index 7b4da87cd..5b144ab69 100644 --- a/skills/paseo-orchestrate/SKILL.md +++ b/skills/paseo-orchestrate/SKILL.md @@ -17,6 +17,7 @@ You are an end-to-end implementation orchestrator. You take a task from understa ## Prerequisites Load these skills before proceeding: + 1. **e2e-playwright** — if the task involves frontend/UI work ## Guard @@ -46,13 +47,13 @@ Merge with defaults for any missing fields. The file maps role categories to `"` - `target`: `"self"` - `every`: `"5m"` @@ -394,6 +399,7 @@ Do the following steps in order: ## Phase 7: Implement Execute phases from the plan sequentially. For each phase: + 1. Launch impl agent(s) with `background: true, notifyOnFinish: true` 2. Wait for notification 3. Verify (Phase 8) @@ -406,6 +412,7 @@ UI passes use `providers.ui` from preferences. All other impl work uses `provide ### TDD — Not Optional Every impl agent works TDD: + 1. Write a failing test that defines the expected behavior 2. Make it pass 3. Refactor if needed @@ -462,6 +469,7 @@ Run typecheck when done. Do NOT commit." ### Handling Blockers If an impl agent reports a blocker: + - Do NOT ask the user (in either mode) - Spin up a researcher to investigate - Spin up an impl agent to fix it @@ -477,13 +485,13 @@ After every implementation phase, deploy auditors to verify the work. Auditors a ### Which Auditors to Deploy -| Phase type | Auditors | -|-----------|----------| -| Refactor | `parity`, `regression`, `types` | -| Feature (backend) | `overeng`, `tests`, `regression`, `types` | +| Phase type | Auditors | +| ------------------ | ------------------------------------------------------ | +| Refactor | `parity`, `regression`, `types` | +| Feature (backend) | `overeng`, `tests`, `regression`, `types` | | Feature (frontend) | `overeng`, `tests`, `types`, `browser` (if applicable) | -| UI pass | `overeng`, `browser` (if applicable) | -| Test-only | `regression` | +| UI pass | `overeng`, `browser` (if applicable) | +| Test-only | `regression` | Deploy all relevant auditors in parallel — they're read-only so they don't conflict. @@ -622,6 +630,7 @@ Do NOT edit files." ### Interpreting Findings If any auditor reports issues: + 1. Check the auditor's activity with Paseo **get agent activity** for details 2. Direct the impl agent to fix them via Paseo **send agent prompt**, or launch a new impl agent if the old one is stale 3. Re-deploy the same auditor after fixes @@ -841,16 +850,16 @@ Every agent has exactly one role. The role determines what the agent does, wheth **Naming:** `-[-]` in kebab-case. -| Role | Job | Edits? | Prompt emphasis | -|------|-----|--------|----------------| -| `researcher` | Gathers info: codebase, docs, web, scripts | No | "Report what you find. Do not suggest solutions. Do not edit files." | -| `planner` | Creates implementation plan from research | No | "Think refactor-first. Design the target shape, not the steps." | -| `plan-reviewer` | Adversarially challenges a plan | No | "Challenge the plan. Find what's wrong, missing, or over-engineered." | -| `impl` | Writes code, works TDD | Yes | "Work TDD. Reshape existing code. Run typecheck AND run any tests you modified. Both must pass. Do NOT commit." | -| `tester` | Writes/fixes tests | Yes | "Verify behavior, not implementation. Run every test you modified and confirm it passes. A test change without running the test is not done." | -| `auditor` | Read-only verification | No | "Check [specialization]. Report YES/NO with evidence. Do NOT edit files." | -| `refactorer` | Targeted cleanup | Yes | "Fix [specialization] only. Run typecheck and any tests you touch. Do NOT commit." | -| `qa` | End-to-end QA, browser testing | No | "Test the actual user experience. Report with evidence." | +| Role | Job | Edits? | Prompt emphasis | +| --------------- | ------------------------------------------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `researcher` | Gathers info: codebase, docs, web, scripts | No | "Report what you find. Do not suggest solutions. Do not edit files." | +| `planner` | Creates implementation plan from research | No | "Think refactor-first. Design the target shape, not the steps." | +| `plan-reviewer` | Adversarially challenges a plan | No | "Challenge the plan. Find what's wrong, missing, or over-engineered." | +| `impl` | Writes code, works TDD | Yes | "Work TDD. Reshape existing code. Run typecheck AND run any tests you modified. Both must pass. Do NOT commit." | +| `tester` | Writes/fixes tests | Yes | "Verify behavior, not implementation. Run every test you modified and confirm it passes. A test change without running the test is not done." | +| `auditor` | Read-only verification | No | "Check [specialization]. Report YES/NO with evidence. Do NOT edit files." | +| `refactorer` | Targeted cleanup | Yes | "Fix [specialization] only. Run typecheck and any tests you touch. Do NOT commit." | +| `qa` | End-to-end QA, browser testing | No | "Test the actual user experience. Report with evidence." | Auditor specializations: `overeng`, `dry`, `tests`, `regression`, `types`, `browser`, `parity` Refactorer specializations: `dry`, `dead-code`, `naming` diff --git a/skills/paseo-orchestrate/references/final-qa.md b/skills/paseo-orchestrate/references/final-qa.md index 496a91072..75b0ae997 100644 --- a/skills/paseo-orchestrate/references/final-qa.md +++ b/skills/paseo-orchestrate/references/final-qa.md @@ -81,6 +81,7 @@ Do NOT edit files." ## If Issues Are Found If any final QA agent reports issues: + 1. Launch an impl or refactorer agent to fix them 2. Re-run the specific QA check that failed 3. Repeat until all checks pass diff --git a/skills/paseo-orchestrate/references/impl-phase.md b/skills/paseo-orchestrate/references/impl-phase.md index 568d3ed5c..e1c076002 100644 --- a/skills/paseo-orchestrate/references/impl-phase.md +++ b/skills/paseo-orchestrate/references/impl-phase.md @@ -5,6 +5,7 @@ Deploy impl agents to execute the plan phase by phase. Each phase is independent ## TDD — Not Optional Every impl agent works TDD: + 1. Write a failing test that defines the expected behavior 2. Make it pass 3. Refactor if needed @@ -17,6 +18,7 @@ If an impl agent finds a broken test, it fixes it. No "pre-existing failures." N Execute phases sequentially from the plan. Refactoring phases first, then feature phases, then UI passes. After each phase: + 1. Verify (see verification.md) 2. Fix any issues found 3. Re-verify @@ -29,11 +31,13 @@ Use the Paseo **create agent** tool with `background: true` and `notifyOnFinish: ### How to describe the work **Describe the problem, not the solution.** Your prompt should tell the agent: + - What's wrong or what needs to be built (the goal) - How it currently fails (error output, test output, user-visible behavior) - The acceptance criteria (what "done" looks like) **Do NOT tell the agent:** + - Which specific lines to change - What code to write - Which functions to call or which patterns to use @@ -95,6 +99,7 @@ Run typecheck when done. Do NOT commit." ## Handling Blockers If an impl agent reports a blocker: + - Do NOT ask the user (in either mode) - Spin up a researcher to investigate - Spin up an impl agent to fix it diff --git a/skills/paseo-orchestrate/references/planning-phase.md b/skills/paseo-orchestrate/references/planning-phase.md index 1cf406374..4fab45833 100644 --- a/skills/paseo-orchestrate/references/planning-phase.md +++ b/skills/paseo-orchestrate/references/planning-phase.md @@ -7,6 +7,7 @@ Deploy planners to create an implementation plan informed by research findings. Every planner prompt must emphasize this: the default agent instinct is to bolt new code on top of existing code. Resist this. The right approach: + 1. Study the existing code — understand why it's shaped the way it is 2. Design the target shape — what would the code look like if this feature had always existed? 3. Identify the refactoring gap — what needs to change so the new feature slots in cleanly? @@ -65,6 +66,7 @@ Challenge the plan: ## Multiple Planners (Order 3+) For cross-module tasks, deploy planners focusing on different slices: + - One for backend phases - One for frontend phases - One for test strategy @@ -74,6 +76,7 @@ Then deploy a plan-reviewer to challenge the combined plan. ## Iteration If the plan-reviewer finds significant issues, either: + 1. Send follow-up instructions via the Paseo **send agent prompt** tool to the planner 2. Launch a new planner if the original is stale diff --git a/skills/paseo-orchestrate/references/preferences.md b/skills/paseo-orchestrate/references/preferences.md index f2b1902a3..d18f43635 100644 --- a/skills/paseo-orchestrate/references/preferences.md +++ b/skills/paseo-orchestrate/references/preferences.md @@ -24,15 +24,16 @@ Maps role categories to `/` strings. These map directly to th - The part before `/` is the `agentType` (e.g., `codex`, `claude`, `opencode`) - The part after `/` is the `model` (e.g., `gpt-5.4`, `opus`) -| Category | Roles covered | -|----------|--------------| -| `impl` | impl, tester, refactorer | -| `ui` | impl agents doing UI/styling work | -| `research` | researcher | -| `planning` | planner, plan-reviewer | -| `audit` | auditor, qa | +| Category | Roles covered | +| ---------- | --------------------------------- | +| `impl` | impl, tester, refactorer | +| `ui` | impl agents doing UI/styling work | +| `research` | researcher | +| `planning` | planner, plan-reviewer | +| `audit` | auditor, qa | If a category is missing, use these defaults: + - `impl` -> `codex/gpt-5.4` - `ui` -> `claude/opus` - `research` -> `codex/gpt-5.4` @@ -44,6 +45,7 @@ If a category is missing, use these defaults: Freeform array of natural language strings. The user states preferences and the orchestrator appends them here. Read these at startup and weave them into your behavior contextually. Examples: + - "Prefer small, focused PRs over large bundled ones" - "Run E2E tests with Maestro, not Playwright" - "Always check mobile responsiveness" diff --git a/skills/paseo-orchestrate/references/triage.md b/skills/paseo-orchestrate/references/triage.md index bc9d65f41..4215338e8 100644 --- a/skills/paseo-orchestrate/references/triage.md +++ b/skills/paseo-orchestrate/references/triage.md @@ -18,46 +18,46 @@ State the order and briefly why: "Order 3 — touches server session management A contained change: fix a bug in one function, add a field to one type, update one component. -| Phase | Agents | -|-------|--------| -| Research | 1 researcher | -| Planning | 0 — orchestrator plans inline | -| Implement | 1 impl | -| Verify | 1-2 auditors | -| Cleanup | 0-1 refactorer | +| Phase | Agents | +| --------- | ----------------------------- | +| Research | 1 researcher | +| Planning | 0 — orchestrator plans inline | +| Implement | 1 impl | +| Verify | 1-2 auditors | +| Cleanup | 0-1 refactorer | ### Order 2 — Single module, few files A feature or fix within one package that touches 3-8 files. Might involve new types, new tests, a few component changes. -| Phase | Agents | -|-------|--------| -| Research | 2 researchers | -| Planning | 1 planner | +| Phase | Agents | +| --------- | ---------------- | +| Research | 2 researchers | +| Planning | 1 planner | | Implement | 1 impl per phase | -| Verify | 2-3 auditors | -| Cleanup | 1 refactorer | +| Verify | 2-3 auditors | +| Cleanup | 1 refactorer | ### Order 3 — Cross-module, multiple packages A feature that spans packages (e.g., server + app, or CLI + server). Multiple concerns, multiple file groups, likely needs interface changes between layers. -| Phase | Agents | -|-------|--------| -| Research | 3-4 researchers (one per area: backend, frontend, tests, external docs) | -| Planning | 2 planners + 1 plan-reviewer | -| Implement | 1-2 impl agents per phase | -| Verify | 3-4 auditors (overeng, tests, regression, types) | -| Cleanup | 1-2 refactorers | +| Phase | Agents | +| --------- | ----------------------------------------------------------------------- | +| Research | 3-4 researchers (one per area: backend, frontend, tests, external docs) | +| Planning | 2 planners + 1 plan-reviewer | +| Implement | 1-2 impl agents per phase | +| Verify | 3-4 auditors (overeng, tests, regression, types) | +| Cleanup | 1-2 refactorers | ### Order 4 — Architectural, system-wide A new subsystem, major refactor, or change that touches most of the codebase. New abstractions, new patterns, potentially breaking changes that need migration. -| Phase | Agents | -|-------|--------| -| Research | 5+ researchers across all relevant areas | -| Planning | 2+ planners (one per major area) + 2 plan-reviewers | -| Implement | 2+ impl agents per phase, sequenced carefully | -| Verify | Full auditor suite per phase | -| Cleanup | 2+ refactorers with different specializations | +| Phase | Agents | +| --------- | --------------------------------------------------- | +| Research | 5+ researchers across all relevant areas | +| Planning | 2+ planners (one per major area) + 2 plan-reviewers | +| Implement | 2+ impl agents per phase, sequenced carefully | +| Verify | Full auditor suite per phase | +| Cleanup | 2+ refactorers with different specializations | diff --git a/skills/paseo-orchestrate/references/verification.md b/skills/paseo-orchestrate/references/verification.md index 55fe31196..d137617d0 100644 --- a/skills/paseo-orchestrate/references/verification.md +++ b/skills/paseo-orchestrate/references/verification.md @@ -6,13 +6,13 @@ After every implementation phase, deploy auditors to verify the work. Auditors a Not every phase needs every auditor. Match auditors to the work: -| Phase type | Auditors | -|-----------|----------| -| Refactor | `parity`, `regression`, `types` | -| Feature (backend) | `overeng`, `tests`, `regression`, `types` | +| Phase type | Auditors | +| ------------------ | ------------------------------------------------------ | +| Refactor | `parity`, `regression`, `types` | +| Feature (backend) | `overeng`, `tests`, `regression`, `types` | | Feature (frontend) | `overeng`, `tests`, `types`, `browser` (if applicable) | -| UI pass | `overeng`, `browser` (if applicable) | -| Test-only | `regression` | +| UI pass | `overeng`, `browser` (if applicable) | +| Test-only | `regression` | Deploy all relevant auditors in parallel — they're read-only so they don't conflict. @@ -151,6 +151,7 @@ Do NOT edit files." ## Interpreting Findings If any auditor reports issues: + 1. Check the auditor's activity with the Paseo **get agent activity** tool for details 2. Direct the impl agent to fix them via the Paseo **send agent prompt** tool, or launch a new impl agent if the old one is stale 3. Re-deploy the same auditor after fixes diff --git a/skills/paseo/SKILL.md b/skills/paseo/SKILL.md index 61716caa2..115b783a3 100644 --- a/skills/paseo/SKILL.md +++ b/skills/paseo/SKILL.md @@ -193,6 +193,7 @@ paseo terminal send-keys --literal "raw text" # No special token `Enter`, `Tab`, `Escape`, `Space`, `BSpace`, `C-c`, `C-d`, `C-z`, `C-l`, `C-a`, `C-e` **Common pattern — launch a process and interact with it:** + ```bash id=$(paseo terminal create --name "my-shell" -q) paseo terminal send-keys "$id" "claude" Enter @@ -208,11 +209,13 @@ paseo terminal kill "$id" ## Available Models **Claude (default provider):** + - `--provider claude/haiku` — Fast/cheap, ONLY for tests (not for real work) - `--provider claude/sonnet` — Good for most tasks - `--provider claude/opus` — For harder reasoning, complex debugging **Codex:** + - `--provider codex/gpt-5.4` — Latest frontier agentic coding model (preferred for all engineering tasks) - `--provider codex/gpt-5.4-mini` — Cheaper, faster, but less capable @@ -236,6 +239,7 @@ Both `paseo run` and `paseo wait` block until the agent completes. Trust them. `paseo run` blocks by default and `--output-schema` returns structured JSON, making it easy to compose agents in bash loops and pipelines. **Detach + wait pattern for parallel work:** + ```bash api_id=$(paseo run -d --name "impl-api" "implement the API" -q) ui_id=$(paseo run -d --name "impl-ui" "implement the UI" -q)