diff --git a/AGENTS.md b/AGENTS.md index 69a70b1..978fe9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,8 +68,13 @@ app/api/ git/status/route.ts GET changed files for a cwd git/diff/route.ts GET diff of one changed file worktrees/route.ts GET/POST/DELETE git worktrees - terminal/route.ts POST create a terminal session - terminal/[id]/route.ts GET { id, cwd } (404 once closed; stream at [id]/events) | POST input/resize | DELETE kill + terminal/route.ts POST { id, cwd, cols, rows } attach to the id's shell, or start it: { id, created } + terminal/[id]/route.ts GET { id, cwd } (404 once closed; stream at [id]/events: output, exit with code and signal, closed) | POST input/resize | DELETE kill + browser/route.ts GET shared browser state (never launches) | POST { action } navigate/back/forward/reload/stop/new_tab/select_tab/close_tab/viewport/clear_data/shutdown + browser/events/route.ts GET SSE: state, the active tab's screencast frames, console entries + browser/input/route.ts POST mouse / key / text input on the active tab (CSS pixels of the viewport) + browser/pick/route.ts POST { x, y } the element at a point, with a ref and a crop (Select element) + browser/ports/route.ts GET TCP ports listening on the machine (the panel's Dev servers chips) mcp/route.ts GET [?cwd=] Settings › MCP overview, files only | POST add/enable/disable/remove/undo/set-enabled/set-exposure/set-in-project/sign-out mcp/test/route.ts POST { scope, name, cwd? } test one server once (entry read from its file) mcp/sign-in/route.ts POST { scope, name, cwd? } start or join an OAuth sign-in @@ -119,7 +124,6 @@ lib/ pi-sdk-internals.ts loader for SDK modules the package does not export (MCP connection, config, OAuth) tool-presets.ts PRESET_NONE/READ_ONLY/DEFAULT/FULL + getPresetFromTools() tool-preset-preference.ts browser-persisted default preset for fresh sessions - tool-fields.ts a tool call's arguments as flat rows: all of them or no table builtin-extensions.ts codemode / tool-search / mcp built-ins, sandbox self-test, -builtin: switches codemode-settings.ts Code mode automatic/always (+codemode in global defaultTools), codemode.mode and inlineBudget; project overrides codemode-view.ts display helpers for codemode cards @@ -145,14 +149,26 @@ lib/ draft-store.ts local draft persistence extension-ui-queue.ts FIFO queues for extension dialogs and custom panels, by request id; isBlockingExtensionUiRequest() (which method a run waits on) session-stats-format.ts client-safe compact counts, costs and active durations of the stats toolbar and agent switcher - turn-duration.ts getTurnDurationMs() / formatTurnDuration(): a finished turn's length from its anchor to its last response's end, on pi's timestamps transcript-notices.ts client-safe: showCacheMissNotices notices on chat messages, TUI number formats, a fold's cache-miss sum transcript-notices-server.ts the notices derived from session entries (cache-stats port), live ones for message_end, the setting markdown.ts shared markdown helpers markdown-blocks.ts splitMarkdownBlocks(): where a streaming reply may be cut into blocks parsed apart - highlight-queue.ts code fences past a task's highlight budget get their colors in idle slices - prism-languages.ts loadPrismLanguages(): every Prism language in a chunk of its own, loaded when a highlighter first needs it + step-summary.ts a turn's steps counted by kind (ran, read, edited, browsed, …) for their one summary line above the answer + trace-line.ts client-safe: a tool call's line in words ("Edited app.py +8 −2"): verb key, target from the project, detail + shortcuts.ts client-safe: the pane and mode keys, claude.ai Code tab's browser set (Ctrl+`, ⌃⇧D, ⌘⌥P, ⌘⇧F, ⌘\, ⌘⇧\, ⌘⇧M); what a terminal lets through + tile-layout.ts client-safe: Claude Code Desktop's tiles: the layout tree, placement, closed slots, sizes, resize, drops, the stored layouts + terminal-tabs.ts client-safe: the Terminal pane's tabs grouped by folder: numbers, close and active rules, rename, move, the window of tabs that fit, user exits, attachments, sessionStorage + terminal-theme.ts client-safe: the terminal's colours read from app/globals.css (`--terminal-*`) + terminal-settings.ts client-safe: the terminal's settings (cursor, blink, copy on select, scrollback) in localStorage, live + terminal-manager.ts the shells (node-pty): one per id, output backlog and offsets, exit with code and signal, the 2-minute lease without a window + terminal-client.ts client: terminal requests (errors with their status) and the ordered input writer + mcp-presets.ts client-safe: one-click connectors (the Hugging Face MCP server, its token read by a `!command` header) + diffs-theme.ts the themes all code is highlighted with: dark+/light+ with their blue tokens in neutral grays, keywords bold, as Highlights' (Zed) and Shiki's + diff-patch.ts split diff rows (apply_patch, previews) as one unified patch for the diff viewer + mermaid-theme.ts Mermaid's neutral base theme + focus.ts rangeFill() for app/focus.css's slider track; keepTabInside() for aria-modal dialogs gfm-autolink-email-loader.cjs bundler loader: remark-gfm's email regex without a lookbehind literal + pierre-diffs-split-loader.cjs bundler loader: @pierre/diffs' newline split without a lookbehind literal node-cli.ts locate bundled npm-cli.js / npx-cli.js to spawn npm/npx without a shell (Windows) npx.ts npx runner for skill install plugin-updates.ts npm view update checks for /api/plugins/check @@ -182,32 +198,53 @@ lib/ mcp-import.ts pure paste importer (+ mcp-import-core/json/cli/links.ts) mcp-server-display.ts client-safe display helpers for McpServerInfo (hidden-character escapes, labels) mcp-tool-display.ts server/tool label of an mcp__ call from its result details, never the name + browser/host.ts the shared Chromium (Playwright): launch, tabs, console ring, screencast, input, pick, the agent's operations, idle close + browser/snapshot.ts in-page snapshot / pick script (refs in data-pi-ref) and the outline formatter + browser/extension.ts browser_* agent tools (normal sessions only) + browser/event-channel.ts one viewer's SSE queue: the latest frame and state replace unsent ones + browser/ports.ts listening ports from /proc/net/tcp{,6} and their processes + browser/requests.ts client-safe: typed-URL normalization, /api/browser action, input and pick parsing + browser/types.ts client-safe: state, event, input, pick and tool-details types, viewport limits + browser/http.ts the browser routes' JSON body check and error responses components/ - AppShell.tsx layout, URL state, tab management + AppShell.tsx layout, URL state, tab management; the panes as tiles (Terminal, Changes, Browser, Files), their keys and focus + SessionHeader.tsx the conversation header: the title's session menu, a button per pane (Changes with "+N −M"), folding into ⋯ when narrow + TileLayout.tsx the chat and the panes positioned as tiles: pane headers, resize handles and corners, drag to move, expand, drop bar + ChangesPane.tsx the Changes pane: changed files, one diff at a time, review comments on lines sent to the agent as one message + BrowserPane.tsx the Browser pane: the machine's Chromium live (frames on a canvas, input back in order), tabs, dev servers, console, Select element, the direct preview the agent acts in + ContextMeter.tsx the composer's context ring and its popover (tokens, cost, Compact) + ComposerModelMenu.tsx the composer's model-and-effort menu + ComposerModeMenu.tsx the composer's mode menu: tool presets as Standard / Plan / Chat (+ pinned sets), ⌘/Ctrl+Shift+M + ui/ Base UI wrappers, styled in app/ui.css (every dropdown's proportions): menu.tsx (MenuSection for labelled groups), tooltip.tsx, popover.tsx, composer-list.tsx (the "/", "@" and history list), scrollbar-inset.ts (a scrolling dropdown's bar takes its right inset) + ModelMark.tsx a model's maker mark for menus (the repo's organisation on a router, else the provider), in grays SessionSidebar.tsx toolbar row (Sessions | Files tabs, or the Pi Web brand with the files below them; new, search): tree, archive, menus, toast; files section (resizable, foldable; one header row with the file keys and changed-files count, the picker's row) or tab (picker boxes + file buttons), FileExplorer - PiWebTitle.tsx the sidebar's "Pi Web" brand (files below the sessions only): a click scrambles it into the versions + PiWebTitle.tsx the sidebar's "Pi Web" brand (files below the sessions only): the pi mark and the name; a click scrambles it into the versions + PiLogo.tsx the pi mark (pi.dev's single-colour badge) in the current text colour SessionTree.tsx virtualized rows of the session tree and the archive view SidebarMenu.tsx sidebar popup menu (bottom sheet on phones), portaled to body: filter field, item secondary actions, the files tab's classic look ProjectWorktreePicker.tsx project and worktree boxes + menus: the files' (one row below the sessions, stacked in the tab) and the new-session bar's - NewSessionContextBar.tsx the picker in its own row under the brand, right above the empty new-session composer; moves go through the sidebar + NewSessionContextBar.tsx the project and worktree picker in the empty new-session composer's chin (after "+", before the mode); moves go through the sidebar WorktreeCreateForm.tsx "New worktree…" form, a SidebarMenu body SidebarToast.tsx sidebar one-line toast: archive Undo/View, failed saves SidebarIcons.tsx sidebar stroke icons ChatWindow.tsx chat composition + completion sound - ChatInput.tsx input bar + model/thinking/tools/compact controls + ChatInput.tsx the composer: draft, tokens behind it, the "/", "@" and history lists, "+", mode, context, model, Send/Stop/Steer MessageView.tsx one message (user/assistant/toolCall/toolResult) CodemodeToolView.tsx codemode card: the tool calls its script made BranchNavigator.tsx in-session branch switcher - ChatMinimap.tsx scroll minimap beside the message list MarkdownBody.tsx markdown renderer + highlighting.ts the one code-highlighting stack: @pierre/diffs + @pierre/highlights in two web workers (Shiki without WebAssembly SIMD), themes, metrics + CodeDiff.tsx diffs (an edit, apply_patch, the file viewer's Diff mode) + CodeFile.tsx all other code: fences, the file viewer's source, tool inputs and outputs + CodeLazy.tsx both, loaded on demand and warmed when idle; plain text until then ModelsConfig.tsx Settings › Models: models.json editor EnabledModelsSection.tsx model switches inside ModelsConfig (enabledModels) ModelContextWindow.tsx a managed model row's context-window field (blank: no override) OAuthPastePanel.tsx paste box for a sign-in's redirected address or code (Models, MCP) ProjectTrustDialog.tsx trust confirmation listing the project's MCP servers AgentsConfig.tsx built-in subagent toggle + agent profile editor - SubagentViewer.tsx read-only right-panel tab of a subagent's transcript (polls the context route) + SubagentViewer.tsx read-only Files-pane tab of a subagent's transcript (polls the context route) PluginsConfig.tsx Settings › Plugins: installed package plugins SkillsConfig.tsx Settings › Skills: loaded, search, install McpConfig.tsx Settings › MCP: servers, switches, exposure, remove/undo, Test, sign-in, Code mode, trust @@ -220,15 +257,19 @@ components/ FileIcons.tsx file icon helpers FileViewer.tsx file content in a tab DelimitedTable.tsx a CSV/TSV file's Preview: virtualized table, gutter of file lines - TabBar.tsx file panel tab bar (file, terminal and subagent tabs) - TranscriptNotices.tsx a message's cache and cost notice lines, the fold header's cache-miss text + TabBar.tsx the Files pane's tabs (file and subagent tabs) in its header, drawn as the Terminal's (24px pills) + SearchCommandBar.tsx the conversation search: a command bar centred over the page (Base UI Dialog), recent conversations, then matches; ⌘K / Ctrl+K + TerminalTabs.tsx the Terminal pane's header: its tabs (Desktop's strip: numbers, ×, menu, rename, drag, More terminals), "+" and the ⋮ settings + TranscriptNotices.tsx a message's cache and cost notice lines, the steps summary's cache-miss text + TerminalPanel.tsx one tab's shell: xterm on the code surface, attach or start, reconnect, the states over it (Shell exited, disconnected, failed), Attach as context, its menu hooks/ useAgentSession.ts messages, streaming, SSE, fork/navigate, reconciliation; built-in slash commands (/session, bare /mcp) useStreamingSelector.ts one value of the streaming store (lib/streaming-message.ts): a chunk re-renders only its subscribers useAudio.ts completion sound + AudioContext unlock useDragDrop.ts chat drop zone: dropped files, folders marked - useFilesPlacement.ts per-browser choice: the desktop sidebar's file browser below the sessions or in its own tab + useFilesPlacement.ts per-browser choice: the file browser in the Files pane (default), below the sessions, or in a sidebar tab + usePaneLayout.ts the tiles' state per session (layout, expanded pane, closed slots, widths) in pi-web:panes.v1; a phone's one pane useGroupDrag.ts dragging a sidebar project group: mouse threshold, touch/pen long-press, auto-scroll, drop useIsMobile.ts responsive breakpoint usePageHidden.ts HIDDEN_PAGE_GRACE_MS; true once the page has been hidden that long (file watches pause) @@ -236,9 +277,7 @@ hooks/ useSidebarMotion.ts the sidebar's fold and archive-view motion: phase, end fallback, reduced motion, what shows meanwhile useKeyboardShortcuts.ts Esc stops the running agent unless a field or nearer handler took it; Ctrl+Alt+N useResizablePanel.ts drag/keyboard panel size kept per key; persistClamp: false never saves a short window's clamp - usePrismLanguages.ts true once the Prism languages are loaded; starts the load for a highlighter useTheme.ts theme state - useTerminalTheme.ts the terminal's own dark/light choice (localStorage), apart from the app theme ``` --- @@ -247,7 +286,7 @@ hooks/ Design decisions and traps live in `docs/agents/`, one note per area. Read every note whose files a change touches before making it. Add new notes to the area's file, not here. -- [sessions.md](docs/agents/sessions.md): AgentSession lifecycle and shutdown, fork vs in-session branching, the sidebar's on-disk Fork, session file rewrites, toolCall normalization, a run's message keys across the file read, SSE reconnect and tool events, a tool card's flat arguments as rows, transcript system / usage / context-edit entries, running-state polling, custom-message collapsing, streamed chunks re-rendering only the reply, streaming markdown block by block, hand-written anchors in rendered Markdown landing after the sanitizer's id prefix, long thinking rendered in pieces, a finished turn's duration, history caches (tool-result images, subagent runs, session views) and deferred code highlighting with Prism languages loaded on demand, a patch shown in one column or two, the session sidebar (toolbar row and its brand, project groups, their order and display names, pins, archive, new-session project adoption, the files below the sessions or in a tab), the project/worktree bar above a fresh composer (the header: brand, then the bar) and what it carries across the remount, exported HTML, the extension status bar and its `command:` buttons, cache and cost notices (`showCacheMissNotices`). Files: `lib/rpc-manager.ts`, `lib/session-reader.ts`, `lib/session-fork*.ts`, `lib/session-delete.ts`, `lib/normalize.ts`, `lib/message-keys.ts`, `lib/session-tree.ts`, `lib/session-ui-state*.ts`, `lib/sidebar-prefs.ts`, `lib/sidebar-actions.ts`, `lib/session-links.ts`, `lib/new-session-context.ts`, `hooks/useAgentSession.ts`, `hooks/useSessionUiState.ts`, `hooks/useSidebarMotion.ts`, `hooks/useGroupDrag.ts`, `hooks/useFilesPlacement.ts`, `hooks/useResizablePanel.ts`, `app/api/agent/**`, `app/api/sessions/**`, `components/SessionSidebar.tsx`, `components/PiWebTitle.tsx`, `components/SessionTree.tsx`, `components/SidebarMenu.tsx`, `components/SidebarToast.tsx`, `components/SidebarIcons.tsx`, `components/ProjectWorktreePicker.tsx`, `components/NewSessionContextBar.tsx`, `components/WorktreeCreateForm.tsx`, `handleNewSession`, `handleSelectSession` and the bar's handlers in `components/AppShell.tsx`, `components/BranchNavigator.tsx`, `components/MessageView.tsx`, `lib/message-display.ts`, `lib/patch.ts`, `components/FileViewer.tsx`, `components/MarkdownBody.tsx`, `lib/markdown.ts`, `lib/markdown-blocks.ts`, `lib/streaming-message.ts`, `hooks/useStreamingSelector.ts`, `lib/turn-duration.ts`, `components/MermaidBlock.tsx`, `lib/highlight-queue.ts`, `lib/prism-languages.ts`, `hooks/usePrismLanguages.ts`, `lib/session-view-cache.ts`, `components/CodemodeToolView.tsx`, `components/ExtensionStatusBar.tsx`, `components/ExtensionWidgets.tsx`, `lib/transcript-notices*.ts`, `components/TranscriptNotices.tsx`, `app/api/cache-miss-notices/**`, `lib/tool-fields.ts`. +- [sessions.md](docs/agents/sessions.md): AgentSession lifecycle and shutdown, fork vs in-session branching, the sidebar's on-disk Fork, session file rewrites, toolCall normalization, a run's message keys across the file read, SSE reconnect and tool events, transcript system / usage / context-edit entries, running-state polling, custom-message collapsing, streamed chunks re-rendering only the reply, streaming markdown block by block, history caches (tool-result images, subagent runs, session views) and code highlighting (the one @pierre/diffs + @pierre/highlights stack, `components/highlighting.ts`), the session sidebar (toolbar row and its brand, project groups, their order and display names, pins, archive, new-session project adoption, the files below the sessions or in a tab), the project/worktree bar above a fresh composer (the header: brand, then the bar) and what it carries across the remount, exported HTML, the extension status bar and its `command:` buttons. Files: `lib/rpc-manager.ts`, `lib/session-reader.ts`, `lib/session-fork*.ts`, `lib/session-delete.ts`, `lib/normalize.ts`, `lib/session-tree.ts`, `lib/session-ui-state*.ts`, `lib/sidebar-prefs.ts`, `lib/sidebar-actions.ts`, `lib/session-links.ts`, `lib/new-session-context.ts`, `hooks/useAgentSession.ts`, `hooks/useSessionUiState.ts`, `hooks/useSidebarMotion.ts`, `hooks/useGroupDrag.ts`, `hooks/useFilesPlacement.ts`, `hooks/useResizablePanel.ts`, `app/api/agent/**`, `app/api/sessions/**`, `components/SessionSidebar.tsx`, `components/PiWebTitle.tsx`, `components/SessionTree.tsx`, `components/SidebarMenu.tsx`, `components/SidebarToast.tsx`, `components/SidebarIcons.tsx`, `components/ProjectWorktreePicker.tsx`, `components/NewSessionContextBar.tsx`, `components/WorktreeCreateForm.tsx`, `handleNewSession`, `handleSelectSession` and the bar's handlers in `components/AppShell.tsx`, `components/BranchNavigator.tsx`, `components/MessageView.tsx`, `components/MarkdownBody.tsx`, `lib/markdown-blocks.ts`, `lib/streaming-message.ts`, `hooks/useStreamingSelector.ts`, `components/MermaidBlock.tsx`, `components/highlighting.ts`, `components/CodeFile.tsx`, `components/CodeDiff.tsx`, `components/CodeLazy.tsx`, `lib/session-view-cache.ts`, `components/CodemodeToolView.tsx`, `components/ExtensionStatusBar.tsx`, `components/ExtensionWidgets.tsx`. - [tools.md](docs/agents/tools.md): tool presets and Chat only, exact system prompts, tool exposure, the codemode / tool-search / mcp built-ins, the read-only MCP policy, the Code mode and PowerShell `defaultTools` switches. Files: `lib/tool-presets.ts`, `lib/tool-preset-preference.ts`, `lib/chat-only.ts`, `lib/exact-system-prompt.ts`, `lib/builtin-extensions.ts`, `lib/mcp-read-only-policy.ts`, `lib/codemode-settings.ts`, `lib/powershell-settings.ts`, `lib/global-settings-file.ts`, `app/api/agent/new/route.ts`, `app/api/tools/settings/route.ts`, tool selection in `lib/rpc-manager.ts`. - [mcp-runtime.md](docs/agents/mcp-runtime.md): the per-session MCP host (when servers register and connect, reported states, trust read on every sync, idle release); `/mcp` in the composer. Files: `lib/mcp-host.ts`, `lib/mcp-transport.ts`, `lib/mcp-status.ts`, `lib/mcp-command.ts`, `lib/mcp-config-key.ts`, MCP wiring in `lib/rpc-manager.ts` and `lib/builtin-extensions.ts`, `/mcp` handling in `hooks/useAgentSession.ts`. - [mcp-settings.md](docs/agents/mcp-settings.md): Settings › MCP reads without running anything, masking, the trust dialog's server list, row states, notices, Code mode choice, trust from Settings, Escape stacking, every `mcp.json` write and undo. Files: `app/api/mcp/route.ts`, `app/api/project-trust/route.ts`, `lib/mcp-config-read.ts`, `lib/mcp-config-file.ts`, `lib/mcp-override.ts`, `lib/mcp-undo.ts`, `lib/mcp-secrets.ts`, `lib/mcp-server-display.ts`, `lib/mcp-json-error.ts`, `lib/project-trust.ts`, `lib/regular-file.ts`, `lib/stacked-dialog.ts`, `lib/settings-navigation.ts`, `components/McpConfig.tsx`, `components/mcp-config-helpers.ts`, `components/ProjectTrustDialog.tsx`, `components/SettingsPanel.tsx`. @@ -255,8 +294,10 @@ Design decisions and traps live in `docs/agents/`, one note per area. Read every - [mcp-add.md](docs/agents/mcp-add.md): Settings › MCP add (paste re-parsed on the server, host-variable confirmation, literal secrets kept global, fresh-folder trust, the add pane) and the paste importer's escaping and grammars. Files: `lib/mcp-add.ts`, `lib/mcp-import*.ts`, `lib/shell-words.ts`, fresh-folder trust in `lib/project-trust.ts`, `components/McpAddServer.tsx`, `components/mcp-add-helpers.ts`, the `add` action of `app/api/mcp/route.ts`. - [models.md](docs/agents/models.md): default model and reasoning level, providers registered at session_start, mid-run reasoning changes, remote provider catalogs, `enabledModels` scoping and minimal edits, managed models' context-window overrides, provider auth listing and credentials, the extension-loaded runtime those routes share. Files: `app/api/models/**`, `app/api/models-config/**`, `app/api/auth/**`, `lib/model-runtime.ts`, `lib/default-preferences.ts`, `lib/model-scope.ts`, `lib/enabled-models*.ts`, `lib/model-context.ts`, `lib/model-catalog-refresh.ts`, `lib/deferred-provider-models.ts`, `lib/provider-listing*.ts`, `components/ModelsConfig.tsx`, `components/EnabledModelsSection.tsx`, `components/ModelContextWindow.tsx`, `components/ModelSelector.tsx`, `components/SelectorRow.tsx`. - [files-and-access.md](docs/agents/files-and-access.md): worktrees and project grouping, the file access allow-list (the `/api/files` security boundary), file tree visibility and its open directories per folder, uploads and chat file drops, the source view's languages by extension, the CSV/TSV table view, web password throttling. Files: `app/api/files/**`, `app/api/cwd/**`, `app/api/worktrees/**`, `app/api/file-index/**`, `app/api/web-auth/**`, `proxy.ts`, `lib/path-security.ts`, `lib/file-access.ts`, `lib/linked-directory.ts`, `lib/session-file-references*.ts`, `lib/file-tree-visibility.ts`, `lib/file-tree-expansion.ts`, `lib/file-upload-client.ts`, `lib/file-version.ts`, `lib/worktree.ts`, `lib/paths.ts`, `lib/auth-throttle.ts`, `components/ProjectWorktreePicker.tsx`, `components/NewSessionContextBar.tsx`, `components/FileExplorer.tsx`, `lib/delimited-table.ts`, `components/DelimitedTable.tsx`, the table branch of `components/FileViewer.tsx`, `hooks/useDragDrop.ts`. -- [settings-ui.md](docs/agents/settings-ui.md): General's sidebar file-browser choice, Plugins and Skills routes, sidebar group switches, the shared `SettingsUi` blocks every settings panel and add pane uses. Files: `app/api/plugins/**`, `app/api/skills/**`, `lib/skills-service.ts`, `components/SettingsUi.tsx`, `components/settings-ui-helpers.ts`, `components/SkillsConfig.tsx`, `components/PluginsConfig.tsx`, `hooks/useFilesPlacement.ts`; also before adding a settings section or add pane. +- [settings-ui.md](docs/agents/settings-ui.md): the Settings dialog (the section column, the page, phones' list and page, rows, switches, segmented choices, theme cards), General's sidebar file-browser choice, Plugins and Skills routes, sidebar group switches, the shared `SettingsUi` blocks every settings panel and add pane uses. Files: `components/SettingsPanel.tsx`, `components/FontSettings.tsx`, `app/settings.css`, `app/api/plugins/**`, `app/api/skills/**`, `lib/skills-service.ts`, `components/SettingsUi.tsx`, `components/settings-ui-helpers.ts`, `components/SkillsConfig.tsx`, `components/PluginsConfig.tsx`, `hooks/useFilesPlacement.ts`; also before adding a settings section or add pane. - [subagents.md](docs/agents/subagents.md): the built-in subagent setting, profiles and their files, run status, completion notifications, the subagent viewer tab. Files: `lib/subagent*.ts`, `app/api/subagents/**`, `components/AgentsConfig.tsx`, `components/SubagentViewer.tsx`, `components/subagent-viewer-state.ts`. +- [browser.md](docs/agents/browser.md): the shared Browser pane's server side: one lazily launched Chromium per process (profile, executable, idle close, crashes, stale profile locks), tabs, console ring, dialogs and downloads, the screencast and the event stream's backpressure, snapshots and refs, the browser_* tools, the HTTP API, input, the element picker, the ports list, direct previews and "one browser for both" (the agent acting in the user's preview through its bridge, one cookie jar), limits. Files: `lib/browser/**`, `app/api/browser/**`, `createBrowserExtension()` in `lib/rpc-manager.ts`. +- [desktop-shell.md](docs/agents/desktop-shell.md): the Claude Code Desktop layout on Base UI: the composer (the controlled-popover "/" and "@" list, the token backdrop, modes, the model menu, the context meter), every dropdown's proportions (the references' measures as `--popup-*`), the header, its session menu and pane buttons, the tiles (layout tree, placement and closed slots, resize, move, expand, keys, focus, stored layouts, phones), the file section portaled from the sidebar, the Terminal pane (tabs in the header, groups per folder, states, reattach or a new shell after a restart, Attach as context, settings), the Changes pane's review comments, the Browser pane's stream and input, the trace lines, the Hugging Face connector. Files: `components/ui/*`, `app/ui.css`, `app/shell.css`, `components/ChatInput.tsx`, `components/Composer*.tsx`, `components/ContextMeter.tsx`, `components/SessionHeader.tsx`, `components/TileLayout.tsx`, `components/ChangesPane.tsx`, `components/BrowserPane.tsx`, `components/TerminalTabs.tsx`, `components/TerminalPanel.tsx`, `lib/terminal-tabs.ts`, `lib/terminal-theme.ts`, `lib/terminal-settings.ts`, `lib/terminal-client.ts`, `lib/terminal-manager.ts`, `app/api/terminal/**`, the pane parts of `components/AppShell.tsx`, `lib/tile-layout.ts`, `hooks/usePaneLayout.ts`, `lib/trace-line.ts`, `lib/shortcuts.ts`, `lib/mcp-presets.ts`, `hooks/useFilesPlacement.ts`. - [client-platform.md](docs/agents/client-platform.md): the global shortcuts (Ctrl+Alt+N on macOS's dead-key Option+N and Windows AltGr, Esc stacking), mobile software keyboard and viewport height, the terminal's extra keys, its own dark/light theme and its font size following the chat's, completion sound. Files: `hooks/useKeyboardShortcuts.ts`, `hooks/useViewportHeight.ts`, `hooks/useAudio.ts`, `components/TerminalExtraKeys.tsx`, `lib/terminal-extra-keys.ts`, `lib/terminal-theme.ts`, `hooks/useTerminalTheme.ts`, `terminalFontSize()` in `hooks/useChatAppearance.ts`, the keyboard-open CSS. --- @@ -264,7 +305,7 @@ Design decisions and traps live in `docs/agents/`, one note per area. Read every ## Old Safari (iOS 16.2) - `/` renders entirely on the client, so one script chunk the browser cannot parse is a blank page. Next 16 targets Safari 16.4+; the `browserslist` in `package.json` lowers Safari and iOS to 16.2 so SWC turns class `static {}` blocks into private static fields. That covers Next's client runtime; other node_modules keep their syntax unless listed in `transpilePackages` (mermaid and `@mermaid-js/parser` are, for their lazy diagram chunks). Keep the other browserslist entries at Next's defaults. -- Never write a RegExp lookbehind (`(?<=`, `(?/_.jsonl` --accent --user-bg --tool-bg --font-mono ``` + +The hf-design branch adds the "Pi Web on Hugging Face" design system's variables, in every theme: + +``` +--bg-surface (fields, the composer, opened tool calls, code) --bg-elevated (menus, dialogs) +--text-strong --primary --primary-contrast (primary buttons) --line (drawings, never borders) +--danger --danger-soft --success --warning --focus-ring --scrim --shadow-pop +``` + +Its light and dark values are written by the design system's `design/build.py` between +`tokens:begin` and `tokens:end`; `--border` is transparent (fills tell surfaces apart). +The same block defines the scales components use instead of literals: `--radius-xs` … `--radius-xl` +(4, 6, 8, 10, 12px), `--size-control-lg` / `-control` / `-control-sm` / `-control-xs` (36, 32, 28, +24px, Claude Desktop's button heights), `--size-row`, `--size-bar` and `--space-*`. Buttons are the +`.ui-btn` classes in `app/globals.css` (`--sm`, `--xs`, `--lg`, `--icon`, `--primary`, …); the +transcript's pieces (`.turn-user`, `.turn-reply`, `.steps-summary`, `.tool-call-row`, `.composer`; the composer's +newer parts, menus, tooltips and popovers in `app/ui.css`, the header and tiles in `app/shell.css`) +are styled there too. +`--accent` is a neutral gray in light and dark (no blue anywhere: links are told apart by their +underline, focus by its line and halo), `--accent-soft` its tint; Mist, Rose and Pine keep their own hues. +All code (fences, the file viewer, tool inputs and outputs, diffs) is highlighted by @pierre/highlights (Shiki where WebAssembly SIMD is missing) in `lib/diffs-theme.ts`'s neutral themes (`components/highlighting.ts`); the terminal keeps its ANSI hues. +Keyboard focus is `app/focus.css`, imported last: one 1px line at 80% of `--focus-ring` +(`--focus-line`), never a thick ring and no halo (`--focus-glow` is empty: beside a filled field or a +selected row the old 3px halo read as a second outline). The line sits 1px inside controls, rows and +fields (a row draws it on the row; its link or toggle draws nothing); what is focusable by tabindex +alone (a menu's surface, a tile) keeps its shadow; ink-filled controls and text links take the line +2px off. `app/focus.test.mjs` checks that no 2px ring or halo is left (the gateway's sheets too), that +the line keeps 3:1 on every surface, that a pane card has one edge (an inset hairline over its +contents, `::after`) and that Settings' separators share `--settings-line`. Hidden row actions fold to width 0 (never `display: none`: Tab would drop +focus); closed panels are `inert`; modals use `keepTabInside`. diff --git a/app/api/browser/events/route.ts b/app/api/browser/events/route.ts new file mode 100644 index 0000000..6cb59c9 --- /dev/null +++ b/app/api/browser/events/route.ts @@ -0,0 +1,71 @@ +import { BrowserEventChannel } from "@/lib/browser/event-channel"; +import { getBrowserHost } from "@/lib/browser/host"; +import { browserSpaceOf } from "@/lib/browser/http"; + +export const dynamic = "force-dynamic"; +export const runtime = "nodejs"; + +const HEARTBEAT_MS = 30_000; + +/** + * The Browser panel's live view of one chat's tabs (`?space=`): + * `state` first and on every change, `frame` (that chat's active tab: live + * JPEG, then a sharp WebP still at `?scale=` density once the page stops + * changing) and `console` events. The stream pulls one + * event at a time (highWaterMark 0), so a slow client skips frames instead of + * queueing them; see lib/browser/event-channel.ts. It stays open while the + * browser is closed and frames start once it launches. + */ +export async function GET(req: Request) { + const host = getBrowserHost(); + await host.refreshAvailability(); + const space = browserSpaceOf(req); + const params = new URL(req.url).searchParams; + const scale = Number(params.get("scale")) || 1; + // A pane showing a direct preview wants the state, not the frames. + const frames = params.get("frames") !== "0"; + // A pane showing a direct preview is also where the agent's tools act (lib/browser/preview-links.ts). + const preview = params.get("preview") === "1"; + const channel = new BrowserEventChannel(); + const encoder = new TextEncoder(); + let unsubscribe: (() => void) | null = null; + let linkId: string | null = null; + const close = () => { + unsubscribe?.(); + unsubscribe = null; + if (linkId) host.previews.detach(linkId); + linkId = null; + channel.close(); + req.signal.removeEventListener("abort", close); + }; + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode(":\n\n")); + unsubscribe = host.subscribe((event) => channel.push(event), { frames, space, scale }); + if (preview) linkId = host.previews.attach(space, (event) => channel.push(event)); + req.signal.addEventListener("abort", close, { once: true }); + if (req.signal.aborted) close(); + }, + async pull(controller) { + const next = await channel.next(HEARTBEAT_MS); + if (next === null) { + close(); + try { controller.close(); } catch { /* already closed */ } + return; + } + controller.enqueue(encoder.encode(next === "heartbeat" ? ":\n\n" : `data: ${JSON.stringify(next)}\n\n`)); + }, + cancel() { + close(); + }, + }, { highWaterMark: 0 }); + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache, no-transform", + Connection: "keep-alive", + "X-Accel-Buffering": "no", + }, + }); +} diff --git a/app/api/browser/input/route.ts b/app/api/browser/input/route.ts new file mode 100644 index 0000000..bca9171 --- /dev/null +++ b/app/api/browser/input/route.ts @@ -0,0 +1,22 @@ +import { NextResponse } from "next/server"; +import { getBrowserHost } from "@/lib/browser/host"; +import { browserErrorResponse, browserSpaceOf, readJsonBody } from "@/lib/browser/http"; +import { parseBrowserInput } from "@/lib/browser/requests"; + +export const dynamic = "force-dynamic"; +export const runtime = "nodejs"; + +/** One mouse, key or text event on the active tab, in CSS pixels of the viewport. */ +export async function POST(req: Request) { + const read = await readJsonBody(req); + if (!read.ok) return read.response; + const parsed = parseBrowserInput(read.body); + if (!parsed.ok) return NextResponse.json({ error: parsed.error }, { status: 400 }); + try { + const host = getBrowserHost(); + await host.inSpace(browserSpaceOf(req), () => host.input(parsed.value)); + return NextResponse.json({ ok: true }); + } catch (error) { + return browserErrorResponse(error); + } +} diff --git a/app/api/browser/pick/route.ts b/app/api/browser/pick/route.ts new file mode 100644 index 0000000..043e9f8 --- /dev/null +++ b/app/api/browser/pick/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from "next/server"; +import { getBrowserHost } from "@/lib/browser/host"; +import { browserErrorResponse, browserSpaceOf, readJsonBody } from "@/lib/browser/http"; +import { parseBrowserPoint } from "@/lib/browser/requests"; + +export const dynamic = "force-dynamic"; +export const runtime = "nodejs"; + +/** The element at `{ x, y }` (CSS pixels of the viewport), for the panel's Select element mode. */ +export async function POST(req: Request) { + const read = await readJsonBody(req); + if (!read.ok) return read.response; + const parsed = parseBrowserPoint(read.body); + if (!parsed.ok) return NextResponse.json({ error: parsed.error }, { status: 400 }); + try { + const host = getBrowserHost(); + return NextResponse.json(await host.inSpace(browserSpaceOf(req), () => host.pick(parsed.value.x, parsed.value.y))); + } catch (error) { + return browserErrorResponse(error); + } +} diff --git a/app/api/browser/ports/route.ts b/app/api/browser/ports/route.ts new file mode 100644 index 0000000..2136376 --- /dev/null +++ b/app/api/browser/ports/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; +import { listDevServerPorts } from "@/lib/browser/ports"; + +export const dynamic = "force-dynamic"; +export const runtime = "nodejs"; + +/** TCP ports listening on this machine, for the panel's Dev servers chips (Linux; empty elsewhere). */ +export async function GET() { + try { + return NextResponse.json({ ports: await listDevServerPorts() }, { headers: { "Cache-Control": "no-store" } }); + } catch (error) { + return NextResponse.json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 }); + } +} diff --git a/app/api/browser/preview/route.ts b/app/api/browser/preview/route.ts new file mode 100644 index 0000000..b7c7182 --- /dev/null +++ b/app/api/browser/preview/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from "next/server"; +import { hasJsonContentType } from "@/lib/request-security"; +import { getBrowserHost } from "@/lib/browser/host"; +import { parsePreviewReport } from "@/lib/browser/requests"; + +export const dynamic = "force-dynamic"; +export const runtime = "nodejs"; + +/** A snapshot of a long page is bigger than the other routes' bodies. */ +const MAX_BODY_BYTES = 8 * 1024 * 1024; + +/** + * A Browser pane's direct preview reporting to the agent's side + * (lib/browser/preview-links.ts): where its page is, or its bridge's answer to + * a command. The link id is the one its event stream sent it. + */ +export async function POST(req: Request) { + if (!hasJsonContentType(req)) return NextResponse.json({ error: "Content-Type must be application/json" }, { status: 415 }); + const text = await req.text(); + if (text.length > MAX_BODY_BYTES) return NextResponse.json({ error: "Request body is too large" }, { status: 413 }); + let body: unknown; + try { + body = JSON.parse(text); + } catch { + return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); + } + const parsed = parsePreviewReport(body); + if (!parsed.ok) return NextResponse.json({ error: parsed.error }, { status: 400 }); + const known = getBrowserHost().previews.report(parsed.value); + return NextResponse.json({ ok: known }, { status: known ? 200 : 404 }); +} diff --git a/app/api/browser/route.ts b/app/api/browser/route.ts new file mode 100644 index 0000000..27160a8 --- /dev/null +++ b/app/api/browser/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from "next/server"; +import { getBrowserHost } from "@/lib/browser/host"; +import { browserErrorResponse, browserSpaceOf, readJsonBody } from "@/lib/browser/http"; +import { parseBrowserAction } from "@/lib/browser/requests"; + +export const dynamic = "force-dynamic"; +export const runtime = "nodejs"; + +/** The shared browser's state, as one chat (`?space=`) sees it; never launches it. */ +export async function GET(req: Request) { + const host = getBrowserHost(); + try { + return NextResponse.json(await host.inSpace(browserSpaceOf(req), () => host.currentState()), { headers: { "Cache-Control": "no-store" } }); + } catch (error) { + return browserErrorResponse(error); + } +} + +/** `{ action, ... }` from the Browser panel; `navigate` and `new_tab` launch the browser when needed. */ +export async function POST(req: Request) { + const read = await readJsonBody(req); + if (!read.ok) return read.response; + const parsed = parseBrowserAction(read.body); + if (!parsed.ok) return NextResponse.json({ error: parsed.error }, { status: 400 }); + const host = getBrowserHost(); + try { + const body = await host.inSpace(browserSpaceOf(req), async () => { + const result = await host.userAction(parsed.value); + return { ok: true, state: await host.currentState(), ...result }; + }); + return NextResponse.json(body); + } catch (error) { + return browserErrorResponse(error); + } +} diff --git a/app/api/default-cwd/route.ts b/app/api/default-cwd/route.ts index fd40164..4cd00a2 100644 --- a/app/api/default-cwd/route.ts +++ b/app/api/default-cwd/route.ts @@ -1,13 +1,16 @@ import { NextResponse } from "next/server"; -import { mkdirSync } from "fs"; -import { defaultCwdPath } from "@/lib/default-cwd"; +import { existsSync, mkdirSync } from "fs"; +import { defaultCwdPath, freshCwdPath } from "@/lib/default-cwd"; -// POST /api/default-cwd -// Creates ~/pi-cwd/ (local date) if it doesn't exist and returns the path. +// POST /api/default-cwd body (optional): { fresh?: boolean } +// Creates ~/pi-cwd/ (local date) if it doesn't exist and returns the path; with +// `fresh`, a new folder of its own, ~/pi-cwd/-, for a chat that should not +// share a folder another chat is working in. // The client then selects it through /api/cwd/validate like any other directory. -export async function POST() { +export async function POST(req: Request) { try { - const dir = defaultCwdPath(); + const body = await req.json().catch(() => ({})) as { fresh?: unknown }; + const dir = body.fresh === true ? freshCwdPath(new Date(), undefined, existsSync) : defaultCwdPath(); mkdirSync(dir, { recursive: true }); return NextResponse.json({ cwd: dir }); } catch (error) { diff --git a/app/api/files/[...path]/route.ts b/app/api/files/[...path]/route.ts index 33bcd5e..f243afd 100644 --- a/app/api/files/[...path]/route.ts +++ b/app/api/files/[...path]/route.ts @@ -405,7 +405,7 @@ function wrapDocxPreviewHtml(bodyHtml: string, fileName: string): string { + + + + + + diff --git a/public/offline.html b/public/offline.html index 0dac4b2..510150c 100644 --- a/public/offline.html +++ b/public/offline.html @@ -3,14 +3,38 @@ - + + Pi Web is offline +
- + +

Pi Web is offline

-

Reconnect to the local Pi Web server, then try again.

+

Check your connection, then try again.