diff --git "a/piweb/hf-design.patch" "b/piweb/hf-design.patch" --- "a/piweb/hf-design.patch" +++ "b/piweb/hf-design.patch" @@ -1,17 +1,8 @@ diff --git a/AGENTS.md b/AGENTS.md -index 497a505..a3d9eee 100644 +index 69a70b1..978fe9a 100644 --- a/AGENTS.md +++ b/AGENTS.md -@@ -50,7 +50,7 @@ app/api/ - sessions/[id]/auto-name/route.ts POST generate a session title - sessions/search/route.ts GET session search - agent/new/route.ts POST { cwd, type: prompt|ensure_session (start only), message?, toolNames?, provider?, modelId?, thinkingLevel? } -- agent/[id]/route.ts GET state | POST any command -+ agent/[id]/route.ts GET state (?systemPrompt=0: without the prompt) | POST any command - agent/[id]/events/route.ts GET SSE stream - agent/running/route.ts GET running + awaiting-input session ids, sidebar pins/archive revision - auth/api-key/[provider]/route.ts POST/DELETE stored provider API key -@@ -67,8 +67,13 @@ app/api/ +@@ -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 @@ -27,7 +18,15 @@ index 497a505..a3d9eee 100644 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 -@@ -142,14 +147,26 @@ lib/ +@@ -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 @@ -57,7 +56,7 @@ index 497a505..a3d9eee 100644 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 -@@ -179,32 +196,53 @@ lib/ +@@ -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 @@ -117,7 +116,7 @@ index 497a505..a3d9eee 100644 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 -@@ -217,24 +255,26 @@ components/ +@@ -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 @@ -139,7 +138,8 @@ index 497a505..a3d9eee 100644 + 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 - useSessionUiState.ts sidebar pins, archive, project order: optimistic writes in order, refetch on revision change + 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 @@ -149,30 +149,28 @@ index 497a505..a3d9eee 100644 ``` --- -@@ -243,24 +283,26 @@ hooks/ +@@ -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, transcript system / usage / context-edit entries, running-state polling, custom-message collapsing, streamed chunks re-rendering only the reply, streaming markdown block by block, 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, 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`, `components/MarkdownBody.tsx`, `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/**`. +-- [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`. - - [mcp-test-sign-in.md](docs/agents/mcp-test-sign-in.md): Settings › MCP Test (route checks, bounded connection, `!command` queue, redaction, status store) and OAuth sign-in / sign-out. Files: `app/api/mcp/test/**`, `app/api/mcp/sign-in/**`, `lib/mcp-test.ts`, `lib/mcp-entry-request.ts`, `lib/mcp-status.ts`, `lib/mcp-sign-in.ts`, `lib/mcp-sign-out.ts`, `components/McpSignIn.tsx`, `components/mcp-sign-in-helpers.ts`, `components/OAuthPastePanel.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`. -+- [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. Files: `app/api/models/**`, `app/api/models-config/**`, `app/api/auth/**`, `lib/default-preferences.ts`, `lib/model-scope.ts`, `lib/enabled-models*.ts`, `lib/model-catalog-refresh.ts`, `lib/deferred-provider-models.ts`, `lib/provider-listing*.ts`, `components/ModelsConfig.tsx`, `components/EnabledModelsSection.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/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`. + - [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/**`, `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`. --- [client-platform.md](docs/agents/client-platform.md): mobile software keyboard and viewport height, the terminal's extra keys and its own dark/light theme, completion sound. Files: `hooks/useViewportHeight.ts`, `hooks/useAudio.ts`, `components/TerminalExtraKeys.tsx`, `lib/terminal-extra-keys.ts`, `lib/terminal-theme.ts`, `hooks/useTerminalTheme.ts`, the keyboard-open CSS. +- [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): mobile software keyboard and viewport height, the terminal's extra keys, completion sound. Files: `hooks/useViewportHeight.ts`, `hooks/useAudio.ts`, `components/TerminalExtraKeys.tsx`, `lib/terminal-extra-keys.ts`, the keyboard-open CSS. + - [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. @@ -181,7 +179,7 @@ index 497a505..a3d9eee 100644 ## Pi Session File Format -@@ -286,3 +328,33 @@ Location: `~/.pi/agent/sessions//_.jsonl` +@@ -290,3 +331,33 @@ Location: `~/.pi/agent/sessions//_.jsonl` --accent --user-bg --tool-bg --font-mono ``` @@ -215,105 +213,6 @@ index 497a505..a3d9eee 100644 +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/README.md b/README.md -index e3069a9..fcce7e5 100644 ---- a/README.md -+++ b/README.md -@@ -110,7 +110,7 @@ npx @agegr/pi-web@latest - - - **Agent data**: Pi Web reads pi data from `~/.pi/agent` by default, including session files under `sessions//_.jsonl`. Set `PI_CODING_AGENT_DIR` to use another pi agent directory. - - **Filesystem access**: Pi Web must be able to read the agent data directory and the working directories recorded by its sessions. Run Pi Web in the same filesystem environment as pi when sharing existing sessions. --- **Shared configuration**: the Models panel uses pi's model, settings, and credential storage, so changes are visible to both interfaces. -+- **Shared configuration**: the Models panel uses pi's model, settings, and credential storage, so changes are visible to both interfaces. An open session applies a saved edit of its model when you select the model again. - - **File access boundary**: the file browser is limited to working directories selected in Pi Web and project or session roots it already knows about; it is not a general filesystem browser. - - **Git worktrees**: see [Worktrees in Pi Web](./docs/worktrees.md) for switcher visibility, worktree creation, and removal behavior. - -diff --git a/app/api/agent/[id]/route.ts b/app/api/agent/[id]/route.ts -index a573dfe..eb4d31e 100644 ---- a/app/api/agent/[id]/route.ts -+++ b/app/api/agent/[id]/route.ts -@@ -71,7 +71,7 @@ export async function POST( - - // GET /api/agent/[id] - Get current agent state - export async function GET( -- _req: Request, -+ req: Request, - { params }: { params: Promise<{ id: string }> } - ) { - const { id } = await params; -@@ -82,7 +82,9 @@ export async function GET( - return NextResponse.json({ running: false }); - } - -- const state = await session.send({ type: "get_state" }); -+ const state = await session.send({ type: "get_state" }) as Record; -+ // Status reads leave out the system prompt, most of the reply (agentStatusUrl()). -+ if (new URL(req.url).searchParams.get("systemPrompt") === "0") delete state.systemPrompt; - return NextResponse.json({ running: true, state }); - } catch (error) { - return NextResponse.json({ error: String(error) }, { status: 500 }); -diff --git a/app/api/agent/state-route.integration.test.mjs b/app/api/agent/state-route.integration.test.mjs -new file mode 100644 -index 0000000..4a8829e ---- /dev/null -+++ b/app/api/agent/state-route.integration.test.mjs -@@ -0,0 +1,56 @@ -+import assert from "node:assert/strict"; -+import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; -+import { tmpdir } from "node:os"; -+import { join } from "node:path"; -+import test from "node:test"; -+import { createJiti } from "jiti"; -+ -+const jiti = createJiti(import.meta.url, { -+ alias: { "@": process.cwd() }, -+ interopDefault: true, -+}); -+ -+test("status reads of a session's state leave out its system prompt (#1302)", async (t) => { -+ const dir = await mkdtemp(join(tmpdir(), "pi-web-agent-state-")); -+ const previousDir = process.env.PI_CODING_AGENT_DIR; -+ process.env.PI_CODING_AGENT_DIR = dir; -+ let session; -+ t.after(async () => { -+ await session?.shutdown(); -+ if (previousDir === undefined) delete process.env.PI_CODING_AGENT_DIR; -+ else process.env.PI_CODING_AGENT_DIR = previousDir; -+ await rm(dir, { recursive: true, force: true }); -+ }); -+ // A project whose context file makes the prompt large, as the issue's ~350 KB replies were. -+ const cwd = join(dir, "project"); -+ await mkdir(cwd); -+ await writeFile(join(cwd, "AGENTS.md"), `# Notes\n\n${"- a project rule the agent must follow\n".repeat(8_000)}`); -+ await writeFile(join(dir, "settings.json"), "{}"); -+ -+ const { agentStatusUrl } = await jiti.import("../../../lib/agent-client.ts"); -+ const { startRpcSession } = await jiti.import("../../../lib/rpc-manager.ts"); -+ const { GET } = await jiti.import("./[id]/route.ts"); -+ const started = await startRpcSession("__new__agent-state", "", cwd, {}); -+ session = started.session; -+ const realSessionId = started.realSessionId; -+ -+ const read = async (path) => { -+ const response = await GET(new Request(`http://localhost${path}`), { params: Promise.resolve({ id: realSessionId }) }); -+ assert.equal(response.status, 200); -+ const text = await response.text(); -+ return { bytes: Buffer.byteLength(text), body: JSON.parse(text) }; -+ }; -+ const full = await read(`/api/agent/${encodeURIComponent(realSessionId)}`); -+ const status = await read(agentStatusUrl(realSessionId)); -+ t.diagnostic(`GET state: ${full.bytes} bytes; status read: ${status.bytes} bytes`); -+ -+ assert.match(full.body.state.systemPrompt, /a project rule the agent must follow/); -+ assert.equal(status.body.running, true); -+ assert.equal("systemPrompt" in status.body.state, false); -+ // Everything else the polls read is still there, unchanged. -+ const rest = { ...full.body.state }; -+ delete rest.systemPrompt; -+ assert.deepEqual(status.body.state, rest); -+ assert.ok(full.bytes > 300_000); -+ assert.ok(status.bytes < 2_000); -+}); diff --git a/app/api/browser/events/route.ts b/app/api/browser/events/route.ts new file mode 100644 index 0000000..6cb59c9 @@ -572,7 +471,7 @@ index fd40164..4cd00a2 100644 return NextResponse.json({ cwd: dir }); } catch (error) { diff --git a/app/api/files/[...path]/route.ts b/app/api/files/[...path]/route.ts -index 67d1d63..66ab988 100644 +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 { @@ -675,39 +574,6 @@ index 9dc27bb..0362104 100644 } catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 }); } -diff --git a/app/api/models-config/route.test.mjs b/app/api/models-config/route.test.mjs -index 0af3eda..3613f37 100644 ---- a/app/api/models-config/route.test.mjs -+++ b/app/api/models-config/route.test.mjs -@@ -101,3 +101,28 @@ test("a hand-edited override pi accepts does not block saving an unrelated provi - assert.equal(response.status, 200); - assert.deepEqual(await (await GET()).json(), edited); - }); -+ -+test("model-level endpoints round-trip through Save, override only that model, and can return to inheritance", async () => { -+ const { ModelRuntime } = await import("@earendil-works/pi-coding-agent"); -+ const config = { providers: { relay: { -+ baseUrl: "https://synthetic.invalid", api: "anthropic-messages", apiKey: "synthetic-not-a-key", -+ models: [ -+ { id: "one", api: "openai-responses", baseUrl: "https://synthetic.invalid/v1", headers: { "x-example": "keep" } }, -+ { id: "two" }, -+ ], -+ } } }; -+ const options = { authPath: join(testAgentDir, "auth.json"), modelsPath, modelsStorePath: join(testAgentDir, "models-store.json"), refreshOnCreate: false }; -+ assert.equal((await PUT(put(config))).status, 200); -+ assert.deepEqual(await (await GET()).json(), config); -+ const runtime = await ModelRuntime.create(options); -+ assert.equal(runtime.getError(), undefined); -+ assert.equal(runtime.getModel("relay", "one").baseUrl, "https://synthetic.invalid/v1"); -+ assert.equal(runtime.getModel("relay", "two").baseUrl, "https://synthetic.invalid"); -+ assert.equal(runtime.getModel("relay", "two").api, "anthropic-messages"); -+ delete config.providers.relay.models[0].baseUrl; -+ assert.equal((await PUT(put(config))).status, 200); -+ assert.deepEqual(await (await GET()).json(), config); -+ const reset = await ModelRuntime.create(options); -+ assert.equal(reset.getModel("relay", "one").baseUrl, "https://synthetic.invalid"); -+ assert.equal(reset.getModel("relay", "one").api, "openai-responses"); -+}); diff --git a/app/api/models/route.test.mjs b/app/api/models/route.test.mjs index f5fe8c9..4b8db6e 100644 --- a/app/api/models/route.test.mjs @@ -1723,10 +1589,10 @@ x=WyK>Q474TgO3Dx+=lt**@4jiTmoXh>QX;DqIUnCXbm^o4n^-mRR&Qm{tL_JHERF> diff --git a/app/focus.css b/app/focus.css new file mode 100644 -index 0000000..36662a0 +index 0000000..e07d4b3 --- /dev/null +++ b/app/focus.css -@@ -0,0 +1,324 @@ +@@ -0,0 +1,323 @@ +/* Keyboard focus, one way everywhere (the design system's "Motion and states"). + Imported last (app/layout.tsx), so these rules win over each component's. + @@ -1941,7 +1807,6 @@ index 0000000..36662a0 + +/* Rows whose control spans the row: the control's own box, with the row's corners. */ +.session-tree .session-tree-pinned-toggle:focus-visible, -+.session-tree .session-tree-footer-button:focus-visible, +.session-tree .session-tree-more-toggle:focus-visible, +.sidebar-files-section-toggle:focus-visible { + border-radius: var(--radius-sm); @@ -4519,7 +4384,7 @@ index 0000000..8d003fe +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/app/globals.css b/app/globals.css -index 49e70be..7dcadeb 100644 +index 5e7936f..7dcadeb 100644 --- a/app/globals.css +++ b/app/globals.css @@ -24,46 +24,109 @@ @@ -5382,7 +5247,7 @@ index 49e70be..7dcadeb 100644 } /* Only ever set background-color here: the `background` shorthand resets background-clip and would snap the thumb to the full 10px on hover. */ -@@ -805,23 +641,10 @@ html[data-keyboard-open] .terminal-extra-keys { +@@ -805,33 +641,10 @@ html[data-keyboard-open] .terminal-extra-keys { } ::-webkit-scrollbar-thumb { border-width: 1px; @@ -5391,6 +5256,16 @@ index 49e70be..7dcadeb 100644 } } +-/* The file panel's tab strip hides its scrollbar: a scrollbar takes its height +- out of the 36px bar and squeezes the tabs (#1225). A wheel, a swipe or the +- arrow keys scroll it, and TabBar keeps the active tab in view. */ +-.tabbar-scroll { +- scrollbar-width: none; +-} +-.tabbar-scroll::-webkit-scrollbar { +- display: none; +-} +- -/* The terminal picks its surface apart from the app theme, so --text-dim - could disappear against it. */ -.terminal-xterm .xterm-viewport::-webkit-scrollbar-thumb { @@ -5407,7 +5282,7 @@ index 49e70be..7dcadeb 100644 .extension-status-shelf { display: flex; flex-shrink: 0; -@@ -832,246 +655,114 @@ html[data-keyboard-open] .terminal-extra-keys { +@@ -842,269 +655,131 @@ html[data-keyboard-open] .terminal-extra-keys { background: transparent; } @@ -5518,29 +5393,40 @@ index 49e70be..7dcadeb 100644 - -.extension-status-row > .extension-status-line { - flex: 0 0 auto; --} -- ++ min-width: 0; ++ min-height: 35px; ++ color: var(--text-muted); ++ font-family: var(--font-mono); ++ font-size: 11px; ++ line-height: 1.45; ++ white-space: pre; + } + -.extension-widget-trigger { - position: relative; -- display: flex; ++/* Every status is a cell of the status line, told apart from its neighbours by a ++ vertical rule like the widget triggers. A `command:` status is a button cell, as ++ tall as the line, so the whole cell can be clicked. */ ++.extension-status-item { + display: flex; - flex: 0 0 108px; -- align-items: center; ++ flex: 0 0 auto; + align-items: center; - width: 108px; - height: 35px; - gap: 5px; - padding: 0 7px; - overflow: hidden; - border: 0; -- border-right: 1px solid color-mix(in srgb, var(--border) 78%, transparent); ++ padding: 8px 12px; + border-right: 1px solid color-mix(in srgb, var(--border) 78%, transparent); - background: transparent; -+ min-width: 0; -+ min-height: 35px; - color: var(--text-muted); - font-family: var(--font-mono); - font-size: 11px; +- color: var(--text-muted); +- font-family: var(--font-mono); +- font-size: 11px; - text-align: left; --} -- + } + -.extension-widget-update-pulse { - position: absolute; - z-index: 0; @@ -5548,63 +5434,62 @@ index 49e70be..7dcadeb 100644 - opacity: 0; - pointer-events: none; - transition: opacity 180ms ease-out; --} -- ++.extension-status-item:last-child { ++ border-right: 0; + } + -.extension-widget-update-pulse::before { - position: absolute; - inset: 0; - background: var(--accent); - content: ""; - opacity: 0.12; --} -- ++.extension-status-command { ++ border-top: 0; ++ border-bottom: 0; ++ border-left: 0; ++ background: transparent; ++ color: var(--text); ++ font: inherit; ++ line-height: 1.45; ++ white-space: nowrap; ++ cursor: pointer; ++ transition: background 0.12s, color 0.12s; + } + -.extension-widget-trigger.is-updating .extension-widget-update-pulse { - opacity: 1; -+ line-height: 1.45; -+ white-space: pre; ++.extension-status-command:hover:not(:disabled) { ++ background: var(--bg-hover); } -.extension-widget-trigger.is-updating .extension-widget-update-pulse::before { - animation: extension-widget-update-pulse 900ms ease-in-out infinite alternate; -+/* Every status is a cell of the status line, told apart from its neighbours by a -+ vertical rule like the widget triggers. A `command:` status is a button cell, as -+ tall as the line, so the whole cell can be clicked. */ -+.extension-status-item { -+ display: flex; -+ flex: 0 0 auto; -+ align-items: center; -+ padding: 8px 12px; -+ border-right: 1px solid color-mix(in srgb, var(--border) 78%, transparent); ++.extension-status-command:focus-visible { ++ outline: 1px solid var(--focus-line); ++ outline-offset: -1px; ++ box-shadow: var(--focus-glow); } -@keyframes extension-widget-update-pulse { - from { opacity: 0.18; } - to { opacity: 0.06; } -+.extension-status-item:last-child { -+ border-right: 0; ++.extension-status-command:disabled { ++ cursor: not-allowed; ++ opacity: 0.5; } -button.extension-widget-trigger { -+.extension-status-command { -+ border-top: 0; -+ border-bottom: 0; -+ border-left: 0; -+ background: transparent; -+ color: var(--text); -+ font: inherit; -+ line-height: 1.45; -+ white-space: nowrap; - cursor: pointer; - transition: background 0.12s, color 0.12s; - } - +- cursor: pointer; +- transition: background 0.12s, color 0.12s; +-} +- -button.extension-widget-trigger:hover, -.extension-widget-trigger.is-expanded { -+.extension-status-command:hover:not(:disabled) { - background: var(--bg-hover); +- background: var(--bg-hover); - color: var(--text); - } - +-} +- -button.extension-widget-trigger:focus-visible { - outline: 2px solid var(--accent); - outline-offset: -2px; @@ -5725,17 +5610,13 @@ index 49e70be..7dcadeb 100644 -.extension-status-command:focus-visible { - outline: 2px solid var(--accent); - outline-offset: -2px; -+.extension-status-command:focus-visible { -+ outline: 1px solid var(--focus-line); -+ outline-offset: -1px; -+ box-shadow: var(--focus-glow); - } - - .extension-status-command:disabled { -@@ -1079,22 +770,16 @@ button.extension-widget-trigger:focus-visible { - opacity: 0.5; - } - +-} +- +-.extension-status-command:disabled { +- cursor: not-allowed; +- opacity: 0.5; +-} +- -/* Touch screens get taller cells when there is something to tap, widget triggers - included, so the row stays level. */ +/* Touch screens get taller cells when there is something to tap. */ @@ -5758,7 +5639,7 @@ index 49e70be..7dcadeb 100644 --font-ui: var(--font-ui-default); --font-mono: var(--font-mono-default); --font-ui-weight: 400; -@@ -1133,7 +818,6 @@ pre, code { +@@ -1143,7 +818,6 @@ pre, code { margin: 10px 0 5px; color: var(--text); line-height: 1.35; @@ -5766,7 +5647,7 @@ index 49e70be..7dcadeb 100644 } .markdown-body h1 { font-size: 1.16em; } .markdown-body h2 { font-size: 1.08em; } -@@ -1150,11 +834,11 @@ pre, code { +@@ -1160,11 +834,11 @@ pre, code { .markdown-body ol > li { list-style-type: decimal; } .markdown-body li > p { margin: 0; } .markdown-body li::marker { @@ -5781,7 +5662,7 @@ index 49e70be..7dcadeb 100644 font-weight: 700; } .markdown-body em { color: var(--text-muted); } -@@ -1163,25 +847,46 @@ pre, code { +@@ -1173,16 +847,16 @@ pre, code { text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; @@ -5798,27 +5679,23 @@ index 49e70be..7dcadeb 100644 margin: 8px 0; - border-radius: 6px; + border-radius: var(--radius-sm); -+} -+/* Firefox draws its audio controls without a background (bare buttons on the -+ page), Chrome inside a grey pill: both get the same frame from the theme. */ -+.markdown-body audio { -+ display: block; -+ width: min(360px, 100%); -+ height: 36px; -+ margin: 8px 0; -+ background: var(--bg-subtle); + } + /* Firefox draws its audio controls without a background (bare buttons on the + page), Chrome inside a grey pill: both get the same frame from the theme. */ +@@ -1192,7 +866,7 @@ pre, code { + height: 36px; + margin: 8px 0; + background: var(--bg-subtle); +- border: 1px solid var(--border); + border: 1px solid var(--line); -+ border-radius: 18px; -+ clip-path: inset(0 round 18px); -+} -+.markdown-body audio::-webkit-media-controls-enclosure { -+ background: transparent; -+} -+.markdown-body video { -+ display: block; -+ max-width: min(640px, 100%); -+ max-height: 360px; -+ margin: 8px 0; + border-radius: 18px; + clip-path: inset(0 round 18px); + } +@@ -1204,16 +878,15 @@ pre, code { + max-width: min(640px, 100%); + max-height: 360px; + margin: 8px 0; +- border-radius: 6px; + border-radius: var(--radius-md); } .markdown-body .markdown-inline-code { @@ -5833,7 +5710,7 @@ index 49e70be..7dcadeb 100644 } .markdown-body .contains-task-list { padding-left: 0; -@@ -1203,7 +908,7 @@ pre, code { +@@ -1235,7 +908,7 @@ pre, code { height: 14px; margin: 0; border: 1px solid var(--border); @@ -5842,7 +5719,7 @@ index 49e70be..7dcadeb 100644 background: var(--bg); display: inline-grid; place-content: center; -@@ -1233,9 +938,10 @@ pre, code { +@@ -1265,9 +938,10 @@ pre, code { .markdown-table-frame { width: 100%; margin: 8px 0; @@ -5855,7 +5732,7 @@ index 49e70be..7dcadeb 100644 } .markdown-table-wrap { width: 100%; -@@ -1282,6 +988,14 @@ pre, code { +@@ -1314,6 +988,14 @@ pre, code { .markdown-user-message p { white-space: pre-wrap; } @@ -5870,7 +5747,7 @@ index 49e70be..7dcadeb 100644 .markdown-custom-message { line-height: 1.32; -@@ -1360,7 +1074,7 @@ pre, code { +@@ -1392,7 +1074,7 @@ pre, code { padding: 7px 8px; overflow: auto; border: 1px solid var(--border); @@ -5879,7 +5756,7 @@ index 49e70be..7dcadeb 100644 background: var(--bg-panel); font-family: var(--font-mono); font-size: 11px; -@@ -1380,16 +1094,13 @@ pre, code { +@@ -1412,16 +1094,13 @@ pre, code { min-width: 0; max-width: 100%; margin: 6px 0; @@ -5899,7 +5776,7 @@ index 49e70be..7dcadeb 100644 color: var(--text-dim); font-size: 11px; display: flex; -@@ -1409,7 +1120,7 @@ pre, code { +@@ -1441,7 +1120,7 @@ pre, code { } .markdown-code-action { border: 1px solid var(--border); @@ -5908,7 +5785,7 @@ index 49e70be..7dcadeb 100644 padding: 2px 7px; background: var(--bg); color: var(--text-muted); -@@ -1463,8 +1174,9 @@ pre, code { +@@ -1495,8 +1174,9 @@ pre, code { opacity: 0.85; } .mermaid-preview-button:focus-visible { @@ -5920,7 +5797,7 @@ index 49e70be..7dcadeb 100644 } .image-preview-dialog { -@@ -1500,7 +1212,7 @@ pre, code { +@@ -1532,7 +1212,7 @@ pre, code { max-width: 100%; max-height: 100%; object-fit: contain; @@ -5929,7 +5806,7 @@ index 49e70be..7dcadeb 100644 box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); } -@@ -1516,7 +1228,7 @@ pre, code { +@@ -1548,7 +1228,7 @@ pre, code { height: 36px; padding: 0; border: 1px solid var(--border); @@ -5938,7 +5815,7 @@ index 49e70be..7dcadeb 100644 background: var(--bg-panel); color: var(--text-muted); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.28); -@@ -1531,7 +1243,7 @@ pre, code { +@@ -1563,7 +1243,7 @@ pre, code { } .image-preview-close:focus-visible { @@ -5947,7 +5824,7 @@ index 49e70be..7dcadeb 100644 outline-offset: 2px; } -@@ -1592,7 +1304,7 @@ pre, code { +@@ -1624,7 +1304,7 @@ pre, code { height: 24px; overflow: hidden; border: 1px solid var(--border); @@ -5956,7 +5833,7 @@ index 49e70be..7dcadeb 100644 background: var(--bg); } .mermaid-zoom-stepper button, -@@ -1619,7 +1331,7 @@ pre, code { +@@ -1651,7 +1331,7 @@ pre, code { .mermaid-zoom-icon-button { flex: 0 0 24px; border: 1px solid var(--border); @@ -5965,7 +5842,7 @@ index 49e70be..7dcadeb 100644 } .mermaid-zoom-stepper button:hover:not(:disabled), .mermaid-zoom-icon-button:hover { -@@ -1632,8 +1344,9 @@ pre, code { +@@ -1664,8 +1344,9 @@ pre, code { } .mermaid-zoom-stepper button:focus-visible, .mermaid-zoom-icon-button:focus-visible { @@ -5977,7 +5854,7 @@ index 49e70be..7dcadeb 100644 } .mermaid-zoom-value { min-width: 48px; -@@ -1656,7 +1369,7 @@ pre, code { +@@ -1688,7 +1369,7 @@ pre, code { .mermaid-zoom-canvas { padding: 12px; border: 1px solid var(--border); @@ -5986,7 +5863,7 @@ index 49e70be..7dcadeb 100644 background: var(--bg-panel); margin: 0 auto; transition: width 0.15s ease; -@@ -1732,7 +1445,7 @@ pre, code { +@@ -1764,7 +1445,7 @@ pre, code { height: 24px; overflow: hidden; border: 1px solid var(--border); @@ -5995,7 +5872,7 @@ index 49e70be..7dcadeb 100644 } .file-viewer-mode-button { -@@ -1743,7 +1456,6 @@ pre, code { +@@ -1775,7 +1456,6 @@ pre, code { font: inherit; font-size: 11px; font-weight: 500; @@ -6003,7 +5880,7 @@ index 49e70be..7dcadeb 100644 cursor: pointer; white-space: nowrap; } -@@ -1775,12 +1487,11 @@ pre, code { +@@ -1807,12 +1487,11 @@ pre, code { height: 32px; padding: 0; box-sizing: border-box; @@ -6018,7 +5895,7 @@ index 49e70be..7dcadeb 100644 opacity: 0; visibility: hidden; pointer-events: none; -@@ -1814,7 +1525,7 @@ pre, code { +@@ -1846,7 +1525,7 @@ pre, code { } .chat-scroll-to-bottom:focus-visible { @@ -6027,7 +5904,7 @@ index 49e70be..7dcadeb 100644 outline-offset: 2px; } -@@ -1838,133 +1549,28 @@ pre, code { +@@ -1870,133 +1549,28 @@ pre, code { } } @@ -6174,7 +6051,7 @@ index 49e70be..7dcadeb 100644 } } -@@ -1972,8 +1578,8 @@ pre, code { +@@ -2004,8 +1578,8 @@ pre, code { tab's stacked boxes, the row below the sessions), as the sidebar drew it on main: two grey boxes in the header buttons' material, 11px, the project's whole path and the worktree's branch in code type, cut at their @@ -6185,7 +6062,7 @@ index 49e70be..7dcadeb 100644 .project-picker { display: flex; align-items: center; -@@ -1994,52 +1600,14 @@ pre, code { +@@ -2026,52 +1600,14 @@ pre, code { min-width: 0; height: 29px; padding: 0 10px; @@ -6241,7 +6118,7 @@ index 49e70be..7dcadeb 100644 } /* The project's name (its display name, else its folder's), cut at its -@@ -2053,8 +1621,9 @@ pre, code { +@@ -2085,8 +1621,9 @@ pre, code { max-width: 28ch; overflow: hidden; color: var(--text-dim); @@ -6252,7 +6129,7 @@ index 49e70be..7dcadeb 100644 } .project-picker-parent { -@@ -2072,10 +1641,6 @@ pre, code { +@@ -2104,10 +1641,6 @@ pre, code { text-overflow: ellipsis; } @@ -6263,7 +6140,7 @@ index 49e70be..7dcadeb 100644 .project-picker-button:hover, .project-picker-button[aria-expanded="true"] { background: var(--bg-selected); -@@ -2083,8 +1648,9 @@ pre, code { +@@ -2115,8 +1648,9 @@ pre, code { } .project-picker-button:focus-visible { @@ -6275,7 +6152,7 @@ index 49e70be..7dcadeb 100644 } .project-picker-icon, -@@ -2123,7 +1689,8 @@ pre, code { +@@ -2155,7 +1689,8 @@ pre, code { path's color and code type, cut at its end. */ .project-picker-label.is-alias { color: var(--text); @@ -6285,7 +6162,7 @@ index 49e70be..7dcadeb 100644 } /* A path or a branch, cut at its left (rtl), the text itself kept left to -@@ -2173,7 +1740,8 @@ pre, code { +@@ -2205,7 +1740,8 @@ pre, code { border-color: color-mix(in srgb, var(--accent) 40%, transparent); background: color-mix(in srgb, var(--accent) 6%, transparent); color: var(--text-dim); @@ -6295,7 +6172,7 @@ index 49e70be..7dcadeb 100644 } /* No worktree list here (the files tab): a box that says why, and does nothing. */ -@@ -2250,7 +1818,7 @@ pre, code { +@@ -2282,7 +1818,7 @@ pre, code { box-sizing: border-box; flex: 0 0 24px; border: 1px solid var(--border); @@ -6304,7 +6181,7 @@ index 49e70be..7dcadeb 100644 background: transparent; color: var(--text-muted); cursor: pointer; -@@ -2275,13 +1843,9 @@ pre, code { +@@ -2307,13 +1843,9 @@ pre, code { .file-viewer-mode-button:focus-visible, .file-viewer-icon-button:focus-visible { @@ -6321,7 +6198,7 @@ index 49e70be..7dcadeb 100644 } @media (max-width: 640px) { -@@ -2309,7 +1873,7 @@ pre, code { +@@ -2341,7 +1873,7 @@ pre, code { .markdown-file-preview pre { background: var(--bg-panel); border: 1px solid var(--border); @@ -6330,7 +6207,7 @@ index 49e70be..7dcadeb 100644 padding: 12px 16px; overflow-x: auto; font-family: var(--font-mono); -@@ -2321,7 +1885,7 @@ pre, code { +@@ -2353,7 +1885,7 @@ pre, code { font-size: 0.9em; background: var(--bg-panel); padding: 1px 5px; @@ -6339,7 +6216,7 @@ index 49e70be..7dcadeb 100644 } .markdown-file-preview pre code { background: none; -@@ -2333,7 +1897,7 @@ pre, code { +@@ -2365,7 +1897,7 @@ pre, code { margin-bottom: 20px; padding: 14px 16px 12px; border: 1px solid var(--border); @@ -6348,7 +6225,7 @@ index 49e70be..7dcadeb 100644 background: var(--bg-panel); } .markdown-frontmatter-title { -@@ -2386,18 +1950,6 @@ pre, code { +@@ -2418,18 +1950,6 @@ pre, code { text-underline-offset: 2px; } @@ -6367,7 +6244,7 @@ index 49e70be..7dcadeb 100644 @keyframes drop-zone-in { from { opacity: 0; transform: scale(0.97); } to { opacity: 1; transform: scale(1); } -@@ -2502,10 +2054,6 @@ span.token.table { +@@ -2534,10 +2054,6 @@ span.token.table { .panel-resize-handle.is-resizing::after { background: color-mix(in srgb, var(--text-muted) 70%, var(--border)); } @@ -6378,7 +6255,7 @@ index 49e70be..7dcadeb 100644 /* Desktop sidebar: animate width */ @media (min-width: 641px) { .sidebar-overlay-backdrop { -@@ -2536,182 +2084,33 @@ span.token.table { +@@ -2568,133 +2084,20 @@ span.token.table { } } @@ -6388,29 +6265,20 @@ index 49e70be..7dcadeb 100644 - overflow: hidden; - transition: width 0.2s ease, min-width 0.2s ease; - flex-shrink: 0; -- } -- .right-panel-container.right-panel-open { -- width: var(--right-panel-width, clamp(360px, 42vw, 640px)); -- min-width: 300px; +/* Mobile sidebar: slide in/out as overlay */ +@media (max-width: 640px) { + .chat-stats-center { + display: none !important; } +- .right-panel-container.right-panel-open { +- width: var(--right-panel-width, clamp(360px, 42vw, 640px)); +- min-width: 300px; +- } - .right-panel-container.right-panel-closed { - width: 0; - min-width: 0; - border-left: none !important; -+ .sidebar-container { -+ position: fixed !important; -+ top: 0; -+ left: env(safe-area-inset-left); -+ bottom: 0; -+ width: 280px; -+ max-width: 85vw; -+ height: var(--app-viewport-height, 100dvh); -+ transition: transform 0.25s ease, box-shadow 0.25s ease; - } +- } - /* keep inner content at fixed width so it doesn't reflow during animation */ - .right-panel-container > * { - width: var(--right-panel-width, clamp(360px, 42vw, 640px)); @@ -6522,16 +6390,19 @@ index 49e70be..7dcadeb 100644 - max-width: 85vw; - height: var(--app-viewport-height, 100dvh); - transition: transform 0.25s ease, box-shadow 0.25s ease; -- } -- .sidebar-container.sidebar-open { -- transform: translateX(0); -- box-shadow: 4px 0 20px rgba(0,0,0,0.15); -+ .sidebar-container.sidebar-open { -+ transform: translateX(0); -+ box-shadow: 4px 0 20px rgba(0,0,0,0.15); ++ .sidebar-container { ++ position: fixed !important; ++ top: 0; ++ left: env(safe-area-inset-left); ++ bottom: 0; ++ width: 280px; ++ max-width: 85vw; ++ height: var(--app-viewport-height, 100dvh); ++ transition: transform 0.25s ease, box-shadow 0.25s ease; } - .sidebar-container.sidebar-closed { - transform: translateX(calc(-100% - env(safe-area-inset-left))); + .sidebar-container.sidebar-open { + transform: translateX(0); +@@ -2705,45 +2108,9 @@ span.token.table { box-shadow: none; } @@ -6578,7 +6449,24 @@ index 49e70be..7dcadeb 100644 textarea, input, select { -@@ -2746,3 +2145,507 @@ span.token.table { +@@ -2763,15 +2130,10 @@ span.token.table { + touch screens opt in too; tablets keep their controls because the keyboard + leaves them enough height. */ + @media (max-width: 640px), (pointer: coarse) and (max-height: 500px) { ++ html[data-keyboard-open] .chat-input-controls, + html[data-keyboard-open] .extension-status-shelf { + display: none !important; + } +- /* Except while its model picker is open: the picker's filter is the field +- the keyboard is for, and hiding its row would hide the picker (#1271). +- Its own rule, so a browser without :has() drops only this one. */ +- html[data-keyboard-open] .chat-input-controls:not(:has(.model-selector [role="listbox"])) { +- display: none !important; +- } + html[data-keyboard-open] .chat-content { + padding-bottom: 0 !important; + } +@@ -2783,3 +2145,507 @@ span.token.table { display: none !important; } } @@ -7252,7 +7140,7 @@ index 79c8282..97731f1 100644 lang: "en", icons: [ diff --git a/app/settings.css b/app/settings.css -index f0e2e95..547fa2a 100644 +index 3820cf8..547fa2a 100644 --- a/app/settings.css +++ b/app/settings.css @@ -35,14 +35,15 @@ @@ -8330,12 +8218,13 @@ index f0e2e95..547fa2a 100644 } .settings-dialog-main { -@@ -1563,194 +1684,198 @@ +@@ -1563,203 +1684,198 @@ min-height: 0; } -/* The page scrolls across the dialog's full width, so its scrollbar sits at the -- dialog's edge; only the content keeps a readable column. */ +- dialog's edge; only the content keeps a readable column, centered so a wide +- dialog leaves its spare width on both sides. */ +/* ---------------------------------------------------------------- General + The page scrolls across its whole width, so its scrollbar sits at the + dialog's edge; the rows keep a readable column on the page title's side. */ @@ -8349,6 +8238,7 @@ index f0e2e95..547fa2a 100644 .settings-general > * { - max-width: 600px; +- margin-inline: auto; + max-width: 760px; } @@ -8587,14 +8477,18 @@ index f0e2e95..547fa2a 100644 - row-gap: 0; +.settings-font-row { + align-items: flex-start; -+} -+ + } + +-.settings-font-rows > .settings-font-row > input[type="range"] { +- grid-column: 1 / -1; +.settings-font-row > .config-row-text { + padding-top: 7px; } --.settings-font-rows > .settings-font-row > input[type="range"] { +-.settings-chat-range-header.settings-font-header { - grid-column: 1 / -1; +- grid-template-columns: max-content minmax(0, 1fr) auto 28px; +- grid-template-columns: subgrid; +.settings-range-value { + color: var(--text-muted); + font-family: var(--font-ui); @@ -8603,10 +8497,8 @@ index f0e2e95..547fa2a 100644 + text-align: right; } --.settings-chat-range-header.settings-font-header { -- grid-column: 1 / -1; -- grid-template-columns: max-content minmax(0, 1fr) auto 28px; -- grid-template-columns: subgrid; +-.settings-chat-range-header.settings-font-header label { +- padding-right: 4px; +.config-row-control input[type="range"] { + display: block; + width: 100%; @@ -8615,8 +8507,11 @@ index f0e2e95..547fa2a 100644 + accent-color: var(--text-strong); } --.settings-chat-range-header.settings-font-header label { -- padding-right: 4px; +-/* A weight's name, not its number: wide enough for the longest so the name field keeps its width while dragging. */ +-.settings-chat-range-header.settings-font-header output { +- min-width: 11ch; +- font-family: inherit; +- text-align: right; +.settings-chat-reset { + width: 28px; + height: 28px; @@ -8652,7 +8547,7 @@ index f0e2e95..547fa2a 100644 font-family: var(--font-mono); font-weight: var(--font-mono-weight); } -@@ -1760,197 +1885,324 @@ +@@ -1769,197 +1885,324 @@ } .settings-font-input:focus-visible { @@ -9108,7 +9003,7 @@ index f0e2e95..547fa2a 100644 } @media (max-width: 640px) { -@@ -1971,13 +2223,13 @@ +@@ -1980,13 +2223,13 @@ width: 100%; height: 190px; border-right: 0; @@ -9124,7 +9019,7 @@ index f0e2e95..547fa2a 100644 } .agents-feature-actions { -@@ -1989,29 +2241,6 @@ +@@ -1998,29 +2241,6 @@ padding: 14px; } @@ -9154,7 +9049,7 @@ index f0e2e95..547fa2a 100644 } @keyframes saved-pop { -@@ -2021,7 +2250,7 @@ +@@ -2030,7 +2250,7 @@ } @keyframes saved-check-draw { @@ -9163,7 +9058,7 @@ index f0e2e95..547fa2a 100644 to { stroke-dashoffset: 0; opacity: 1; } } -@@ -2150,7 +2379,7 @@ +@@ -2159,7 +2379,7 @@ max-width: 100%; padding: 0 5px; border: 1px solid var(--border); @@ -9172,7 +9067,7 @@ index f0e2e95..547fa2a 100644 background: var(--bg-panel); color: var(--text); font-family: var(--font-mono); -@@ -2169,7 +2398,7 @@ +@@ -2178,7 +2398,7 @@ padding: 6px 8px; overflow: auto; border: 1px solid var(--border); @@ -9181,7 +9076,7 @@ index f0e2e95..547fa2a 100644 background: var(--bg-panel); color: var(--text-muted); font-family: var(--font-mono); -@@ -2256,27 +2485,14 @@ +@@ -2265,27 +2485,14 @@ min-width: 0; height: 32px; padding: 0 10px; @@ -9211,7 +9106,7 @@ index f0e2e95..547fa2a 100644 .mcp-add-toggle { display: flex; -@@ -2350,10 +2566,9 @@ select.mcp-add-input { +@@ -2359,10 +2566,9 @@ select.mcp-add-input { max-width: 100%; max-height: 100%; overflow: hidden; @@ -9225,7 +9120,7 @@ index f0e2e95..547fa2a 100644 } /* Focus moves to the dialog itself when it opens (tabIndex -1); it is not a control, so no ring. */ -@@ -2366,7 +2581,7 @@ select.mcp-add-input { +@@ -2375,7 +2581,7 @@ select.mcp-add-input { flex: 1 1 auto; gap: 12px; min-height: 0; @@ -9234,7 +9129,7 @@ index f0e2e95..547fa2a 100644 overflow: auto; overscroll-behavior: contain; } -@@ -2399,7 +2614,7 @@ select.mcp-add-input { +@@ -2408,7 +2614,7 @@ select.mcp-add-input { margin-top: 10px; padding: 8px 10px; border: 1px solid var(--border); @@ -9243,7 +9138,7 @@ index f0e2e95..547fa2a 100644 background: var(--bg); color: var(--text); font-family: var(--font-mono); -@@ -2417,7 +2632,7 @@ select.mcp-add-input { +@@ -2426,7 +2632,7 @@ select.mcp-add-input { .project-trust-error { flex-shrink: 0; max-height: 30vh; @@ -9252,7 +9147,7 @@ index f0e2e95..547fa2a 100644 overflow: auto; color: #ef4444; font-size: 12px; -@@ -2437,19 +2652,20 @@ select.mcp-add-input { +@@ -2446,19 +2652,20 @@ select.mcp-add-input { flex-shrink: 0; justify-content: flex-end; gap: 8px; @@ -9280,7 +9175,7 @@ index f0e2e95..547fa2a 100644 } .project-trust-button:disabled { -@@ -2457,10 +2673,8 @@ select.mcp-add-input { +@@ -2466,10 +2673,8 @@ select.mcp-add-input { } .project-trust-button.is-primary { @@ -9293,7 +9188,7 @@ index f0e2e95..547fa2a 100644 } .project-trust-button.is-primary:disabled { -@@ -2534,7 +2748,7 @@ select.mcp-add-input { +@@ -2543,7 +2748,7 @@ select.mcp-add-input { min-width: 0; padding: 8px 10px; border: 1px solid var(--border); @@ -9302,7 +9197,7 @@ index f0e2e95..547fa2a 100644 background: var(--bg); } -@@ -2556,7 +2770,7 @@ select.mcp-add-input { +@@ -2565,7 +2770,7 @@ select.mcp-add-input { .project-trust-mcp-tag { flex-shrink: 0; padding: 1px 5px; @@ -9311,7 +9206,7 @@ index f0e2e95..547fa2a 100644 background: rgba(120, 120, 120, 0.12); color: var(--text-dim); font-size: 10px; -@@ -2577,7 +2791,7 @@ select.mcp-add-input { +@@ -2586,7 +2791,7 @@ select.mcp-add-input { display: inline-block; margin: 2px 4px 0 0; padding: 0 4px; @@ -10856,7 +10751,7 @@ index 737e7a5..fdf88a2 100644 .sidebar-sheet .sidebar-menu-question-text { diff --git a/app/sidebar.css b/app/sidebar.css -index 0b8f66a..27eaf4f 100644 +index 0b8f66a..c70f0e7 100644 --- a/app/sidebar.css +++ b/app/sidebar.css @@ -91,8 +91,9 @@ @@ -11245,16 +11140,52 @@ index 0b8f66a..27eaf4f 100644 color: var(--text-dim); } -@@ -787,7 +846,7 @@ a.session-tree-main { - gap: 3px; - min-width: 0; - padding: 0 4px 0 5px; +@@ -772,44 +831,6 @@ a.session-tree-main { + color: var(--text); + } + +-/* Footer links: open another project, the archive. Their 12px icons in the +- chevron column, their labels in the text column. */ +-.session-tree-footer { +- right: var(--session-tree-inset-right); +- left: var(--session-tree-inset-left); +- align-items: stretch; +-} +- +-.session-tree-footer-button { +- display: flex; +- flex: 1; +- align-items: center; +- gap: 3px; +- min-width: 0; +- padding: 0 4px 0 5px; - border-radius: 7px; -+ border-radius: var(--radius-sm); - color: var(--text-dim); - font-size: 12px; - text-align: left; -@@ -822,15 +881,21 @@ a.session-tree-main { +- color: var(--text-dim); +- font-size: 12px; +- text-align: left; +-} +- +-.session-tree-footer-button:hover { +- background: var(--bg-hover); +- color: var(--text-muted); +-} +- +-.session-tree-footer-label { +- flex: 1; +- min-width: 0; +- overflow: hidden; +- text-overflow: ellipsis; +- white-space: nowrap; +-} +- +-.session-tree-footer-chevron { +- flex: none; +-} +- + /* Archive view: one header per project above its archived sessions, a main + group's header (chevron, name, ⋯ on hover) that folds its rows, with the + count after the name in the meta color, shown folded or not. */ +@@ -822,15 +843,21 @@ a.session-tree-main { /* Phone layout: 44px rows, a step larger type for titles and project names, ⋯ and the group buttons always shown, no archive quick button. */ @@ -11279,7 +11210,7 @@ index 0b8f66a..27eaf4f 100644 } .session-tree.is-mobile .session-tree-session:hover .session-tree-meta { -@@ -842,7 +907,10 @@ a.session-tree-main { +@@ -842,7 +869,10 @@ a.session-tree-main { } .session-tree.is-mobile .session-tree-group-actions { @@ -11291,7 +11222,7 @@ index 0b8f66a..27eaf4f 100644 } .session-tree.is-mobile .session-tree-group .session-tree-summary { -@@ -867,10 +935,22 @@ a.session-tree-main { +@@ -867,10 +897,22 @@ a.session-tree-main { /* Touch screens wider than a phone have no hover: keep ⋯ reachable. */ @media (hover: none) { .session-tree-more-action { @@ -11315,7 +11246,7 @@ index 0b8f66a..27eaf4f 100644 @media (prefers-reduced-motion: reduce) { .session-tree-unread::after { animation: none; -@@ -925,9 +1005,10 @@ a.session-tree-main { +@@ -925,9 +967,10 @@ a.session-tree-main { display: flex; flex: none; align-items: stretch; @@ -11327,17 +11258,18 @@ index 0b8f66a..27eaf4f 100644 } /* The tablist itself holds only the two tabs (ARIA allows nothing else in it). */ -@@ -937,44 +1018,30 @@ a.session-tree-main { +@@ -937,69 +980,45 @@ a.session-tree-main { align-items: stretch; } -/* The chat bar's cells: a 2px line on top that takes the accent when the - cell is the one chosen (a tab, the open search). */ -+/* The header's controls: quiet 28px pills on the sidebar, no dividing lines; -+ the chosen tab (or the open search) is filled, never marked with a colour. */ - .sidebar-tab, - .sidebar-new-button, - .sidebar-search-toggle { +-.sidebar-tab, +-.sidebar-new-button, +-.sidebar-search-toggle { ++/* The header's tabs: quiet 28px pills on the sidebar, no dividing lines; ++ the chosen tab is filled, never marked with a colour. */ ++.sidebar-tab { display: flex; flex: none; align-items: center; @@ -11379,17 +11311,26 @@ index 0b8f66a..27eaf4f 100644 .sidebar-tab-icon { flex: none; color: var(--text-dim); -@@ -989,17 +1056,12 @@ a.session-tree-main { + } + +-.sidebar-tab:hover, +-.sidebar-new-button:not(:disabled):hover, +-.sidebar-search-toggle:hover { ++.sidebar-tab:hover { + background: var(--bg-hover); + color: var(--text); + } - .sidebar-tab.is-selected, - .sidebar-search-toggle.is-active { +-.sidebar-tab.is-selected, +-.sidebar-search-toggle.is-active { - border-top-color: var(--accent); - background: var(--bg-selected); +- background: var(--bg-selected); - color: var(--text); -} - --.sidebar-tab.is-selected { + .sidebar-tab.is-selected { - font-weight: 500; ++ background: var(--bg-selected); + color: var(--text-strong); } @@ -11399,7 +11340,7 @@ index 0b8f66a..27eaf4f 100644 } .sidebar-tab-count { -@@ -1019,27 +1081,36 @@ a.session-tree-main { +@@ -1019,27 +1038,36 @@ a.session-tree-main { beside the two cells at the sidebar's 180px minimum, and at 11px so do the versions with a digit to spare). */ .sidebar-brand { @@ -11447,7 +11388,7 @@ index 0b8f66a..27eaf4f 100644 } .sidebar-brand:focus { -@@ -1047,8 +1118,10 @@ a.session-tree-main { +@@ -1047,22 +1075,107 @@ a.session-tree-main { } .sidebar-brand:focus-visible { @@ -11457,51 +11398,157 @@ index 0b8f66a..27eaf4f 100644 + outline-offset: -1px; + box-shadow: var(--focus-glow); + outline-offset: 0; ++} ++ ++/* Search, then New, under the header, as Claude Code's sidebar ++ (design/sidebar-search-claude-reference.jpg, measured at 2x): a field the ++ width of the rows (10px in from each side, their right edge) and a row of ++ its own for New. The magnifier and New's disc are centred on one column, ++ 15px into the field as in the reference; the placeholder and New's label ++ start on another, 18px after it. Both are 28px tall, as the rows are (the ++ reference's field is 26px at its own scale). */ ++.sidebar-actions { ++ display: flex; ++ flex: none; ++ flex-direction: column; ++ /* The field's bottom to New's disc: 13px, as in the reference. */ ++ gap: 8px; ++ padding: 2px 10px 6px; } - .sidebar-header-spacer { -@@ -1056,12 +1129,18 @@ a.session-tree-main { +-.sidebar-header-spacer { +- flex: 1; ++.sidebar-search-field, ++.sidebar-new-button { ++ display: flex; ++ align-items: center; ++ height: 28px; ++ margin: 0; ++ border-radius: var(--radius-sm); ++ font-family: var(--font-ui); ++ font-size: 13px; ++ white-space: nowrap; ++ cursor: pointer; ++ transition: background 0.12s, border-color 0.12s, color 0.12s; ++} ++ ++/* A field, not a pill: a hairline and a faint fill, the placeholder quiet. It opens the ++ conversation search (or the files tab's), so it is a button that looks like a field. */ ++.sidebar-search-field { ++ gap: 11px; ++ padding: 0 0 0 7px; ++ /* The reference's hairline is well above its fill (53 and 29 on a 17 sidebar): the theme's ++ --border is the fill's own grey here, so both come from the text colour. */ ++ border: 1px solid color-mix(in srgb, var(--text) 16%, transparent); ++ background: color-mix(in srgb, var(--text) 5%, transparent); ++ color: var(--text-dim); ++ text-align: left; ++} ++ ++.sidebar-search-field:hover { ++ border: 1px solid color-mix(in srgb, var(--text) 26%, transparent); ++ color: var(--text-muted); ++} ++ ++.sidebar-search-field.is-active { ++ background: var(--bg-selected); ++ color: var(--text-muted); ++} ++ ++.sidebar-search-field svg { ++ flex: none; ++} ++ ++.sidebar-search-field-label { min-width: 0; ++ overflow: hidden; ++ text-overflow: ellipsis; } -+/* Spaced by ink, not boxes: Lucide's plus draws the middle 7px of its 12px box, and "N" -+ sits 1.5px into its own. The plus's ink starts about 8.6px from the left, as the label's -+ ends from the right (within a device pixel at 4x), and they are 6.5px apart: our -+ buttons' icon-to-label space (the composer's mode button). */ ++/* The disc is ink with the sidebar's colour for its plus, as the reference's light disc on a ++ dark sidebar (and dark on light). */ .sidebar-new-button { -+ gap: 2px; -+ padding: 0 9px 0 6px; - font-weight: 500; +- font-weight: 500; ++ gap: 9px; ++ padding: 0 0 0 6px; ++ border: 0; ++ background: transparent; ++ color: var(--text); ++ font-weight: 400; ++ text-align: left; } - .sidebar-search-toggle { +-.sidebar-search-toggle { - width: 36px; -+ width: 28px; - padding: 0; +- padding: 0; ++.sidebar-new-button:not(:disabled):hover { ++ background: var(--bg-hover); ++ color: var(--text-strong); ++} ++ ++.sidebar-new-disc { ++ display: inline-flex; ++ flex: none; ++ align-items: center; ++ justify-content: center; ++ width: 18px; ++ height: 18px; ++ border-radius: 50%; ++ /* The reference's disc is 191 on a 17 sidebar: most of the text's ink, a step below it. */ ++ background: color-mix(in srgb, var(--text) 82%, var(--bg)); ++ color: var(--bg); ++} ++ ++.sidebar-new-button:not(:disabled):hover .sidebar-new-disc { ++ background: var(--text-strong); + } + + .sidebar-new-button:disabled { +@@ -1070,16 +1183,14 @@ a.session-tree-main { + cursor: not-allowed; } -@@ -1076,7 +1155,7 @@ a.session-tree-main { - label is still each one's name. */ - .sidebar-header[data-fit="1"] .sidebar-new-button, - .sidebar-header[data-fit="2"] .sidebar-new-button { +-/* Where the labels do not fit (useHeaderFit measures them: their widths +- change with the language): New keeps its + (data-fit 1), then the tabs +- their icons, or the brand's versions take a smaller type (2). A hidden +- label is still each one's name. */ +-.sidebar-header[data-fit="1"] .sidebar-new-button, +-.sidebar-header[data-fit="2"] .sidebar-new-button { - width: 36px; -+ width: 28px; - padding: 0; +- padding: 0; ++.sidebar-new-button:disabled .sidebar-new-disc { ++ background: var(--text-dim); } -@@ -1088,6 +1167,11 @@ a.session-tree-main { ++/* Where the labels do not fit (useHeaderFit measures them: their widths ++ change with the language): the tabs keep their icons, or the brand's ++ versions take a smaller type (data-fit 2). A hidden label is still each ++ one's name. */ + .sidebar-header[data-fit="2"] .sidebar-tab { + padding: 0 11px; + } +@@ -1088,8 +1199,11 @@ a.session-tree-main { font-size: 11px; } +-.sidebar-header[data-fit="1"] .sidebar-new-label, +-.sidebar-header[data-fit="2"] .sidebar-new-label, +/* At that width the versions need the mark's room. */ +.sidebar-header[data-fit="2"] .sidebar-brand.is-version .sidebar-brand-logo { + display: none; +} + - .sidebar-header[data-fit="1"] .sidebar-new-label, - .sidebar-header[data-fit="2"] .sidebar-new-label, .sidebar-header[data-fit="2"] .sidebar-tab-label { -@@ -1106,8 +1190,9 @@ a.session-tree-main { + position: absolute; + width: 1px; +@@ -1100,14 +1214,15 @@ a.session-tree-main { + } + + .sidebar-new-button:focus-visible, +-.sidebar-search-toggle:focus-visible, ++.sidebar-search-field:focus-visible, + .sidebar-tab:focus-visible, + .sidebar-archive-back:focus-visible, .sidebar-archive-menu-button:focus-visible, .sidebar-tool-button:focus-visible, .sidebar-files-section-toggle:focus-visible { @@ -11513,7 +11560,7 @@ index 0b8f66a..27eaf4f 100644 } .sidebar-panel { -@@ -1186,7 +1271,7 @@ a.session-tree-main { +@@ -1186,7 +1301,7 @@ a.session-tree-main { the root too), and its time 4px inside the row. The header row's keys and the picker's boxes end there. */ .session-sidebar.is-files-below { @@ -11522,7 +11569,7 @@ index 0b8f66a..27eaf4f 100644 } /* The files section's header row (files below the sessions only), one row -@@ -1230,7 +1315,7 @@ a.session-tree-main { +@@ -1230,7 +1345,7 @@ a.session-tree-main { padding: 0 5px; overflow: hidden; border: 0; @@ -11531,7 +11578,7 @@ index 0b8f66a..27eaf4f 100644 background: transparent; color: var(--text-muted); font: inherit; -@@ -1256,10 +1341,8 @@ a.session-tree-main { +@@ -1256,10 +1371,8 @@ a.session-tree-main { .sidebar-files-section-label { min-width: 0; overflow: hidden; @@ -11543,7 +11590,7 @@ index 0b8f66a..27eaf4f 100644 } /* The changed files' count, as the pinned section's (.session-tree-pinned-count). */ -@@ -1278,7 +1361,7 @@ a.session-tree-main { +@@ -1278,7 +1391,7 @@ a.session-tree-main { display: flex; flex: 1000 0 calc(7 * 18px); align-items: center; @@ -11552,7 +11599,7 @@ index 0b8f66a..27eaf4f 100644 } /* The section's keys: the files tab's square keys, a step under the row's -@@ -1286,9 +1369,9 @@ a.session-tree-main { +@@ -1286,9 +1399,9 @@ a.session-tree-main { .sidebar-files-keys .sidebar-tool-button { flex: 1 1 0; min-width: 18px; @@ -11565,7 +11612,7 @@ index 0b8f66a..27eaf4f 100644 } /* Under that row (or the whole files tab): the head, then the tree filling -@@ -1301,31 +1384,7 @@ a.session-tree-main { +@@ -1301,31 +1414,7 @@ a.session-tree-main { min-height: 0; } @@ -11598,7 +11645,70 @@ index 0b8f66a..27eaf4f 100644 slides in over the tree, which stays mounted under it (hidden once the archive is in), and away again. SessionTree fills each. */ .sidebar-sessions-views { -@@ -1397,7 +1456,7 @@ a.session-tree-main { +@@ -1380,6 +1469,62 @@ a.session-tree-main { + } + } + ++/* The Conversations header, above every conversation (and the archive view): a ++ section label as the Pinned section's (11px, muted), on the chevrons' column, ++ and its settings on the right, where the rows' own buttons sit ++ (design/conversations-menu-reference.jpg). */ ++.sidebar-conversations-header { ++ display: flex; ++ flex: none; ++ align-items: center; ++ height: 28px; ++ /* The button's centre on the session rows' ⋯ (24px from the edge; 28px on a phone, below). */ ++ padding: 0 13px 0 11px; ++ box-sizing: border-box; ++} ++ ++.sidebar-conversations-header.is-mobile { ++ padding-right: 17px; ++} ++ ++.sidebar-conversations-title { ++ flex: 1; ++ min-width: 0; ++ overflow: hidden; ++ color: var(--text-muted); ++ font-size: 11px; ++ font-weight: 400; ++ text-overflow: ellipsis; ++ white-space: nowrap; ++} ++ ++.sidebar-conversations-menu-button { ++ display: flex; ++ flex: none; ++ align-items: center; ++ justify-content: center; ++ width: 22px; ++ height: 22px; ++ padding: 0; ++ border: 0; ++ border-radius: var(--radius-sm); ++ background: transparent; ++ color: var(--text-dim); ++ cursor: pointer; ++} ++ ++.sidebar-conversations-menu-button:hover, ++.sidebar-conversations-menu-button.is-active { ++ background: var(--bg-hover); ++ color: var(--text); ++} ++ ++.sidebar-conversations-menu-button:focus-visible { ++ outline: 1px solid var(--focus-line); ++ outline-offset: -1px; ++ box-shadow: var(--focus-glow); ++} ++ + .sidebar-archive-bar { + display: flex; + flex: none; +@@ -1397,7 +1542,7 @@ a.session-tree-main { height: 26px; padding: 0 6px; border: 0; @@ -11607,7 +11717,7 @@ index 0b8f66a..27eaf4f 100644 background: transparent; color: var(--text-muted); font-size: 12px; -@@ -1485,10 +1544,11 @@ a.session-tree-main { +@@ -1485,10 +1630,11 @@ a.session-tree-main { six (the changes view is disabled, not gone, without changes), so nothing moves as the count flips. At the sidebar's 180px minimum they give way together, down to 21px wide, and the gap before the views closes. */ @@ -11620,7 +11730,7 @@ index 0b8f66a..27eaf4f 100644 margin-top: 4px; } -@@ -1498,17 +1558,18 @@ a.session-tree-main { +@@ -1498,17 +1644,18 @@ a.session-tree-main { margin-left: auto; } @@ -11644,7 +11754,7 @@ index 0b8f66a..27eaf4f 100644 background: transparent; color: var(--text-dim); cursor: pointer; -@@ -1516,8 +1577,8 @@ a.session-tree-main { +@@ -1516,8 +1663,8 @@ a.session-tree-main { } .sidebar-tool-button:not(:disabled):hover { @@ -11655,6 +11765,15 @@ index 0b8f66a..27eaf4f 100644 } .sidebar-tool-button.is-active, +@@ -1615,7 +1762,7 @@ a.session-tree-main { + @media (prefers-reduced-motion: reduce) { + .sidebar-tab, + .sidebar-new-button, +- .sidebar-search-toggle, ++ .sidebar-search-field, + .sidebar-tool-button { + transition: none; + } diff --git a/app/ui.css b/app/ui.css new file mode 100644 index 0000000..2dfbbbd @@ -12633,14 +12752,10 @@ index 0000000..2dfbbbd +} +.ui-select:has(> select:disabled) > .ui-select-chevron { opacity: 0.45; } diff --git a/components/AgentSessionPanel.tsx b/components/AgentSessionPanel.tsx -index 8d51f3c..6655b11 100644 +index e775621..6655b11 100644 --- a/components/AgentSessionPanel.tsx +++ b/components/AgentSessionPanel.tsx -@@ -2,9 +2,12 @@ - - import { useEffect, useMemo, useState } from "react"; - import { useI18n } from "@/hooks/useI18n"; -+import { countRunningSubagents } from "@/lib/session-family"; +@@ -6,6 +6,8 @@ import { countRunningSubagents } from "@/lib/session-family"; import type { SessionUsageSummary } from "@/lib/session-stats"; import { formatActiveDuration, formatCompactCount, formatCostShort } from "@/lib/session-stats-format"; import type { SessionInfo, SubagentSessionStatus } from "@/lib/types"; @@ -12649,7 +12764,7 @@ index 8d51f3c..6655b11 100644 interface Props { rootSession: SessionInfo; -@@ -90,9 +93,7 @@ function UsageLine({ usage, style }: { usage: SessionUsageSummary; style?: React +@@ -91,9 +93,7 @@ function UsageLine({ usage, style }: { usage: SessionUsageSummary; style?: React > {usage.totalActiveMs > 0 && ( @@ -12660,7 +12775,7 @@ index 8d51f3c..6655b11 100644 {formatActiveDuration(usage.totalActiveMs)} )} -@@ -136,30 +137,21 @@ export function statusColor(status: SubagentSessionStatus): string { +@@ -137,30 +137,21 @@ export function statusColor(status: SubagentSessionStatus): string { export function StatusIcon({ status }: { status: SubagentSessionStatus }) { if (status === "running" || status === "starting") { return ( @@ -12695,7 +12810,7 @@ index 8d51f3c..6655b11 100644 ); } -@@ -228,13 +220,9 @@ function AgentRow({ +@@ -229,13 +220,9 @@ function AgentRow({ > {main ? ( @@ -12711,7 +12826,7 @@ index 8d51f3c..6655b11 100644 )} -@@ -268,10 +256,7 @@ function AgentRow({ +@@ -269,10 +256,7 @@ function AgentRow({ onMouseEnter={(event) => { event.currentTarget.style.background = "var(--bg-hover)"; if (!tabShown) event.currentTarget.style.color = "var(--text)"; }} onMouseLeave={(event) => { event.currentTarget.style.background = "none"; if (!tabShown) event.currentTarget.style.color = "var(--text-muted)"; }} > @@ -12723,16 +12838,7 @@ index 8d51f3c..6655b11 100644 )} -@@ -295,7 +280,7 @@ export function AgentSessionPanel({ rootSession, subagents, selectedSessionId, r - .some((value) => value?.toLowerCase().includes(normalizedQuery)); - }) - : sortedSubagents; -- const runningCount = subagents.filter((session) => runningSessionIds.has(session.id)).length; -+ const runningCount = countRunningSubagents(subagents, runningSessionIds); - const familyIds = useMemo(() => [rootSession.id, ...subagents.map((session) => session.id)], [rootSession.id, subagents]); - const runningKey = familyIds.filter((id) => runningSessionIds.has(id)).join(","); - const usage = useAgentUsage(familyIds, runningKey); -@@ -325,15 +310,8 @@ export function AgentSessionPanel({ rootSession, subagents, selectedSessionId, r +@@ -326,15 +310,8 @@ export function AgentSessionPanel({ rootSession, subagents, selectedSessionId, r
-@@ -365,7 +343,7 @@ export function AgentSessionPanel({ rootSession, subagents, selectedSessionId, r +@@ -366,7 +343,7 @@ export function AgentSessionPanel({ rootSession, subagents, selectedSessionId, r aria-label={t("agentSwitcher.search")} style={{ width: "100%", height: 32, padding: "0 10px", @@ -12862,7 +12968,7 @@ index e232a55..0000000 - assert.deepEqual(saved, { tabs: [{ ...tab, restored: true }], activeId: null, open: false }); -}); diff --git a/components/AppShell.tsx b/components/AppShell.tsx -index bcc3807..1a8346e 100644 +index e02bdd8..1a8346e 100644 --- a/components/AppShell.tsx +++ b/components/AppShell.tsx @@ -3,6 +3,8 @@ @@ -12934,12 +13040,9 @@ index bcc3807..1a8346e 100644 import type { McpErrorResponse, ProjectTrustStatus } from "@/lib/api-types"; import type { ChatInputHandle } from "./ChatInput"; import type { AgentEndInfo, NewSessionChoices } from "@/hooks/useAgentSession"; -@@ -73,8 +91,22 @@ import type { SessionStatsInfo } from "@/lib/pi-types"; - import { formatActiveDuration, formatCompactCount, formatCostShort } from "@/lib/session-stats-format"; - import type { FileViewerState } from "@/lib/file-viewer-state"; +@@ -75,6 +93,20 @@ import type { FileViewerState } from "@/lib/file-viewer-state"; import { PRESET_READ_ONLY, type ToolEntry } from "@/lib/tool-presets"; --import { getSessionFamily } from "@/lib/session-family"; -+import { getAgentsBadge, getSessionFamily } from "@/lib/session-family"; + import { getAgentsBadge, getSessionFamily } from "@/lib/session-family"; import { getLastSettingsSection, settingsSectionRequiresProject, type SettingsSection } from "@/lib/settings-navigation"; +import { LucideIcon } from "./LucideIcon"; +import * as Lucide from "./lucide/icons"; @@ -12978,18 +13081,7 @@ index bcc3807..1a8346e 100644 const AGENT_PANEL_WIDTH = 420; // pi's built-in tools that never change a file; any other tool may (#1144). const READ_ONLY_TOOL_NAMES = new Set(PRESET_READ_ONLY); -@@ -180,6 +223,10 @@ export function AppShell() { - return ids; - }); - }, []); -+ const agentsBadge = activeSessionFamily ? getAgentsBadge(activeSessionFamily.subagents, runningSessionIds) : null; -+ const agentsButtonLabel = agentsBadge?.running -+ ? translate("agentSwitcher.titleRunning", { count: agentsBadge.count }) -+ : translate("agentSwitcher.title"); - // The temporary id distinguishes consecutive fresh composers in one cwd. - const [newSessionCwd, setNewSessionCwd] = useState(null); - const [newSessionDraftId, setNewSessionDraftId] = useState("initial"); -@@ -190,6 +237,10 @@ export function AppShell() { +@@ -194,6 +237,10 @@ export function AppShell() { // draft to the composer that replaces it, which takes them once. const sidebarControlRef = useRef(null); const [sidebarNewSessionContext, setSidebarNewSessionContext] = useState(null); @@ -13000,7 +13092,7 @@ index bcc3807..1a8346e 100644 // The bar's last move until the sidebar reports that cwd (a commit later), // so the bar of the new composer shows the target's project, and a worktree // it just created, from its first frame. -@@ -228,41 +279,37 @@ export function AppShell() { +@@ -232,41 +279,37 @@ export function AppShell() { const [projectTrustError, setProjectTrustError] = useState(null); const [sidebarOpen, setSidebarOpen] = useState(() => !initialNavigation.sidebarCollapsed); const desktopSidebarOpenRef = useRef(!initialNavigation.sidebarCollapsed); @@ -13066,7 +13158,7 @@ index bcc3807..1a8346e 100644 ); const sidebarResizer = useResizablePanel({ ariaLabel: translate("layout.resizeSidebar"), -@@ -275,20 +322,6 @@ export function AppShell() { +@@ -279,20 +322,6 @@ export function AppShell() { storageKey: "pi-sidebar-width", widthRef: sidebarWidthRef, }); @@ -13087,7 +13179,7 @@ index bcc3807..1a8346e 100644 // On mobile the sidebar is an overlay drawer; hide it by default so the chat // is visible on load. Runs once the breakpoint resolves after hydration. // Mobile drawer actions must not change the remembered desktop preference. -@@ -298,11 +331,6 @@ export function AppShell() { +@@ -302,11 +331,6 @@ export function AppShell() { useEffect(() => { setMobileSidebarReady(true); }, []); @@ -13099,7 +13191,7 @@ index bcc3807..1a8346e 100644 const chatInputRef = useRef(null); const [pendingQuotePrompt, setPendingQuotePrompt] = useState<{ sessionId: string; text: string } | null>(null); const topBarRef = useRef(null); -@@ -330,7 +358,6 @@ export function AppShell() { +@@ -334,7 +358,6 @@ export function AppShell() { const [systemInfoLoading, setSystemInfoLoading] = useState(false); const systemInfoLoaderRef = useRef<(() => Promise) | null>(null); const systemInfoLoadIdRef = useRef(0); @@ -13107,7 +13199,7 @@ index bcc3807..1a8346e 100644 const handleSystemPromptChange = useCallback((prompt: string | null) => { setSystemPrompt(prompt); -@@ -356,6 +383,23 @@ export function AppShell() { +@@ -360,6 +383,23 @@ export function AppShell() { const handleSessionStatsChange = useCallback((stats: SessionStatsInfo | null) => { setSessionStats(stats); }, []); @@ -13131,7 +13223,7 @@ index bcc3807..1a8346e 100644 const [copiedSessionField, setCopiedSessionField] = useState(null); const sessionCopyTimerRef = useRef | null>(null); const handleCopySessionField = useCallback((field: SessionCopyField, value: string) => { -@@ -395,10 +439,6 @@ export function AppShell() { +@@ -399,10 +439,6 @@ export function AppShell() { } }, [hasSubagentSessions]); @@ -13142,7 +13234,7 @@ index bcc3807..1a8346e 100644 const toggleTopPanel = useCallback(( panel: "agents" | "branches" | "system" | "tools" | "session", keepMobileToolbarOpen = false, -@@ -452,26 +492,6 @@ export function AppShell() { +@@ -456,26 +492,6 @@ export function AppShell() { }); }, [isMobile]); @@ -13169,7 +13261,7 @@ index bcc3807..1a8346e 100644 useEffect(() => { if (!mobileToolbarMoreOpen) return; -@@ -519,7 +539,7 @@ export function AppShell() { +@@ -523,7 +539,7 @@ export function AppShell() { return () => ro.disconnect(); }, [activeTopPanel, isMobile]); @@ -13178,7 +13270,7 @@ index bcc3807..1a8346e 100644 const [fileTabs, setFileTabs] = useState([]); const [activeFileTabId, setActiveFileTabId] = useState(null); const fileWorkspaceStatesRef = useRef(new Map()); -@@ -528,20 +548,17 @@ export function AppShell() { +@@ -532,20 +548,17 @@ export function AppShell() { // tab just parked goes to its workspace, not to a same-path tab restored. const [fileWorkspaceGeneration, setFileWorkspaceGeneration] = useState(0); const fileWorkspaceGenerationRef = useRef(0); @@ -13204,7 +13296,7 @@ index bcc3807..1a8346e 100644 ...agentTabs.map((tab) => ({ id: `agent:${tab.sessionId}`, label: tab.label, -@@ -552,12 +569,8 @@ export function AppShell() { +@@ -556,12 +569,8 @@ export function AppShell() { useEffect(() => { try { @@ -13219,7 +13311,7 @@ index bcc3807..1a8346e 100644 } catch { /* storage is optional */ } setTerminalsRestored(true); }, []); -@@ -565,13 +578,9 @@ export function AppShell() { +@@ -569,13 +578,9 @@ export function AppShell() { useEffect(() => { if (!terminalsRestored) return; try { @@ -13235,7 +13327,7 @@ index bcc3807..1a8346e 100644 const handleFileViewerStateChange = useCallback(( generation: number, -@@ -592,37 +601,34 @@ export function AppShell() { +@@ -596,37 +601,34 @@ export function AppShell() { const next = switchFileWorkspace(fileWorkspaceStatesRef.current, currentKey, nextKey, { tabs: fileTabs, activeTabId: activeFileId, @@ -13283,7 +13375,7 @@ index bcc3807..1a8346e 100644 const initialSessionId = initialNavigation.sessionId; const initialLinkedSessionId = linkedSessionId(initialNavigation); -@@ -695,8 +701,11 @@ export function AppShell() { +@@ -699,8 +701,11 @@ export function AppShell() { // the just-created empty chat during that initial synchronization. suppressCwdBumpRef.current = true; const draftId = `initial:${requestedCwd}`; @@ -13296,7 +13388,7 @@ index bcc3807..1a8346e 100644 setNewSessionCwd(data.cwd); setInitialCwdStatus("ready"); if (!new URLSearchParams(window.location.search).get("cwd")) { -@@ -894,8 +903,17 @@ export function AppShell() { +@@ -898,8 +903,17 @@ export function AppShell() { } }, [activeCwd, changeFileWorkspace, invalidateWorkspaceRestore, router, isMobile, newSessionCwd, selectedSession]); @@ -13315,26 +13407,16 @@ index bcc3807..1a8346e 100644 const draftKey = `new:${sessionId}:${cwd}`; // Leaving a fresh composer for another cwd parks its draft there, as a // workspace switch does; New in the same cwd still starts empty. The bar -@@ -936,7 +954,17 @@ export function AppShell() { - setActiveTopPanel(null); - if (isMobile) setSidebarOpen(false); - router.replace(`?cwd=${encodeURIComponent(cwd)}`, { scroll: false }); +@@ -950,7 +964,7 @@ export function AppShell() { + chatInputRef.current?.focus(); + }); + } - }, [activeCwd, changeFileWorkspace, invalidateWorkspaceRestore, isMobile, newSessionCwd, router, selectedSession]); -+ // The fresh composer takes the caret once it renders (#1313), unless the -+ // move came from the new-session bar, whose control keeps focus instead -+ // (newSessionBarFocusRef), or a phone would raise the keyboard for it. -+ // A dialog still open over the page (Ctrl+Alt+N under Settings) keeps it. -+ if (!isMobile && !newSessionBarFocusRef.current) { -+ requestAnimationFrame(() => { -+ if (document.activeElement?.closest("[aria-modal='true']")) return; -+ chatInputRef.current?.focus(); -+ }); -+ } + }, [activeCwd, changeFileWorkspace, chatWorktreeOn, invalidateWorkspaceRestore, isMobile, newSessionCwd, router, selectedSession, sidebarNewSessionContext]); // Global keyboard shortcuts (handles Esc, Ctrl+Alt+N etc.) useGlobalKeyboardShortcuts({ -@@ -970,10 +998,12 @@ export function AppShell() { +@@ -984,10 +998,12 @@ export function AppShell() { invalidateWorkspaceRestore(); activeNewSessionDraftKeyRef.current = null; setNewSessionCwd(null); @@ -13348,7 +13430,7 @@ index bcc3807..1a8346e 100644 const deliverSessionNotification = useCallback(({ targetSession, -@@ -1184,39 +1214,37 @@ export function AppShell() { +@@ -1198,39 +1214,37 @@ export function AppShell() { tabId, })); setActiveFileTabId(tabId); @@ -13404,7 +13486,7 @@ index bcc3807..1a8346e 100644 ); const handleToggleSubagentTab = (sessionId: string, label: string) => { if (openSubagentTabIds.has(sessionId)) { -@@ -1227,40 +1255,221 @@ export function AppShell() { +@@ -1241,40 +1255,221 @@ export function AppShell() { handleOpenSubagentTab(sessionId, label); }; @@ -13526,7 +13608,8 @@ index bcc3807..1a8346e 100644 + if (!focusWasInside) return; + requestAnimationFrame(() => { + if (!neighbour || !focusTile(neighbour)) focusTile("chat"); -+ }); + }); +- }, [fileTabs, terminalTabs, agentTabs]); + }, [panes.layout, closePaneTile]); + + // The header's "+N −M": the working tree against HEAD, refreshed when the agent's tools ran. @@ -13615,8 +13698,7 @@ index bcc3807..1a8346e 100644 + if (terminalGroup(state, activeCwd).length === 0) return addTerminalTab(state, activeCwd).state; + const tab = activeTerminalTab(state, activeCwd); + return tab?.dormant ? updateTerminalTab(state, tab.id, { dormant: false }) : state; - }); -- }, [fileTabs, terminalTabs, agentTabs]); ++ }); + }, [terminalsRestored, terminalInSight, activeCwd, shownTerminal?.id, shownTerminal?.dormant]); + + const browserOpen = panes.isOpen("browser"); @@ -13646,7 +13728,7 @@ index bcc3807..1a8346e 100644 const handleViewFullHistory = useCallback(() => { if (!selectedSession) return; -@@ -1279,6 +1488,14 @@ export function AppShell() { +@@ -1293,6 +1488,14 @@ export function AppShell() { useLayoutEffect(() => { activeNewSessionDraftKeyRef.current = newSessionDraftKey; }, [newSessionDraftKey]); @@ -13661,7 +13743,7 @@ index bcc3807..1a8346e 100644 // The bar above a fresh composer. Only a fresh composer moves, and only // somewhere else: the folder in use would remount it for nothing. The -@@ -1315,6 +1532,76 @@ export function AppShell() { +@@ -1329,6 +1532,76 @@ export function AppShell() { const handleNewSessionBarFocusDone = useCallback(() => { newSessionBarFocusRef.current = null; }, []); @@ -13738,7 +13820,7 @@ index bcc3807..1a8346e 100644 const newSessionContextBar = selectedSession === null && effectiveNewSessionCwd ? ( ) : null; const showChat = selectedSession !== null || effectiveNewSessionCwd !== null; -@@ -1397,9 +1685,6 @@ export function AppShell() { +@@ -1411,9 +1685,6 @@ export function AppShell() { if (cwd === projectTrustCwd) setProjectTrust(status); }, [projectTrustCwd]); @@ -13756,7 +13838,7 @@ index bcc3807..1a8346e 100644 const activeCwdName = activeCwd ? getFileName(activeCwd) || activeCwd : null; const windowTitle = activeCwdName ? `${activeCwdName} - Pi Web` : "Pi Web"; -@@ -1414,6 +1699,28 @@ export function AppShell() { +@@ -1428,6 +1699,28 @@ export function AppShell() { return () => observer.disconnect(); }, [windowTitle]); @@ -13785,7 +13867,7 @@ index bcc3807..1a8346e 100644 const sidebarContent = ( <> ); -@@ -1520,520 +1796,212 @@ export function AppShell() { +@@ -1534,535 +1796,212 @@ export function AppShell() { }} data-mobile-trust-banner={mobileBanner ? "true" : undefined} > @@ -14105,12 +14187,12 @@ index bcc3807..1a8346e 100644 +
); - })()} -- {hasSubagentSessions && ( +- {hasSubagentSessions && agentsBadge && ( - - )} @@ -14558,7 +14655,7 @@ index bcc3807..1a8346e 100644