mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 02:59:35 +00:00
186 lines
12 KiB
Markdown
186 lines
12 KiB
Markdown
# Pi Web - Development Notes
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
npm run dev # port 30141
|
|
```
|
|
|
|
Typecheck: `node_modules/.bin/tsc --noEmit`
|
|
Lint: `npm run lint`
|
|
**Never run `next build` during dev** — pollutes `.next/` and breaks `npm run dev`.
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Browser Next.js frontend Pi Agent Server
|
|
│ │ │
|
|
├─ GET/POST /api/* ────▶│ rewrite to :30142 ─────▶│ Hono routes
|
|
├─ SSE connect ────────▶│ transparent proxy ─────▶│ AgentSession events
|
|
│◀── data: {...} ───────│◀────────────────────────│
|
|
```
|
|
|
|
`pi-web/` owns only React/Next.js UI code. All filesystem access, authentication,
|
|
configuration, Pi packages, API routes, SSE streams, and AgentSession state live
|
|
in `../server/`. The frontend proxies `/api/*` through `next.config.ts`; restarting
|
|
or hot-reloading Next.js must not interrupt an active Agent session.
|
|
|
|
**Session browsing** (read-only): the backend reads `.jsonl` files through SDK
|
|
`SessionManager` helpers and `server/src/lib/session-reader.ts`.
|
|
**Sending a message**: `startRpcSession()` in `server/src/lib/rpc-manager.ts`
|
|
creates and retains the AgentSession in the standalone backend process.
|
|
|
|
---
|
|
|
|
## File Map
|
|
|
|
```
|
|
../server/src/
|
|
index.ts Hono server, health endpoint, route registration
|
|
security.ts host/origin gate for every /api request
|
|
routes/agent.ts Agent creation, commands, state, and SSE
|
|
routes/auth.ts OAuth/device-code and API-key routes
|
|
routes/config.ts extensions, MCP, vision, and plugin configuration
|
|
routes/files.ts guarded file reading, preview, download, and upload
|
|
routes/misc.ts cwd, runtime, file index, worktrees, and project trust
|
|
routes/models.ts Git state, models, discovery, catalog, and tests
|
|
routes/sessions.ts session listing, context, naming, export, and deletion
|
|
routes/skills.ts skill listing, search, install, and update
|
|
lib/rpc-manager.ts AgentSessionWrapper registry and startRpcSession()
|
|
lib/session-reader.ts SessionManager wrappers and session path cache
|
|
|
|
lib/
|
|
agent-client.ts typed fetch helper for /api/agent commands
|
|
draft-store.ts local draft persistence helpers
|
|
file-paths.ts client-side path encoding helpers
|
|
markdown.ts shared markdown helpers
|
|
pi-types.ts structural copies of API/session types; no Pi dependency
|
|
tool-presets.ts PRESET_NONE/DEFAULT/FULL + getPresetFromTools()
|
|
types.ts shared TypeScript types
|
|
normalize.ts normalizeToolCalls() — field name mismatch between file format and our types
|
|
|
|
components/
|
|
AppShell.tsx layout + URL state + tab management
|
|
SessionSidebar.tsx session tree + FileExplorer
|
|
ChatWindow.tsx chat composition + completion sound wrapper
|
|
ChatInput.tsx input bar + model/thinking/tools/compact controls
|
|
MessageView.tsx renders one message (user/assistant/toolCall/toolResult)
|
|
BranchNavigator.tsx in-session branch switcher
|
|
ChatMinimap.tsx scroll minimap alongside the message list
|
|
MarkdownBody.tsx markdown renderer
|
|
ModelsConfig.tsx modal for editing models.json (opened from sidebar bottom)
|
|
PluginsConfig.tsx modal for installed package plugins
|
|
SkillsConfig.tsx modal for loaded/search/installable skills
|
|
FileExplorer.tsx file tree inside sidebar
|
|
FileIcons.tsx file icon helpers
|
|
FileViewer.tsx file content in a tab
|
|
TabBar.tsx tab bar (Chat + open file tabs)
|
|
|
|
hooks/
|
|
useAgentSession.ts messages + streaming + SSE + fork/navigate/reconciliation logic
|
|
useAudio.ts completion sound + browser AudioContext unlock
|
|
useDragDrop.ts shared drag/drop state
|
|
useIsMobile.ts responsive breakpoint hook
|
|
useTheme.ts theme state
|
|
```
|
|
|
|
---
|
|
|
|
## Key Design Decisions & Traps
|
|
|
|
### AgentSession lifecycle (`server/src/lib/rpc-manager.ts`)
|
|
- One `AgentSessionWrapper` per session id, keyed in `globalThis.__piSessions`
|
|
- The standalone backend process survives Next.js hot reloads and restarts
|
|
- Idle timeout: 10 minutes. Concurrent `startRpcSession()` calls share a single start Promise (`globalThis.__piStartLocks`)
|
|
|
|
### Fork must destroy the wrapper immediately
|
|
`AgentSession.fork()` **mutates the wrapper's inner state in-place** — after fork, `inner.sessionId` is the *new* session's id. If the wrapper stays alive in the registry under the old id, the next request gets the already-forked state and subsequent forks produce a corrupt `parentSession` chain.
|
|
|
|
**Fix**: `send("fork")` captures `newSessionId`, then calls `this.destroy()` before returning. The next request for the original session reloads a clean AgentSession from the original file.
|
|
|
|
### Two kinds of branching — don't confuse them
|
|
- **Fork** (Fork button on user message): creates a new independent `.jsonl` file. Shown as a child in the sidebar tree via `parentSession` header field.
|
|
- **In-session branch** (Continue button / BranchNavigator): calls `navigate_tree` within the same file. Multiple entries share the same `parentId`. Switching between them calls `/api/sessions/[id]/context?leafId=`.
|
|
|
|
### Session files can be fully rewritten
|
|
`parentSession` in the header is **display metadata only** — has zero effect on chat content. Safe to `writeFileSync` the entire file (pi does this itself during migrations). Used when cascade-reparenting children on delete.
|
|
|
|
### ToolCall field normalization
|
|
Pi stores toolCall blocks as `{type:"toolCall", id, name, arguments}` but `ToolCallContent` uses `{toolCallId, toolName, input}`. `normalizeToolCalls()` in `lib/normalize.ts` handles this — called in both `session-reader.ts` (file load) and `ChatWindow.handleAgentEvent()` (streaming).
|
|
|
|
### New session tool preset
|
|
Tool names are passed at session creation (`POST /api/agent/new` → `toolNames[]`). For existing sessions, the active preset is inferred on mount via `get_tools` → `getPresetFromTools()`. When tools are fully disabled (`toolNames = []`), `rpc-manager.ts` passes an empty tool allow-list and forces `agent.state.systemPrompt = ""` after startup/reload/resource discovery.
|
|
|
|
### Model defaults for new sessions
|
|
`GET /api/models` returns `defaultModel` read from `~/.pi/agent/settings.json`. `ChatWindow` pre-selects this on mount for new sessions.
|
|
|
|
### SSE reconnect on page refresh mid-stream
|
|
On `ChatWindow` mount, `GET /api/agent/[id]` is called. If `state.isStreaming === true`, SSE is reconnected automatically. `thinkingLevel` and `isCompacting` are also synced from this response.
|
|
|
|
### Compaction SSE events
|
|
Newer pi emits `compaction_start` / `compaction_end`; older versions emitted `auto_compaction_start` / `auto_compaction_end`. `handleAgentEvent` accepts both sets to keep `isCompacting` in sync. Manual compact is a blocking POST — the button stays disabled until the response returns.
|
|
|
|
### Running state SSE + reconciliation
|
|
- The sidebar listens to `/api/agent/running/events`, backed by `subscribeRunningSessions()` in `server/src/lib/rpc-manager.ts`, so running badges update without polling.
|
|
- `useAgentSession` still treats per-session SSE as primary for chat events, but while a run is active it periodically calls `GET /api/agent/[id]` and also reconciles on `visibilitychange`/`online`. This fixes missed `agent_end` events from background tabs or half-open connections.
|
|
- Prompt runs use a monotonic run id; late SSE or slow reconciliation responses from an old run must be ignored so they cannot resurrect stale streaming bubbles.
|
|
|
|
### Worktrees and project grouping
|
|
- `server/src/lib/worktree.ts` resolves linked worktree top-levels back to the main repo `projectRoot`; `listAllSessions()` attaches that to each `SessionInfo` so all worktrees for one repo are grouped together in the sidebar.
|
|
- Worktree operations are served by `/api/worktrees` and guarded by the same allowed-root rules as `/api/files`.
|
|
- New worktrees are created under `<repoRoot>-worktrees/<sanitized-branch>`. Existing branches are reused; otherwise `git worktree add -b` creates the branch.
|
|
- Removing a dirty worktree returns `409` with `{ dirty: true }` so the UI can ask before retrying with `force`.
|
|
- Sessions whose cwd points at a removed worktree are inferred back into the main project instead of becoming a phantom project row.
|
|
|
|
### File access allow-list
|
|
- `/api/files` is intentionally not a general filesystem browser. Allowed roots come from session cwds, their resolved project roots, `~/pi-cwd-*`, and roots explicitly added with `allowFileRoot()`.
|
|
- `/api/cwd/validate`, `/api/default-cwd`, and `/api/worktrees` call `allowFileRoot()` when they make a new location browsable.
|
|
|
|
### Plugins and skills
|
|
- `/api/plugins` uses pi's `SettingsManager` + `DefaultPackageManager` for global/project package install, remove, update, enable, and disable. Disabling writes empty `extensions/skills/prompts/themes` arrays for that package entry.
|
|
- `/api/skills` uses `DefaultResourceLoader` so settings paths, package skills, and project `.agents/skills` are listed the same way the runtime sees them.
|
|
- Skill toggling edits only the `disable-model-invocation` frontmatter key on the target `SKILL.md`; keep that surgical so user formatting survives.
|
|
- `/api/skills/install` shells through `npx skills add ... --agent pi`; project installs run with the selected cwd.
|
|
|
|
### Auth and model config
|
|
- `ModelsConfig` combines models from `~/.pi/agent/models.json` with provider auth status from pi's `AuthStorage`/`ModelRegistry`.
|
|
- OAuth/device-code/manual-code flows are streamed by `GET /api/auth/login/[provider]`; manual code responses POST back with a short-lived token stored in `globalThis.__piLoginCallbacks`.
|
|
- API-key routes store and remove keys through `AuthStorage`. Status endpoints must never return the raw key.
|
|
- The model test endpoint is registered in `server/src/routes/models.ts` as `/api/models-config/test`; `/api/models/test` is not a real route.
|
|
|
|
### Completion sound
|
|
- `hooks/useAudio.ts` stores the toggle in `localStorage` as `pi-sound-enabled` and reuses one `AudioContext`.
|
|
- Browser autoplay policy means sound must be unlocked from a user gesture; `ChatInput` calls the unlock hook from interactive controls, and `ChatWindow` plays the tone from `onAgentEnd`.
|
|
|
|
### Exported session HTML
|
|
- `/api/sessions/[id]/export` delegates to pi's export helper, then patches recursive tree helpers in the generated HTML to iterative versions so very deep linear sessions do not overflow the browser call stack.
|
|
|
|
## Pi Session File Format
|
|
|
|
Location: `~/.pi/agent/sessions/<encoded-cwd>/<timestamp>_<uuid>.jsonl`
|
|
|
|
```jsonl
|
|
{"type":"session","version":3,"id":"<uuid>","timestamp":"...","cwd":"/path","parentSession":"/abs/path/to/parent.jsonl"}
|
|
{"type":"model_change","id":"<8hex>","parentId":null,"provider":"zenmux","modelId":"claude-sonnet-4-6","timestamp":"..."}
|
|
{"type":"message","id":"<8hex>","parentId":"<8hex>","message":{"role":"user","content":"..."}}
|
|
{"type":"message","id":"<8hex>","parentId":"<8hex>","message":{"role":"assistant","content":[...],...}}
|
|
{"type":"message","id":"<8hex>","parentId":"<8hex>","message":{"role":"toolResult","toolCallId":"...","content":[...]}}
|
|
{"type":"compaction","id":"<8hex>","parentId":"<8hex>","summary":"...","firstKeptEntryId":"<8hex>","tokensBefore":N}
|
|
{"type":"session_info","id":"...","parentId":"...","name":"user-defined name"}
|
|
```
|
|
|
|
`entryIds[]` in `SessionContext` is a parallel array to `messages[]` — maps each displayed message back to its `.jsonl` entry id, used for fork and navigate_tree calls.
|
|
|
|
---
|
|
|
|
## CSS Variables (`app/globals.css`)
|
|
|
|
```
|
|
--bg --bg-panel --bg-hover --bg-selected --border
|
|
--text --text-muted --text-dim
|
|
--accent --user-bg --tool-bg
|
|
--font-mono
|
|
```
|