Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ CORS is not a complete security boundary. It controls which browser origins can

Paseo validates the `Host` header on every HTTP request and every WebSocket upgrade against an allowlist (Vite-style semantics). By default, only `localhost`, `*.localhost`, and any literal IP address (IPv4 or IPv6) are accepted. Additional hostnames can be configured via `hostnames` in `config.json` or the `PASEO_HOSTNAMES` env var (comma-separated; entries beginning with `.` match a domain and its subdomains; the value `true` disables the allowlist entirely). Requests with unrecognized hosts are rejected with `403 Host not allowed`.

WebSocket upgrades from browser origins that are neither same-origin nor allowlisted are rejected — unless the upgrade carries a valid daemon-password bearer subprotocol. Proving knowledge of the password at upgrade time is a stronger credential than an origin allowlist entry, so this lets a daemon web UI served from one host connect directly to another password-protected daemon (for example, the visitor's own machine at `localhost:6767`) without CORS configuration. Daemons without a password keep the strict origin policy.

## Agent authentication

Paseo wraps agent CLIs (Claude Code, Codex, OpenCode) but does not manage their authentication. Each agent provider handles its own credentials. Paseo never stores or transmits provider API keys. Agents run in your user context with your existing credentials.
Expand Down
33 changes: 33 additions & 0 deletions docs/agent-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,39 @@ Provider session connection owns every process it spawns until the session is re
`connect()` must dispose that process before rethrowing; the manager cannot clean up a session it never
received.

## Delete

Delete is a **hard remove**: the Paseo agent record is permanently erased from disk and the provider's
own native session history is deleted too (best-effort). There is no undo.

Delete runs through `Session.deleteAgentFully` (`packages/server/src/server/session.ts`):

History uses a Manage mode; only archived sessions are selectable; batch delete via confirmDialog. No long-press.

1. Load the stored record (captures `provider` + `persistence` before the delete fence)
2. **Cascade-delete children** — any agent whose `paseo.parent-agent-id` label matches the deleted
agent is recursively deleted first, each emitting its own `agent_deleted` event
3. Set the delete fence so in-flight background writes are discarded
4. Close the runtime (kills the provider process if still running)
5. Flush queued persistence writes to prevent a background write from recreating the record
6. **Delete native session (best-effort)** — calls `AgentClient.deleteNativeSession?` with the
captured persistence handle; errors are logged as warnings, not propagated
7. Remove the Paseo agent record from storage and clear in-memory agent state
8. Emit `agent_deleted` to connected clients

### Provider coverage

| Provider | What gets deleted |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Claude | `~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl` + sidecar dir `<sessionId>/` |
| OpenCode | Native session via `session.delete` RPC against the running OpenCode server |
| Codex | Rollout JSONL file resolved via `resolveCodexRolloutFile` (file delete only; no API call) |
| Pi | Session file path from `nativeHandle` |
| OMP | Session file path from `nativeHandle` |
| All ACP providers | Capability-gated ACP `session/delete` via a short-lived probe (`ACPAgentClient.deleteNativeSession`). Agents that omit `sessionCapabilities.delete` skip this step; soft vs hard delete is agent-defined. |
| Grok (ACP) local fallback | Also removes `~/.grok/sessions/<encodeURIComponent(cwd)>/<sessionId>/` (Grok does not advertise protocol delete today). |
| Cursor (ACP) local fallback | Also removes `~/.cursor/acp-sessions/<sessionId>/` (Cursor ACP returns Method not found for `session/delete` today). |

## Tabs vs archive

These are two distinct concepts that used to be conflated:
Expand Down
2 changes: 2 additions & 0 deletions docs/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Each agent is stored as a separate JSON file, grouped by project directory.
| `runtimeInfo` | `RuntimeInfo?` | Live runtime state (see below) |
| `features` | `AgentFeature[]?` | Provider-reported features (toggles/selects) |
| `persistence` | `PersistenceHandle?` | Handle for resuming sessions |
| `lastUsage` | `AgentUsage?` | Last-known token/cost usage (context-window meter fields included). Persisted so a reopened or restart-rehydrated agent shows its prior context-window meter before a live session re-reports usage. Written on the same snapshot flushes that already persist usage updates, so it adds no extra disk writes. |
| `lastError` | `string?` (nullable) | Last error message, if any |
| `requiresAttention` | `boolean?` | Whether the agent needs user attention |
| `attentionReason` | `"finished" \| "error" \| "permission"?` | Why attention is needed |
Expand Down Expand Up @@ -214,6 +215,7 @@ Single file, validated with `PersistedConfigSchema`.
}
},
features: {
// Voice/dictation are opt-in: missing `enabled` means off (no model load / no composer controls).
dictation: { enabled, stt: { provider, model, language, confidenceThreshold } },
voiceMode: { enabled, llm, stt: { provider, model, language }, turnDetection, tts: { provider, model, voice, speakerId, speed } }
},
Expand Down
16 changes: 15 additions & 1 deletion docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ Cards inside a section sit closer than sections. Rows inside a card touch — on

Rows have generous vertical padding: roughly 16px of content plus 16px of vertical padding for settings rows, 8–12px for sidebar list items where many rows must fit. Compressing rows below the established density to fit more on the screen is wrong. Too many rows means more cards or more sections, not smaller rows.

### Agent reply vertical rhythm

The agent stream owns between-item spacing via `gapBelow` from `packages/app/src/agent-stream/spacing.ts` (`STREAM_ITEM_GAP`). Process rows (thinking / tool / todo) must not add their own outer `marginBottom` on `ExpandableBadge` — stacking badge margin with stream gap is how reply rhythm goes uneven.

Horizontal gutters belong on the scroll content container only: native FlatList uses `listContentContainer` (`spacing[3]` on xs / `spacing[4]` on md). The web virtualizer in `strategy-web.tsx` must mirror the same tokens — a tighter hardcoded mobile value makes expanded tool cards look edge-to-edge.

| Gap | Token | px | Use |
| ---------- | ------------ | --- | -------------------------------------------------------------- |
| continuous | `spacing[2]` | 8 | Within a process sequence; split blocks of one assistant reply |
| related | `spacing[3]` | 12 | Process row ↔ assistant prose |
| turn | `spacing[4]` | 16 | Turn boundaries (e.g. user message → tools) |

Assistant markdown prose in `packages/app/src/styles/markdown-styles.ts` follows the same scale: paragraph bottom 8, h1/h2 top 16 / bottom 8, table / fence vertical 8. Fence and `code_block` must set theme `surface2` background, `border`, and `foreground` explicitly — `react-native-markdown-display` defaults to light `#f5f5f5` / `#CCCCCC`, which leaks into dark themes if left unoverridden.

The whitespace is the design.

---
Expand Down Expand Up @@ -167,7 +181,7 @@ Terminology:

## 10. States

Loading is inline by default. `<LoadingSpinner size={14} color={foregroundMuted} />` sits next to the thing it relates to (`packages/app/src/screens/settings/providers-section.tsx:227-231`). Page-level loading is a centered `<LoadingSpinner size="large">` (`packages/app/src/screens/sessions-screen.tsx:69-72`). Card-level loading is a single short line, not a spinner. In-row dropdown items use `<DropdownMenuItem status="pending" pendingLabel="Removing...">`; the menu item handles its own pending state.
Loading is inline by default. `<LoadingSpinner size={14} color={foregroundMuted} />` sits next to the thing it relates to (`packages/app/src/screens/settings/providers-section.tsx:227-231`). Page-level loading is a centered thin `<LoadingSpinner size="large">` (18px) (`packages/app/src/screens/sessions-screen.tsx:69-72`). Card-level loading is a single short line, not a spinner. In-row dropdown items use `<DropdownMenuItem status="pending" pendingLabel="Removing...">`; the menu item handles its own pending state.

Empty states are short noun phrases. Centered, muted, one or two lines. Sessions screen pairs the empty noun with a single ghost button to navigate back (`packages/app/src/screens/sessions-screen.tsx:74-81`); that pairing is the maximum elaboration. Illustrations and CTAs disguised as empty states are wrong.

Expand Down
28 changes: 28 additions & 0 deletions docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ The only built-in ACP provider today is `copilot` (`copilot-acp-agent.ts`). `Gen

Copilot custom agents are exposed through ACP session config, not the slash-command list. When custom agents are available, Copilot returns a select config option with `id: "agent"` and `category: "_agent"`; Paseo maps that to the `agent` provider feature. Copilot uses the agent display name as the option value, and the blank value means the default Copilot agent.

Cursor ACP advertises `clientCapabilities._meta.parameterizedModelPicker: true` so the agent exposes model parameters as session config options instead of collapsed variant ids. Paseo maps Cursor's `context` (context length) and `fast` select options to provider features. When both `session/set_model` and a model config option are available, prefer `session/set_config_option` for model changes — Cursor's `set_model` path discards the refreshed config options, which would leave context/fast stuck on the previous model.

### Direct

Implement the `AgentClient` and `AgentSession` interfaces from `agent-sdk-types.ts` yourself. This gives full control but requires you to handle process management, streaming, permissions, and session persistence from scratch.
Expand Down Expand Up @@ -89,6 +91,32 @@ Keep the protocol shape provider-agnostic. Do not add provider-specific renderer

Kimi Code usage follows the CLI-managed credential file at `KIMI_CODE_HOME` or `~/.kimi-code/credentials/kimi-code.json`; do not probe the legacy `~/.kimi` path as the primary source for current Kimi Code installs.

Cursor plan usage resolves an access token from, in order: `CURSOR_ACCESS_TOKEN` / `CURSOR_TOKEN`, the desktop app's `state.vscdb` (`cursorAuthStatus`), then the Cursor Agent CLI session file at `~/.config/cursor/auth.json`. Headless hosts that only have `cursor-agent` logged in need that CLI auth file (or an env token); without it the fetcher returns `unavailable` and the usage UI hides Cursor.

Cursor `GetCurrentPeriodUsage` amounts are cents. `totalSpend = includedSpend + bonusSpend`, but the Ultra dashboard is percentage-first (Total / First-party / API). When `apiPercentUsed` is present, the dollar bar is **`limit × apiPercentUsed / 100`** against the included API cap (e.g. 39.2% of $400 → $156.80) — do not use raw `includedSpend`/`totalSpend`, which often pin included at the cap while API % is still mid-cycle. `bonusSpend` is free retail overage, not "already used" of the $400 rail.

## Context-window meter

The context-window meter is separate from plan usage. It reads `AgentUsage.contextWindowMaxTokens` / `contextWindowUsedTokens`, which flow from `usage_updated` / `turn_completed` stream events into the agent's `lastUsage` (persisted to the stored record so reopened agents show their last-known ring). Each provider sources these fields differently:

- **Claude** — `contextWindowMaxTokens` is baked into the curated model manifest (`claude/model-manifest.ts`); used tokens arrive from the SDK.
- **Grok** — reads the CLI's per-session `signals.json` (`grok-context-usage.ts`) for both max and used.
- **Cursor / OpenCode** — resolve max from catalog model metadata; used tokens arrive from ACP context updates.
- **Codex** — the app-server's `thread/tokenUsage/updated` notification is unreliable across CLI versions and the model list carries no context window, so `codex/context-usage.ts` reads the latest `token_count` event from the session rollout `.jsonl` (`~/.codex/sessions/{date}/rollout-{ts}-{id}.jsonl`) as the authoritative fallback. It reads only a bounded tail of the file (rollouts can reach hundreds of MB) and merges `model_context_window` + `last_token_usage.total_tokens` into `lastUsage` on connect (reopened sessions) and turn completion. `toAgentUsage` accepts both the flat and `info`-wrapped shapes.

### Grok ACP message envelopes

Grok injects model-facing XML into the ACP user/tool channels. `GrokACPAgentClient` normalizes these before they hit the timeline (`grok-message-tags.ts`):

| Envelope | Treatment |
| -------------------------------------------------- | ---------------------------------------------------------------------------- |
| `<system-reminder>`, `<user_info>`, `<git_status>` | Hide when they are the whole message; strip when prefixed onto a real prompt |
| `<user_query>` | Unwrap; show the inner text |
| `<spoken-input>` / `<instruction>` | Unwrap spoken text; drop the instruction block |
| `<workspace_result>` (tool output) | Unwrap via `toolSnapshotTransformer` |

Hook points on the ACP base: `userMessageTextTransformer` (return `null` to suppress) and `toolSnapshotTransformer`.

---

## ACP Provider Checklist
Expand Down
15 changes: 15 additions & 0 deletions docs/terminal-activity.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ Codex hook mapping:
- `PermissionRequest` → `needs-input`
- `Stop` → `idle`

Grok hook mapping:

- `UserPromptSubmit`, `PreToolUse`, `PostToolUse` → `running`
- `Stop`, `StopFailure`, `SessionEnd` → `idle`
- `Notification` with `reason` or `matcher` equal to `idle_prompt` → `needs-input`

Cursor hook mapping (Cursor `hooks.json` camelCase events):

- `beforeSubmitPrompt`, `preToolUse`, `postToolUse` → `running`
- `stop`, `sessionEnd` → `idle`

Cursor’s documented hooks target the Cursor agent loop via `~/.cursor/hooks.json`. Paseo still installs them so Cursor Agent / any `cursor-agent` build that loads that file can report activity. Outside a Paseo terminal they stay inert because the command is gated on `PASEO_TERMINAL_ID`. If a given `cursor-agent` build ignores user hooks, the Cursor terminal profile still launches; activity dots simply will not update until the CLI honors hooks.

OpenCode uses a server plugin instead of command hooks. The plugin listens to OpenCode bus events and emits these Paseo hook events:

- `session.status` with `busy` or `retry` → `running`
Expand Down Expand Up @@ -89,6 +102,8 @@ When enabled, Paseo installs provider hooks globally:

- Claude hooks are written to `~/.claude/settings.json` (or `CLAUDE_CONFIG_DIR/settings.json` when that override is set).
- Codex hooks are written to `~/.codex/hooks.json` (or `CODEX_HOME/hooks.json` when that override is set). Codex supports a native `commandWindows`, so each Paseo hook includes both POSIX and Windows commands. Non-managed Codex hooks are trust-gated by Codex; users may see Codex's hook review prompt before the hook runs.
- Grok hooks are written to `~/.grok/hooks/paseo-terminal-activity.json` (or `$GROK_HOME/hooks/paseo-terminal-activity.json` when that override is set). The dedicated file keeps Paseo’s markers out of other Grok hook files.
- Cursor hooks are merged into `~/.cursor/hooks.json` (or `CURSOR_CONFIG_DIR/hooks.json` when that override is set) using Cursor’s flat `{ version, hooks }` shape.
- OpenCode gets a self-contained plugin at `$XDG_CONFIG_HOME/opencode/plugins/paseo-terminal-activity.js` (or `~/.config/opencode/plugins/paseo-terminal-activity.js` when XDG is unset; `OPENCODE_CONFIG_DIR` still wins when set).

Installation is marker-based/idempotent for config hooks and exact-file/idempotent for the OpenCode plugin. Paseo preserves user hooks, removes only its own marker-matched command hooks, and leaves hooks installed across daemon shutdown. Outside a Paseo terminal they are inert because the command or plugin is gated on `PASEO_TERMINAL_ID`.
Expand Down
Loading