From c3c4717dd676a11995d102e51ce51b2e8855ab97 Mon Sep 17 00:00:00 2001 From: setkyar Date: Fri, 18 Sep 2026 20:25:51 +0800 Subject: [PATCH 1/3] docs: align guides with the current code and fill gaps Architecture docs: - Describe the Svelte SPA as the only live frontend; drop the nonexistent /login/LoginPage route (auth is the Go-rendered token prompt) and add /schedules everywhere routes are listed. - backend.md: add internal/chatqueue + chat_queue*.go/request.go to the package layout, list all eleven SQLite tables, add /api/chat/queue, and sync the Server/Manager struct snapshots. - CSS is inlined into the SPA shell; pwa.go no longer serves /theme.css, /index.css, /menu.css, /palette.css. Only /custom-themes.css is routed. - Running-status recent-activity window is 800 ms, not 3 s. - Tailscale Serve only runs when PI_WEB_TOKEN is set; server startup also launches the schedule loop and chat-queue drainer outside dev mode. - chat returns 202 {"status":"queued"}; existing session files get three append paths (rename, auto-title, labels). - Mention-autocomplete helpers live under components/session/chat/. - Custom themes are picked in Settings -> Appearance (the header theme toggle is gone); index the design-system + metrics docs. User docs: - Roadmap: move shipped steering/queue, scheduler, display defaults, and git diff out of Next up/Planned. - Keyboard shortcuts: document Cmd/Ctrl+/ (help) and Cmd/Ctrl+, (settings). - llm-debug: /pi-web status is a pi command, not a binary subcommand. - install: ?token= sets a cookie and is redirected away, so it does not linger in the address bar or history. --- docs/README.md | 2 + docs/architecture/README.md | 14 ++-- docs/architecture/backend.md | 72 ++++++++++++--------- docs/architecture/data-flow.md | 8 ++- docs/architecture/frontend.md | 22 ++++--- docs/architecture/system-overview.md | 17 +++-- docs/design/design-system.md | 4 +- docs/dev/templates-vs-web.md | 8 ++- docs/sequence-flows/chat.md | 5 +- docs/sequence-flows/live-reload.md | 2 +- docs/sequence-flows/mention-autocomplete.md | 14 ++-- docs/sequence-flows/server-startup.md | 11 +++- user-docs/en/install.md | 2 +- user-docs/en/keyboard-shortcuts.md | 3 + user-docs/en/llm-debug.md | 2 +- user-docs/en/roadmap.md | 16 +++-- 16 files changed, 123 insertions(+), 79 deletions(-) diff --git a/docs/README.md b/docs/README.md index 574cfa71..0df8df01 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,6 +6,8 @@ | How the system is built (packages, data flow, frontend/backend) | [architecture/](architecture/) | | What happens at runtime (startup, chat, live reload, sharing) | [sequence-flows/](sequence-flows/) | | Frontend build details (templates vs. web, export design) | [dev/](dev/) | +| Design system (themes, CSS tokens, custom themes) | [design/design-system.md](design/design-system.md) | +| Worker metrics dashboard | [dev/metrics-dashboard.md](dev/metrics-dashboard.md) | | End-to-end browser testing (Playwright) | [dev/e2e-testing.md](dev/e2e-testing.md) | | Keyboard shortcuts | [../user-docs/en/keyboard-shortcuts.md](../user-docs/en/keyboard-shortcuts.md) | | Why this exists | [../user-docs/en/why.md](../user-docs/en/why.md) | diff --git a/docs/architecture/README.md b/docs/architecture/README.md index abd8886f..5e5f3033 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -8,7 +8,7 @@ This directory contains the architecture documentation for **pi-web**, a local w |----------|-------------| | [system-overview.md](./system-overview.md) | High-level system architecture, component diagram, and tech stack | | [backend.md](./backend.md) | Go backend: packages, responsibilities, and key types | -| [frontend.md](./frontend.md) | Frontend architecture: embedded templates, Vite build, and vanilla JS | +| [frontend.md](./frontend.md) | Frontend architecture: Svelte SPA, Vite build, embedded shell, and static export | | [data-flow.md](./data-flow.md) | Session file format, data model, and storage layout | ## Architecture at a Glance @@ -17,9 +17,9 @@ This directory contains the architecture documentation for **pi-web**, a local w ┌─────────────────────────────────────────────────────────────────────┐ │ Browser │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────────┐ │ -│ │ / (index) │ │ /session?id │ │ SSE /events │ │ -│ │ vanilla JS │ │ Embedded │ │ Live reload + status │ │ -│ │ (Vite) │ │ HTML/CSS │ │ updates │ │ +│ │ / /session │ │ /settings │ │ SSE /events │ │ +│ │ /schedules │ │ Svelte SPA │ │ Live reload + status │ │ +│ │ Svelte SPA │ │ │ │ updates │ │ │ └─────────────┘ └─────────────┘ └─────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ │ @@ -46,14 +46,12 @@ This directory contains the architecture documentation for **pi-web**, a local w ## Key Design Decisions -1. **Append-only session metadata**: pi-web reads from `~/.pi/agent/sessions/` and avoids rewriting session history. New sessions can be created via the web UI, and browser rename appends a `session_info` metadata line to the existing JSONL file. +1. **Append-only session metadata**: pi-web reads from `~/.pi/agent/sessions/` and avoids rewriting session history. New sessions can be created via the web UI; rename and auto-title append a `session_info` metadata line, and entry labels append a `label` line, to the existing JSONL file. 2. **Live updates via SSE**: The browser opens an EventSource connection. The server watches session files via `fsnotify` (with polling fallback) and pushes `reload` events; session pages fetch `/api/session` to reconcile canonical JSONL entries. Browser chat can also receive best-effort `chat-preview` SSE events before JSONL reconciliation. 3. **Chat via RPC workers**: Each session gets a dedicated `pi --mode rpc` subprocess. Workers are cached and reaped after 10 minutes of idle time. -4. **Dual frontend strategy**: - - **Index page** (`/`): Built with Vite + vanilla JS, served from embedded `web/dist` - - **Session page** (`/session`): Server-rendered HTML shell with Vite-built session JS +4. **Single Svelte SPA + static export**: All live browser routes (`/`, `/session`, `/settings`, `/schedules`) are Svelte 5 components built by Vite and served by one embedded shell (`internal/ui/embedded/app.html`). Sharing/export renders a separate self-contained snapshot (`internal/ui/export.go`). The pre-auth token prompt is the only other HTML page, rendered by the Go auth middleware. 5. **Security**: Token-based auth (`PI_WEB_TOKEN`) is required when binding to non-loopback addresses (e.g., Tailscale). diff --git a/docs/architecture/backend.md b/docs/architecture/backend.md index 36d10ad4..eae084f5 100644 --- a/docs/architecture/backend.md +++ b/docs/architecture/backend.md @@ -26,7 +26,7 @@ pi-web/ │ │ ├── spa_page.go # Live SPA shell renderer (RenderAppShell) │ │ ├── app_script.go # SPA Vite module URL path + script tag │ │ ├── session_page.go # Session page data prep (bootstrap base64 + CSS) -│ │ ├── live_page.go # Live document shell + theme/font providers +│ │ ├── live_page.go # Shared live document head + theme/font providers │ │ ├── export.go # Static export renderer │ │ ├── auth_page.go # Auth/token entry page │ │ ├── pwa.go # PWA routes: manifest, sw.js, icons, css, cat.webm @@ -35,6 +35,8 @@ pi-web/ │ │ └── auth.go # Token-based HTTP middleware │ ├── chat/ │ │ └── request.go # Multipart chat request parser (text + images) +│ ├── chatqueue/ +│ │ └── chatqueue.go # SQLite-backed per-session chat queue (items + paused state) │ ├── files/ │ │ └── files.go # Bounded read-only dir listing for @mention autocomplete │ ├── render/ @@ -53,7 +55,10 @@ pi-web/ │ ├── server/ │ │ ├── server.go # Server type, deps, SSE registry, route registration, SQLite open │ │ ├── handlers.go # index, session, api/session(s), new, fork/clone, rename, locations, models, custom-themes +│ │ ├── request.go # Shared JSON body decoding + request-size caps │ │ ├── chat.go # Chat, set-model, set-thinking, worker-status, commands handlers +│ │ ├── chat_queue.go # /api/chat/queue handler (list/add/delete/pause) +│ │ ├── chat_queue_drainer.go # Autonomous queue dispatcher (running → idle, 5s tick, mutation kick) │ │ ├── new_session.go # New-session creation logic │ │ ├── git.go # /api/git/info, /api/git/rename-branch handlers │ │ ├── diff.go # /api/git/diff, /api/diff/reviews handlers @@ -119,28 +124,32 @@ type Server struct { lastKnown map[string]struct{} // sessions currently broadcast as running lastKnownMu sync.Mutex push *PushManager // web-push subscriptions + done notifications - db *sql.DB // SQLite (~/.pi/agent/pi-web.sqlite) + stopCh chan struct{} + stopOnce sync.Once + wg sync.WaitGroup + + taskMu sync.Mutex // shuts out new tasks and owns taskCtx + taskCtx context.Context // server lifecycle context for background work + taskCancel context.CancelFunc + stopping bool + + db *sql.DB // SQLite (~/.pi/agent/pi-web.sqlite) + schedules *schedules.Store // cron definitions + run history + chatQueue *chatqueue.Store // per-session queue items + paused state + queueDrainer *queueDrainer // autonomous queue dispatcher updater *updater.Checker // optional; nil disables /api/version etc. runInstall func(ctx context.Context) error // optional self-update install runRestart func() error // optional self-update restart updateMu sync.Mutex // serializes install/restart - stopCh chan struct{} - stopOnce sync.Once - wg sync.WaitGroup + disableBackgroundJobs bool // development server: no scheduler/drainer/auto-title/push fileWalk *fileWalkCache // bounded dir-listing cache for @mention autocomplete fileWalkOnce sync.Once - startedAt time.Time // process uptime for the metrics dashboard - metricsSampler processSampler // swappable in tests - metricsCPUMu sync.Mutex - metricsCPULast map[int]cpuMark // per-PID CPU baselines for delta %CPU - - titleMu sync.Mutex // auto-title bookkeeping (see auto_title.go) - titleInFlight map[string]bool - titledName map[string]string // sessID -> title pi-web last set - titledCount map[string]int // sessID -> user-msg count at last titling - titleUserOwned map[string]bool // sessID -> user named it; never auto-title + // Metrics dashboard (see metrics.go) and auto-title bookkeeping (see + // auto_title.go), grouped so each subsystem owns its fields + lock. + metrics metricsState + autoTitle autoTitleState } ``` @@ -154,12 +163,16 @@ and serves the shared data, but does not run scheduling, queue draining, auto-titling, or push-delivery side effects. On `New`, the server opens (and migrates) a SQLite database at -`~/.pi/agent/pi-web.sqlite` with six tables: `scratchpads` (per project path), +`~/.pi/agent/pi-web.sqlite` with eleven tables: `scratchpads` (per project path), `settings` (server-backed user settings key/value), `project_prefs` (which projects are enabled), `app_settings` (the project-filter master switch, default -off), `btw_sessions` (the btw scratch-chat registry), and `annotations` -(per-session review notes keyed by session id; see `annotations.go`). See -`projects.go`, `settings.go`, and `btw.go`. The pool is capped to a single +off), `btw_sessions` (the btw scratch-chat registry), `annotations` (per-session +review notes keyed by session id; see `annotations.go`), `schedules` + +`schedule_runs` (definitions and firing history; see `internal/schedules`), +`review_comments` (per-session diff review notes; see `diff.go`), and +`chat_queue_items` + `chat_queue_state` (queued messages and per-session pause +state; see `internal/chatqueue`). See `projects.go`, `settings.go`, `btw.go`, +`schedules.go`, and `chat_queue.go`. The pool is capped to a single connection (`SetMaxOpenConns(1)`) so concurrent writers queue instead of failing with "database is locked". A `PushManager` (when configured) persists web-push subscriptions and VAPID keys under the agent dir. @@ -200,13 +213,14 @@ Manages `pi --mode rpc` subprocesses per session. ```go type Manager struct { - mu sync.Mutex - workers map[string]ChatWorker // sessionID → worker - creating map[string]*createCall // single-flight: coalesce concurrent creates per session - factory Factory // (sessionID, sessionPath) → ChatWorker - idleTTL time.Duration // default 10m - reaperStop chan struct{} - reaperDone chan struct{} + mu sync.Mutex + workers map[string]ChatWorker // sessionID → worker + creating map[string]*createCall // single-flight: coalesce concurrent creates per session + factory Factory // (sessionID, sessionPath) → ChatWorker + pendingSends map[string]int // accepted Sends not yet acked; keeps Status from dipping to idle + idleTTL time.Duration // default 10m + reaperStop chan struct{} + reaperDone chan struct{} } ``` @@ -250,11 +264,11 @@ type piRPCWorker struct { | `/` | GET | `handleIndex` | Render SPA shell for the sessions route | | `/session` | GET | `handleSession` | Render SPA shell for the session route | | `/settings` | GET | `handleSettingsPage` | Render SPA shell for the settings route | -| `/login` | GET | `handleAppShell` | Render SPA shell for the login route | | `/api/session` | GET | `handleApiSession` | JSON session data | | `/api/sessions` | GET | `handleApiSessions` | JSON list of session summaries | | `/api/chat` | POST | `handleChat` | Send chat message (multipart) | | `/api/chat/cancel` | POST | `handleCancelChat` | Abort running chat worker | +| `/api/chat/queue` | GET/POST/DELETE/PATCH | `handleChatQueue` | Per-session queued messages + pause state (SQLite) | | `/api/set-model` | POST | `handleSetModel` | Change model for session | | `/api/set-thinking-level` | POST | `handleSetThinkingLevel` | Change thinking level | | `/api/models` | GET | `handleAvailableModels` | List available AI models | @@ -301,7 +315,7 @@ PWA / static asset routes (registered outside `Server.Register`): | Route | Source | |-------|--------| -| `/manifest.webmanifest`, `/sw.js`, `/icon.svg`, `/icon-maskable.svg`, `/pi-logo.svg`, `/cat.webm`, `/theme.css`, `/index.css`, `/menu.css`, `/palette.css` | `internal/ui/pwa.go` (embedded assets) | +| `/manifest.webmanifest`, `/sw.js`, `/icon.svg`, `/icon-maskable.svg`, `/pi-logo.svg`, `/cat.webm` | `internal/ui/pwa.go` (embedded assets) | | `/static/assets/app-*.js`, `/static/assets/...` | Embedded Vite SPA bundle and chunks (`internal/app/app.go` + `internal/frontend`) | ## Auth Flow @@ -386,6 +400,6 @@ Three signals are OR'd together to determine if a session is "running": 1. **session-status file** (`~/.pi/agent/session-status/`): written by the terminal pi process 2. **In-process chat worker**: `chatSender.Status(id).State == running` -3. **Recent file activity**: modtime within 3 seconds +3. **Recent file activity**: JSONL file modtime within 800 ms Status changes are broadcast as SSE `status-delta` events to `__all__` subscribers. A 1-second sweeper periodically revalidates all known running sessions to clean up stale states. diff --git a/docs/architecture/data-flow.md b/docs/architecture/data-flow.md index bff5372b..be9398a8 100644 --- a/docs/architecture/data-flow.md +++ b/docs/architecture/data-flow.md @@ -140,7 +140,7 @@ Browser POST /api/chat?id= │ ├──▶ Await response on pending channel │ └──▶ Update status → running │ - └──▶ Return {"ok": true, "status": "accepted"} + └──▶ Return {"ok": true, "status": "queued"} ``` ## Data Flow: Rename Session @@ -160,7 +160,11 @@ Browser POST /api/rename-session?id= └──▶ Return {"ok": true, "name": "New Name"} ``` -Rename is the only intentional pi-web write to an existing session JSONL file. It appends metadata history; it does not rewrite existing entries. Creating a new session is the other direct write path, but it only creates a fresh JSONL file. +pi-web only ever **appends** metadata to an existing session JSONL file — it never +rewrites existing entries. There are three append paths: browser rename +(`session_info`), auto-titling (`session_info`, marked so a user rename always +wins), and entry labels (`label`). Creating new sessions (including btw, fork, +clone, and schedule runs) writes fresh JSONL files instead. ## Data Flow: Live Reload diff --git a/docs/architecture/frontend.md b/docs/architecture/frontend.md index 83459c0b..c92c39c7 100644 --- a/docs/architecture/frontend.md +++ b/docs/architecture/frontend.md @@ -32,7 +32,8 @@ Browser routes served by the SPA shell: - `/session?id=…` → `web/src/routes/SessionPage.svelte` - `/schedules` → `web/src/routes/SchedulesPage.svelte` - `/settings` → `web/src/routes/SettingsPage.svelte` -- `/login` → `web/src/routes/LoginPage.svelte` + +An unrecognized browser path renders `NotFoundPage.svelte`. There is **no SPA login route**: when `PI_WEB_TOKEN` is set and a request is unauthenticated, the auth middleware replies with the standalone Go-rendered token prompt (`internal/ui/auth_page.go` + `internal/ui/embedded/auth.html`) before the SPA ever loads. API, SSE, PWA, sound, and static asset routes remain server-handled and are not intercepted by the SPA fallback. @@ -48,12 +49,12 @@ Data comes from existing APIs such as `/api/sessions`, `/api/new-session`, `/api `SessionPage.svelte` owns the route, fetches session JSON from `/api/session?id=…`, and **orchestrates the whole viewer as Svelte components**. It creates the reactive `SessionDataModel` once, provides it via context, and installs the live session runtime context (`model`, navigator, `navigateTo`, `reconcileEntries`, content runtime) before child components mount. Live components read that explicit runtime context instead of `window.__pi*` aliases. `SessionPage`'s `onMount` runs `startSessionPageRuntime()` (bootstrap, `setupSessionUi`, content-runtime wiring, header handlers, initial nav) and `setupSessionGlobals()` (page-global glue). Annotation wiring is declarative: `SessionShell` passes the annotation config to `` as props (via ``) rather than an imperative `init()` up-call. There is **no `session.js` orchestrator** — see `docs/dev/templates-vs-web.md` § Current Migration State. -The message pane is rendered by Svelte components (no string-building renderer): `SessionContent` → `SessionEntry` → `ToolCall` → `ToolOutput`/`AskQuestion`, with `{@html}` used only for markdown + pre-rendered ANSI tool output. Other session UI components: `SessionTree`/`SessionSidebarProjects`/`SessionSidebarSessions`/`SessionTreeNodes`/`TreeNode`, `SessionInfoHeader`, `SessionHeader`, `RightSidebar` (+ `ArtifactPanel`, `AnnotationLayer`), `ChatComposer` (+ `GitFooter`), `LiveReload`, `CommandMenu`, `ImageModal`, the modals (`ShortcutsModal`/`ModelUsageModal`/`ForkModal`/`LabelModal`/`ShareDialog`), `BtwPopup`, `CatGatekeeper`. The left sidebar tabs between projects, sessions in one selected project, and the active session's message outline. The Projects tab requests 20-project pages from `/api/projects?limit=20&offset=…`, pins the current project into the first page, and appends another page as the main list is scrolled. An interrupted incremental request is retried once automatically, then leaves a compact retry control at the list boundary if the connection remains unavailable. That first response also bundles the current project's first five sessions so the expanded folder does not require a second archive scan. Other folders and later session pages independently request five-session pages from `/api/sessions?project=…&limit=5&offset=…` as their nested lists are scrolled. Project rows show the shared animated running indicator when any child session is active; the initial project response supplies existing running IDs and later SSE status deltas carry the cached project path. The Sessions project card opens a searchable project switcher; selecting a project reloads only that tab's session list and leaves the viewed session unchanged. The Sessions tab requests 20-item pages through `/api/sessions?project=…&limit=…&offset=…`, and its debounced search uses the API's `q` parameter so unloaded sessions remain searchable. All three tabs end with a matching count footer; the Sessions footer also owns the previous/next page controls. The old runners/renderers have been replaced by Svelte components plus focused helpers: `web/src/session/` holds the reactive model, pure helpers, live-only helpers, and a few shared utilities: +The message pane is rendered by Svelte components (no string-building renderer): `SessionContent` → `SessionEntry` → `ToolCall` → `ToolOutput`/`AskQuestion`, with `{@html}` used only for markdown + pre-rendered ANSI tool output. Other session UI components: `SessionTree`/`SessionSidebarProjects`/`SessionSidebarSessions`/`SessionTreeNodes`/`TreeNode`, `SessionInfoHeader`, `SessionHeader`, `RightSidebar` (+ `ArtifactPanel`, `AnnotationLayer`), `ChatComposer` (+ `ChatToolbar`/`QueuePanel`/`GitFooter`), `LiveReload`, `LoadEarlier`, `CommandMenu`, `ImageModal`, the modals (`ShortcutsModal`/`ModelUsageModal`/`ForkModal`/`LabelModal`/`DiffModal`/`ShareDialog`), `BtwPopup`, `CatGatekeeper`. The left sidebar tabs between projects, sessions in one selected project, and the active session's message outline. The Projects tab requests 20-project pages from `/api/projects?limit=20&offset=…`, pins the current project into the first page, and appends another page as the main list is scrolled. An interrupted incremental request is retried once automatically, then leaves a compact retry control at the list boundary if the connection remains unavailable. That first response also bundles the current project's first five sessions so the expanded folder does not require a second archive scan. Other folders and later session pages independently request five-session pages from `/api/sessions?project=…&limit=5&offset=…` as their nested lists are scrolled. Project rows show the shared animated running indicator when any child session is active; the initial project response supplies existing running IDs and later SSE status deltas carry the cached project path. The Sessions project card opens a searchable project switcher; selecting a project reloads only that tab's session list and leaves the viewed session unchanged. The Sessions tab requests 20-item pages through `/api/sessions?project=…&limit=…&offset=…`, and its debounced search uses the API's `q` parameter so unloaded sessions remain searchable. All three tabs end with a matching count footer; the Sessions footer also owns the previous/next page controls. The old runners/renderers have been replaced by Svelte components plus focused helpers: `web/src/session/` holds the reactive model, pure helpers, live-only helpers, and a few shared utilities: - `data/` — payload decoding + the reactive `SessionDataModel` (`session-data.svelte.js`, the single source of truth: entries/lookups/tree/active-path/view-state, `reconcile()`) - `tree/`, `render/`, `navigation/` — **pure** tree/format/markdown/navigation helpers consumed by the Svelte components (and the export). The message renderer is now ``/``; `render/` keeps `session-format`, `markdown`, `entry-format`, `session-entry-actions` (download/share/copy) - `session-globals.js`, `session-content-runtime.js`, `lazy-highlight.js` — the relocated live glue (see above) -- `chat/` — **pure/shared helpers**: `chat-api` + `git-api` (fetch wrappers), `chat-selectors` (pure model/thinking helpers), `done-notifier` (shared notification/sound/push util, also used by the settings page). Live composer DOM helpers live under `web/src/components/session/chat/`, wired together by `chat-composer-runtime.js` (`runChatComposer`, mounted by ``). +- `chat/` — **pure/shared helpers**: `chat-api` + `git-api` (fetch wrappers), `chat-selectors` (pure model/thinking helpers), `diff-api` + `diff-review` (diff modal data + review comments), `done-notifier` (shared notification/sound/push util, also used by the settings page). Live composer DOM helpers live under `web/src/components/session/chat/`, wired together by `chat-composer-runtime.js` (`runChatComposer`, mounted by ``). - `live/` — live-only helpers used by ``: `live-connection.js` (SSE connection/reconnect lifecycle), `live-events.js` (SSE/reload primitives), `live-scroll.js` (low-level scroll primitives), `live-follow.js` (`createFollowScrollController` — follow-mode decision state + follow button), `live-stats.js` (header stats), and `chat-preview.js` (streaming-preview helper, also used by ``) - `ui/` — sidebar/search/toggle/session-ui-runner helpers used by `setupSessionUi` and `RightSidebar` - `artifacts/`, `annotations/` — pure registries/filters/ranges + the fetch API wrappers; the panels themselves are `ArtifactPanel.svelte`/`AnnotationLayer.svelte` @@ -93,6 +94,7 @@ The index route listens to `/events?id=__all__` for `new-session`, `status-snaps - `web/src/shared/escape.js` — HTML escaping - `web/src/shared/theme.js` — theme toggle (dark/light/nord/dracula/custom) - `web/src/shared/version.js` — pure version formatting/changelog/fetch helpers; `VersionController.svelte` owns the update modal/status UI +- `web/src/shared/keybindings.js` — remappable keyboard-action registry (default combos for global, navigation, and composer shortcuts); `keyboard-nav.js` and the session globals consume it - `web/src/shared/keyboard-nav.js` — vim-style j/k/gg/G navigation - `web/src/components/shared/CommandPalette.svelte` — shared ⌘K session search palette @@ -103,11 +105,13 @@ The index route listens to `/events?id=__all__` for `new-session`, `status-snaps | Vite SPA bundle | `web/dist/assets/app-*.js` | `/static/assets/app-*.js` | | Vite lazy chunks | `web/dist/assets/*.js` | `/static/assets/*.js` | | Static export JS | `internal/ui/embedded/export/export.js` + vendors | inline in exported HTML | -| Theme CSS | `internal/ui/embedded/styles/theme.css` | `/theme.css` (PWA route) | -| Index CSS | `internal/ui/embedded/styles/index.css` | `/index.css` (PWA route) | -| Session CSS | `internal/ui/embedded/styles/session.css` | inlined in SPA shell | -| Menu CSS | `internal/ui/embedded/styles/menu.css` | `/menu.css` and inlined in SPA shell | -| Palette CSS | `internal/ui/embedded/styles/palette.css` | `/palette.css` and inlined in SPA shell | +| Theme CSS | `internal/ui/embedded/styles/theme.css` | inlined in SPA shell + export | +| Index CSS | `internal/ui/embedded/styles/index.css` | inlined in SPA shell | +| Settings CSS | `internal/ui/embedded/styles/settings.css` | inlined in SPA shell | +| Schedules CSS | `internal/ui/embedded/styles/schedules.css` | inlined in SPA shell | +| Session CSS | `internal/ui/embedded/styles/session.css` | inlined in SPA shell + export | +| Menu CSS | `internal/ui/embedded/styles/menu.css` | inlined in SPA shell | +| Palette CSS | `internal/ui/embedded/styles/palette.css` | inlined in SPA shell | | Custom themes | `~/.pi/agent/pi-web/custom-themes.css` (optional) | `/custom-themes.css` | | PWA manifest | `internal/ui/embedded/assets/manifest.webmanifest` | `/manifest.webmanifest` | | Service worker | `internal/ui/embedded/assets/sw.js` | `/sw.js` | @@ -117,4 +121,4 @@ The index route listens to `/events?id=__all__` for `new-session`, `status-snaps ## Theme System -The live SPA shell uses `theme.css`, `index.css`, `settings.css`, `session.css`, `menu.css`, and `palette.css` from `internal/ui/embedded/styles/`. The shell still injects the server-backed theme and font variables before the app starts so first paint matches the installed PWA theme without a flash. +The live SPA shell uses `theme.css`, `index.css`, `settings.css`, `schedules.css`, `session.css`, `menu.css`, and `palette.css` from `internal/ui/embedded/styles/`. They are inlined into the shell (there are no standalone CSS routes; only `/custom-themes.css` is served). The shell still injects the server-backed theme and font variables before the app starts so first paint matches the installed PWA theme without a flash. diff --git a/docs/architecture/system-overview.md b/docs/architecture/system-overview.md index 91a9b646..32f8d374 100644 --- a/docs/architecture/system-overview.md +++ b/docs/architecture/system-overview.md @@ -14,8 +14,8 @@ pi-web is a local HTTP server that lets you browse and interact with your pi cod | Styling | Custom CSS (multi-theme: dark/light/nord/dracula/custom) | | Live Updates | Server-Sent Events (SSE) | | Chat RPC | JSONL over stdin/stdout via `pi --mode rpc` | -| Session Storage | JSONL files on disk; pi-web creates new session files and appends `session_info` for browser rename | -| Local DB | SQLite (`~/.pi/agent/pi-web.sqlite`) for per-project scratchpads, per-session review annotations, project visibility prefs, server-backed user settings, and the btw scratch-chat registry | +| Session Storage | JSONL files on disk; pi-web creates new session files and only appends metadata (`session_info` for rename/auto-title, `label` for entry labels) | +| Local DB | SQLite (`~/.pi/agent/pi-web.sqlite`) for per-project scratchpads, per-session review annotations, project visibility prefs, server-backed user settings, the btw scratch-chat registry, schedules + run history, the chat queue, and diff review comments | | Auth | Token cookie/query/header (optional on localhost) | ## Component Diagram @@ -32,7 +32,7 @@ pi-web is a local HTTP server that lets you browse and interact with your pi cod │ │ /session → SessionPage (Svelte │ │ • new-session (index) │ │ │ │ components + reactive model) │ │ • status-delta │ │ │ │ /settings → SettingsPage (Svelte) │ │ • status-snapshot │ │ -│ │ /login → LoginPage │ │ • annotations, btw… │ │ +│ │ /schedules → SchedulesPage (Svelte) │ │ • annotations, btw… │ │ │ │ shared: CommandPalette, Version UI │ │ │ │ │ └──────────────────────────────────────┘ └─────────────────────────┘ │ └──────────────────────────────────────────────────────────────────────────┘ @@ -45,10 +45,12 @@ pi-web is a local HTTP server that lets you browse and interact with your pi cod │ GET / → handleIndex (SPA shell) │ │ GET /session → handleSession (SPA shell) │ │ GET /settings → handleSettingsPage (SPA shell) │ +│ GET /schedules → handleAppShell (SPA shell, catch-all route) │ │ GET /api/session → handleApiSession (JSON) │ │ GET /api/sessions → handleApiSessions (JSON list) │ │ POST /api/chat → handleChat (multipart or JSON) │ │ POST /api/chat/cancel → handleCancelChat │ +│ GET/POST/DELETE/PATCH /api/chat/queue → handleChatQueue (SQLite) │ │ POST /api/set-model → handleSetModel │ │ POST /api/set-thinking-level → handleSetThinkingLevel │ │ POST /api/new-session / fork-session / clone-session │ @@ -64,6 +66,7 @@ pi-web is a local HTTP server that lets you browse and interact with your pi cod │ GET/POST/DELETE /api/diff/reviews → diff review comments (SQLite) │ │ GET/POST /api/scratchpad → scratchpad (SQLite) │ │ GET/POST/DELETE /api/annotations → review annotations (SQLite, SSE) │ +│ GET/POST /api/schedules /api/schedule(/run|/runs) → schedules (SQLite) │ │ GET/POST /api/settings → user settings (SQLite, write-through cache) │ │ GET/POST /api/projects → project visibility prefs (SQLite) │ │ GET /api/sounds / GET /sounds/… (notification sounds) │ @@ -115,8 +118,8 @@ pi-web is a local HTTP server that lets you browse and interact with your pi cod Non-loopback → PI_WEB_TOKEN required (or --insecure) Loopback → Auth optional -When no --host override is supplied and Tailscale is running, pi-web also -configures Tailscale Serve: +When no --host override is supplied, Tailscale is running, and +`PI_WEB_TOKEN` is set, pi-web also configures Tailscale Serve: tailscale serve --bg --https= http://127.0.0.1: @@ -138,7 +141,7 @@ name, while pi-web itself continues listening only on localhost. ├── session-status/ │ ├── 2026-01-15T10-30-00.000Z_a1b2c3d4.jsonl ← terminal writes here │ └── … -├── pi-web.sqlite ← scratchpads + annotations + project visibility prefs + user settings + btw registry +├── pi-web.sqlite ← scratchpads, annotations, review comments, project prefs, settings, btw registry, schedules, chat queue └── pi-web/ ├── pi-web-state.json ← regular server state + lock ├── pi-web-state-dev.json ← development state + lock (while running) @@ -180,7 +183,7 @@ across devices. See `internal/server/projects.go`. 3. Determine bind host (flag → localhost) 4. Enforce auth for explicit non-loopback binds 5. Build `server.Deps` (renderers, cache, workers, auth) -6. Create `Server` → starts file watcher + status watcher + sweeper +6. Create `Server` → opens SQLite, starts file watcher + status watcher + sweeper, and (unless in development mode) the schedule loop + chat-queue drainer 7. Register routes on `http.ServeMux` 8. Load Vite manifest and register static assets 9. Optionally configure Tailscale Serve HTTPS for localhost diff --git a/docs/design/design-system.md b/docs/design/design-system.md index 930f16e5..a34f29f5 100644 --- a/docs/design/design-system.md +++ b/docs/design/design-system.md @@ -7,7 +7,7 @@ This document details the core design system for `pi-web`. The design system is ## 1. Core Principles 1. **Monospace Typography:** Highly tailored to developer workflows, using a clean monospace typeface stack. -2. **Obsidian Obsidian Dark by Default:** Provides a premium, high-contrast visual footprint that is comfortable for long hours of pairing. +2. **Obsidian Dark by Default:** Provides a premium, high-contrast visual footprint that is comfortable for long hours of pairing. 3. **Fully Semantic Visual Tokens:** No hardcoded hex values in CSS rules or component styles. Every color, border, padding, and layout attribute references semantic tokens. (Note: `` requires a literal color value and is the sole exception.) 4. **Zero Compilation Overhead:** Themes are resolved purely at runtime by the browser, removing the need for server-side CSS precompilation. 5. **Local Custom Themes:** Anyone can configure custom themes by adding a simple CSS stylesheet in their active configuration directory. @@ -72,7 +72,7 @@ Create `~/.pi/agent/pi-web/custom-themes.css` and paste the following structure: } ``` -Once saved, reload the page and open the **Session Actions menu (⋯)** in the top-right of the session header, then cycle through the **Theme** toggle until **⚙ Custom** appears. +Once saved, reload the page and pick **Custom** under **Settings → Appearance → Theme** (open Settings with `⌘,` / `Ctrl+,`, or from the session actions menu). --- diff --git a/docs/dev/templates-vs-web.md b/docs/dev/templates-vs-web.md index 98f07b45..285c40cb 100644 --- a/docs/dev/templates-vs-web.md +++ b/docs/dev/templates-vs-web.md @@ -24,7 +24,8 @@ internal/ui/embedded/app.html ├── routes/SessionsPage.svelte (/) ├── routes/SessionPage.svelte (/session?id=…) ├── routes/SettingsPage.svelte (/settings) - └── routes/LoginPage.svelte (/login) + ├── routes/SchedulesPage.svelte (/schedules) + └── routes/NotFoundPage.svelte (any other path) ``` The Go shell intentionally preserves the current PWA-first boot path: @@ -49,11 +50,14 @@ The live SPA shell inlines the core CSS needed by all migrated routes: - `styles/theme.css` - `styles/index.css` - `styles/settings.css` +- `styles/schedules.css` - `styles/session.css` - `styles/menu.css` - `styles/palette.css` -Some CSS is also exposed as PWA/static routes by `internal/ui/pwa.go` (`/theme.css`, `/index.css`, `/menu.css`, `/palette.css`, `/settings.css`) for compatibility and install/offline behavior. +The inlined CSS is the only delivery path for these files: `internal/ui/pwa.go` serves just the manifest, service worker, icons, and `cat.webm`. The one standalone stylesheet route is `/custom-themes.css` (user CSS). + +The pre-auth token prompt is not part of the SPA shell: the auth middleware renders the standalone `embedded/auth.html` page (`internal/ui/auth_page.go`). --- diff --git a/docs/sequence-flows/chat.md b/docs/sequence-flows/chat.md index 04e94a44..eaf7f96a 100644 --- a/docs/sequence-flows/chat.md +++ b/docs/sequence-flows/chat.md @@ -76,7 +76,7 @@ This flow covers a user typing a message (with optional image attachment) in the │ │ │ │ │ │ │ │◀───────────── nil ──────────────│ │ │ │ │ │ │ │ │ - │◀──────────── {ok: true, status: "accepted"} ─│ │ │ + │◀──────── {ok: true, status: "queued"} ───────│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ @@ -137,7 +137,8 @@ Content-Type: image/png ### 3. Worker Resolution After parsing succeeds, the server registers the send as server-owned -background work and immediately returns `202 Accepted`. The task uses the +background work and immediately returns `202 Accepted` with +`{"ok": true, "status": "queued"}`. The task uses the server lifecycle context: graceful shutdown cancels an in-flight RPC wait and waits for the task to exit before closing shared resources. diff --git a/docs/sequence-flows/live-reload.md b/docs/sequence-flows/live-reload.md index 2fa92ac8..cc02d4e9 100644 --- a/docs/sequence-flows/live-reload.md +++ b/docs/sequence-flows/live-reload.md @@ -141,7 +141,7 @@ Polling scans all `.jsonl` files and compares modtimes against `fileMod` map. 1. **session-status file** exists and has `state: "running"` and `updatedAt` within 10s TTL 2. **Chat worker** status is `running` (in-process) -3. **Recent file activity**: JSONL file modtime within 3 seconds +3. **Recent file activity**: JSONL file modtime within 800 ms ### Status Sweeper diff --git a/docs/sequence-flows/mention-autocomplete.md b/docs/sequence-flows/mention-autocomplete.md index 9e834fbb..46a54612 100644 --- a/docs/sequence-flows/mention-autocomplete.md +++ b/docs/sequence-flows/mention-autocomplete.md @@ -10,16 +10,16 @@ real `cwd`. It is never part of the export/Gist snapshot (which has no composer) ## Components -Frontend (`web/src/components/session/ChatComposer.svelte`'s `