Imported from abcwyc/pi-agent-desktop (
AGENTS.md). Install upstream withnpx skills add abcwyc/pi-agent-desktop. Copyright stays with the author.
Pi Web - Development Notes
Quick Start
npm run dev # port 30141
Typecheck: node_modules/.bin/tsc --noEmit · Lint: npm run lint
Never run next build during dev: it pollutes .next/ and breaks npm run dev.
Local environment traps
- Building (or starting a fresh
next dev) from inside the Pi desktop app's shell fails. The app exportsNODE_ENV=production,__NEXT_PRIVATE_ORIGINand__NEXT_PRIVATE_STANDALONE_CONFIG(its own serialized Next config, from which function values are gone). Next's CLI prefers that serialized config overnext.config.ts, sonext builddies withTypeError: generate is not a functionand a freshnext devwithMissing field turbopackMemoryEviction— while a dev server that was already running keeps working, which makes it look like a repo bug.scripts/prepare-desktop.mjsdeletes both__NEXT_PRIVATE_*vars for this exact reason; do the same (or run from a normal terminal) before anynext build/npm run test:e2e. - The
nodeonPATHmay not be the one the project runs on: the Pi app bundles its own runtime, while a dev server started from a terminal uses another install. Check withps -o command= -p $(lsof -nP -iTCP:30141 -sTCP:LISTEN -t). npm run test:e2erefuses to start while a dev server holds.next/dev/lock, andE2E_SERVER_MODE=start(the mode CI uses) needs a production build. Both can be satisfied without touching the dev state by building into the isolated desktop dir and pointing the harness at it:PI_WEB_DESKTOP_BUILD=1 node_modules/next/dist/bin/next build --webpackthenE2E_SERVER_MODE=start PI_WEB_DESKTOP_BUILD=1 node e2e/run.mjs(next.config.tsswapsdistDirto.next-desktop, which is gitignored).- A worktree's
node_modulescan lag the lockfile (created before an SDK bump), which fails tests that pin newer SDK behaviour.npm testrunsnpm run check:depsfirst and stops with the mismatched packages;npm cifixes it. next devrewritesAGENTS.md: it appends a "This is NOT the Next.js you know" block to this file on every start. Preserve the project and user instructions here; review generated additions separately and never reset the entire file to discard development output.
Architecture
- Browsing (read-only, no AgentSession):
GET /api/sessionslists~/.pi/agent/sessions/;GET /api/sessions/[id]reads the.jsonlthrough SDKSessionManagerhelpers andlib/session-reader.ts, or an open wrapper's in-memorySessionManager.GET /api/agent/runningsnapshots the running ids. - Sending:
POST /api/agent/[id]→startRpcSession()(lib/rpc-manager.ts) creates the AgentSession in-process (createAgentSessionFromServices());session.send(cmd)→session.prompt(). - Events:
GET /api/agent/[id]/eventsstreams SSEdata: {...}fromsession.onEvent(), fed bysession.subscribe().
File Map
app/api/
sessions/route.ts GET list all sessions
sessions/[id]/route.ts GET/PATCH/DELETE session
sessions/[id]/context/route.ts GET ?leafId= — context for a specific leaf
sessions/[id]/export/route.ts GET exported HTML for a session
agent/new/route.ts POST { cwd, message, toolNames?, provider?, modelId? }
agent/[id]/route.ts GET state | POST any command
agent/[id]/events/route.ts GET SSE stream
agent/running/route.ts GET currently-running session ids
agent/running/events/route.ts GET SSE stream of currently-running session ids
auth/all-providers/route.ts GET API-key provider list
auth/api-key/[provider]/route.ts GET/POST/DELETE provider API key status/storage
auth/login/[provider]/route.ts GET OAuth/device-code SSE | POST manual code
auth/logout/[provider]/route.ts POST OAuth logout
auth/providers/route.ts GET OAuth provider list
cwd/validate/route.ts POST validate/select a cwd
default-cwd/route.ts POST create ~/pi-cwd-YYYYMMDD
files/[...path]/route.ts GET file contents for viewer
home/route.ts GET user home directory
models/route.ts GET { models, modelList, defaultModel }
models/enabled/route.ts GET/PUT enabledModels switches for the Models panel
models/refresh/route.ts POST fetch provider catalogs from pi.dev on demand
models/nonchat/route.ts GET classifier + image models (with sign-in state) and the global Code mode choice
models-config/route.ts GET/PUT — read/write ~/.pi/agent/models.json
models-config/catalog/route.ts GET models.dev pricing presets
models-config/discover/route.ts POST fetch a configured provider's upstream model list
models-config/test/route.ts POST test a configured model/provider
plugins/route.ts GET/POST package plugin management
skills/route.ts GET/PATCH loaded skills and disable-model-invocation
skills/install/route.ts POST install skills through npx skills add
skills/search/route.ts GET/POST skills.sh search
scheduled-tasks/** GET/POST tasks, PATCH/DELETE [id], POST [id]/run, GET [id]/runs, POST run seen, POST preview, GET events (SSE)
worktrees/route.ts GET list / POST create / PUT switch branch / DELETE git worktrees
worktrees/fetch/route.ts POST git fetch --prune + fresh branch lists
desktop/read-images/route.ts POST read natively-picked images into attachment payloads
desktop/save/route.ts POST copy a file to, or write bytes at, a natively-picked path
artifacts/settings/route.ts GET/PUT the "artifacts in the side panel" prompt switch
lib/
agent-client.ts typed fetch helper for /api/agent commands
artifacts.ts `:::artifact` scanner, markdown encoding, version collection
artifact-store.ts client store shared by ChatWindow, message cards and the artifact panel
artifact-settings.ts / artifact-guide-extension.ts prompt switch file + `<artifacts>` prompt section
html-preview.ts inline HTML/SVG/React preview detection, sandbox, srcdoc bootstrap, React document
preview-runtime.ts loads public/preview-runtime/ (bundled React, Sucrase, Tailwind) for React previews
app-prefs.ts APP_PREF_KEYS + typed localStorage accessors (SSR/private-mode safe)
desktop-connection.ts useDesktopConnection() — local server health probe for the reconnect banner
desktop-native.ts native dialogs, open/reveal path, external links, save/import, window lifecycle
desktop-notify.ts native completion notifications (skipped while the window is focused)
draft-store.ts local draft persistence helpers
file-access.ts allowed file roots for /api/files and worktrees
file-paths.ts client/server path encoding helpers
enabled-models.ts pure minimal-edit engine for the `enabledModels` pattern list
enabled-models-runtime.ts SDK adapter: per-pattern resolution, provider kinds, settings IO
markdown.ts shared markdown helpers
npx.ts npx runner used by skill install
pi-types.ts local structural types for pi SDK objects
rpc-manager.ts AgentSessionWrapper + registry + startRpcSession
scheduled-tasks/ scheduler, runner, cron, store, run index for scheduled tasks (see docs/agents/scheduled-tasks.md)
session-reader.ts SessionManager wrappers + path cache + buildSessionContext adapter
tool-presets.ts PRESET_NONE/DEFAULT/FULL + getPresetFromTools()
types.ts shared TypeScript types
normalize.ts normalizeToolCalls() — field name mismatch between file format and our types
workspace-state.ts persisted session/cwd/file-tab restore for desktop cold starts
worktree.ts project/worktree resolution and git worktree operations
components/
AppShell.tsx layout + URL state + tab management
SessionSidebar.tsx session tree + FileExplorer
ChatWindow.tsx chat composition + completion sound wrapper
ChatInput.tsx input bar + model/thinking/tools/compact controls
MessageView.tsx renders one message (user/assistant/toolCall/toolResult)
BranchNavigator.tsx in-session branch switcher
MarkdownBody.tsx markdown renderer
HtmlPreviewBlock.tsx / HtmlPreviewErrorBar.tsx inline previews and their error strip
ArtifactCard.tsx / ArtifactPanel.tsx / artifact-tabs.ts artifact card in messages, right-panel tab
ModelsConfig.tsx modal for editing models.json (opened from sidebar bottom)
EnabledModelsSection.tsx model switches inside ModelsConfig, backed by enabledModels
AgentsConfig.tsx built-in subagent toggle + agent profile editor
PluginsConfig.tsx modal for installed package plugins
SkillsConfig.tsx modal for loaded/search/installable skills
scheduled/ Scheduled page (list, detail, editor, run history) and its sidebar row
SettingsPanel.tsx settings dialog; its General tab hosts the app intro, Version & Updates and the desktop-only switches
AppUpdatesSection.tsx Version & Updates block at the top of General
desktop/DesktopAppSection.tsx window/tray prefs; renders nothing outside Tauri
FileExplorer.tsx file tree inside sidebar
FileIcons.tsx file icon helpers
FileViewer.tsx file content in a tab
TabBar.tsx tab bar (Chat + open file tabs)
hooks/
useAgentSession.ts messages, streaming, SSE, fork/navigate, reconciliation; built-in slash commands (/session, bare /mcp)
useAudio.ts completion sound + AudioContext unlock
useDragDrop.ts shared drag/drop state
useIsMobile.ts responsive breakpoint
useScheduledTasks.ts shared store + SSE for scheduled tasks
useKeyboardShortcuts.ts Esc stops the running agent unless a field or nearer handler took it; Ctrl+Alt+N
useTheme.ts theme state
Topic Notes
Read the doc for the area you touch before changing it. Upstream docs (no prefix) come from pi-web; fork-* docs hold this fork's notes and win where the two disagree.
- sessions.md: AgentSession lifecycle and shutdown, fork vs in-session branching, session file rewrites, toolCall normalization, SSE reconnect and tool events, transcript system / usage / context-edit entries, running-state polling, exported HTML. Files:
lib/rpc-manager.ts,lib/session-reader.ts,lib/normalize.ts,hooks/useAgentSession.ts,app/api/agent/**,app/api/sessions/**,components/BranchNavigator.tsx,components/MessageView.tsx,components/CodemodeToolView.tsx. - 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
defaultToolsswitches. 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 inlib/rpc-manager.ts. - mcp-runtime.md: the per-session MCP host (when servers register and connect, reported states, trust read on every sync, idle release);
/mcpin 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 inlib/rpc-manager.tsandlib/builtin-extensions.ts,/mcphandling inhooks/useAgentSession.ts. - 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.jsonwrite 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-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: Settings › MCP Test (route checks, bounded connection,
!commandqueue, 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. - 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 inlib/project-trust.ts,components/McpAddServer.tsx,components/mcp-add-helpers.ts, theaddaction ofapp/api/mcp/route.ts. - models.md: default model and reasoning level, mid-run reasoning changes, remote provider catalogs,
enabledModelsscoping and minimal edits, 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/provider-listing*.ts,components/ModelsConfig.tsx,components/EnabledModelsSection.tsx,components/ModelSelector.tsx,components/SelectorRow.tsx. - files-and-access.md: worktrees and project grouping, the file access allow-list (the
/api/filessecurity boundary), file tree visibility, 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/worktree.ts,lib/paths.ts,lib/auth-throttle.ts,components/FileExplorer.tsx. - settings-ui.md: Plugins and Skills routes, sidebar group switches, the shared
SettingsUiblocks every settings panel and add pane uses. Files:app/api/plugins/**,app/api/skills/**,components/SettingsUi.tsx,components/settings-ui-helpers.ts,components/SkillsConfig.tsx,components/PluginsConfig.tsx; also before adding a settings section or add pane. - subagents.md: the built-in subagent setting, profiles and their files, run status, completion notifications. Files:
lib/subagent*.ts,app/api/subagents/**,components/AgentsConfig.tsx. - scheduled-tasks.md: scheduled tasks (the Scheduled page): server-side scheduler and its lease, catch-up and overlap rules, read-only default, the run index that hides runs from the project tree, why shared state is on
globalThis. Files:lib/scheduled-tasks/**,app/api/scheduled-tasks/**,instrumentation-node.ts,hooks/useScheduledTasks.ts,components/scheduled/**, the Scheduled row incomponents/SessionSidebar.tsx, the page overlay incomponents/AppShell.tsx. - artifacts.md: inline HTML/SVG/React previews and
:::artifactblocks: what renders where, the sandbox rules and the desktopabout:srcdocallowance, the auto-run rule for scripted pages, the bundled React runtime (inlined, because sandboxed frames cannot reach localhost;npm run preview-runtimeregenerates it), the fix flow and the prompt switch. Files:lib/artifacts.ts,lib/artifact-store.ts,lib/html-preview.ts,lib/artifact-settings.ts,lib/artifact-guide-extension.ts,hooks/useArtifactPublisher.ts,hooks/useHtmlPreviewFrame.ts,lib/preview-runtime.ts,scripts/build-preview-runtime.mjs,public/preview-runtime/,components/Artifact*.tsx,components/HtmlPreview*.tsx,components/artifact-tabs.ts,app/api/artifacts/**,is_inline_frame_documentinsrc-tauri/src/lib.rs. - client-platform.md: mobile software keyboard and viewport height, completion sound. Files:
hooks/useViewportHeight.ts,hooks/useAudio.ts, the keyboard-open CSS.
Fork notes:
- fork-sessions.md: branch summaries, the not-yet-flushed new session, the SDK → wire → client event pipeline, the incremental session scanner,
useAgentSessionguards (request ids, load deadline, streaming seed, render-phase scroll save), compaction / retry / 0.86 events,isRunning()andisIdle, running-state propagation and reconciliation, session automation, the single-shot composer send,!commandoutput, stats, transcript system messages, extension command session actions. - fork-models.md: routed model, cost by model and cache warming, classifier and image models, the four thinking-level states, model defaults, remote provider catalogs (refresh only on request),
enabledModelsminimal edits andresync, auth and credential deletion. - fork-tools-subagents.md: new-session tool presets and Chat only, exact system prompts through
before_agent_start, built-in subagents (setting, disabled built-ins, profile files, notifications). - fork-files-security.md: security posture (web password, OAuth tokens, preview sandboxes), worktrees and branch switching, the file access allow-list, web password throttling.
- fork-desktop.md: the Tauri shell (desktop token, native menus in Rust, file context menu, window state, tauri plugin version pairing), workspace terminals and node-pty / quickjs staging, Windows overwrite installs.
- fork-ui.md: who contains whom in the topbar / chat column / right panel, the settings dialog tabs, touch affordances, completion sound, plugins and skills.
- fork-chat-workspace.md: the non-project Chats section, the hidden
chat-workspacecwd andworkspaceKindidentity, project-workbench suppression, and the server-enforced no-tools / no-shell / no-context-files policy.
AgentSession lifecycle (lib/rpc-manager.ts)
- One
AgentSessionWrapperper session id, keyed inglobalThis.__piSessions globalThissurvives Next.js hot-reload; plain module-level Map does not- Idle timeout: 10 minutes. Concurrent
startRpcSession()calls share a single start Promise (globalThis.__piStartLocks)
Hard rules
Each one undid a real regression; the linked note has the reasoning.
AgentSessionWrapper.isRunning()ORs ininner.isIdle === false; dropping it evicts wrappers mid-retry. (fork-sessions)- Never assign
agent.state.systemPrompt(a getter since pi 0.86). Exact prompts go only throughlib/exact-system-prompt.ts; do not add a second path. (fork-tools-subagents) useAgentSession: keep the monotonic request ids,fetchWithRetryfor the session load,seedStreamingSnapshot(), the render-phase scroll save, and thesessionHookMountedRefre-assert beforemaintainEventsConnected(). (fork-sessions)- The composer sends exactly one
onSendper submit; a second raw send restores the text into the box. (fork-sessions) - Do not re-add the legacy
auto_compaction_*event cases or a manual DeepSeek xhigh clamp. (fork-sessions) enabledModels: never compare patterns as strings, never assumeprovider/*covers a provider, never rebuild the list fromgetAvailable(); every toggle is a minimal edit and onlyresyncrepairs entries. (fork-models)- Provider catalogs refresh over the network only when the user presses "Refresh catalog". (fork-models)
- Credential deletes go through
removeStoredCredentialIfType(), and both provider lists refresh after any auth change. (fork-models) - Built-in subagent profiles are switched off by name in
disabledBuiltIns, never copied out to a.md; keepSUBAGENT_NOTIFICATION_PREFIXin code. (fork-tools-subagents) - Never give one iframe both
allow-scriptsandallow-same-origin; keepSERVE_EXT_TO_MIMEingetServeMime. (fork-files-security) - Manual-code OAuth handshake tokens are
crypto.randomUUID(). The auth throttle stays global, and its reset window stays longer than the max delay. (fork-files-security) - No literal
homedir()in session-scanner fs calls; useuserHome(). (fork-sessions) - Native-dialog routes (
/api/desktop/*,?type=import) require the per-process desktop token; never a static token or User-Agent check.read-imagesderives its cap fromMAX_ATTACHED_IMAGES. (fork-desktop) - Open local paths with
explorer.exeon Windows, nevercmd.exe /C start. (fork-desktop) - Native popup menus are built in Rust (
show_popup_menu); do not go back to the JSMenuAPI. (fork-desktop) tauri-plugin-*crates and@tauri-apps/plugin-*packages stay on the same minor; fix drift inCargo.lock, never by pinning npm down. (fork-desktop)- Window state flags are
SIZE | POSITION | MAXIMIZED; neverVISIBLEorDECORATIONS. (fork-desktop) - After an SDK bump run
npm run desktop:prepare && npm run desktop:verify; never hard-code a nested staging destination; keep all three Windowsnode_modulesguards. (fork-desktop) - Layout: the center column contains topbar and chat; the right-panel block is a full-height sibling inside the outer row; no
paddingLeft: sidebarResizer.widthon the topbar. Pinned byAppShell.right-panel-row.test.mjs. (fork-ui) SettingsPanelkeeps theonBusyChangeguard so a desktop update cannot be dismissed mid-install. (fork-ui)- Chats use a server-owned hidden cwd and
workspaceKind; never derive that path in the browser, group it with projects, or expose it to project trust / worktrees / files / terminals. Server policy must keep chats tool-less, shell-less, and context-file-free even for persisted sessions. (fork-chat-workspace)
Two kinds of branching — don't confuse them
- Fork (Fork button on user message): creates a new independent
.jsonlfile. Shown as a child in the sidebar tree viaparentSessionheader field. - In-session branch (Continue button / BranchNavigator): calls
navigate_treewithin the same file. Multiple entries share the sameparentId. Switching between them calls/api/sessions/[id]/context?leafId=.
Upstream merge sentinels (not the same "fork" as session fork)
This repo is a fork of agegr/pi-web and periodically merges upstream. components/fork-extractions.test.mjs guards the failure mode that a clean merge can still be wrong: code this fork moved to another file reappears at its origin, or a fork change is silently reverted. Git does not track cross-file moves, so neither shows up in tsc, eslint, or the upstream suite.
When one of those assertions fails, the default is to restore the behaviour, not to delete the assertion. A failure saying a marker is "absent from the origin but also from the extraction targets — it looks deleted, not moved" means the feature is probably gone: that is exactly how the project-picker filter box was found missing after a restyle had left every project past the 7-row cap unreachable. Risk levels live in scripts/fork-ownership.json; the reasoning is in docs/ownership-boundaries.md.
The third direction is forbiddenMarkers on a FORK_FEATURES entry: a region the fork deleted that upstream still ships. A clean merge re-adds it silently, so the entry asserts those strings stay absent from the file (currently the new-session branding header in components/ChatWindow.tsx). A failure there means "put the removal back", never "drop the marker".
Upstream merge decisions (standing rules)
Full per-merge reasoning (segments E, F, G, with what was adopted and what was declined) is in merge-history.md. The rules that still bind a future merge:
- Upstream owns SDK adaptation. Exact system prompts go through
lib/exact-system-prompt.ts(before_agent_start); the session list comes fromlib/session-list-scanner.ts; MCP and Code mode run on ADR 0006'sbuiltin:*extensions (createMcpExtension()is never passed as a bare inline factory) andresolveActiveToolNames()replacedwithExtensionTools(). Do not re-add a second path for any of these. - Declined upstream UI stays declined: the sidebar explorer and resizable session/explorer panes (
data-resize-handle="sidebar-sections"),DirectoryPicker,SessionSearch,ChatMinimap,getSessionListIndiceswindowing, the second mobile toolbar group, the topbar models/skills/settings quick-buttons,ExtensionStatusBar(the fork'sExtensionStatusLinereplaces it). A merge that brings one back is re-introducing declined UI, not a test to satisfy. - Fork layers that must survive a merge:
userHome()routing instead of literalhomedir(), models-config literal-key redaction,resolveSessionReferencesin the send/queue paths, the monotonic request-id guards inuseAgentSession, the project-tree sidebar with the full-height layout, desktop i18n and Settings sections. lib/startup-preferences.tsstays deleted (new-session model picks are session-scoped);PUT /api/models/defaultis the only writer of global defaults.package-lock.jsonkeeps the tauri plugin versions pinned tosrc-tauri/Cargo.lock: restore HEAD's lock beforenpm install --package-lock-only, or the carets re-resolve andrelease-workflows.test.mjsfails.- After a merge:
node scripts/upstream-css-baseline.mjs update <upstream-ref>andnpm run drift.
Pi Session File Format
Location: ~/.pi/agent/sessions/<encoded-cwd>/<timestamp>_<uuid>.jsonl
{"type":"session","version":3,"id":"<uuid>","timestamp":"...","cwd":"/path","parentSession":"/abs/path/to/parent.jsonl"}
{"type":"model_change","id":"<8hex>","parentId":null,"provider":"zenmux","modelId":"claude-sonnet-4-6","timestamp":"..."}
{"type":"message","id":"<8hex>","parentId":"<8hex>","message":{"role":"user","content":"..."}}
{"type":"message","id":"<8hex>","parentId":"<8hex>","message":{"role":"assistant","content":[...],...}}
{"type":"message","id":"<8hex>","parentId":"<8hex>","message":{"role":"toolResult","toolCallId":"...","content":[...]}}
{"type":"compaction","id":"<8hex>","parentId":"<8hex>","summary":"...","firstKeptEntryId":"<8hex>","tokensBefore":N}
{"type":"session_info","id":"...","parentId":"...","name":"user-defined name"}
SessionContext.entryIds[] parallels messages[]: each displayed message's .jsonl entry id, used for fork and navigate_tree.
Styling: fork CSS lives only in app/native-theme.css
UI consistency is mandatory (user requirement). Every application menu, dropdown,
option picker and autocomplete popup must use the shared .app-menu surface and menu
tokens in app/native-theme.css. Form choices use components/AppSelect.tsx; do not
introduce raw <select> controls or OS-native application option menus. Preserve the
same palette, nested radii, inset, row spacing, selected/disabled states and focus
treatment in both themes. Use the existing design as the reference, consult the
sources in docs/ui-menu-audit.md, and inspect actual rendered states at desktop and
narrow widths before claiming visual consistency. System file/directory dialogs are
separate OS tasks. Run node --test scripts/ui-menu-consistency.test.mjs after menu work;
changes to shared choices also run node e2e/option-controls.mjs.
app/globals.css and app/settings.css are pi-web's and stay byte-identical to upstream — scripts/upstream-css-baseline.test.mjs fails on any edit. Put every fork rule in app/native-theme.css (imported last in app/layout.tsx); overrides of upstream rules that other theme rules must beat go in its "fork base layer" at the top. After an upstream merge run node scripts/upstream-css-baseline.mjs update <upstream-ref> (the sync workflow does). Layout of the theme file and the design intent to restore after a merge: docs/native-theme.md.
CSS Variables (app/globals.css, re-tokened in app/native-theme.css)
--bg --bg-panel --bg-hover --bg-selected --border
--text --text-muted --text-dim
--accent --user-bg --tool-bg
--font-mono
