diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fdc470b..faf5ae10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,15 +12,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: 'go.mod' - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '20' cache: 'npm' @@ -37,15 +37,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: 'go.mod' - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '20' cache: 'npm' @@ -60,7 +60,7 @@ jobs: # the key changes only when @playwright/test is bumped. - name: Cache Playwright browsers id: playwright-cache - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('e2e/package-lock.json') }} @@ -86,7 +86,7 @@ jobs: - name: Upload Playwright report if: ${{ failure() }} - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: playwright-report path: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 91f0c849..00488998 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,15 +15,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: 'go.mod' - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '20' cache: 'npm' @@ -56,15 +56,15 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: 'go.mod' - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '20' cache: 'npm' @@ -87,7 +87,7 @@ jobs: go build -ldflags="-s -w -X main.version=${VERSION}" -o "${output}" ./cmd/pi-web - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: pi-web-${{ matrix.goos }}-${{ matrix.goarch }} path: pi-web-${{ matrix.goos }}-${{ matrix.goarch }}* @@ -98,7 +98,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download all artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: artifacts pattern: pi-web-* @@ -120,10 +120,10 @@ jobs: id-token: write steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '20' registry-url: 'https://registry.npmjs.org' diff --git a/AGENTS.md b/AGENTS.md index a93063cd..b7c20983 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,134 +1,38 @@ -# pi-web — Agent Context +# Development Rules -pi-web is a local Go HTTP server that renders [pi](https://pi.dev) coding agent sessions in the browser. It reads JSONL session files from `~/.pi/agent/sessions/`, serves them with a dark-themed UI, and enables browser-based chat continuation via headless `pi --mode rpc` workers. +- Only create an abstraction if it's actually needed +- Prefer clear function/variable names over inline comments +- Avoid helper functions when a simple inline expression would suffice +- Use knip to remove unused code if making large changes -## Docs to Read First +## Docs -Before making structural changes, read the relevant doc in `docs/`: +Read the relevant doc in `@docs/` before structural changes, and update the matching doc whenever your change makes it out of date. -| If you're... | Read | -|---|---| -| New to the codebase | `docs/architecture/system-overview.md` | -| Changing frontend build/template embedding | `docs/dev/templates-vs-web.md` | -| Working on chat, SSE, or live-reload | `docs/sequence-flows/chat.md`, `docs/sequence-flows/live-reload.md` | -| Working on artifacts | `docs/sequence-flows/artifacts.md` | -| Working on annotations | `docs/sequence-flows/annotations.md` | -| Working on the @mention path autocomplete | `docs/sequence-flows/mention-autocomplete.md` | -| Working on btw floating scratch-chats | `docs/sequence-flows/btw.md` | -| Working on export/share | `docs/sequence-flows/share.md` | -| Working on the worker metrics dashboard | `docs/dev/metrics-dashboard.md` | -| Writing or debugging E2E / browser tests | `docs/dev/e2e-testing.md` | - -The most important doc for frontend work is **`docs/dev/templates-vs-web.md`**. The live app is a **Svelte 5 SPA** (`web/src/`) served through a single Go-embedded shell (`internal/ui/embedded/app.html`); export/share is a separate static snapshot rendered from `internal/ui/embedded/session.html`. The export bundle is built by Vite from `web/src/export/export-entry.js`, which **reuses the live session modules** in `web/src/session/` — there is no longer a hand-maintained parallel copy. - -## Tech Stack - -- **Backend:** Go 1.25+ (`fsnotify`, `x/sys`) -- **Frontend:** Svelte 5 SPA (reactive `SessionDataModel`; the session viewer is fully component-driven), Vite, Vitest + `@testing-library/svelte` + jsdom -- **Session data:** JSONL from `~/.pi/agent/sessions/`; pi-web normally treats existing files as read-only, except it appends a `session_info` metadata line for browser rename and for auto-titling (the latter marked `autoTitle:true`). New-session creation writes a fresh JSONL file with a header and optional implicit model/thinking entries. -- **Live updates:** SSE driven by `fsnotify` file watchers -- **Auth:** `PI_WEB_TOKEN` required for non-loopback binds (e.g. Tailscale) - -## Architecture - -### Backend (`internal/`) -| Package | Purpose | -|---------|---------| -| `server` | HTTP handlers, SSE plumbing, file watcher, chat orchestration (incl. btw scratch-chats, auto-titling, metrics, push) | -| `sessions` | JSONL parsing, caching, lookup by ID, new-session/fork/clone creation | -| `auth` | Token middleware (`Authorization: Bearer`, `X-Pi-Token`, `?token=` cookie) | -| `workers` | Per-session `pi --mode rpc` lifecycle (spawn, reuse, reap, crash recovery) | -| `rpc` | Subprocess wrapper + streaming previews | -| `share` | Export a session to a private GitHub Gist via the `gh` CLI (`Runner` interface) | -| `git` | Low-level git queries used by `/api/git/*` (branch info, rename) | -| `files` | Bounded filesystem walk backing the `@mention` autocomplete (`GET /api/files`) | -| `updater` | Self-update version/changelog checks (powers `/api/version`, `/api/check-update`) | -| `render` | Tiny shared HTTP/render helpers (Vite manifest lookup, JSON responses) | -| `frontend` | Vite output embedding + manifest parsing + static asset serving | -| `agentdir` | Resolves the `~/.pi/agent` base directory | -| `ui` | Live SPA shell + export-snapshot rendering (see Key Files) | - -### Frontend - -The live UI is a Svelte 5 SPA. `web/src/main.js` mounts `App.svelte` into `#spa-root`; `App.svelte` routes by `window.location.pathname` (`/`, `/session`, `/settings`, `/login`) and mounts shared live chrome such as `VersionController`. The app routes are Svelte-component-driven: `SessionsPage.svelte` owns the sessions index (list/cards, command palette, home menu, new-session/project modals, index-wide SSE status); `SettingsPage.svelte` hydrates settings and each `components/settings/*Settings.svelte` section owns its controls; `SessionPage.svelte` creates the reactive `SessionDataModel` (`session/data/session-data.svelte.js`), provides it via context, and renders the viewer as self-contained Svelte components (tree, the message pane `SessionContent`→`SessionEntry`→`ToolCall`, header, right sidebar + artifacts/annotations, chat composer, live reload, modals, btw, cat). There is no `session.js` orchestrator — its glue lives in `SessionPage`'s `onMount` plus `session/{session-globals,session-content-runtime,lazy-highlight}.js`. The runners/renderers (chat composer + selectors, live reload + its SSE/scroll/stats primitives, the entry renderer) have been absorbed into their components; `session/chat/` + `session/live/` now hold only pure/shared helpers (`chat-api`, `git-api`, `chat-selectors`, `done-notifier`, `chat-preview`). - -| Directory | Purpose | -|-----------|---------| -| `web/src/main.js`, `web/src/App.svelte` | SPA entry + client-side router | -| `web/src/routes/` | Page components: `SessionsPage`, `SessionPage`, `SettingsPage`, `LoginPage` | -| `web/src/components/` | Extracted Svelte UI: `session/`, `index/`, `settings/`, `shared/` | -| `web/src/index/` | Pure sessions-index helpers (normalization, grouping/filtering, API wrappers); `SessionsPage.svelte` drives the UI | -| `web/src/session/` | Session-viewer support: reactive `data/session-data.svelte.js` model, pure `tree/`/`render/`/`navigation/` helpers, the relocated live glue (`session-globals.js`, `session-content-runtime.js`, `lazy-highlight.js`), and pure/shared `chat/` + `live/` helpers. Orchestrated by `SessionPage.svelte` (no `session.js`) | -| `web/src/session/chat/` | Pure/shared chat helpers: `chat-api`/`git-api` (fetch), `chat-selectors` (pure model/thinking logic), `done-notifier` (notification/sound/push, shared with settings). The composer runtime + the model/thinking/slash/`@mention` selectors live in `ChatComposer.svelte`'s ` + + {{.ThemeCss}} + + {{.ThemeBoot}}