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}}