@firstpick/pi-package-webui 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,370 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
7
+ const [pkgRaw, html, css, app, server, extension, readme, manifestRaw, serviceWorker, appleIcon, icon192, icon512] = await Promise.all([
8
+ readFile(join(root, "package.json"), "utf8"),
9
+ readFile(join(root, "public", "index.html"), "utf8"),
10
+ readFile(join(root, "public", "styles.css"), "utf8"),
11
+ readFile(join(root, "public", "app.js"), "utf8"),
12
+ readFile(join(root, "bin", "pi-webui.mjs"), "utf8"),
13
+ readFile(join(root, "index.ts"), "utf8"),
14
+ readFile(join(root, "README.md"), "utf8"),
15
+ readFile(join(root, "public", "manifest.webmanifest"), "utf8"),
16
+ readFile(join(root, "public", "service-worker.js"), "utf8"),
17
+ readFile(join(root, "public", "apple-touch-icon.png")),
18
+ readFile(join(root, "public", "icon-192.png")),
19
+ readFile(join(root, "public", "icon-512.png")),
20
+ ]);
21
+ const pkg = JSON.parse(pkgRaw);
22
+ const manifest = JSON.parse(manifestRaw);
23
+
24
+ assert.match(html, /viewport-fit=cover/, "viewport should opt into safe-area-aware full-screen layout");
25
+ assert.match(html, /interactive-widget=resizes-content/, "viewport should request keyboard-driven content resizing where supported");
26
+ assert.match(html, /<meta name="theme-color" content="#11111b" \/>/, "PWA should declare a mobile browser theme color");
27
+ assert.match(html, /<link rel="manifest" href="\/manifest\.webmanifest" \/>/, "PWA should expose a web app manifest");
28
+ assert.match(html, /<link rel="apple-touch-icon" href="\/apple-touch-icon\.png" \/>/, "PWA should expose the conventional iOS home-screen icon path");
29
+ assert.match(html, /id="terminalTabsToggleButton"/, "mobile should expose a compact terminal-tabs toggle");
30
+ assert.match(html, /id="sidePanelBackdrop"/, "mobile side panel needs an overlay/backdrop close target");
31
+ assert.match(html, /id="themeSelect"/, "side panel should expose a theme selector");
32
+ assert.match(html, /<label for="themeSelect">Theme<\/label>/, "theme selector should be labeled in side-panel controls");
33
+ assert.match(html, /id="jumpToLatestButton"/, "chat should expose a jump-to-latest control for non-forced streaming");
34
+ assert.match(html, /id="stickyUserPromptButton"/, "chat should expose a fixed last-user-prompt jump control");
35
+ assert.match(html, /id="feedbackTray"/, "chat should expose a queued action-feedback tray");
36
+ assert.match(html, /id="sendFeedbackButton"/, "action feedback should be submittable after the agent finishes");
37
+ assert.match(html, /<textarea id="promptInput"[^>]*rows="1"[^>]*enterkeyhint="enter"/, "prompt textarea should start at one row and hint that Return inserts a newline");
38
+ assert.match(html, /id="composerActionsButton"/, "mobile composer should expose a compact actions trigger");
39
+ assert.match(html, /id="composerActionsPanel"/, "secondary composer controls should live in a mobile actions panel");
40
+ assert.match(html, /id="steerButton"[\s\S]*?data-tooltip="Steer usage:/, "Steer should explain type-first usage in a tooltip");
41
+ assert.match(html, /id="followUpButton"[\s\S]*?data-tooltip="Follow-up usage:/, "Follow-up should explain type-first usage in a tooltip");
42
+ assert.ok(
43
+ html.indexOf('<main class="layout">') < html.indexOf('id="sidePanelBackdrop"') &&
44
+ html.indexOf('id="sidePanelBackdrop"') < html.indexOf('id="sidePanel"'),
45
+ "side-panel backdrop should live inside the layout before the panel so the panel can stack above it",
46
+ );
47
+
48
+ assert.match(css, /--visual-viewport-height:\s*100dvh/, "CSS should define a visual viewport height fallback");
49
+ assert.match(css, /color-scheme:\s*var\(--theme-color-scheme\)/, "CSS should allow JS-selected themes to update browser color-scheme");
50
+ assert.match(css, /--background-glow-pink/, "CSS should expose theme-controlled page glow colors");
51
+ assert.match(css, /height:\s*var\(--visual-viewport-height, 100dvh\)/, "layout should consume visual viewport height");
52
+ assert.match(css, /button, select, input \{ min-height: 44px; \}/, "base controls should meet 44px touch-target height");
53
+ assert.match(css, /\.composer-row button[\s\S]*?min-height:\s*44px/, "mobile composer buttons should keep 44px touch targets");
54
+ assert.match(css, /#promptInput \{[\s\S]*?min-height:\s*calc\(1\.5em \+ 1\.8rem\)/, "prompt input should default to a compact single-line height");
55
+ assert.match(css, /#promptInput \{[\s\S]*?overflow-y:\s*hidden/, "prompt input should be JS-resized instead of showing a scrollbar by default");
56
+ assert.match(css, /\.sticky-user-prompt-button \{[\s\S]*?grid-template-columns:\s*auto minmax\(0, 1fr\) auto/, "last-user-prompt jump control should render as a fixed transcript header");
57
+ assert.match(css, /\.message\.extension,[\s\S]*?\.message\.native/, "extension and native command output should have visible transcript styling");
58
+ assert.match(css, /\.message\.run-indicator-message \{[\s\S]*?border-color/, "active agent runs should render a visible transcript indicator card");
59
+ assert.match(css, /\.message\.thinking,\n\.message\.toolCall,\n\.message\.assistantEvent/, "thinking and assistant events should have non-assistant transcript card styling");
60
+ assert.match(css, /\.message\.toolResult, \.message\.bashExecution, \.message\.compactionSummary/, "compaction summaries should render as compact collapsible transcript cards");
61
+ assert.match(css, /\.run-indicator-pulse \{[\s\S]*?animation:\s*run-indicator-pulse/, "active agent run indicator should have an animated pulse");
62
+ assert.match(css, /\.todo-widget \{[\s\S]*?display:\s*grid/, "todo-progress widget should render as a styled checklist card");
63
+ assert.match(css, /\.todo-widget-item\.partial \.todo-widget-marker/, "todo-progress partial items should have distinct styling");
64
+ assert.match(css, /\.todo-widget-item\.done \.todo-widget-text[\s\S]*?text-decoration:\s*line-through/, "todo-progress completed items should be visually crossed out");
65
+ assert.match(css, /\.message\.warn \.message-role \{ color: var\(--ctp-yellow\); \}/, "warning-level command output should be visually distinct");
66
+ assert.match(css, /\.commands-box \{[\s\S]*?max-height:\s*min\(32rem, 52vh\)/, "side-panel commands should use expanded viewport-aware height");
67
+ assert.match(css, /\.command-item \{[\s\S]*?width:\s*100%/, "side-panel commands should render as full-width click targets");
68
+ assert.match(css, /\.command-item:hover,[\s\S]*?\.command-item:focus-visible/, "side-panel commands should have hover and keyboard focus affordances");
69
+ assert.match(css, /\.feedback-tray\[hidden\] \{ display: none; \}/, "queued action-feedback tray should hide when empty");
70
+ assert.match(css, /\.action-feedback-controls \{[\s\S]*?position:\s*absolute/, "action reactions should be absolutely positioned so they do not expand cards");
71
+ assert.match(css, /\.action-feedback-controls \{[\s\S]*?top:\s*calc\(100% - 0\.18rem\)/, "action reactions should sit outside the message box by default");
72
+ assert.match(css, /\.action-feedback-controls \{[\s\S]*?opacity:\s*0/, "action reactions should stay hidden until hovered or focused");
73
+ assert.match(css, /\.action-feedback-controls:hover,[\s\S]*?\.action-feedback-controls:focus-within/, "action reactions should reveal on hover or keyboard focus");
74
+ assert.match(css, /\.action-feedback-controls:not\(:hover\):not\(:focus-within\) \.action-feedback-button/, "hidden action reactions should not expose button hit targets until the hover area is reached");
75
+ assert.match(css, /\.action-feedback-button\.feedback-question\.active/, "question-mark reaction should have selected styling");
76
+ assert.match(css, /\.composer-row button\[data-tooltip\]::after/, "composer button tooltips should be shared across Git, Steer, and Follow-up buttons");
77
+ assert.match(css, /\.composer-row button\[data-tooltip\]\.tooltip-open::after/, "composer button tooltips should be triggerable from JS for empty mobile taps");
78
+ assert.match(css, /\.composer-actions-panel[\s\S]*?bottom:\s*calc\(100% \+ 0\.42rem\)/, "mobile composer actions should open as an above-composer sheet");
79
+ assert.match(css, /body\.composer-actions-open \.composer-actions-panel \{ display: grid; \}/, "composer actions panel should only open when toggled");
80
+ assert.match(css, /\.terminal-tabs-toggle-button \{ display: none; \}/, "terminal tab toggle should be hidden outside mobile CSS");
81
+ assert.match(css, /body\.mobile-tabs-expanded \.terminal-tabs \{ display: flex; \}/, "mobile tabs should expand only when toggled");
82
+ assert.match(css, /\.terminal-tab-activity-indicator/, "terminal tabs should expose per-tab agent activity indicators");
83
+ assert.match(css, /\.terminal-tab-group-item \{[\s\S]*?background:\s*var\(--ctp-crust\)/, "grouped terminal tab items should use opaque backgrounds");
84
+ assert.match(css, /\.terminal-tab-group\.active \{[\s\S]*?background:[\s\S]*?var\(--ctp-crust\)/, "active terminal tab groups should keep opaque backgrounds");
85
+ assert.match(css, /\.terminal-tab-group\.stopped \{[\s\S]*?opacity:\s*1/, "stopped terminal tab groups should not become transparent");
86
+ assert.match(css, /\.terminal-tabs:has\(\.terminal-tab-group\.menu-open\)/, "open terminal tab groups should keep the tab strip usable across rerenders");
87
+ assert.match(css, /\.terminal-tab-group\.menu-open \.terminal-tab-group-menu \{[\s\S]*?display:\s*flex/, "open terminal tab group menus should remain visible without hover");
88
+ assert.match(css, /\.terminal-tab\.activity-working[\s\S]*?terminal-tab-working-pulse/, "working tab indicators should be visibly animated");
89
+ assert.match(css, /\.terminal-tab\.activity-blocked[\s\S]*?rgba\(250, 179, 135/, "blocked tab indicators should use orange styling");
90
+ assert.match(css, /\.terminal-tab\.activity-blocked \.terminal-tab-activity-indicator[\s\S]*?background:\s*var\(--ctp-peach\)/, "blocked tab indicator dots should be orange");
91
+ assert.match(css, /\.terminal-tab\.activity-done/, "completed unseen work should have a distinct tab style");
92
+ assert.match(css, /\.terminal-tabs[\s\S]*?position:\s*absolute/, "expanded mobile tabs should overlay instead of consuming transcript space");
93
+ assert.match(css, /body\.mobile-keyboard-open \.terminal-tabs-shell,[\s\S]*?body\.mobile-keyboard-open \.widget-area,[\s\S]*?body\.mobile-keyboard-open \.statusbar/, "mobile keyboard mode should hide header, widgets, and footer");
94
+ assert.match(css, /body\.mobile-keyboard-open \.composer-actions-button,[\s\S]*?body\.mobile-keyboard-open #steerButton,[\s\S]*?body\.mobile-keyboard-open #followUpButton/, "mobile keyboard mode should hide secondary composer buttons");
95
+ assert.match(css, /body:not\(\.pi-run-active\):not\(\.mobile-keyboard-open\) \.composer-row button\.primary \{ grid-column: span 4; \}/, "idle mobile composer should keep Actions and Send on one compact row");
96
+ assert.match(css, /\.composer-actions-panel > #followUpButton,[\s\S]*?\.composer-actions-panel > #steerButton/, "idle Steer and Follow-up should fit inside the Actions sheet");
97
+ assert.match(css, /\.footer-details-toggle \{ display: none; \}/, "footer details toggle should be hidden outside mobile CSS");
98
+ assert.match(css, /\.footer-workspace,\n\s+\.footer-context \{ display: grid !important; \}/, "collapsed mobile footer should primarily show cwd and context");
99
+ assert.match(css, /\.footer-model \{ order: 7; \}/, "model should move into expanded footer details on mobile");
100
+ assert.match(css, /\.footer-model-picker[\s\S]*?position:\s*absolute/, "footer model picker should render as a dropdown/popover");
101
+ assert.match(css, /@media \(max-width: 720px\), \(max-device-width: 720px\), \(pointer: coarse\) and \(hover: none\)[\s\S]*?\.footer-model-picker \{[\s\S]*?position:\s*fixed/, "mobile footer model picker should escape footer-details stacking as a fixed overlay on narrow, device-width-narrow, or touch-only devices");
102
+ assert.match(css, /bottom:\s*var\(--footer-model-picker-bottom/, "mobile footer model picker should be anchored by a JS-computed viewport offset");
103
+ assert.match(css, /\.footer-model-option\.active/, "footer model picker should style the selected scoped model");
104
+ assert.match(css, /body\.footer-details-expanded \.footer-line-meta[\s\S]*?display:\s*grid/, "mobile footer metadata should be expandable");
105
+ assert.match(css, /(?:^|\n)\s*\.side-panel-backdrop\s*\{[\s\S]*?position:\s*fixed/, "mobile side panel backdrop should be fixed overlay UI");
106
+ assert.match(css, /(?:^|\n)\s*\.side-panel\s*\{[\s\S]*?position:\s*fixed/, "mobile side panel should be an overlay drawer instead of stacked content");
107
+ assert.match(css, /\.extension-dialog[\s\S]*?max-height:\s*calc\(var\(--visual-viewport-height/, "dialogs should fit the visual viewport on mobile");
108
+ assert.match(css, /\.extension-dialog[\s\S]*?inset:\s*auto 0 0 0/, "mobile dialogs should behave like bottom sheets");
109
+ assert.match(css, /#dialogMessage \{[\s\S]*?white-space:\s*pre-wrap/, "extension dialog messages should preserve multiline prompts");
110
+ assert.match(css, /\.extension-dialog\.guardrail-dialog[\s\S]*?border-color:\s*rgba\(249, 226, 175/, "guardrail dialogs should have warning-specific styling");
111
+
112
+ assert.match(app, /const MOBILE_VIEW_QUERY = "\(max-width: 720px\), \(max-device-width: 720px\), \(pointer: coarse\) and \(hover: none\)"/, "mobile detection should include phones that report desktop-like layout widths");
113
+ assert.match(app, /const THEME_STORAGE_KEY = "pi-webui-theme"/, "theme selection should be persisted in browser storage");
114
+ assert.match(app, /async function initializeThemes\(\)/, "frontend should initialize bundled themes");
115
+ assert.match(app, /api\("\/api\/themes", \{ scoped: false \}\)/, "theme loading should use the unscoped themes endpoint");
116
+ assert.match(app, /function applyTheme\(theme/, "frontend should apply a selected theme to CSS variables");
117
+ assert.match(app, /Restart Web UI to load themes/, "frontend should explain when a stale server cannot serve the themes endpoint");
118
+ assert.match(app, /themeSelect\.addEventListener\("change"/, "side-panel theme selector should switch themes immediately");
119
+ assert.match(app, /open \? "Close for network" : "Open to network"/, "network button should toggle from open to close action");
120
+ assert.match(app, /api\("\/api\/network\/close", \{ method: "POST"/, "network close action should call the close endpoint");
121
+ assert.match(server, /async function closeNetworkAccess\(\)/, "server should expose a local-only rebind helper for closing network access");
122
+ assert.match(server, /url\.pathname === "\/api\/network\/close" && req\.method === "POST"/, "server should route network close requests");
123
+ assert.match(server, /server\.closeAllConnections\?\.\(\)/, "network rebind should force-close long-lived clients so close-to-localhost can complete");
124
+ assert.match(server, /connection: "close"/, "network rebind API responses should not hold keep-alive sockets open");
125
+ assert.match(readme, /toggles to "Close for network"/, "README should document the close-network toggle");
126
+ assert.match(app, /window\.visualViewport/, "app should listen to VisualViewport for keyboard/viewport updates");
127
+ assert.match(html, /<textarea id="promptInput"[^>]*autofocus/, "prompt composer should autofocus for new Web UI/app launches");
128
+ assert.match(app, /function syncMobileChatToBottomForInput\(\)/, "mobile input focus should force the output view to the latest message");
129
+ assert.match(app, /function focusPromptInput\(\{ defer = false \} = \{\}\)/, "frontend should focus the prompt composer programmatically after tab/app startup");
130
+ assert.match(app, /async function switchTab\(tabId\)[\s\S]*?restoreActiveDraft\(\);\n\s+focusPromptInput\(\{ defer: true \}\);/, "switching to a newly opened tab should focus the prompt input immediately");
131
+ assert.match(app, /async function initializeTabs\(\)[\s\S]*?restoreActiveDraft\(\);\n\s+focusPromptInput\(\{ defer: true \}\);/, "starting the Web UI should focus the prompt input after restoring the active tab");
132
+ assert.match(app, /resizePromptInput\(\);\nfocusPromptInput\(\{ defer: true \}\);\nupdateComposerModeButtons\(\);/, "startup should request prompt focus before waiting for tab state refreshes");
133
+ assert.match(app, /elements\.promptInput\.addEventListener\("focus", \(\) => \{\n\s+syncMobileChatToBottomForInput\(\);/, "focusing mobile input should scroll output to bottom");
134
+ assert.match(app, /navigator\.serviceWorker\.register\("\/service-worker\.js"\)/, "PWA service worker should be registered by the app");
135
+ assert.match(app, /function isChatNearBottom\(/, "chat should detect whether the user is reading above the bottom");
136
+ assert.match(app, /function scheduleChatFollowScroll\(/, "chat auto-follow should retry after layout settles during fast streaming");
137
+ assert.match(app, /function setChatScrollTopInstant\(top\)[\s\S]*?scrollBehavior = "auto"/, "chat auto-follow should bypass smooth scrolling while chasing fast output");
138
+ assert.match(app, /function syncAutoFollowFromChatScroll\(/, "programmatic scroll events should not accidentally disable auto-follow");
139
+ assert.match(app, /elements\.chat\.addEventListener\("wheel", noteChatUserScrollIntent/, "manual wheel scrolling should still be able to pause auto-follow");
140
+ assert.match(app, /function stripAnsi\(text\)/, "widget rendering should strip ANSI color escapes before display");
141
+ assert.match(app, /\(\?:\\x1B\|\\u241B\)/, "ANSI stripping should handle literal escape characters and visible escape glyphs");
142
+ assert.match(app, /function renderAnsiText\(parent, text\)/, "extension dialogs should render ANSI-colored TUI text as browser spans");
143
+ assert.match(app, /function applyAnsiSgr\(codes, state\)/, "ANSI SGR color state should be parsed for dialog rendering");
144
+ assert.match(app, /function normalizeDialogPrompt\(request\)/, "extension dialogs should split multiline prompts into title and body");
145
+ assert.match(app, /plainMessage: stripAnsi\(message\)/, "dialog prompt parsing should keep a plain-text copy for detection and visibility");
146
+ assert.match(app, /renderAnsiText\(elements\.dialogMessage, prompt\.message\)/, "dialog prompts should preserve TUI highlight colors in the browser");
147
+ assert.match(app, /elements\.dialog\.classList\.toggle\("guardrail-dialog", isGuardrailDialog\)/, "guardrail extension dialogs should get dedicated styling");
148
+ assert.match(app, /guardrail-safe-action/, "guardrail dialogs should distinguish safe and allow actions");
149
+ assert.match(app, /function hasQueuedDialogRequest\(id\)/, "frontend should deduplicate replayed extension UI dialogs by request id");
150
+ assert.match(app, /if \(request\.replayed\) addEvent\(`recovered pending \$\{request\.method\} request`, "warn"\)/, "frontend should surface replayed extension UI blockers");
151
+ assert.match(app, /case "webui_extension_ui_cancelled":/, "frontend should close dialogs cancelled by backend abort handling");
152
+ assert.match(app, /function parseTodoProgressWidget\(lines\)/, "todo-progress widgets should be parsed from extension widget lines");
153
+ assert.match(app, /key === "todo-progress" \? renderTodoProgressWidget\(key, lines\) : null/, "todo-progress should use the specialized widget renderer");
154
+ assert.match(app, /let transientMessages = \[\]/, "frontend should keep transient Web UI/extension output messages");
155
+ assert.match(app, /const ACTION_FEEDBACK_REACTIONS = \{/, "frontend should define direct feedback reactions");
156
+ assert.match(app, /message\?\.role === "assistant" \|\| message\?\.role === "toolResult" \|\| message\?\.role === "bashExecution"/, "frontend should allow reactions on final assistant output as well as actions");
157
+ assert.match(app, /function renderActionFeedbackControls\(/, "frontend should render per-message reaction controls");
158
+ assert.match(app, /function assistantDisplayMessages\(message\)/, "assistant history should split thinking and tool-call parts out of the final Assistant output card");
159
+ assert.match(app, /return content\.trim\(\) \? \[\{ \.\.\.message, title: "Assistant" \}\] : \[\]/, "assistant messages with stripped empty text should not render empty Assistant cards");
160
+ assert.match(app, /part\.type === "text"\) return typeof part\.text === "string" && part\.text\.trim\(\) \? part : null/, "empty assistant text parts should be filtered after todo/widget extraction");
161
+ assert.match(app, /displayMessage\.role === "assistant" \? messageIndex : -1/, "only final Assistant output cards should keep the assistant message index for feedback");
162
+ assert.match(app, /function ensureStreamingThinkingBubble\(\)/, "live thinking should render in a dedicated non-assistant streaming card");
163
+ assert.match(app, /function assistantStreamingMessage\(event\)/, "live streaming should read the authoritative partial assistant message from RPC events like the TUI");
164
+ assert.match(app, /assistantThinkingTextFromMessage\(assistantStreamingMessage\(event\)\) \|\| thinkingDeltaText\(update\)/, "live thinking end should replace deltas with the final partial-message thinking content");
165
+ assert.match(app, /if \(typeof partialText === "string"\) streamRawText = partialText;/, "live assistant text should synchronize from partial messages instead of relying only on deltas");
166
+ assert.match(app, /const TODO_PROGRESS_LINE_REGEX = /, "frontend should recognize live todo progress lines that will be moved into the todo widget");
167
+ assert.match(app, /function stripTodoProgressLines\(text, \{ streaming = false \} = \{\}\)/, "live Assistant output should strip todo-progress lines before rendering final-output text");
168
+ assert.match(app, /streamRawText \+= delta;[\s\S]*?const assistantText = stripTodoProgressLines\(streamRawText, \{ streaming: true \}\)/, "streamed Assistant text should classify from accumulated output without flashing partial todo-progress lines");
169
+ assert.match(app, /const created = appendMessage\(\{ role: "assistant", title: "Assistant"/, "live Assistant cards should be created only for final output text");
170
+ assert.match(app, /api\("\/api\/action-feedback", \{ method: "POST"/, "queued action feedback should post to the server after the run is idle");
171
+ assert.match(app, /function postQueuedFeedback\(tabId, items\)/, "queued feedback should have a backward-compatible submit path");
172
+ assert.match(app, /\/api\/action-feedback not found; falling back to a normal prompt/, "new frontend should gracefully handle older running Web UI servers");
173
+ assert.match(app, /actionFeedbackSteerMessage\(item\)/, "live action feedback should be sent as steering while the agent is running");
174
+ assert.match(app, /function addTransientMessage\(\{ role = "notice"/, "frontend should render transient command output into the transcript");
175
+ assert.match(app, /addTransientMessage\(\{ role: "extension", title: "extension output"/, "extension notify output should appear in the transcript, not only the event log");
176
+ assert.match(app, /function renderRunIndicator\(/, "frontend should render a transcript-level active agent indicator");
177
+ assert.match(app, /return "Agent is still runing: ";/, "active agent indicator should use the requested headline wording");
178
+ assert.doesNotMatch(app, /"agent running"/, "active agent indicator should not render a separate title/header label");
179
+ assert.doesNotMatch(app, /runIndicatorTimestamp/, "active agent indicator should not render a separate live timestamp header");
180
+ assert.match(app, /runIndicatorBubble = make\("article", "message runIndicator run-indicator-message streaming"\)/, "active agent indicator should use a dedicated streaming transcript card");
181
+ assert.match(app, /runIndicatorShowsElapsed\(\) \? `\$\{detail\} · run time/, "active agent indicator should label elapsed run time instead of showing a bare counter");
182
+ assert.match(app, /Abort requested; checking whether Pi stopped/, "abort feedback should clarify that Web UI is checking stop status");
183
+ assert.match(app, /let runIndicatorGraceCheckTimer = null/, "local-only run indicators should have a grace-check timer");
184
+ assert.match(app, /const RUN_INDICATOR_STATE_RECHECK_MS = 5000/, "active run indicators should periodically re-check state");
185
+ assert.match(app, /function scheduleRunIndicatorGraceCheck\(/, "local-only run indicators should schedule a follow-up state check");
186
+ assert.match(app, /function maybeRefreshRunIndicatorState\([\s\S]*?refreshState\(\)/, "active run indicators should poll state so missed completion events do not leave stale cards");
187
+ assert.match(app, /function clearRunIndicatorActivity[\s\S]*?clearRunIndicatorGraceCheck\(\)[\s\S]*?runIndicatorLastStateCheckAt = 0/, "clearing the active agent indicator should cancel pending grace checks and reset state polling");
188
+ assert.match(app, /scheduleRunIndicatorGraceCheck\(\)[\s\S]*?refreshState\(\)/, "stale local-only run indicators should re-check state after the start grace period");
189
+ assert.match(app, /function scheduleAbortStateChecks\(/, "abort handling should poll state so the active indicator can clear after stop confirmation");
190
+ assert.match(app, /case "tool_execution_start":[\s\S]*?setRunIndicatorActivity\(`Running tool:/, "tool execution should update the active agent transcript indicator");
191
+ assert.match(app, /setRunIndicatorActivity\("Requesting context compaction…"\);\n\s+scrollChatToBottom\(\{ force: true \}\);/, "manual compaction should force-follow the transcript to the bottom status card");
192
+ assert.match(app, /case "agent_end":[\s\S]*?clearRunIndicatorActivity\(\)/, "agent completion should remove the active agent transcript indicator");
193
+ assert.match(app, /function getPathTrigger\(\)/, "prompt composer should detect @ file\/path reference triggers");
194
+ assert.match(app, /api\(`\/api\/path-suggestions\?query=\$\{encodeURIComponent\(trigger\.query\)\}`/, "@ reference suggestions should load from the server as the user types");
195
+ assert.match(app, /formatPathReference\(suggestion\.path, trigger\.quoted\)/, "accepting a path suggestion should insert an @ reference into the prompt");
196
+ assert.match(app, /function resizePromptInput\(\)/, "prompt textarea should auto-resize from a one-line default");
197
+ assert.match(app, /elements\.promptInput\.addEventListener\("input", \(\) => \{\n\s+resizePromptInput\(\);/, "prompt textarea should resize whenever the user edits it");
198
+ assert.match(app, /function updateComposerModeButtons\(\)/, "composer should relocate Steer and Follow-up based on run state");
199
+ assert.match(app, /const target = runActive \? elements\.composerRow : elements\.composerActionsPanel/, "Steer and Follow-up should live in Actions unless a run is active");
200
+ assert.match(app, /document\.body\.classList\.toggle\("pi-run-active", runActive\)/, "run-active state should be reflected in CSS for mobile composer layout");
201
+ assert.match(app, /function showComposerButtonTooltip\(button\)/, "empty mode-button taps should show the usage tooltip");
202
+ assert.match(app, /sendPromptFromModeButton\("steer", elements\.steerButton\)/, "Steer should show tooltip instead of silently doing nothing when input is empty");
203
+ assert.match(app, /sendPromptFromModeButton\("follow-up", elements\.followUpButton\)/, "Follow-up should show tooltip instead of silently doing nothing when input is empty");
204
+ assert.match(app, /async function sendPrompt\(kind = "prompt", explicitMessage\)/, "prompt sending should accept direct messages that bypass the input field");
205
+ assert.match(app, /const rawMessage = usesPromptInput \? elements\.promptInput\.value : explicitMessage/, "direct prompt sends should not read the input textarea");
206
+ assert.match(app, /if \(usesPromptInput\) \{\n\s+elements\.promptInput\.value = "";/, "direct prompt sends should preserve the input textarea draft");
207
+ assert.match(app, /make\("button", "command-item"\)[\s\S]*?sendPrompt\("prompt", `\/\$\{command\.name\}`\)/, "side-panel command clicks should send the slash command directly");
208
+ assert.match(app, /function shouldSendPromptFromEnter\(event\)/, "prompt keyboard handling should be centralized");
209
+ assert.match(app, /return !isMobileView\(\);/, "plain Enter should send only outside mobile view so mobile Return can insert newlines");
210
+ assert.match(app, /mobile-keyboard-open/, "JS should toggle mobile keyboard mode from viewport/focus state");
211
+ assert.match(app, /maxVisualViewportHeight - viewportHeight > 120/, "keyboard mode should detect viewport shrink even when keyboard inset is unavailable");
212
+ assert.match(app, /jumpToLatestButton/, "jump-to-latest button should be wired in JS");
213
+ assert.match(app, /function updateStickyUserPromptButton\(/, "last user prompt header should update from scroll position");
214
+ assert.match(app, /function jumpToStickyUserPrompt\(/, "last user prompt header should jump to its source message");
215
+ assert.match(app, /data-user-prompt/, "user prompt messages should be marked for sticky prompt navigation");
216
+ assert.match(app, /function resetChatOutput\(\)[\s\S]*?stickyUserPromptButton/, "chat rerenders should preserve the sticky user prompt control inside the transcript scroller");
217
+ assert.match(app, /LAST_USER_PROMPT_STORAGE_KEY/, "last user prompt should be cached so compaction cannot remove the sticky prompt preview");
218
+ assert.match(app, /function syncLastUserPromptFromMessages\(messages = latestMessages\)/, "message refresh should preserve the latest user prompt across compacted transcripts");
219
+ assert.match(app, /dataset\.compacted/, "sticky prompt should expose a compacted fallback state when its source message was summarized away");
220
+ assert.match(app, /stickyUserPromptButton\?\.addEventListener\("click", jumpToStickyUserPrompt\)/, "last user prompt header should be clickable without breaking stale cached HTML");
221
+ assert.match(app, /function setComposerActionsOpen\(/, "mobile composer actions panel should be JS-toggleable");
222
+ assert.match(app, /function setMobileTabsExpanded\(/, "mobile tab strip should be JS-toggleable");
223
+ assert.match(app, /let openTerminalTabGroupKey = null/, "frontend should track the open terminal tab group across tab bar rerenders");
224
+ assert.match(app, /function updateTerminalTabGroupOpenState\(\)/, "frontend should be able to reapply open terminal tab group state after rerenders");
225
+ assert.match(app, /wrapper\.addEventListener\("pointerenter", \(\) => setOpenTerminalTabGroup\(group\.key\)\)/, "terminal tab groups should mark themselves open while hovered");
226
+ assert.match(app, /if \(openTerminalTabGroupKey\) \{\n\s+scheduleRefreshTabs\(600\);/, "tab polling should defer full tab refreshes while a group menu is open");
227
+ assert.match(app, /function shouldRenderTerminalTabGroup\(group, groupCount\) \{\n\s+return groupCount > 1 && group\.tabs\.length > 1 && Boolean\(group\.cwd\);\n\}/, "terminal tabs should only collapse cwd groups when multiple groups are available");
228
+ assert.match(app, /const groups = tabCwdGroups\(\);[\s\S]*?for \(const group of groups\) \{\n\s+if \(shouldRenderTerminalTabGroup\(group, groups\.length\)\)[\s\S]*?for \(const tab of group\.tabs\) elements\.tabBar\.append\(renderTerminalTab\(tab\)\);/, "terminal tabs should render every tab ungrouped when grouping is skipped");
229
+ assert.match(app, /let tabSeenCompletionSerials = new Map\(\)/, "frontend should track which tab completions have been seen");
230
+ assert.match(app, /function tabIndicator\(tab\)/, "frontend should derive idle, working, blocked, and work-done tab indicator states");
231
+ assert.match(app, /pendingBlockerCount > 0[\s\S]*?state: "blocked"/, "frontend should show blocked tabs when extension UI blockers are pending");
232
+ assert.match(app, /const EXTENSION_UI_BLOCKING_METHODS = new Set\(\["select", "confirm", "input", "editor"\]\)/, "frontend should share blocking extension UI method detection for dialogs and notifications");
233
+ assert.match(app, /function notifyBlockedTab\(/, "frontend should send blocked-tab notifications when extension UI blocks a run");
234
+ assert.match(app, /function showBlockedTabBrowserNotification\(/, "frontend should use browser notifications for blocked tabs when permission allows");
235
+ assert.match(app, /Notification\.requestPermission\(\)/, "frontend should request notification permission before blocked-tab browser alerts");
236
+ assert.match(app, /syncBlockedTabNotificationsFromTabs\(tabs, previousTabs\)/, "tab refreshes should notify when a background tab becomes blocked");
237
+ assert.match(app, /notifyBlockedTab\(request\.tabId, \{ request, count: request\.pendingExtensionUiRequestCount \}\)/, "extension UI requests should trigger blocked-tab notifications");
238
+ assert.match(app, /pendingExtensionUiRequestCount[\s\S]*?setTabPendingBlockerCount/, "frontend should ingest pending blocker counts from tab events");
239
+ assert.match(app, /function markTabOutputSeen\(/, "frontend should clear work-done indicators once output is seen");
240
+ assert.match(app, /function markTabDoneLocally\(/, "frontend should locally recover tabs that were left working after idle state refreshes");
241
+ assert.match(app, /function syncActiveTabActivityFromState\(state = currentState\)/, "frontend should reconcile active-tab indicators from authoritative state snapshots");
242
+ assert.match(app, /event\.command === "get_state" && event\.tabId === activeTabId[\s\S]*?syncActiveTabActivityFromState\(currentState\)/, "get_state response events should update stale active-tab activity");
243
+ assert.match(app, /function applyResponseTab\(response\)/, "frontend should merge tab metadata returned by prompt responses");
244
+ assert.match(app, /case "webui_tab_renamed":/, "frontend should update tab labels from backend rename events");
245
+ assert.match(app, /terminalTabsToggleButton\.addEventListener\("click"/, "terminal tabs trigger should be wired in JS");
246
+ assert.match(app, /composerActionsButton\.addEventListener\("click"/, "composer actions trigger should be wired in JS");
247
+ assert.match(app, /function setMobileFooterExpanded\(/, "mobile footer should have an expandable details state");
248
+ assert.match(app, /function updateFooterModelPickerPosition\(\)/, "mobile model picker should compute a fixed overlay position above the footer");
249
+ assert.match(app, /mobileFooterExpanded = false;[\s\S]*?document\.body\.classList\.remove\("footer-details-expanded"\)/, "opening mobile model picker should collapse footer details so details cannot cover the dropdown");
250
+ assert.match(app, /footerMeta\("context", contextLabel, "footer-context"\)/, "footer should render context as a primary mobile meta item");
251
+ assert.match(app, /footerMeta\("model", modelLine, "footer-model", \{/, "footer model item should be clickable");
252
+ assert.match(app, /function renderFooterModelPicker\(\)/, "footer should render a scoped-model picker dropdown");
253
+ assert.match(app, /api\("\/api\/scoped-models"\)/, "footer model picker should load scoped models instead of all available models");
254
+ assert.match(app, /for \(const model of footerScopedModels\)/, "footer model picker should render only scoped models");
255
+ assert.match(app, /api\("\/api\/model", \{ method: "POST"/, "footer model picker should apply selected model through the model API");
256
+ assert.match(app, /footer-details-toggle/, "footer details toggle should be rendered by JS");
257
+ assert.match(app, /bindMobileViewChanges\(/, "side panel state should react to mobile breakpoint changes");
258
+ assert.match(app, /function restoreSidePanelState\(\) \{\n\s+if \(isMobileView\(\)\)/, "mobile should start with side panel collapsed even if desktop state was expanded");
259
+ assert.match(app, /case "webui_tab_reloaded":/, "frontend should handle native /reload tab restart events");
260
+ assert.match(app, /addTransientMessage\(\{ role: "native", title: "\/reload"/, "native /reload should produce visible transcript output");
261
+ assert.match(app, /navigator\.clipboard\.writeText\(response\.data\.copyText\)/, "native /copy should use the browser clipboard when available");
262
+ assert.match(app, /Clipboard access failed:[\s\S]*?response\.data\.copyText/, "native /copy should show text in transcript when clipboard access fails");
263
+ assert.match(app, /setTimeout\(\(\) => refreshAll\(\)\.catch/, "frontend should refresh state after native /reload restarts the RPC process");
264
+ assert.match(app, /api\("\/api\/path-fast-picks"/, "frontend should load/save fast picks through the server API");
265
+ assert.match(app, /loadLegacyFastPicks\(/, "frontend should migrate existing browser-local fast picks");
266
+
267
+ assert.equal(manifest.display, "standalone", "PWA manifest should request standalone display");
268
+ assert.equal(manifest.start_url, "/", "PWA manifest should start at the web UI root");
269
+ assert.ok(manifest.icons?.some((icon) => icon.src === "/apple-touch-icon.png" && icon.sizes === "180x180"), "PWA manifest should include a conventional 180px apple touch icon");
270
+ assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-192.png" && icon.sizes === "192x192"), "PWA manifest should include a 192px icon");
271
+ assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-512.png" && icon.sizes === "512x512"), "PWA manifest should include a 512px icon");
272
+ assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v6"/, "PWA service worker should define an app-shell cache");
273
+ assert.match(serviceWorker, /self\.addEventListener\("notificationclick"/, "PWA service worker should focus Web UI when blocked-tab notifications are clicked");
274
+ assert.match(serviceWorker, /event\.notification\.data\?\.url/, "blocked-tab notifications should carry a URL for service-worker click handling");
275
+ assert.match(serviceWorker, /"\/apple-touch-icon\.png"/, "PWA service worker should cache the apple touch icon");
276
+ assert.match(serviceWorker, /url\.pathname\.startsWith\("\/api\/"\)/, "PWA service worker should not cache live API or SSE calls");
277
+ assert.ok(appleIcon.length > 1000, "PWA apple touch icon should be present");
278
+ assert.ok(icon192.length > 1000, "PWA 192px icon should be present");
279
+ assert.ok(icon512.length > icon192.length, "PWA 512px icon should be present and larger than 192px icon");
280
+
281
+ assert.match(server, /const NATIVE_SLASH_COMMANDS = \[/, "server should define Pi native slash commands for autocomplete");
282
+ assert.match(server, /\{ name: "reload", description: "Reload keybindings, extensions, skills, prompts, and themes" \}/, "native /reload should be advertised for autocomplete");
283
+ assert.match(server, /function parseSlashCommand\(message\)/, "server should parse native slash commands before prompt forwarding");
284
+ assert.match(server, /function generatedTabTitleFromPrompt\(message\)/, "server should derive concise automatic tab titles from first prompts");
285
+ assert.match(server, /function maybeNameTabForConversation\(tab, command\)/, "server should auto-name default tabs when a conversation starts");
286
+ assert.match(server, /function createTabActivity\(/, "server should track per-tab activity for idle, working, and completed work");
287
+ assert.match(server, /function reconcileTabActivityFromState\(tab, state/, "server should recover stale working tab activity from get_state snapshots");
288
+ assert.match(server, /pendingExtensionUiRequests\(tab\)\.length > 0[\s\S]*?markTabWorking\(tab, timestamp\)/, "server should keep tabs with pending blockers in working activity until the blocker resolves");
289
+ assert.match(server, /case "response":[\s\S]*?event\.command === "get_state"[\s\S]*?reconcileTabActivityFromState\(tab, event\.data/, "server should reconcile tab activity when get_state responses flow through RPC events");
290
+ assert.match(server, /async function listTabsWithReconciledActivity\(\)/, "server tab listing should reconcile stale working tabs before returning metadata");
291
+ assert.match(server, /tabs: await listTabsWithReconciledActivity\(\)/, "GET /api/tabs should use reconciled tab activity metadata");
292
+ assert.match(server, /tabActivity: tabActivitySnapshot\(tab\)/, "server should expose tab activity over tab metadata and events");
293
+ assert.match(server, /const EXTENSION_UI_BLOCKING_METHODS = new Set\(\["select", "confirm", "input", "editor"\]\)/, "server should know which extension UI requests can block Pi runs");
294
+ assert.match(server, /function trackPendingExtensionUiRequest\(tab, event\)/, "server should track blocking extension UI requests per tab");
295
+ assert.match(server, /pendingExtensionUiRequests: new Map\(\)/, "new tabs should initialize pending extension UI request storage");
296
+ assert.match(server, /trackPendingExtensionUiRequest\(tab, scopedEvent\)/, "RPC events should populate pending extension UI storage before broadcasting");
297
+ assert.match(server, /scopedEvent = \{ \.\.\.scopedEvent,[\s\S]*?pendingExtensionUiRequestCount: pendingExtensionUiRequests\(tab\)\.length \}/, "RPC events should broadcast pending blocker counts for tab indicators");
298
+ assert.match(server, /function replayPendingExtensionUiRequests\(tab, res\)/, "server should be able to replay missed extension UI requests on SSE reconnect");
299
+ assert.match(server, /replayPendingExtensionUiRequests\(tab, res\)/, "SSE connections should replay pending extension UI blockers");
300
+ assert.match(server, /pendingExtensionUiRequests: pendingExtensionUiRequestSummaries\(tab\)/, "detailed Web UI status should expose pending extension UI blockers");
301
+ assert.match(server, /resolvePendingExtensionUiRequest\(tab, payload\.id\)/, "extension UI responses should clear the pending blocker cache");
302
+ assert.match(server, /type: "webui_extension_ui_resolved"[\s\S]*?pendingExtensionUiRequestCount/, "extension UI responses should notify clients that a blocker resolved");
303
+ assert.match(server, /command\.type === "abort"[\s\S]*?cancelPendingExtensionUiRequests\(tab\)/, "abort should cancel hidden pending extension UI requests");
304
+ assert.match(server, /type: "webui_extension_ui_cancelled"/, "server should notify browsers when pending extension UI requests are cancelled");
305
+ assert.match(server, /async function handleNativeSlashCommand\(tab, body\)/, "server should intercept supported native slash commands");
306
+ assert.match(server, /const restoreTabs = readRestoreTabsFromEnv\(\)/, "server should accept restart tab restore descriptors from the launcher environment");
307
+ assert.match(server, /delete process\.env\.PI_WEBUI_RESTORE_TABS/, "server should avoid leaking restore descriptors into spawned Pi RPC processes");
308
+ assert.match(server, /if \(sessionFile && !options\.noSession\) piArgs\.push\("--session", sessionFile\)/, "restored tabs should resume previous session files");
309
+ assert.match(server, /const closedRestorableTabs = \[\]/, "server should remember recently closed tabs for slash-command restarts");
310
+ assert.match(server, /async function closeTab\(id\)[\s\S]*?rememberClosedRestorableTab\(tab, restorableState\)/, "closing a tab should capture its restorable session before stopping RPC");
311
+ assert.match(server, /function rememberTabState\(tab, state\)/, "server should cache last-known tab state for restart-safe session restoration");
312
+ assert.match(server, /sessionFile: tabRestorableSessionFile\(tab\)/, "tab metadata should expose cached session files for health/status restore descriptors");
313
+ assert.match(server, /restorableTabs: mergeRestorableTabDescriptors\(statusTabs, closedRestorableTabs\)/, "status should expose open plus recently closed restorable tabs");
314
+ assert.match(server, /data\.restorableTabs = mergeRestorableTabDescriptors\(detailedTabs, closedRestorableTabs\)/, "detailed status should include session-aware closed tab restore descriptors");
315
+ assert.match(server, /const stateData = stateResult\.ok \? stateResult\.data : tab\.lastState \|\| null/, "detailed status should fall back to cached state when live RPC state is temporarily unavailable");
316
+ assert.match(server, /const initialTabs = await createInitialTabs\(\)/, "server should recreate restored tabs before listening");
317
+ assert.match(extension, /api\/webui-status\?detailed=1&events=0/, "launcher should capture detailed tab status before restarting an existing Web UI");
318
+ assert.match(extension, /function mergeRestorableTabsFromStatusSources\(sources: unknown\[\], options: StartWebuiOptions\)/, "launcher should merge all available restore sources instead of trusting only the first one");
319
+ assert.match(extension, /statusData\?\.restorableTabs, statusData\?\.tabs, existing\.restorableTabs, existing\.tabs/, "launcher should combine detailed, health, open, and closed restore descriptors");
320
+ assert.match(extension, /env\.PI_WEBUI_RESTORE_TABS = JSON\.stringify\(restoreTabs\)/, "launcher should pass restorable tabs to the new detached server");
321
+ assert.match(extension, /pi\.registerCommand\("start-webui"/, "extension should expose /start-webui as an alias for users who invoke that command name");
322
+ assert.match(server, /if \(state\.data\?\.sessionFile && !options\.noSession\) piArgs\.push\("--session", state\.data\.sessionFile\)/, "native /reload should resume the same session file when restarting the RPC tab");
323
+ assert.match(server, /case "reload": \{[\s\S]*?restartTabRpc\(tab, "slash-command"\)/, "native /reload should restart the active RPC tab");
324
+ assert.match(server, /message: "Reloaded keybindings, extensions, skills, prompts, and themes\."/, "native /reload should return visible command output");
325
+ assert.match(server, /case "name": \{[\s\S]*?renameTab\(tab, parsed\.args, \{ source: "explicit" \}\)/, "native /name should also rename the browser tab");
326
+ assert.match(server, /maybeNameTabForConversation\(tab, command\);[\s\S]*?markTabWorking\(tab\)/, "server should auto-name tabs before starting visible prompt work");
327
+ assert.match(server, /function formatSessionOutput\(tab, state, stats\)/, "native /session should have visible Web UI output");
328
+ assert.match(server, /case "session": \{[\s\S]*?formatSessionOutput\(tab, state\.data \|\| \{\}, stats\.success === false \? null : stats\.data\)/, "native /session should render state and stats through Web UI");
329
+ assert.match(server, /case "copy": \{[\s\S]*?get_last_assistant_text[\s\S]*?copyText: text/, "native /copy should return text for browser clipboard handling");
330
+ assert.match(server, /case "hotkeys": \{[\s\S]*?webuiHotkeysOutput\(\)/, "native /hotkeys should return Web UI hotkey output");
331
+ assert.match(server, /url\.pathname === "\/api\/commands" && req\.method === "GET"[\s\S]*?getCommandData\(tab\)/, "GET /api/commands should merge native and RPC-visible commands");
332
+ assert.match(server, /specific Web UI action or final-output cards/, "server feedback-learning prompt should cover final outputs as well as actions");
333
+ assert.match(server, /function formatActionFeedbackLearningPrompt\(items\)/, "server should convert feedback into a LEARNING prompt");
334
+ assert.match(server, /url\.pathname === "\/api\/action-feedback" && req\.method === "POST"[\s\S]*?handleActionFeedback\(tab, body\)/, "POST /api/action-feedback should trigger the feedback-learning prompt");
335
+ assert.match(server, /Wait for the current agent run or compaction to finish before sending feedback\./, "server should only accept post-run feedback submissions");
336
+ assert.match(server, /url\.pathname === "\/api\/prompt" && req\.method === "POST"[\s\S]*?handleNativeSlashCommand\(tab, body\)/, "POST /api/prompt should intercept native slash commands before normal prompt forwarding");
337
+ assert.match(server, /function fastPicksStorageFile\(/, "server should define a persistent fast-picks storage file");
338
+ assert.match(server, /PI_WEBUI_FAST_PICKS_FILE/, "server should allow overriding the fast-picks storage path");
339
+ assert.match(server, /async function getPathSuggestionData\(tab, rawQuery\)/, "server should compute @ file\/path reference suggestions for the active tab cwd");
340
+ assert.match(server, /url\.pathname === "\/api\/path-suggestions" && req\.method === "GET"/, "server should expose GET /api/path-suggestions for @ reference autocomplete");
341
+ assert.match(server, /url\.pathname === "\/api\/path-fast-picks" && req\.method === "GET"/, "server should expose GET /api/path-fast-picks");
342
+ assert.match(server, /url\.pathname === "\/api\/path-fast-picks" && req\.method === "POST"/, "server should expose POST /api/path-fast-picks");
343
+ assert.match(server, /url\.pathname === "\/api\/scoped-models" && req\.method === "GET"/, "server should expose GET /api/scoped-models");
344
+ assert.match(server, /@firstpick\/pi-themes-bundle/, "server should discover themes from the bundled theme package");
345
+ assert.match(server, /url\.pathname === "\/api\/themes" && req\.method === "GET"/, "server should expose GET /api/themes");
346
+ assert.match(server, /readBundledThemes\(\)/, "server should read bundled theme JSON files for the browser");
347
+ assert.match(server, /"apple-touch-icon\.png", "icon-192\.png"/, "server should serve the conventional apple touch icon path");
348
+ assert.match(server, /"manifest\.webmanifest", "service-worker\.js"/, "server should serve PWA manifest and service worker as static assets");
349
+ assert.match(server, /\["\.webmanifest", "application\/manifest\+json; charset=utf-8"\]/, "server should serve manifest with the correct MIME type");
350
+ assert.match(server, /\["\.png", "image\/png"\]/, "server should serve PWA PNG icons with the correct MIME type");
351
+ assert.match(server, /function configuredScopedModelPatterns\(cwd = options\.cwd\)/, "server should read Pi configured scoped-model patterns for the active tab cwd");
352
+ assert.match(server, /readJsonFileIfExists\(path\.join\(cwd, "\.pi", "settings\.json"\)\)/, "server should read project-local scoped-model settings from active tab cwd");
353
+ assert.match(server, /resolveScopedModelsFromPatterns\(patterns, response\.data\?\.models/, "server should resolve scoped patterns against available models");
354
+ assert.match(server, /writeFile\(tmpFile[\s\S]*?rename\(tmpFile, storageFile\)/, "server should persist fast picks with an atomic temp-file rename");
355
+ assert.match(readme, /Automatic tab naming from the first prompt/, "README should describe automatic terminal-tab naming");
356
+ assert.match(readme, /Feedback reactions \(`👍`, `👎`, `\?`\) on final assistant output plus tool\/bash action cards/, "README should describe final-output and action feedback reactions");
357
+ assert.match(readme, /POST \/api\/action-feedback\?tab=<tabId>/, "README should document the action-feedback endpoint");
358
+ assert.match(readme, /`@` file\/path references with live suggestions/, "README should describe @ file/path reference autocomplete");
359
+ assert.match(readme, /GET \/api\/path-suggestions\?tab=<tabId>&query=<path>/, "README should document the path-suggestions endpoint");
360
+ assert.match(readme, /server-persisted fast picks/, "README should describe server-persisted fast picks");
361
+ assert.match(readme, /browser notifications when a tab needs an extension UI response/, "README should describe blocked-tab notifications");
362
+ assert.match(readme, /blocked-tab browser notifications require browser service-worker\/notification support/, "README should document blocked-tab notification requirements");
363
+ assert.match(readme, /Side-panel theme picker backed by the bundled `@firstpick\/pi-themes-bundle` themes/, "README should describe bundled theme selection");
364
+
365
+ assert.equal(pkg.scripts?.test, "node tests/mobile-static.test.mjs", "package test script should run the mobile static harness");
366
+ assert.equal(pkg.dependencies?.["@firstpick/pi-themes-bundle"], "^0.1.0", "webui package should depend on the bundled theme package");
367
+ assert.ok(pkg.scripts?.check?.includes("node --check public/app.js"), "check script should syntax-check app.js");
368
+ assert.ok(pkg.scripts?.check?.includes("node tests/mobile-static.test.mjs"), "check script should include mobile static assertions");
369
+
370
+ console.log("mobile static checks passed");