@firstpick/pi-package-webui 0.1.5 → 0.1.7
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.
- package/README.md +9 -5
- package/bin/pi-webui.mjs +425 -13
- package/index.ts +82 -10
- package/package.json +1 -1
- package/public/app.js +2853 -234
- package/public/catppuccin-mocha-background.png +0 -0
- package/public/index.html +165 -54
- package/public/matrix-background.webp +0 -0
- package/public/service-worker.js +3 -1
- package/public/styles.css +695 -16
- package/tests/mobile-static.test.mjs +155 -30
|
@@ -4,7 +4,7 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
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([
|
|
7
|
+
const [pkgRaw, html, css, app, server, extension, readme, manifestRaw, serviceWorker, appleIcon, icon192, icon512, matrixBackground, mochaBackground] = await Promise.all([
|
|
8
8
|
readFile(join(root, "package.json"), "utf8"),
|
|
9
9
|
readFile(join(root, "public", "index.html"), "utf8"),
|
|
10
10
|
readFile(join(root, "public", "styles.css"), "utf8"),
|
|
@@ -17,6 +17,8 @@ const [pkgRaw, html, css, app, server, extension, readme, manifestRaw, serviceWo
|
|
|
17
17
|
readFile(join(root, "public", "apple-touch-icon.png")),
|
|
18
18
|
readFile(join(root, "public", "icon-192.png")),
|
|
19
19
|
readFile(join(root, "public", "icon-512.png")),
|
|
20
|
+
readFile(join(root, "public", "matrix-background.webp")),
|
|
21
|
+
readFile(join(root, "public", "catppuccin-mocha-background.png")),
|
|
20
22
|
]);
|
|
21
23
|
const pkg = JSON.parse(pkgRaw);
|
|
22
24
|
const manifest = JSON.parse(manifestRaw);
|
|
@@ -40,9 +42,22 @@ assert.match(html, /id="closeAllTabsButton"[\s\S]*?>Close all Tabs<\/button>/, "
|
|
|
40
42
|
assert.match(html, /id="sidePanelBackdrop"/, "mobile side panel needs an overlay/backdrop close target");
|
|
41
43
|
assert.match(html, /id="themeSelect"/, "side panel should expose a theme selector");
|
|
42
44
|
assert.match(html, /<label for="themeSelect">Theme<\/label>/, "theme selector should be labeled in side-panel controls");
|
|
45
|
+
assert.match(html, /id="backgroundInput"[^>]*type="file"[^>]*accept="image\/png,image\/jpeg,image\/webp,image\/gif"/, "side panel should expose an image picker for custom backgrounds");
|
|
46
|
+
assert.match(html, /id="backgroundClearButton"[\s\S]*?>×<\/button>/, "side-panel background control should expose an X remove button");
|
|
43
47
|
assert.match(html, /id="agentDoneNotificationsToggle"/, "side panel should expose an agent-done notifications toggle");
|
|
44
48
|
assert.match(html, /id="agentDoneNotificationsStatus"/, "agent-done notifications toggle should expose status text");
|
|
49
|
+
assert.match(html, /id="thinkingVisibilityToggle"/, "side panel should expose a thinking-output visibility toggle");
|
|
50
|
+
assert.match(html, /id="thinkingVisibilityStatus"/, "thinking-output visibility toggle should expose status text");
|
|
51
|
+
assert.match(html, /id="nativeCommandDialog"/, "native slash selector UI should have a dedicated dialog");
|
|
52
|
+
assert.match(html, /id="nativeCommandSearch"[^>]*type="search"/, "native slash selector dialog should expose a filter box");
|
|
45
53
|
assert.match(html, /id="optionalFeaturesBox"/, "side panel should expose optional feature status and controls");
|
|
54
|
+
assert.match(html, /id="serverOfflinePanel"/, "PWA/offline shell should expose a backend-offline recovery panel");
|
|
55
|
+
assert.match(html, /id="copyServerCommandButton"/, "backend-offline recovery panel should expose a start-command copy button");
|
|
56
|
+
assert.match(html, /id="retryServerConnectionButton"/, "backend-offline recovery panel should expose a retry button");
|
|
57
|
+
assert.match(html, /data-side-panel-section="controls"/, "side panel controls should live in a collapsible section");
|
|
58
|
+
assert.match(html, /data-side-panel-section="commands"/, "side panel commands should live in a collapsible section");
|
|
59
|
+
assert.match(html, /class="side-panel-section-toggle"[^>]*aria-controls="sidePanelSectionControls"/, "side panel section toggles should target their content panels");
|
|
60
|
+
assert.match(html, /class="side-panel-section-label">Events<\/span>/, "side panel events should expose a section toggle label");
|
|
46
61
|
assert.match(html, /id="jumpToLatestButton"/, "chat should expose a jump-to-latest control for non-forced streaming");
|
|
47
62
|
assert.match(html, /id="stickyUserPromptButton"/, "chat should expose a fixed last-user-prompt jump control");
|
|
48
63
|
assert.match(html, /id="feedbackTray"/, "chat should expose a queued action-feedback tray");
|
|
@@ -50,6 +65,9 @@ assert.match(html, /id="sendFeedbackButton"/, "action feedback should be submitt
|
|
|
50
65
|
assert.match(html, /<textarea id="promptInput"[^>]*rows="1"[^>]*enterkeyhint="enter"/, "prompt textarea should start at one row and hint that Return inserts a newline");
|
|
51
66
|
assert.match(html, /id="composerActionsButton"/, "mobile composer should expose a compact actions trigger");
|
|
52
67
|
assert.match(html, /id="composerActionsPanel"/, "secondary composer controls should live in a mobile actions panel");
|
|
68
|
+
assert.match(html, /<div class="composer-row">[\s\S]*id="abortButton"[\s\S]*id="sendButton"/, "Abort should live in the bottom composer row beside Send");
|
|
69
|
+
assert.match(html, /id="abortButton"[^>]*Esc or hold/, "Abort should advertise Esc and long-press affordances");
|
|
70
|
+
assert.doesNotMatch(html, /class="side-panel-controls"[\s\S]*id="abortButton"/, "Abort should not be buried in the side-panel controls");
|
|
53
71
|
assert.match(html, /id="publishButton"[\s\S]*?aria-controls="publishMenu"/, "composer should expose a Publish workflow menu button");
|
|
54
72
|
assert.match(html, /id="releaseNpmButton"[^>]*data-command="\/release-npm"[\s\S]*?<span>NPM Release<\/span>/, "Publish menu should include the npm release workflow by label");
|
|
55
73
|
assert.match(html, /id="releaseAurButton"[^>]*data-command="\/release-aur"[\s\S]*?<span>AUR Release<\/span>/, "Publish menu should include the AUR release workflow by label");
|
|
@@ -67,9 +85,16 @@ assert.match(css, /--visual-viewport-height:\s*100dvh/, "CSS should define a vis
|
|
|
67
85
|
assert.match(css, /color-scheme:\s*var\(--theme-color-scheme\)/, "CSS should allow JS-selected themes to update browser color-scheme");
|
|
68
86
|
assert.match(css, /font-size:\s*80%/, "Web UI should render at 80% base scale for denser layout");
|
|
69
87
|
assert.match(css, /--background-glow-pink/, "CSS should expose theme-controlled page glow colors");
|
|
88
|
+
assert.match(css, /--theme-background-image:\s*none/, "CSS should expose a theme-controlled page background image variable");
|
|
89
|
+
assert.match(css, /var\(--theme-background-image\)/, "body background should include the selected theme background image layer");
|
|
90
|
+
assert.match(css, /\.background-control-row \{[\s\S]*?grid-template-columns:\s*minmax\(0, 1fr\) auto/, "side-panel background controls should keep the remove button beside the picker");
|
|
91
|
+
assert.match(css, /\.background-clear-button \{[\s\S]*?color:\s*var\(--ctp-red\)/, "background remove button should be visually destructive");
|
|
70
92
|
assert.match(css, /height:\s*var\(--visual-viewport-height, 100dvh\)/, "layout should consume visual viewport height");
|
|
71
93
|
assert.match(css, /button, select, input \{ min-height: 44px; \}/, "base controls should meet 44px touch-target height");
|
|
72
94
|
assert.match(css, /\.composer-row button[\s\S]*?min-height:\s*44px/, "mobile composer buttons should keep 44px touch targets");
|
|
95
|
+
assert.match(css, /\.composer-abort-button,\n\.composer-row button\.primary \{[\s\S]*?min-width:/, "Abort and Send should share stable bottom-row sizing");
|
|
96
|
+
assert.match(css, /\.composer-abort-button\.long-pressing::after[\s\S]*?animation:\s*abort-long-press-fill 700ms linear forwards/, "Abort should expose a visible long-press progress affordance");
|
|
97
|
+
assert.match(css, /body\.pi-run-active:not\(\.mobile-keyboard-open\) \.composer-abort-button:not\(\[hidden\]\) \{ grid-column: span 2; \}/, "active mobile runs should keep Abort beside Send in the bottom controls");
|
|
73
98
|
assert.match(css, /#promptInput \{[\s\S]*?min-height:\s*calc\(1\.5em \+ 1\.8rem\)/, "prompt input should default to a compact single-line height");
|
|
74
99
|
assert.match(css, /#promptInput \{[\s\S]*?overflow-y:\s*hidden/, "prompt input should be JS-resized instead of showing a scrollbar by default");
|
|
75
100
|
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");
|
|
@@ -79,10 +104,17 @@ assert.match(css, /\.message\.action-enter \{[\s\S]*?action-card-slide-in/, "new
|
|
|
79
104
|
assert.match(css, /@keyframes action-card-slide-in \{[\s\S]*?translateY\(0\.42rem\)/, "action-card entry animation should start below the final position");
|
|
80
105
|
assert.match(css, /\.message\.thinking,\n\.message\.toolCall,\n\.message\.assistantEvent/, "thinking and assistant events should have non-assistant transcript card styling");
|
|
81
106
|
assert.match(css, /\.message\.toolResult, \.message\.bashExecution, \.message\.compactionSummary/, "compaction summaries should render as compact collapsible transcript cards");
|
|
107
|
+
assert.match(css, /\.message\.toolExecution \{[\s\S]*?border-color/, "paired tool executions should render as distinct TUI-like action cards");
|
|
108
|
+
assert.match(css, /\.tool-diff \{[\s\S]*?font-family:/, "edit tool diffs should have a dedicated monospace renderer");
|
|
109
|
+
assert.match(css, /\.markdown-body \{[\s\S]*?line-height:/, "assistant Markdown output should have dedicated readable styling");
|
|
110
|
+
assert.match(css, /\.markdown-table-wrapper \{[\s\S]*?overflow-x:\s*auto/, "assistant Markdown tables should be horizontally scrollable on narrow screens");
|
|
82
111
|
assert.match(css, /\.tool-result-preview \{[\s\S]*?padding:/, "collapsed tool results should show a preview area by default");
|
|
83
112
|
assert.match(css, /\.message-collapse\[open\] \+ \.tool-result-preview \{[\s\S]*?display:\s*none/, "tool result preview should hide when full output is expanded");
|
|
84
113
|
assert.match(css, /\.run-indicator-pulse \{[\s\S]*?animation:\s*run-indicator-pulse/, "active agent run indicator should have an animated pulse");
|
|
85
114
|
assert.match(css, /\.optional-features-box \{[\s\S]*?display:\s*grid/, "optional features should render as a side-panel feature list");
|
|
115
|
+
assert.match(css, /\.side-panel-section-toggle \{[\s\S]*?justify-content:\s*space-between/, "side panel section toggles should align labels and chevrons");
|
|
116
|
+
assert.match(css, /\.side-panel-section\.collapsed \.side-panel-section-content,\n\.side-panel-section-content\[hidden\] \{\n\s+display:\s*none;/, "collapsed side panel section content should be hidden");
|
|
117
|
+
assert.match(css, /\.side-panel-section:not\(\.collapsed\) \.side-panel-section-chevron/, "expanded side panel sections should rotate the chevron");
|
|
86
118
|
assert.match(css, /\.optional-feature-pill\.enabled/, "optional features should visually distinguish enabled state");
|
|
87
119
|
assert.match(css, /\.todo-widget \{[\s\S]*?display:\s*grid/, "todo-progress widget should render as a styled checklist card");
|
|
88
120
|
assert.match(css, /\.todo-widget-item\.partial \.todo-widget-marker/, "todo-progress partial items should have distinct styling");
|
|
@@ -128,9 +160,10 @@ assert.match(css, /\.terminal-tab\.activity-blocked \.terminal-tab-activity-indi
|
|
|
128
160
|
assert.match(css, /\.terminal-tab\.activity-done/, "completed unseen work should have a distinct tab style");
|
|
129
161
|
assert.match(css, /\.terminal-tabs[\s\S]*?position:\s*absolute/, "expanded mobile tabs should overlay instead of consuming transcript space");
|
|
130
162
|
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");
|
|
131
|
-
assert.match(css, /body\.mobile-keyboard-open \.composer-actions-button,[\s\S]*?body\.mobile-keyboard-open
|
|
163
|
+
assert.match(css, /body\.mobile-keyboard-open \.composer-actions-button,[\s\S]*?body\.mobile-keyboard-open \.composer-actions-panel/, "mobile keyboard mode should hide the secondary actions sheet while keeping active-run controls available");
|
|
164
|
+
assert.match(css, /\.server-offline-panel/, "PWA/offline shell should style a backend-offline recovery panel");
|
|
132
165
|
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");
|
|
133
|
-
assert.match(css,
|
|
166
|
+
assert.match(css, /button\[hidden\] \{ display: none !important; \}/, "hidden bottom-row controls should not occupy layout space");
|
|
134
167
|
assert.match(css, /\.footer-details-toggle \{ display: none; \}/, "footer details toggle should be hidden outside mobile CSS");
|
|
135
168
|
assert.match(css, /\.footer-workspace,\n\s+\.footer-context \{ display: grid !important; \}/, "collapsed mobile footer should primarily show cwd and context");
|
|
136
169
|
assert.match(css, /\.footer-model \{ order: 7; \}/, "model should move into expanded footer details on mobile");
|
|
@@ -144,6 +177,9 @@ assert.match(css, /(?:^|\n)\s*\.side-panel\s*\{[\s\S]*?position:\s*fixed/, "mobi
|
|
|
144
177
|
assert.match(css, /\.extension-dialog[\s\S]*?max-height:\s*calc\(var\(--visual-viewport-height/, "dialogs should fit the visual viewport on mobile");
|
|
145
178
|
assert.match(css, /\.extension-dialog[\s\S]*?inset:\s*auto 0 0 0/, "mobile dialogs should behave like bottom sheets");
|
|
146
179
|
assert.match(css, /#dialogMessage \{[\s\S]*?white-space:\s*pre-wrap/, "extension dialog messages should preserve multiline prompts");
|
|
180
|
+
assert.match(css, /\.native-command-dialog \{[\s\S]*?width:\s*min\(56rem/, "native slash selector dialog should have roomy desktop layout");
|
|
181
|
+
assert.match(css, /\.native-selector-item \{[\s\S]*?--tree-depth/, "native slash selector choices should support tree indentation");
|
|
182
|
+
assert.match(css, /\.native-settings-grid,[\s\S]*?\.native-tree-options \{[\s\S]*?grid-template-columns:/, "native settings and tree selector options should use responsive grids");
|
|
147
183
|
assert.match(css, /\.extension-dialog\.guardrail-dialog[\s\S]*?border-color:\s*rgba\(249, 226, 175/, "guardrail dialogs should have warning-specific styling");
|
|
148
184
|
assert.match(css, /\.extension-dialog\.release-dialog[\s\S]*?width:\s*min\(64rem/, "release confirmation dialogs should have more horizontal room");
|
|
149
185
|
assert.match(css, /\.extension-dialog\.release-dialog #dialogMessage[\s\S]*?max-height:\s*min\(56vh, 34rem\)/, "release confirmation summaries should scroll in a roomy panel");
|
|
@@ -152,10 +188,27 @@ assert.match(css, /\.release-dialog-danger \{ color: var\(--ctp-red\); \}/, "rel
|
|
|
152
188
|
|
|
153
189
|
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");
|
|
154
190
|
assert.match(app, /const THEME_STORAGE_KEY = "pi-webui-theme"/, "theme selection should be persisted in browser storage");
|
|
191
|
+
assert.match(app, /const CUSTOM_BACKGROUND_STORAGE_KEY = "pi-webui-custom-background"/, "custom backgrounds should keep a legacy persistent browser storage key for migration");
|
|
192
|
+
assert.match(app, /const CUSTOM_BACKGROUNDS_STORAGE_KEY = "pi-webui-custom-backgrounds"/, "custom backgrounds should be persisted per theme in browser storage");
|
|
193
|
+
assert.match(app, /const CUSTOM_BACKGROUND_IDB_NAME = "pi-webui-custom-background"/, "custom backgrounds should prefer IndexedDB persistence for large images");
|
|
194
|
+
assert.match(app, /const SIDE_PANEL_SECTION_STORAGE_KEY = "pi-webui-side-panel-sections-collapsed"/, "side-panel section collapse state should be persisted in browser storage");
|
|
155
195
|
assert.match(app, /const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications"/, "agent-done notification preference should be persisted in browser storage");
|
|
156
196
|
assert.match(app, /async function initializeThemes\(\)/, "frontend should initialize bundled themes");
|
|
157
197
|
assert.match(app, /api\("\/api\/themes", \{ scoped: false \}\)/, "theme loading should use the unscoped themes endpoint");
|
|
158
198
|
assert.match(app, /function applyTheme\(theme/, "frontend should apply a selected theme to CSS variables");
|
|
199
|
+
assert.match(app, /const LOCAL_BACKGROUND_IMAGE_PATTERN = /, "frontend should restrict theme background images to local static URLs");
|
|
200
|
+
assert.match(app, /"--theme-background-image": themeExportCssValue\(theme, "backgroundImage", "none", LOCAL_BACKGROUND_IMAGE_PATTERN\)/, "frontend should apply theme export background images to CSS variables");
|
|
201
|
+
assert.match(app, /applyCustomBackgroundOverride\(\{ render: false \}\);/, "theme changes should preserve the user's custom background override");
|
|
202
|
+
assert.match(app, /function customBackgroundThemeKey\(themeName = currentThemeName\)/, "custom background persistence should be keyed by the active theme");
|
|
203
|
+
assert.match(app, /async function setCustomBackgroundFromFile\(file\)/, "frontend should load side-panel background image files");
|
|
204
|
+
assert.match(app, /persistCustomBackground\(background, themeName\)/, "side-panel background changes should save under the selected theme");
|
|
205
|
+
assert.match(app, /clearStoredCustomBackground\(themeName, \{ includeLegacy: true \}\)/, "side-panel X button should remove only the selected theme background while clearing legacy state");
|
|
206
|
+
assert.match(app, /await loadCustomBackgroundForTheme\(theme\.name/, "theme switching should load that theme's saved custom background");
|
|
207
|
+
assert.match(app, /URL\.createObjectURL\(file\)/, "custom backgrounds should apply through short blob URLs instead of huge CSS data URLs");
|
|
208
|
+
assert.match(app, /function dataUrlToBlob\(dataUrl\)/, "saved custom backgrounds should be restored from persisted data URLs into blob URLs");
|
|
209
|
+
assert.match(app, /async function clearCustomBackground\(\)/, "frontend should support removing the custom background");
|
|
210
|
+
assert.match(app, /backgroundClearButton\.addEventListener\("click"/, "side-panel X button should clear the custom background");
|
|
211
|
+
assert.match(app, /initializeCustomBackground\(\)\.catch/, "startup should restore the saved custom background when theme loading fails");
|
|
159
212
|
assert.match(app, /Restart Web UI to load themes/, "frontend should explain when a stale server cannot serve the themes endpoint");
|
|
160
213
|
assert.match(app, /themeSelect\.addEventListener\("change"/, "side-panel theme selector should switch themes immediately");
|
|
161
214
|
assert.match(app, /open \? "Close for network" : "Open to network"/, "network button should toggle from open to close action");
|
|
@@ -174,6 +227,10 @@ assert.match(app, /async function initializeTabs\(\)[\s\S]*?restoreActiveDraft\(
|
|
|
174
227
|
assert.match(app, /resizePromptInput\(\);\nfocusPromptInput\(\{ defer: true \}\);\nupdateComposerModeButtons\(\);/, "startup should request prompt focus before waiting for tab state refreshes");
|
|
175
228
|
assert.match(app, /elements\.promptInput\.addEventListener\("focus", \(\) => \{\n\s+syncMobileChatToBottomForInput\(\);/, "focusing mobile input should scroll output to bottom");
|
|
176
229
|
assert.match(app, /navigator\.serviceWorker\.register\("\/service-worker\.js"\)/, "PWA service worker should be registered by the app");
|
|
230
|
+
assert.match(app, /function serverStartCommandText\(\)[\s\S]*pi-webui --cwd/, "PWA/offline shell should build a pi-webui --cwd recovery command");
|
|
231
|
+
assert.match(app, /Pi Web UI server is offline/, "PWA/offline shell should clearly report backend-down state");
|
|
232
|
+
assert.match(app, /navigator\.clipboard\.writeText\(text\)/, "backend-offline recovery panel should copy the start command when possible");
|
|
233
|
+
assert.match(app, /retryServerConnectionButton.*retryServerConnection/s, "backend-offline recovery panel should wire a retry action");
|
|
177
234
|
assert.match(app, /function isChatNearBottom\(/, "chat should detect whether the user is reading above the bottom");
|
|
178
235
|
assert.match(app, /function scheduleChatFollowScroll\(/, "chat auto-follow should retry after layout settles during fast streaming");
|
|
179
236
|
assert.match(app, /function setChatScrollTopInstant\(top\)[\s\S]*?scrollBehavior = "auto"/, "chat auto-follow should bypass smooth scrolling while chasing fast output");
|
|
@@ -200,11 +257,18 @@ assert.match(app, /function parseTodoProgressWidget\(lines\)/, "todo-progress wi
|
|
|
200
257
|
assert.match(app, /Optional feature detection intentionally checks loaded Pi capabilities/, "optional Web UI features should be detected through loaded capabilities, not package folders");
|
|
201
258
|
assert.match(app, /function resetOptionalFeatureAvailability\(\)/, "optional feature state should reset across active-tab and reload boundaries");
|
|
202
259
|
assert.match(app, /function renderOptionalFeaturePanel\(\)/, "side panel should render optional feature installed/enabled state");
|
|
260
|
+
assert.match(app, /function setSidePanelSectionCollapsed\(record, collapsed/, "side panel sections should have explicit collapse/expand behavior");
|
|
261
|
+
assert.match(app, /restoreSidePanelSectionState\(\);\nbindSidePanelSectionToggles\(\);/, "side panel section state should restore before toggles are bound");
|
|
203
262
|
assert.match(app, /OPTIONAL_FEATURES_STORAGE_KEY/, "optional feature disable toggles should persist in browser storage");
|
|
263
|
+
assert.match(app, /function renderOptionalFeatureDependentDisplays\(\)[\s\S]*renderOptionalFeatureControls\(\);[\s\S]*renderThemeSelect\(\);[\s\S]*renderWidgets\(\);[\s\S]*renderStatus\(\);[\s\S]*renderCommands\(\);[\s\S]*renderAllMessages\(\{ preserveScroll: true \}\);[\s\S]*if \(streamRawText\) renderStreamingAssistantText\(\);/, "optional feature toggles should immediately refresh visible controls, commands, transcript, and live stream displays");
|
|
264
|
+
assert.match(app, /function setOptionalFeatureDisabled\(featureId, disabled\)[\s\S]*renderOptionalFeatureDependentDisplays\(\);[\s\S]*const tabContext = activeTabContext\(\);[\s\S]*refreshCommands\(tabContext\)/, "optional feature enable/disable should re-render the GUI and then refresh command capabilities");
|
|
265
|
+
assert.match(app, /function setOptionalControlState\(button, available, unavailableTitle\)[\s\S]*setAttribute\("aria-label", nextAriaLabel\)[\s\S]*setAttribute\("data-tooltip", nextTooltip\)/, "optional feature button disabled state should update accessible labels and visible tooltips");
|
|
266
|
+
assert.match(app, /function renderCommands\(\)/, "side-panel commands should be re-renderable from current optional feature state");
|
|
204
267
|
assert.match(app, /function installOptionalFeature\(featureId\)/, "optional features should expose an install action");
|
|
205
268
|
assert.match(app, /api\("\/api\/optional-feature-install"/, "optional feature install action should call the backend installer endpoint");
|
|
206
269
|
assert.match(app, /function updateOptionalFeatureAvailability\(\)[\s\S]*hasAvailableCommand\("git-staged-msg"\)[\s\S]*hasAvailableCommand\("release-npm"\)[\s\S]*hasAvailableCommand\("release-aur"\)[\s\S]*hasAvailableCommand\("todo-progress-status"\)/, "optional feature detection should call RPC-visible commands directly");
|
|
207
270
|
assert.match(app, /if \(!isOptionalFeatureEnabled\("todoProgressWidget"\)\) return String\(text \|\| ""\)/, "todo progress line stripping should only run when the todo feature is detected and enabled");
|
|
271
|
+
assert.match(app, /const releasePrompt = detectedReleasePrompt && isOptionalFeatureEnabled\(detectedReleasePrompt\.featureId\) \? detectedReleasePrompt : null/, "release confirmation dialogs should use specialized rendering only when their release optional feature is enabled");
|
|
208
272
|
assert.match(app, /case "webui_tab_reloaded":[\s\S]*resetOptionalFeatureAvailability\(\)/, "optional feature state should reset when the RPC tab reloads resources");
|
|
209
273
|
assert.match(app, /function runPublishWorkflow\(command\)[\s\S]*hasAvailableCommand\(commandName\)/, "publish workflow launch should guard on loaded slash commands");
|
|
210
274
|
assert.match(app, /if \(!isOptionalFeatureEnabled\("gitWorkflow"\)\)/, "guided git workflow should guard on enabled /git-staged-msg feature");
|
|
@@ -217,18 +281,29 @@ assert.match(app, /let transientMessages = \[\]/, "frontend should keep transien
|
|
|
217
281
|
assert.match(app, /function orderedTranscriptItems\(\)/, "frontend should merge persisted and transient messages chronologically");
|
|
218
282
|
assert.match(app, /items\.sort\(\(a, b\) => a\.timestampMs - b\.timestampMs \|\| a\.order - b\.order\)/, "transient extension output should not pin itself below newer persisted messages");
|
|
219
283
|
assert.match(app, /const ACTION_FEEDBACK_REACTIONS = \{/, "frontend should define direct feedback reactions");
|
|
220
|
-
assert.match(app, /message\?\.role === "assistant" \|\| message\?\.role === "toolResult" \|\| message\?\.role === "bashExecution"/, "frontend should allow reactions on final assistant output as well as actions");
|
|
284
|
+
assert.match(app, /message\?\.role === "assistant" \|\| message\?\.role === "toolExecution" \|\| message\?\.role === "toolResult" \|\| message\?\.role === "bashExecution"/, "frontend should allow reactions on final assistant output as well as actions");
|
|
221
285
|
assert.match(app, /function renderActionFeedbackControls\(/, "frontend should render per-message reaction controls");
|
|
222
286
|
assert.match(app, /function toolResultPreviewText\(message, lineLimit = 10\)/, "tool results should derive a ten-line collapsed preview");
|
|
287
|
+
assert.match(app, /function renderToolExecution\(parent, message\)[\s\S]*?WEBUI_TOOL_RENDERERS/, "paired tool cards should use the browser-side built-in tool renderer registry");
|
|
288
|
+
assert.match(app, /appendToolRawDetails\(parent, tool\)/, "paired tool cards should keep a safe raw-data expander for debugging renderer mismatches");
|
|
289
|
+
assert.match(app, /function toolStateMeta\(tool\)/, "tool cards should expose consistent status and elapsed metadata across built-in renderers");
|
|
290
|
+
assert.match(app, /const TOOL_LIVE_UPDATE_THROTTLE_MS = 80/, "live tool cards should coalesce rapid partial updates before re-rendering");
|
|
291
|
+
assert.match(app, /function updateLiveToolCard\(bubble, message\)[\s\S]*?body\.replaceChildren\(\);[\s\S]*?renderToolExecution\(body, message\);/, "live tool card updates should re-render the existing card body in place");
|
|
292
|
+
assert.match(app, /function scheduleLiveToolRunRender\(run[\s\S]*?liveToolRenderQueue\.set[\s\S]*?TOOL_LIVE_UPDATE_THROTTLE_MS/, "live tool update events should be queued and throttled for smoother browser output");
|
|
293
|
+
assert.match(app, /function handleToolExecutionUpdate\(event\)[\s\S]*?event\.partialResult[\s\S]*?scheduleLiveToolRunRender\(run, \{ scroll: false \}\)/, "live tool_execution_update events should update transcript-visible tool cards without replacing them per event");
|
|
223
294
|
assert.match(app, /appendText\(preview, toolResultPreviewText\(message, 10\), "code-block tool-result-preview-text"\)/, "collapsed tool results should render the first ten preview lines by default");
|
|
224
295
|
assert.match(app, /function assistantDisplayMessages\(message\)/, "assistant history should split thinking and tool-call parts out of the final Assistant output card");
|
|
225
296
|
assert.match(app, /function assistantHasToolCallAfter\(content, index\)/, "assistant text that precedes a tool call should be detectable and suppressible");
|
|
226
297
|
assert.match(app, /if \(!assistantHasToolCallAfter\(content, index\)\) finalParts\.push\(finalPart\);/, "assistant history should not render pre-tool-call assistant text as final output");
|
|
227
|
-
assert.match(app, /return content\.trim\(\) \? \[\{ \.\.\.message, title: "
|
|
228
|
-
assert.match(app, /part\.type === "text"\)
|
|
229
|
-
assert.match(app, /
|
|
230
|
-
assert.match(app, /function
|
|
231
|
-
assert.match(app,
|
|
298
|
+
assert.match(app, /return content\.trim\(\) \? \[\{ \.\.\.message, title: "final output" \}\] : \[\]/, "assistant messages with stripped empty text should not render empty final-output cards");
|
|
299
|
+
assert.match(app, /function isEmptyAssistantTextPart\(part\)[\s\S]*?part\.type === "text"[\s\S]*?!assistantTextPartText\(part\)\.trim\(\)/, "empty assistant text parts should be recognized as skippable provider metadata");
|
|
300
|
+
assert.match(app, /if \(isEmptyAssistantTextPart\(part\)\) continue;/, "empty assistant text parts should not render as assistant-event cards");
|
|
301
|
+
assert.match(app, /function assistantFinalOutputPart\(part\)[\s\S]*?if \(part\.type === "text"\) \{[\s\S]*?const text = assistantTextPartText\(part\);[\s\S]*?return text\.trim\(\) \? \{ \.\.\.part, type: "text", text \} : null;/, "assistant text parts should normalize supported text payload shapes");
|
|
302
|
+
assert.match(app, /\["assistant", "toolExecution"\]\.includes\(transcriptMessage\.role\) \? messageIndex : -1/, "final Assistant output and paired tool action cards should keep the source message index for feedback");
|
|
303
|
+
assert.match(app, /function ensureStreamingThinkingBubble\(\)[\s\S]*if \(!thinkingOutputVisible\) return false/, "live thinking should respect the show/hide thinking-output toggle");
|
|
304
|
+
assert.match(app, /if \(thinkingOutputVisible && \(thinkingText \|\| !streaming\)\) appendText\(body, thinkingText \|\| "No thinking content was exposed by the provider\.", "thinking-text"\);/, "empty live thinking cards should not render the no-thinking fallback before deltas arrive");
|
|
305
|
+
assert.match(app, /const THINKING_VISIBILITY_STORAGE_KEY = "pi-webui-thinking-visible"/, "thinking visibility should persist in browser storage");
|
|
306
|
+
assert.match(app, /function setThinkingOutputVisible\(visible[\s\S]*renderAllMessages\(\{ preserveScroll: true \}\)/, "thinking visibility changes should immediately re-render the transcript");
|
|
232
307
|
assert.match(app, /function assistantStreamingMessage\(event\)/, "live streaming should read the authoritative partial assistant message from RPC events like the TUI");
|
|
233
308
|
assert.match(app, /assistantThinkingTextFromMessage\(assistantStreamingMessage\(event\)\) \|\| thinkingDeltaText\(update\)/, "live thinking end should replace deltas with the final partial-message thinking content");
|
|
234
309
|
assert.match(app, /if \(typeof partialText === "string"\) streamRawText = partialText;/, "live assistant text should synchronize from partial messages instead of relying only on deltas");
|
|
@@ -238,12 +313,16 @@ assert.match(app, /function renderStreamingAssistantText\(\)[\s\S]*?const assist
|
|
|
238
313
|
assert.match(app, /const STREAM_OUTPUT_HIDE_DELAY_MS = 300/, "stream output hiding should be debounced to prevent rapid flicker");
|
|
239
314
|
assert.match(app, /const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220/, "live assistant text should be briefly guarded so pre-tool-call text can be suppressed");
|
|
240
315
|
assert.match(app, /function scheduleStreamBubbleHide\([\s\S]*?STREAM_OUTPUT_MIN_VISIBLE_MS/, "stream output cards should observe a minimum visible duration before hiding");
|
|
241
|
-
assert.match(app, /if \(assistantText\) \{[\s\S]*?streamText
|
|
316
|
+
assert.match(app, /if \(assistantText\) \{[\s\S]*?renderMarkdown\(streamText, assistantText\);[\s\S]*?\} else \{\n\s+scheduleStreamBubbleHide\(\);/, "empty filtered stream output should schedule hide while visible stream output renders as Markdown");
|
|
242
317
|
assert.match(app, /if \(streamToolCallSeen \|\| streamBubble\) renderStreamingAssistantText\(\);\n\s+else scheduleStreamingAssistantTextRender\(\);/, "live assistant text should wait briefly before showing unless it is already visible or follows a tool call");
|
|
243
318
|
assert.match(app, /streamToolCallSeen = true;\n\s+suppressStreamingAssistantTextBeforeToolCall\(\);/, "tool-call starts should remove pending assistant text from the live transcript");
|
|
244
|
-
assert.match(app, /const created = appendMessage\(\{ role: "assistant", title: "
|
|
319
|
+
assert.match(app, /const created = appendMessage\(\{ role: "assistant", title: "final output"/, "live Assistant cards should be created only for final output text without a noisy Assistant label");
|
|
320
|
+
assert.match(app, /function renderMarkdownInto\(parent, text\)/, "assistant output should have a browser-native Markdown renderer");
|
|
321
|
+
assert.match(app, /safeMarkdownLinkHref\(url\)/, "Markdown links should be sanitized before rendering");
|
|
322
|
+
assert.match(app, /renderContent\(body, message\.content, \{ markdown: message\.role === "assistant" \}\)/, "final assistant output should render through the Markdown path");
|
|
323
|
+
assert.match(app, /const hideMessageHeader = message\.role === "assistant" && !isCollapsibleOutput/, "assistant final-output cards should hide the redundant role header");
|
|
245
324
|
assert.match(app, /api\("\/api\/action-feedback", \{ method: "POST"/, "queued action feedback should post to the server after the run is idle");
|
|
246
|
-
assert.match(app, /function postQueuedFeedback\(tabId, items\)/, "queued feedback should have a backward-compatible submit path");
|
|
325
|
+
assert.match(app, /function postQueuedFeedback\(tabId, items, tabContext = activeTabContext\(tabId\)\)/, "queued feedback should have a backward-compatible submit path");
|
|
247
326
|
assert.match(app, /\/api\/action-feedback not found; falling back to a normal prompt/, "new frontend should gracefully handle older running Web UI servers");
|
|
248
327
|
assert.match(app, /actionFeedbackSteerMessage\(item\)/, "live action feedback should be sent as steering while the agent is running");
|
|
249
328
|
assert.match(app, /function addTransientMessage\(\{ role = "notice"/, "frontend should render transient command output into the transcript");
|
|
@@ -254,18 +333,25 @@ assert.doesNotMatch(app, /"agent running"/, "active agent indicator should not r
|
|
|
254
333
|
assert.doesNotMatch(app, /runIndicatorTimestamp/, "active agent indicator should not render a separate live timestamp header");
|
|
255
334
|
assert.match(app, /runIndicatorBubble = make\("article", "message runIndicator run-indicator-message streaming"\)/, "active agent indicator should use a dedicated streaming transcript card");
|
|
256
335
|
assert.match(app, /runIndicatorShowsElapsed\(\) \? `\$\{detail\} · run time/, "active agent indicator should label elapsed run time instead of showing a bare counter");
|
|
257
|
-
assert.match(app, /Abort requested
|
|
336
|
+
assert.match(app, /Abort requested/, "abort feedback should clarify that Web UI is checking stop status");
|
|
337
|
+
assert.match(app, /const ABORT_LONG_PRESS_MS = 700/, "Abort long-press timing should be explicit");
|
|
338
|
+
assert.match(app, /async function abortActiveRun\(\{ source = "button" \} = \{\}\)/, "Abort should be centralized for button, Esc, and long-press triggers");
|
|
339
|
+
assert.match(app, /elements\.abortButton\.addEventListener\("pointerdown", startAbortLongPress\)/, "Abort should support pointer long-press");
|
|
340
|
+
assert.match(app, /abortActiveRun\(\{ source: "escape" \}\)/, "Escape should trigger the abort action when available");
|
|
258
341
|
assert.match(app, /function addAbortTranscriptNotice\(/, "abort button should render a transcript-visible aborted notice");
|
|
259
342
|
assert.match(app, /this transcript marks the run as aborted/, "abort notice should clearly mark the agent output as aborted");
|
|
260
343
|
assert.match(app, /await api\("\/api\/abort"[\s\S]*?addAbortTranscriptNotice\(\{ activeRun: hadActiveRun \}\)/, "abort button should add the aborted transcript notice after the abort request succeeds");
|
|
261
344
|
assert.match(app, /let runIndicatorGraceCheckTimer = null/, "local-only run indicators should have a grace-check timer");
|
|
262
345
|
assert.match(app, /const RUN_INDICATOR_STATE_RECHECK_MS = 5000/, "active run indicators should periodically re-check state");
|
|
263
346
|
assert.match(app, /function scheduleRunIndicatorGraceCheck\(/, "local-only run indicators should schedule a follow-up state check");
|
|
264
|
-
assert.match(app, /function maybeRefreshRunIndicatorState\([\s\S]*?refreshState\(\)/, "active run indicators should poll state so missed completion events do not leave stale cards");
|
|
347
|
+
assert.match(app, /function maybeRefreshRunIndicatorState\([\s\S]*?refreshState\(tabContext\)/, "active run indicators should poll state so missed completion events do not leave stale cards");
|
|
265
348
|
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");
|
|
266
|
-
assert.match(app, /scheduleRunIndicatorGraceCheck\(\)[\s\S]*?refreshState\(\)/, "stale local-only run indicators should re-check state after the start grace period");
|
|
349
|
+
assert.match(app, /scheduleRunIndicatorGraceCheck\(tabContext = activeTabContext\(\)\)[\s\S]*?refreshState\(tabContext\)/, "stale local-only run indicators should re-check state after the start grace period");
|
|
267
350
|
assert.match(app, /function scheduleAbortStateChecks\(/, "abort handling should poll state so the active indicator can clear after stop confirmation");
|
|
268
|
-
assert.match(app, /case "tool_execution_start":[\s\S]*?setRunIndicatorActivity\(`Running tool:/, "tool execution should update the active agent
|
|
351
|
+
assert.match(app, /case "tool_execution_start":[\s\S]*?suppressStreamingAssistantTextBeforeToolCall\(\)[\s\S]*?handleToolExecutionStart\(event\)[\s\S]*?setRunIndicatorActivity\(`Running tool:/, "tool execution should suppress pre-tool assistant text, then update the active agent indicator and live tool card");
|
|
352
|
+
assert.match(app, /case "tool_execution_update":[\s\S]*?handleToolExecutionUpdate\(event\)/, "tool execution updates should be handled as transcript output");
|
|
353
|
+
assert.match(app, /case "auto_retry_start":[\s\S]*?addTransientMessage\(\{ role: "warn", title: "auto retry"/, "auto-retry starts should be transcript-visible warnings");
|
|
354
|
+
assert.match(app, /case "extension_error":[\s\S]*?addTransientMessage\(\{ role: "error", title: "extension error"/, "extension errors should be transcript-visible error cards");
|
|
269
355
|
assert.match(app, /setRunIndicatorActivity\("Requesting context compaction…"\);\n\s+scrollChatToBottom\(\{ force: true \}\);/, "manual compaction should force-follow the transcript to the bottom status card");
|
|
270
356
|
assert.match(app, /case "agent_end":[\s\S]*?clearRunIndicatorActivity\(\)/, "agent completion should remove the active agent transcript indicator");
|
|
271
357
|
assert.match(app, /case "agent_end":[\s\S]*?notifyAgentDone\(event\.tabId \|\| activeTabId/, "agent completion should trigger optional done notifications");
|
|
@@ -283,8 +369,11 @@ assert.doesNotMatch(app, /addEventListener\("mouseenter", \(\) => setActiveComma
|
|
|
283
369
|
assert.match(app, /function resizePromptInput\(\)/, "prompt textarea should auto-resize from a one-line default");
|
|
284
370
|
assert.match(app, /elements\.promptInput\.addEventListener\("input", \(\) => \{\n\s+resizePromptInput\(\);/, "prompt textarea should resize whenever the user edits it");
|
|
285
371
|
assert.match(app, /function updateComposerModeButtons\(\)/, "composer should relocate Steer and Follow-up based on run state");
|
|
286
|
-
assert.match(app, /const target = runActive \? elements\.composerRow : elements\.composerActionsPanel/, "Steer and Follow-up should
|
|
287
|
-
assert.match(app, /
|
|
372
|
+
assert.match(app, /const target = runActive \? elements\.composerRow : elements\.composerActionsPanel/, "Steer and Follow-up should move into the bottom row only while an agent run is active");
|
|
373
|
+
assert.match(app, /const before = runActive \? elements\.abortButton : null/, "active Steer and Follow-up controls should sit before Abort and Send");
|
|
374
|
+
assert.match(app, /button\.hidden = !runActive;\n\s+button\.disabled = !runActive;/, "Steer and Follow-up should be hidden and disabled when the agent is not running");
|
|
375
|
+
assert.match(app, /elements\.abortButton\.hidden = !abortAvailable;\n\s+elements\.abortButton\.disabled = !abortAvailable \|\| abortRequestInFlight;/, "Abort should only be exposed in the bottom bar while a run can be aborted");
|
|
376
|
+
assert.match(app, /document\.body\.classList\.toggle\("pi-run-active", runActive \|\| abortAvailable\)/, "run-active or abort-available state should be reflected in CSS for mobile composer layout");
|
|
288
377
|
assert.match(app, /function showComposerButtonTooltip\(button\)/, "empty mode-button taps should show the usage tooltip");
|
|
289
378
|
assert.match(app, /sendPromptFromModeButton\("steer", elements\.steerButton\)/, "Steer should show tooltip instead of silently doing nothing when input is empty");
|
|
290
379
|
assert.match(app, /sendPromptFromModeButton\("follow-up", elements\.followUpButton\)/, "Follow-up should show tooltip instead of silently doing nothing when input is empty");
|
|
@@ -295,8 +384,18 @@ assert.match(app, /releaseNpmButton\.addEventListener\("click", \(\) => runPubli
|
|
|
295
384
|
assert.match(app, /releaseAurButton\.addEventListener\("click", \(\) => runPublishWorkflow\("\/release-aur"\)\)/, "Publish menu should launch /release-aur");
|
|
296
385
|
assert.match(app, /async function sendPrompt\(kind = "prompt", explicitMessage\)/, "prompt sending should accept direct messages that bypass the input field");
|
|
297
386
|
assert.match(app, /const rawMessage = usesPromptInput \? elements\.promptInput\.value : explicitMessage/, "direct prompt sends should not read the input textarea");
|
|
298
|
-
assert.match(app, /if \(usesPromptInput\) \{\
|
|
387
|
+
assert.match(app, /if \(usesPromptInput\) \{[\s\S]*?if \(targetStillActive\) \{[\s\S]*?elements\.promptInput\.value = "";/, "direct prompt sends should preserve the input textarea draft");
|
|
299
388
|
assert.match(app, /make\("button", "command-item"\)[\s\S]*?sendPrompt\("prompt", `\/\$\{command\.name\}`\)/, "side-panel command clicks should send the slash command directly");
|
|
389
|
+
assert.match(app, /const NATIVE_SELECTOR_COMMANDS = new Set\(\["model", "settings", "theme", "fork", "clone", "resume", "tree", "login", "logout", "scoped-models"\]\)/, "frontend should route native slash commands into selector UIs");
|
|
390
|
+
assert.match(app, /async function handleNativeSlashSelectorCommand\(message/, "frontend should intercept exact native slash commands before prompt forwarding");
|
|
391
|
+
assert.match(app, /kind === "prompt" && attachments\.length === 0 && await handleNativeSlashSelectorCommand/, "prompt sending should open native selector dialogs before marking a run active");
|
|
392
|
+
assert.match(app, /function openNativeModelSelector\(\)[\s\S]*?nativeCommandApi\("\/api\/models"\)/, "native /model selector should load models through the active tab API");
|
|
393
|
+
assert.match(app, /function openNativeSettingsDialog\(\)[\s\S]*?\/api\/steering-mode[\s\S]*?\/api\/follow-up-mode[\s\S]*?\/api\/auto-compaction/, "native /settings selector should expose queue and compaction controls");
|
|
394
|
+
assert.match(app, /function openNativeForkSelector\(\)[\s\S]*?\/api\/fork-messages[\s\S]*?\/api\/fork/, "native /fork selector should pair fork-point loading with the fork action");
|
|
395
|
+
assert.match(app, /function openNativeResumeSelector\(scope = "current"\)[\s\S]*?\/api\/sessions\?scope=\$\{encodeURIComponent\(selectedScope\)\}/, "native /resume selector should list current-cwd or all sessions");
|
|
396
|
+
assert.match(app, /function openNativeTreeSelector\(\)[\s\S]*?\/api\/session-tree[\s\S]*?\/api\/tree-navigate/, "native /tree selector should list tree entries and navigate through the backend helper");
|
|
397
|
+
assert.match(app, /Provider credential entry is intentionally not implemented in the browser yet/, "native /login should remain a safe non-secret guidance dialog");
|
|
398
|
+
assert.match(app, /const HIDDEN_COMMAND_NAMES = new Set\(\["webui-tree-navigate"\]\)/, "internal Web UI helper commands should stay out of command pickers");
|
|
300
399
|
assert.match(app, /function shouldSendPromptFromEnter\(event\)/, "prompt keyboard handling should be centralized");
|
|
301
400
|
assert.match(app, /return !isMobileView\(\);/, "plain Enter should send only outside mobile view so mobile Return can insert newlines");
|
|
302
401
|
assert.match(app, /mobile-keyboard-open/, "JS should toggle mobile keyboard mode from viewport/focus state");
|
|
@@ -323,6 +422,10 @@ assert.match(app, /WARNING: \$\{activeAgentTabs\.length\}[\s\S]*?still running o
|
|
|
323
422
|
assert.match(app, /elements\.closeAllTabsButton\.addEventListener\("click", \(\) => closeAllTerminalTabs\(\)\)/, "close-all tabs button should be wired in JS");
|
|
324
423
|
assert.match(app, /const groups = tabCwdGroups\(\);[\s\S]*?for \(const group of groups\) \{\n\s+if \(shouldRenderTerminalTabGroup\(group, groups\.length\)\)[\s\S]*?renderTerminalTabGroup\(group, groups\.length\)[\s\S]*?for \(const tab of group\.tabs\) elements\.tabBar\.append\(renderTerminalTab\(tab\)\);/, "terminal tabs should render groups with group count and ungrouped tabs when grouping is skipped");
|
|
325
424
|
assert.match(app, /let tabSeenCompletionSerials = new Map\(\)/, "frontend should track which tab completions have been seen");
|
|
425
|
+
assert.match(app, /let activeTabGeneration = 0/, "frontend should version active-tab UI state to reject stale async work");
|
|
426
|
+
assert.match(app, /function isCurrentTabContext\(context\)/, "frontend should identify stale active-tab refresh contexts");
|
|
427
|
+
assert.match(app, /function connectEvents\(tabContext = activeTabContext\(\)\)[\s\S]*?eventSource !== source/, "frontend should ignore stale SSE messages from old active tabs");
|
|
428
|
+
assert.match(app, /async function refreshMessages\(tabContext = activeTabContext\(\)\)[\s\S]*?if \(!isCurrentTabContext\(tabContext\)\) return;/, "message refreshes should not render after the user switches tabs");
|
|
326
429
|
assert.match(app, /function tabIndicator\(tab\)/, "frontend should derive idle, working, blocked, and work-done tab indicator states");
|
|
327
430
|
assert.match(app, /pendingBlockerCount > 0[\s\S]*?state: "blocked"/, "frontend should show blocked tabs when extension UI blockers are pending");
|
|
328
431
|
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");
|
|
@@ -351,7 +454,7 @@ assert.match(app, /mobileFooterExpanded = false;[\s\S]*?document\.body\.classLis
|
|
|
351
454
|
assert.match(app, /footerMeta\("context", contextLabel, "footer-context"\)/, "footer should render context as a primary mobile meta item");
|
|
352
455
|
assert.match(app, /footerMeta\("model", modelLine, "footer-model", \{/, "footer model item should be clickable");
|
|
353
456
|
assert.match(app, /function renderFooterModelPicker\(\)/, "footer should render a scoped-model picker dropdown");
|
|
354
|
-
assert.match(app, /api\("\/api\/scoped-models"\)/, "footer model picker should load scoped models instead of all available models");
|
|
457
|
+
assert.match(app, /api\("\/api\/scoped-models", \{ tabId: tabContext\.tabId \}\)/, "footer model picker should load scoped models instead of all available models");
|
|
355
458
|
assert.match(app, /for \(const model of footerScopedModels\)/, "footer model picker should render only scoped models");
|
|
356
459
|
assert.match(app, /api\("\/api\/model", \{ method: "POST"/, "footer model picker should apply selected model through the model API");
|
|
357
460
|
assert.match(app, /footer-details-toggle/, "footer details toggle should be rendered by JS");
|
|
@@ -361,7 +464,7 @@ assert.match(app, /case "webui_tab_reloaded":/, "frontend should handle native /
|
|
|
361
464
|
assert.match(app, /addTransientMessage\(\{ role: "native", title: "\/reload"/, "native /reload should produce visible transcript output");
|
|
362
465
|
assert.match(app, /navigator\.clipboard\.writeText\(response\.data\.copyText\)/, "native /copy should use the browser clipboard when available");
|
|
363
466
|
assert.match(app, /Clipboard access failed:[\s\S]*?response\.data\.copyText/, "native /copy should show text in transcript when clipboard access fails");
|
|
364
|
-
assert.match(app, /setTimeout\(\(\) => refreshAll\(\)\.catch/, "frontend should refresh state after native /reload restarts the RPC process");
|
|
467
|
+
assert.match(app, /setTimeout\(\(\) => \{[\s\S]*?refreshAll\(tabContext\)\.catch/, "frontend should refresh state after native /reload restarts the RPC process");
|
|
365
468
|
assert.match(app, /api\("\/api\/path-fast-picks"/, "frontend should load/save fast picks through the server API");
|
|
366
469
|
assert.match(app, /loadLegacyFastPicks\(/, "frontend should migrate existing browser-local fast picks");
|
|
367
470
|
|
|
@@ -370,14 +473,18 @@ assert.equal(manifest.start_url, "/", "PWA manifest should start at the web UI r
|
|
|
370
473
|
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");
|
|
371
474
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-192.png" && icon.sizes === "192x192"), "PWA manifest should include a 192px icon");
|
|
372
475
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-512.png" && icon.sizes === "512x512"), "PWA manifest should include a 512px icon");
|
|
373
|
-
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-
|
|
476
|
+
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v16"/, "PWA service worker should define an app-shell cache");
|
|
374
477
|
assert.match(serviceWorker, /self\.addEventListener\("notificationclick"/, "PWA service worker should focus Web UI when blocked-tab notifications are clicked");
|
|
375
478
|
assert.match(serviceWorker, /event\.notification\.data\?\.url/, "blocked-tab notifications should carry a URL for service-worker click handling");
|
|
376
479
|
assert.match(serviceWorker, /"\/apple-touch-icon\.png"/, "PWA service worker should cache the apple touch icon");
|
|
480
|
+
assert.match(serviceWorker, /"\/matrix-background\.webp"/, "PWA service worker should cache the Matrix background image");
|
|
481
|
+
assert.match(serviceWorker, /"\/catppuccin-mocha-background\.png"/, "PWA service worker should cache the Catppuccin Mocha background image");
|
|
377
482
|
assert.match(serviceWorker, /url\.pathname\.startsWith\("\/api\/"\)/, "PWA service worker should not cache live API or SSE calls");
|
|
378
483
|
assert.ok(appleIcon.length > 1000, "PWA apple touch icon should be present");
|
|
379
484
|
assert.ok(icon192.length > 1000, "PWA 192px icon should be present");
|
|
380
485
|
assert.ok(icon512.length > icon192.length, "PWA 512px icon should be present and larger than 192px icon");
|
|
486
|
+
assert.ok(matrixBackground.length > 100000, "Matrix background image should be present as an optimized WebP asset");
|
|
487
|
+
assert.ok(mochaBackground.length > 8000, "Catppuccin Mocha background image should be present as a compact PNG asset");
|
|
381
488
|
|
|
382
489
|
assert.match(server, /const NATIVE_SLASH_COMMANDS = \[/, "server should define Pi native slash commands for autocomplete");
|
|
383
490
|
assert.match(server, /\{ name: "reload", description: "Reload keybindings, extensions, skills, prompts, and themes" \}/, "native /reload should be advertised for autocomplete");
|
|
@@ -408,21 +515,28 @@ assert.match(server, /const restoreTabs = readRestoreTabsFromEnv\(\)/, "server s
|
|
|
408
515
|
assert.match(server, /delete process\.env\.PI_WEBUI_RESTORE_TABS/, "server should avoid leaking restore descriptors into spawned Pi RPC processes");
|
|
409
516
|
assert.match(server, /if \(sessionFile && !options\.noSession\) piArgs\.push\("--session", sessionFile\)/, "restored tabs should resume previous session files");
|
|
410
517
|
assert.doesNotMatch(server, /args\.push\("--name"/, "Web UI tab titles should not be forwarded as Pi CLI --name flags because older bundled Pi CLIs reject them");
|
|
411
|
-
assert.match(server, /const closedRestorableTabs = \[\]/, "server should
|
|
412
|
-
assert.match(server, /async function closeTab\(id\)[\s\S]*?rememberClosedRestorableTab\(tab, restorableState\)/, "closing a tab should capture its
|
|
518
|
+
assert.match(server, /const closedRestorableTabs = \[\]/, "server should track recently closed tabs separately from restart restore descriptors");
|
|
519
|
+
assert.match(server, /async function closeTab\(id\)[\s\S]*?rememberClosedRestorableTab\(tab, restorableState\)/, "closing a tab should capture its session before stopping RPC for detailed closed-tab status");
|
|
413
520
|
assert.match(server, /async function closeTabs\(ids\)[\s\S]*?if \(targetTabs\.length >= tabs\.size\) \{\n\s+await createTab/, "bulk tab close should create a replacement before closing every current tab");
|
|
414
521
|
assert.match(server, /url\.pathname === "\/api\/tabs\/close" && req\.method === "POST"[\s\S]*?closedIds: closed\.map\(\(tab\) => tab\.id\)/, "server should expose a bulk close-tabs endpoint");
|
|
415
522
|
assert.match(server, /function rememberTabState\(tab, state\)/, "server should cache last-known tab state for restart-safe session restoration");
|
|
416
523
|
assert.match(server, /sessionFile: tabRestorableSessionFile\(tab\)/, "tab metadata should expose cached session files for health/status restore descriptors");
|
|
417
|
-
assert.match(server, /restorableTabs: mergeRestorableTabDescriptors\(statusTabs
|
|
418
|
-
assert.match(server, /data\.restorableTabs = mergeRestorableTabDescriptors\(detailedTabs
|
|
524
|
+
assert.match(server, /restorableTabs: mergeRestorableTabDescriptors\(statusTabs\)/, "status should expose only currently open tabs as restart restore descriptors");
|
|
525
|
+
assert.match(server, /data\.restorableTabs = mergeRestorableTabDescriptors\(detailedTabs\)/, "detailed status should keep restart restore descriptors limited to open tabs");
|
|
526
|
+
assert.match(server, /data\.closedTabs = closedRestorableTabs\.slice\(\)/, "detailed status should expose recently closed tabs separately");
|
|
419
527
|
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");
|
|
420
528
|
assert.match(server, /const initialTabs = await createInitialTabs\(\)/, "server should recreate restored tabs before listening");
|
|
421
529
|
assert.match(extension, /api\/webui-status\?detailed=1&events=0/, "launcher should capture detailed tab status before restarting an existing Web UI");
|
|
422
|
-
assert.match(extension, /function mergeRestorableTabsFromStatusSources\(sources: unknown\[\], options: StartWebuiOptions\)/, "launcher should merge
|
|
423
|
-
assert.match(extension, /
|
|
530
|
+
assert.match(extension, /function mergeRestorableTabsFromStatusSources\(sources: unknown\[\], options: StartWebuiOptions\)/, "launcher should merge available restore sources instead of trusting only the first one");
|
|
531
|
+
assert.match(extension, /const openTabSources: unknown\[\] = \[\]/, "launcher should collect explicit open-tab sources for restart restore");
|
|
532
|
+
assert.match(extension, /const detailedTabs = statusData\?\.tabs;\n\s+if \(Array\.isArray\(detailedTabs\)\) openTabSources\.push\(detailedTabs\)/, "launcher should prefer detailed open tabs over restorableTabs that may include closed tabs");
|
|
533
|
+
assert.match(extension, /if \(openTabSources\.length > 0\) return mergeRestorableTabsFromStatusSources\(openTabSources, options\)/, "launcher should restore only open tabs when open tab lists are available");
|
|
534
|
+
assert.match(extension, /return mergeRestorableTabsFromStatusSources\(\[statusData\?\.restorableTabs, existing\.restorableTabs\], options\)/, "launcher should use restorableTabs only as a legacy fallback");
|
|
424
535
|
assert.match(extension, /env\.PI_WEBUI_RESTORE_TABS = JSON\.stringify\(restoreTabs\)/, "launcher should pass restorable tabs to the new detached server");
|
|
425
|
-
assert.match(extension, /pi\.registerCommand\("start
|
|
536
|
+
assert.match(extension, /pi\.registerCommand\("webui-start"/, "extension should expose the canonical /webui-start command");
|
|
537
|
+
assert.match(extension, /pi\.registerCommand\("webui-tree-navigate"/, "extension should expose the internal Web UI tree navigation command");
|
|
538
|
+
assert.match(extension, /ctx\.navigateTree\(payload\.entryId/, "internal Web UI tree command should call the native session tree navigation API");
|
|
539
|
+
assert.doesNotMatch(extension, /pi\.registerCommand\("start-webui"/, "extension should not expose the older /start-webui alias");
|
|
426
540
|
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");
|
|
427
541
|
assert.match(server, /case "reload": \{[\s\S]*?restartTabRpc\(tab, "slash-command"\)/, "native /reload should restart the active RPC tab");
|
|
428
542
|
assert.match(server, /message: "Reloaded keybindings, extensions, skills, prompts, and themes\."/, "native /reload should return visible command output");
|
|
@@ -447,6 +561,16 @@ assert.match(server, /url\.pathname === "\/api\/path-suggestions" && req\.method
|
|
|
447
561
|
assert.match(server, /url\.pathname === "\/api\/path-fast-picks" && req\.method === "GET"/, "server should expose GET /api/path-fast-picks");
|
|
448
562
|
assert.match(server, /url\.pathname === "\/api\/path-fast-picks" && req\.method === "POST"/, "server should expose POST /api/path-fast-picks");
|
|
449
563
|
assert.match(server, /url\.pathname === "\/api\/scoped-models" && req\.method === "GET"/, "server should expose GET /api/scoped-models");
|
|
564
|
+
assert.match(server, /url\.pathname === "\/api\/fork-messages" && req\.method === "GET"/, "server should expose fork-point data for the native /fork selector");
|
|
565
|
+
assert.match(server, /url\.pathname === "\/api\/sessions" && req\.method === "GET"/, "server should expose session lists for the native /resume selector");
|
|
566
|
+
assert.match(server, /url\.pathname === "\/api\/session-tree" && req\.method === "GET"/, "server should expose session-tree data for the native /tree selector");
|
|
567
|
+
assert.match(server, /url\.pathname === "\/api\/switch-session" && req\.method === "POST"/, "server should expose session switching for the native /resume selector");
|
|
568
|
+
assert.match(server, /url\.pathname === "\/api\/tree-navigate" && req\.method === "POST"/, "server should expose tree navigation through the Web UI helper command");
|
|
569
|
+
assert.match(server, /function configuredSessionDir\(\)/, "server should honor forwarded --session-dir for session selectors");
|
|
570
|
+
assert.match(server, /SessionManager\.listAll\(sessionDir\) : await SessionManager\.list\(tab\.cwd, sessionDir\)/, "server should support current-cwd and all-session resume scopes");
|
|
571
|
+
assert.match(server, /type: "set_steering_mode"/, "server should expose steering queue-mode changes for native /settings");
|
|
572
|
+
assert.match(server, /type: "set_follow_up_mode"/, "server should expose follow-up queue-mode changes for native /settings");
|
|
573
|
+
assert.match(server, /type: "set_auto_compaction"/, "server should expose auto-compaction changes for native /settings");
|
|
450
574
|
assert.match(server, /@firstpick\/pi-themes-bundle/, "server should discover themes from the optional theme package");
|
|
451
575
|
assert.match(server, /const OPTIONAL_FEATURE_PACKAGES = new Map/, "server should whitelist optional feature packages for install actions");
|
|
452
576
|
assert.match(server, /function installOptionalFeaturePackage\(featureId\)/, "server should provide optional feature package installation helper");
|
|
@@ -455,9 +579,10 @@ assert.match(server, /Installing optional Web UI features is only allowed from l
|
|
|
455
579
|
assert.match(server, /url\.pathname === "\/api\/themes" && req\.method === "GET"/, "server should expose GET /api/themes");
|
|
456
580
|
assert.match(server, /readBundledThemes\(\)/, "server should read bundled theme JSON files for the browser");
|
|
457
581
|
assert.match(server, /"apple-touch-icon\.png", "icon-192\.png"/, "server should serve the conventional apple touch icon path");
|
|
458
|
-
assert.match(server, /"manifest\.webmanifest", "service-worker\.js"/, "server should serve
|
|
582
|
+
assert.match(server, /"catppuccin-mocha-background\.png", "matrix-background\.webp", "manifest\.webmanifest", "service-worker\.js"/, "server should serve theme background images as static assets");
|
|
459
583
|
assert.match(server, /\["\.webmanifest", "application\/manifest\+json; charset=utf-8"\]/, "server should serve manifest with the correct MIME type");
|
|
460
584
|
assert.match(server, /\["\.png", "image\/png"\]/, "server should serve PWA PNG icons with the correct MIME type");
|
|
585
|
+
assert.match(server, /\["\.webp", "image\/webp"\]/, "server should serve Matrix WebP backgrounds with the correct MIME type");
|
|
461
586
|
assert.match(server, /function configuredScopedModelPatterns\(cwd = options\.cwd\)/, "server should read Pi configured scoped-model patterns for the active tab cwd");
|
|
462
587
|
assert.match(server, /readJsonFileIfExists\(path\.join\(cwd, "\.pi", "settings\.json"\)\)/, "server should read project-local scoped-model settings from active tab cwd");
|
|
463
588
|
assert.match(server, /resolveScopedModelsFromPatterns\(patterns, response\.data\?\.models/, "server should resolve scoped patterns against available models");
|