Files
pi-agent-integrated/pi-web/AGENTS.md
T

12 KiB

Pi Web - Development Notes

Quick Start

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

{"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