@firstpick/pi-package-webui 0.1.4 → 0.1.6
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 +34 -11
- package/bin/pi-webui.mjs +608 -26
- package/index.ts +82 -10
- package/package.json +34 -4
- package/public/app.js +3118 -211
- package/public/catppuccin-mocha-background.png +0 -0
- package/public/index.html +152 -52
- package/public/matrix-background.webp +0 -0
- package/public/service-worker.js +3 -1
- package/public/styles.css +772 -17
- package/tests/mobile-static.test.mjs +231 -36
|
@@ -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,9 +17,20 @@ 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);
|
|
25
|
+
const companionDependencies = {
|
|
26
|
+
"@firstpick/pi-extension-git-footer-status": "^0.2.1",
|
|
27
|
+
"@firstpick/pi-extension-release-aur": "^0.1.3",
|
|
28
|
+
"@firstpick/pi-extension-release-npm": "^0.3.3",
|
|
29
|
+
"@firstpick/pi-extension-stats": "^0.2.0",
|
|
30
|
+
"@firstpick/pi-extension-todo-progress": "^0.1.7",
|
|
31
|
+
"@firstpick/pi-prompts-git-pr": "^0.1.0",
|
|
32
|
+
"@firstpick/pi-themes-bundle": "^0.1.1",
|
|
33
|
+
};
|
|
23
34
|
|
|
24
35
|
assert.match(html, /viewport-fit=cover/, "viewport should opt into safe-area-aware full-screen layout");
|
|
25
36
|
assert.match(html, /interactive-widget=resizes-content/, "viewport should request keyboard-driven content resizing where supported");
|
|
@@ -27,11 +38,23 @@ assert.match(html, /<meta name="theme-color" content="#11111b" \/>/, "PWA should
|
|
|
27
38
|
assert.match(html, /<link rel="manifest" href="\/manifest\.webmanifest" \/>/, "PWA should expose a web app manifest");
|
|
28
39
|
assert.match(html, /<link rel="apple-touch-icon" href="\/apple-touch-icon\.png" \/>/, "PWA should expose the conventional iOS home-screen icon path");
|
|
29
40
|
assert.match(html, /id="terminalTabsToggleButton"/, "mobile should expose a compact terminal-tabs toggle");
|
|
41
|
+
assert.match(html, /id="closeAllTabsButton"[\s\S]*?>Close all Tabs<\/button>/, "tab header should expose a top-right close-all tabs action");
|
|
30
42
|
assert.match(html, /id="sidePanelBackdrop"/, "mobile side panel needs an overlay/backdrop close target");
|
|
31
43
|
assert.match(html, /id="themeSelect"/, "side panel should expose a theme selector");
|
|
32
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");
|
|
33
47
|
assert.match(html, /id="agentDoneNotificationsToggle"/, "side panel should expose an agent-done notifications toggle");
|
|
34
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");
|
|
53
|
+
assert.match(html, /id="optionalFeaturesBox"/, "side panel should expose optional feature status and controls");
|
|
54
|
+
assert.match(html, /data-side-panel-section="controls"/, "side panel controls should live in a collapsible section");
|
|
55
|
+
assert.match(html, /data-side-panel-section="commands"/, "side panel commands should live in a collapsible section");
|
|
56
|
+
assert.match(html, /class="side-panel-section-toggle"[^>]*aria-controls="sidePanelSectionControls"/, "side panel section toggles should target their content panels");
|
|
57
|
+
assert.match(html, /class="side-panel-section-label">Events<\/span>/, "side panel events should expose a section toggle label");
|
|
35
58
|
assert.match(html, /id="jumpToLatestButton"/, "chat should expose a jump-to-latest control for non-forced streaming");
|
|
36
59
|
assert.match(html, /id="stickyUserPromptButton"/, "chat should expose a fixed last-user-prompt jump control");
|
|
37
60
|
assert.match(html, /id="feedbackTray"/, "chat should expose a queued action-feedback tray");
|
|
@@ -39,6 +62,9 @@ assert.match(html, /id="sendFeedbackButton"/, "action feedback should be submitt
|
|
|
39
62
|
assert.match(html, /<textarea id="promptInput"[^>]*rows="1"[^>]*enterkeyhint="enter"/, "prompt textarea should start at one row and hint that Return inserts a newline");
|
|
40
63
|
assert.match(html, /id="composerActionsButton"/, "mobile composer should expose a compact actions trigger");
|
|
41
64
|
assert.match(html, /id="composerActionsPanel"/, "secondary composer controls should live in a mobile actions panel");
|
|
65
|
+
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");
|
|
66
|
+
assert.match(html, /id="abortButton"[^>]*Esc or hold/, "Abort should advertise Esc and long-press affordances");
|
|
67
|
+
assert.doesNotMatch(html, /class="side-panel-controls"[\s\S]*id="abortButton"/, "Abort should not be buried in the side-panel controls");
|
|
42
68
|
assert.match(html, /id="publishButton"[\s\S]*?aria-controls="publishMenu"/, "composer should expose a Publish workflow menu button");
|
|
43
69
|
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");
|
|
44
70
|
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");
|
|
@@ -54,18 +80,39 @@ assert.ok(
|
|
|
54
80
|
|
|
55
81
|
assert.match(css, /--visual-viewport-height:\s*100dvh/, "CSS should define a visual viewport height fallback");
|
|
56
82
|
assert.match(css, /color-scheme:\s*var\(--theme-color-scheme\)/, "CSS should allow JS-selected themes to update browser color-scheme");
|
|
83
|
+
assert.match(css, /font-size:\s*80%/, "Web UI should render at 80% base scale for denser layout");
|
|
57
84
|
assert.match(css, /--background-glow-pink/, "CSS should expose theme-controlled page glow colors");
|
|
85
|
+
assert.match(css, /--theme-background-image:\s*none/, "CSS should expose a theme-controlled page background image variable");
|
|
86
|
+
assert.match(css, /var\(--theme-background-image\)/, "body background should include the selected theme background image layer");
|
|
87
|
+
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");
|
|
88
|
+
assert.match(css, /\.background-clear-button \{[\s\S]*?color:\s*var\(--ctp-red\)/, "background remove button should be visually destructive");
|
|
58
89
|
assert.match(css, /height:\s*var\(--visual-viewport-height, 100dvh\)/, "layout should consume visual viewport height");
|
|
59
90
|
assert.match(css, /button, select, input \{ min-height: 44px; \}/, "base controls should meet 44px touch-target height");
|
|
60
91
|
assert.match(css, /\.composer-row button[\s\S]*?min-height:\s*44px/, "mobile composer buttons should keep 44px touch targets");
|
|
92
|
+
assert.match(css, /\.composer-abort-button,\n\.composer-row button\.primary \{[\s\S]*?min-width:/, "Abort and Send should share stable bottom-row sizing");
|
|
93
|
+
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");
|
|
94
|
+
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");
|
|
61
95
|
assert.match(css, /#promptInput \{[\s\S]*?min-height:\s*calc\(1\.5em \+ 1\.8rem\)/, "prompt input should default to a compact single-line height");
|
|
62
96
|
assert.match(css, /#promptInput \{[\s\S]*?overflow-y:\s*hidden/, "prompt input should be JS-resized instead of showing a scrollbar by default");
|
|
63
97
|
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");
|
|
64
98
|
assert.match(css, /\.message\.extension,[\s\S]*?\.message\.native/, "extension and native command output should have visible transcript styling");
|
|
65
99
|
assert.match(css, /\.message\.run-indicator-message \{[\s\S]*?border-color/, "active agent runs should render a visible transcript indicator card");
|
|
100
|
+
assert.match(css, /\.message\.action-enter \{[\s\S]*?action-card-slide-in/, "new action cards should subtly slide in from the bottom");
|
|
101
|
+
assert.match(css, /@keyframes action-card-slide-in \{[\s\S]*?translateY\(0\.42rem\)/, "action-card entry animation should start below the final position");
|
|
66
102
|
assert.match(css, /\.message\.thinking,\n\.message\.toolCall,\n\.message\.assistantEvent/, "thinking and assistant events should have non-assistant transcript card styling");
|
|
67
103
|
assert.match(css, /\.message\.toolResult, \.message\.bashExecution, \.message\.compactionSummary/, "compaction summaries should render as compact collapsible transcript cards");
|
|
104
|
+
assert.match(css, /\.message\.toolExecution \{[\s\S]*?border-color/, "paired tool executions should render as distinct TUI-like action cards");
|
|
105
|
+
assert.match(css, /\.tool-diff \{[\s\S]*?font-family:/, "edit tool diffs should have a dedicated monospace renderer");
|
|
106
|
+
assert.match(css, /\.markdown-body \{[\s\S]*?line-height:/, "assistant Markdown output should have dedicated readable styling");
|
|
107
|
+
assert.match(css, /\.markdown-table-wrapper \{[\s\S]*?overflow-x:\s*auto/, "assistant Markdown tables should be horizontally scrollable on narrow screens");
|
|
108
|
+
assert.match(css, /\.tool-result-preview \{[\s\S]*?padding:/, "collapsed tool results should show a preview area by default");
|
|
109
|
+
assert.match(css, /\.message-collapse\[open\] \+ \.tool-result-preview \{[\s\S]*?display:\s*none/, "tool result preview should hide when full output is expanded");
|
|
68
110
|
assert.match(css, /\.run-indicator-pulse \{[\s\S]*?animation:\s*run-indicator-pulse/, "active agent run indicator should have an animated pulse");
|
|
111
|
+
assert.match(css, /\.optional-features-box \{[\s\S]*?display:\s*grid/, "optional features should render as a side-panel feature list");
|
|
112
|
+
assert.match(css, /\.side-panel-section-toggle \{[\s\S]*?justify-content:\s*space-between/, "side panel section toggles should align labels and chevrons");
|
|
113
|
+
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");
|
|
114
|
+
assert.match(css, /\.side-panel-section:not\(\.collapsed\) \.side-panel-section-chevron/, "expanded side panel sections should rotate the chevron");
|
|
115
|
+
assert.match(css, /\.optional-feature-pill\.enabled/, "optional features should visually distinguish enabled state");
|
|
69
116
|
assert.match(css, /\.todo-widget \{[\s\S]*?display:\s*grid/, "todo-progress widget should render as a styled checklist card");
|
|
70
117
|
assert.match(css, /\.todo-widget-item\.partial \.todo-widget-marker/, "todo-progress partial items should have distinct styling");
|
|
71
118
|
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");
|
|
@@ -95,6 +142,8 @@ assert.match(css, /\.composer-actions-panel > \.composer-publish-menu[\s\S]*?gri
|
|
|
95
142
|
assert.match(css, /\.composer-actions-panel[\s\S]*?bottom:\s*calc\(100% \+ 0\.42rem\)/, "mobile composer actions should open as an above-composer sheet");
|
|
96
143
|
assert.match(css, /body\.composer-actions-open \.composer-actions-panel \{ display: grid; \}/, "composer actions panel should only open when toggled");
|
|
97
144
|
assert.match(css, /\.terminal-tabs-toggle-button \{ display: none; \}/, "terminal tab toggle should be hidden outside mobile CSS");
|
|
145
|
+
assert.match(css, /\.terminal-close-all-button \{[\s\S]*?color:\s*var\(--ctp-red\)/, "close-all tabs action should render as a top-right destructive tab action");
|
|
146
|
+
assert.match(css, /\.terminal-tab-group-close \{[\s\S]*?border-left-color/, "terminal tab groups should style their close button distinctly");
|
|
98
147
|
assert.match(css, /body\.mobile-tabs-expanded \.terminal-tabs \{ display: flex; \}/, "mobile tabs should expand only when toggled");
|
|
99
148
|
assert.match(css, /\.terminal-tab-activity-indicator/, "terminal tabs should expose per-tab agent activity indicators");
|
|
100
149
|
assert.match(css, /\.terminal-tab-group-item \{[\s\S]*?background:\s*var\(--ctp-crust\)/, "grouped terminal tab items should use opaque backgrounds");
|
|
@@ -108,9 +157,9 @@ assert.match(css, /\.terminal-tab\.activity-blocked \.terminal-tab-activity-indi
|
|
|
108
157
|
assert.match(css, /\.terminal-tab\.activity-done/, "completed unseen work should have a distinct tab style");
|
|
109
158
|
assert.match(css, /\.terminal-tabs[\s\S]*?position:\s*absolute/, "expanded mobile tabs should overlay instead of consuming transcript space");
|
|
110
159
|
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");
|
|
111
|
-
assert.match(css, /body\.mobile-keyboard-open \.composer-actions-button,[\s\S]*?body\.mobile-keyboard-open
|
|
160
|
+
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");
|
|
112
161
|
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");
|
|
113
|
-
assert.match(css,
|
|
162
|
+
assert.match(css, /button\[hidden\] \{ display: none !important; \}/, "hidden bottom-row controls should not occupy layout space");
|
|
114
163
|
assert.match(css, /\.footer-details-toggle \{ display: none; \}/, "footer details toggle should be hidden outside mobile CSS");
|
|
115
164
|
assert.match(css, /\.footer-workspace,\n\s+\.footer-context \{ display: grid !important; \}/, "collapsed mobile footer should primarily show cwd and context");
|
|
116
165
|
assert.match(css, /\.footer-model \{ order: 7; \}/, "model should move into expanded footer details on mobile");
|
|
@@ -124,6 +173,9 @@ assert.match(css, /(?:^|\n)\s*\.side-panel\s*\{[\s\S]*?position:\s*fixed/, "mobi
|
|
|
124
173
|
assert.match(css, /\.extension-dialog[\s\S]*?max-height:\s*calc\(var\(--visual-viewport-height/, "dialogs should fit the visual viewport on mobile");
|
|
125
174
|
assert.match(css, /\.extension-dialog[\s\S]*?inset:\s*auto 0 0 0/, "mobile dialogs should behave like bottom sheets");
|
|
126
175
|
assert.match(css, /#dialogMessage \{[\s\S]*?white-space:\s*pre-wrap/, "extension dialog messages should preserve multiline prompts");
|
|
176
|
+
assert.match(css, /\.native-command-dialog \{[\s\S]*?width:\s*min\(56rem/, "native slash selector dialog should have roomy desktop layout");
|
|
177
|
+
assert.match(css, /\.native-selector-item \{[\s\S]*?--tree-depth/, "native slash selector choices should support tree indentation");
|
|
178
|
+
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");
|
|
127
179
|
assert.match(css, /\.extension-dialog\.guardrail-dialog[\s\S]*?border-color:\s*rgba\(249, 226, 175/, "guardrail dialogs should have warning-specific styling");
|
|
128
180
|
assert.match(css, /\.extension-dialog\.release-dialog[\s\S]*?width:\s*min\(64rem/, "release confirmation dialogs should have more horizontal room");
|
|
129
181
|
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");
|
|
@@ -132,10 +184,27 @@ assert.match(css, /\.release-dialog-danger \{ color: var\(--ctp-red\); \}/, "rel
|
|
|
132
184
|
|
|
133
185
|
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");
|
|
134
186
|
assert.match(app, /const THEME_STORAGE_KEY = "pi-webui-theme"/, "theme selection should be persisted in browser storage");
|
|
187
|
+
assert.match(app, /const CUSTOM_BACKGROUND_STORAGE_KEY = "pi-webui-custom-background"/, "custom backgrounds should keep a legacy persistent browser storage key for migration");
|
|
188
|
+
assert.match(app, /const CUSTOM_BACKGROUNDS_STORAGE_KEY = "pi-webui-custom-backgrounds"/, "custom backgrounds should be persisted per theme in browser storage");
|
|
189
|
+
assert.match(app, /const CUSTOM_BACKGROUND_IDB_NAME = "pi-webui-custom-background"/, "custom backgrounds should prefer IndexedDB persistence for large images");
|
|
190
|
+
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");
|
|
135
191
|
assert.match(app, /const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications"/, "agent-done notification preference should be persisted in browser storage");
|
|
136
192
|
assert.match(app, /async function initializeThemes\(\)/, "frontend should initialize bundled themes");
|
|
137
193
|
assert.match(app, /api\("\/api\/themes", \{ scoped: false \}\)/, "theme loading should use the unscoped themes endpoint");
|
|
138
194
|
assert.match(app, /function applyTheme\(theme/, "frontend should apply a selected theme to CSS variables");
|
|
195
|
+
assert.match(app, /const LOCAL_BACKGROUND_IMAGE_PATTERN = /, "frontend should restrict theme background images to local static URLs");
|
|
196
|
+
assert.match(app, /"--theme-background-image": themeExportCssValue\(theme, "backgroundImage", "none", LOCAL_BACKGROUND_IMAGE_PATTERN\)/, "frontend should apply theme export background images to CSS variables");
|
|
197
|
+
assert.match(app, /applyCustomBackgroundOverride\(\{ render: false \}\);/, "theme changes should preserve the user's custom background override");
|
|
198
|
+
assert.match(app, /function customBackgroundThemeKey\(themeName = currentThemeName\)/, "custom background persistence should be keyed by the active theme");
|
|
199
|
+
assert.match(app, /async function setCustomBackgroundFromFile\(file\)/, "frontend should load side-panel background image files");
|
|
200
|
+
assert.match(app, /persistCustomBackground\(background, themeName\)/, "side-panel background changes should save under the selected theme");
|
|
201
|
+
assert.match(app, /clearStoredCustomBackground\(themeName, \{ includeLegacy: true \}\)/, "side-panel X button should remove only the selected theme background while clearing legacy state");
|
|
202
|
+
assert.match(app, /await loadCustomBackgroundForTheme\(theme\.name/, "theme switching should load that theme's saved custom background");
|
|
203
|
+
assert.match(app, /URL\.createObjectURL\(file\)/, "custom backgrounds should apply through short blob URLs instead of huge CSS data URLs");
|
|
204
|
+
assert.match(app, /function dataUrlToBlob\(dataUrl\)/, "saved custom backgrounds should be restored from persisted data URLs into blob URLs");
|
|
205
|
+
assert.match(app, /async function clearCustomBackground\(\)/, "frontend should support removing the custom background");
|
|
206
|
+
assert.match(app, /backgroundClearButton\.addEventListener\("click"/, "side-panel X button should clear the custom background");
|
|
207
|
+
assert.match(app, /initializeCustomBackground\(\)\.catch/, "startup should restore the saved custom background when theme loading fails");
|
|
139
208
|
assert.match(app, /Restart Web UI to load themes/, "frontend should explain when a stale server cannot serve the themes endpoint");
|
|
140
209
|
assert.match(app, /themeSelect\.addEventListener\("change"/, "side-panel theme selector should switch themes immediately");
|
|
141
210
|
assert.match(app, /open \? "Close for network" : "Open to network"/, "network button should toggle from open to close action");
|
|
@@ -177,56 +246,101 @@ assert.match(app, /function hasQueuedDialogRequest\(id\)/, "frontend should dedu
|
|
|
177
246
|
assert.match(app, /if \(request\.replayed\) addEvent\(`recovered pending \$\{request\.method\} request`, "warn"\)/, "frontend should surface replayed extension UI blockers");
|
|
178
247
|
assert.match(app, /case "webui_extension_ui_cancelled":/, "frontend should close dialogs cancelled by backend abort handling");
|
|
179
248
|
assert.match(app, /function parseTodoProgressWidget\(lines\)/, "todo-progress widgets should be parsed from extension widget lines");
|
|
249
|
+
assert.match(app, /Optional feature detection intentionally checks loaded Pi capabilities/, "optional Web UI features should be detected through loaded capabilities, not package folders");
|
|
250
|
+
assert.match(app, /function resetOptionalFeatureAvailability\(\)/, "optional feature state should reset across active-tab and reload boundaries");
|
|
251
|
+
assert.match(app, /function renderOptionalFeaturePanel\(\)/, "side panel should render optional feature installed/enabled state");
|
|
252
|
+
assert.match(app, /function setSidePanelSectionCollapsed\(record, collapsed/, "side panel sections should have explicit collapse/expand behavior");
|
|
253
|
+
assert.match(app, /restoreSidePanelSectionState\(\);\nbindSidePanelSectionToggles\(\);/, "side panel section state should restore before toggles are bound");
|
|
254
|
+
assert.match(app, /OPTIONAL_FEATURES_STORAGE_KEY/, "optional feature disable toggles should persist in browser storage");
|
|
255
|
+
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");
|
|
256
|
+
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");
|
|
257
|
+
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");
|
|
258
|
+
assert.match(app, /function renderCommands\(\)/, "side-panel commands should be re-renderable from current optional feature state");
|
|
259
|
+
assert.match(app, /function installOptionalFeature\(featureId\)/, "optional features should expose an install action");
|
|
260
|
+
assert.match(app, /api\("\/api\/optional-feature-install"/, "optional feature install action should call the backend installer endpoint");
|
|
261
|
+
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");
|
|
262
|
+
assert.match(app, /if \(!isOptionalFeatureEnabled\("todoProgressWidget"\)\) return String\(text \|\| ""\)/, "todo progress line stripping should only run when the todo feature is detected and enabled");
|
|
263
|
+
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");
|
|
264
|
+
assert.match(app, /case "webui_tab_reloaded":[\s\S]*resetOptionalFeatureAvailability\(\)/, "optional feature state should reset when the RPC tab reloads resources");
|
|
265
|
+
assert.match(app, /function runPublishWorkflow\(command\)[\s\S]*hasAvailableCommand\(commandName\)/, "publish workflow launch should guard on loaded slash commands");
|
|
266
|
+
assert.match(app, /if \(!isOptionalFeatureEnabled\("gitWorkflow"\)\)/, "guided git workflow should guard on enabled /git-staged-msg feature");
|
|
180
267
|
assert.match(app, /function renderReleaseNpmOutputWidget\(\)/, "release-npm live output should use a specialized Web UI renderer");
|
|
181
268
|
assert.match(app, /function renderReleaseAurOutputWidget\(\)/, "release-aur live output should use a specialized Web UI renderer");
|
|
182
269
|
assert.match(app, /releaseNpmActionButton\("Abort", "\/release-abort", "danger"\)/, "release-npm Web UI output should expose an abort action");
|
|
183
270
|
assert.match(app, /releaseNpmActionButton\("Abort", "\/release-aur abort", "danger"\)/, "release-aur Web UI output should expose an abort action");
|
|
184
|
-
assert.match(app, /key === "todo-progress" \? renderTodoProgressWidget\(key, lines\) : null/, "todo-progress should use the specialized widget renderer");
|
|
271
|
+
assert.match(app, /key === "todo-progress" && isOptionalFeatureEnabled\("todoProgressWidget"\) \? renderTodoProgressWidget\(key, lines\) : null/, "todo-progress should use the specialized widget renderer only when enabled");
|
|
185
272
|
assert.match(app, /let transientMessages = \[\]/, "frontend should keep transient Web UI/extension output messages");
|
|
186
273
|
assert.match(app, /function orderedTranscriptItems\(\)/, "frontend should merge persisted and transient messages chronologically");
|
|
187
274
|
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");
|
|
188
275
|
assert.match(app, /const ACTION_FEEDBACK_REACTIONS = \{/, "frontend should define direct feedback reactions");
|
|
189
|
-
assert.match(app, /message\?\.role === "assistant" \|\| message\?\.role === "toolResult" \|\| message\?\.role === "bashExecution"/, "frontend should allow reactions on final assistant output as well as actions");
|
|
276
|
+
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");
|
|
190
277
|
assert.match(app, /function renderActionFeedbackControls\(/, "frontend should render per-message reaction controls");
|
|
278
|
+
assert.match(app, /function toolResultPreviewText\(message, lineLimit = 10\)/, "tool results should derive a ten-line collapsed preview");
|
|
279
|
+
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");
|
|
280
|
+
assert.match(app, /appendToolRawDetails\(parent, tool\)/, "paired tool cards should keep a safe raw-data expander for debugging renderer mismatches");
|
|
281
|
+
assert.match(app, /function toolStateMeta\(tool\)/, "tool cards should expose consistent status and elapsed metadata across built-in renderers");
|
|
282
|
+
assert.match(app, /function handleToolExecutionUpdate\(event\)[\s\S]*?event\.partialResult/, "live tool_execution_update events should update transcript-visible tool cards");
|
|
283
|
+
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");
|
|
191
284
|
assert.match(app, /function assistantDisplayMessages\(message\)/, "assistant history should split thinking and tool-call parts out of the final Assistant output card");
|
|
192
|
-
assert.match(app, /
|
|
193
|
-
assert.match(app, /
|
|
194
|
-
assert.match(app, /
|
|
195
|
-
assert.match(app, /function
|
|
285
|
+
assert.match(app, /function assistantHasToolCallAfter\(content, index\)/, "assistant text that precedes a tool call should be detectable and suppressible");
|
|
286
|
+
assert.match(app, /if \(!assistantHasToolCallAfter\(content, index\)\) finalParts\.push\(finalPart\);/, "assistant history should not render pre-tool-call assistant text as final output");
|
|
287
|
+
assert.match(app, /return content\.trim\(\) \? \[\{ \.\.\.message, title: "final output" \}\] : \[\]/, "assistant messages with stripped empty text should not render empty final-output cards");
|
|
288
|
+
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");
|
|
289
|
+
assert.match(app, /if \(isEmptyAssistantTextPart\(part\)\) continue;/, "empty assistant text parts should not render as assistant-event cards");
|
|
290
|
+
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");
|
|
291
|
+
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");
|
|
292
|
+
assert.match(app, /function ensureStreamingThinkingBubble\(\)[\s\S]*if \(!thinkingOutputVisible\) return false/, "live thinking should respect the show/hide thinking-output toggle");
|
|
293
|
+
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");
|
|
294
|
+
assert.match(app, /const THINKING_VISIBILITY_STORAGE_KEY = "pi-webui-thinking-visible"/, "thinking visibility should persist in browser storage");
|
|
295
|
+
assert.match(app, /function setThinkingOutputVisible\(visible[\s\S]*renderAllMessages\(\{ preserveScroll: true \}\)/, "thinking visibility changes should immediately re-render the transcript");
|
|
196
296
|
assert.match(app, /function assistantStreamingMessage\(event\)/, "live streaming should read the authoritative partial assistant message from RPC events like the TUI");
|
|
197
297
|
assert.match(app, /assistantThinkingTextFromMessage\(assistantStreamingMessage\(event\)\) \|\| thinkingDeltaText\(update\)/, "live thinking end should replace deltas with the final partial-message thinking content");
|
|
198
298
|
assert.match(app, /if \(typeof partialText === "string"\) streamRawText = partialText;/, "live assistant text should synchronize from partial messages instead of relying only on deltas");
|
|
199
299
|
assert.match(app, /const TODO_PROGRESS_LINE_REGEX = /, "frontend should recognize live todo progress lines that will be moved into the todo widget");
|
|
200
300
|
assert.match(app, /function stripTodoProgressLines\(text, \{ streaming = false \} = \{\}\)/, "live Assistant output should strip todo-progress lines before rendering final-output text");
|
|
201
|
-
assert.match(app, /
|
|
301
|
+
assert.match(app, /function renderStreamingAssistantText\(\)[\s\S]*?const assistantText = stripTodoProgressLines\(streamRawText, \{ streaming: true \}\)/, "streamed Assistant text should classify from accumulated output without flashing partial todo-progress lines");
|
|
202
302
|
assert.match(app, /const STREAM_OUTPUT_HIDE_DELAY_MS = 300/, "stream output hiding should be debounced to prevent rapid flicker");
|
|
303
|
+
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");
|
|
203
304
|
assert.match(app, /function scheduleStreamBubbleHide\([\s\S]*?STREAM_OUTPUT_MIN_VISIBLE_MS/, "stream output cards should observe a minimum visible duration before hiding");
|
|
204
|
-
assert.match(app, /if \(assistantText\) \{[\s\S]*?streamText
|
|
205
|
-
assert.match(app, /
|
|
305
|
+
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");
|
|
306
|
+
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");
|
|
307
|
+
assert.match(app, /streamToolCallSeen = true;\n\s+suppressStreamingAssistantTextBeforeToolCall\(\);/, "tool-call starts should remove pending assistant text from the live transcript");
|
|
308
|
+
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");
|
|
309
|
+
assert.match(app, /function renderMarkdownInto\(parent, text\)/, "assistant output should have a browser-native Markdown renderer");
|
|
310
|
+
assert.match(app, /safeMarkdownLinkHref\(url\)/, "Markdown links should be sanitized before rendering");
|
|
311
|
+
assert.match(app, /renderContent\(body, message\.content, \{ markdown: message\.role === "assistant" \}\)/, "final assistant output should render through the Markdown path");
|
|
312
|
+
assert.match(app, /const hideMessageHeader = message\.role === "assistant" && !isCollapsibleOutput/, "assistant final-output cards should hide the redundant role header");
|
|
206
313
|
assert.match(app, /api\("\/api\/action-feedback", \{ method: "POST"/, "queued action feedback should post to the server after the run is idle");
|
|
207
|
-
assert.match(app, /function postQueuedFeedback\(tabId, items\)/, "queued feedback should have a backward-compatible submit path");
|
|
314
|
+
assert.match(app, /function postQueuedFeedback\(tabId, items, tabContext = activeTabContext\(tabId\)\)/, "queued feedback should have a backward-compatible submit path");
|
|
208
315
|
assert.match(app, /\/api\/action-feedback not found; falling back to a normal prompt/, "new frontend should gracefully handle older running Web UI servers");
|
|
209
316
|
assert.match(app, /actionFeedbackSteerMessage\(item\)/, "live action feedback should be sent as steering while the agent is running");
|
|
210
317
|
assert.match(app, /function addTransientMessage\(\{ role = "notice"/, "frontend should render transient command output into the transcript");
|
|
211
318
|
assert.match(app, /addTransientMessage\(\{ role: "extension", title: "extension output"/, "extension notify output should appear in the transcript, not only the event log");
|
|
212
319
|
assert.match(app, /function renderRunIndicator\(/, "frontend should render a transcript-level active agent indicator");
|
|
213
|
-
assert.match(app, /return "Agent is
|
|
320
|
+
assert.match(app, /return "Agent is running: ";/, "active agent indicator should use the requested headline wording");
|
|
214
321
|
assert.doesNotMatch(app, /"agent running"/, "active agent indicator should not render a separate title/header label");
|
|
215
322
|
assert.doesNotMatch(app, /runIndicatorTimestamp/, "active agent indicator should not render a separate live timestamp header");
|
|
216
323
|
assert.match(app, /runIndicatorBubble = make\("article", "message runIndicator run-indicator-message streaming"\)/, "active agent indicator should use a dedicated streaming transcript card");
|
|
217
324
|
assert.match(app, /runIndicatorShowsElapsed\(\) \? `\$\{detail\} · run time/, "active agent indicator should label elapsed run time instead of showing a bare counter");
|
|
218
|
-
assert.match(app, /Abort requested
|
|
325
|
+
assert.match(app, /Abort requested/, "abort feedback should clarify that Web UI is checking stop status");
|
|
326
|
+
assert.match(app, /const ABORT_LONG_PRESS_MS = 700/, "Abort long-press timing should be explicit");
|
|
327
|
+
assert.match(app, /async function abortActiveRun\(\{ source = "button" \} = \{\}\)/, "Abort should be centralized for button, Esc, and long-press triggers");
|
|
328
|
+
assert.match(app, /elements\.abortButton\.addEventListener\("pointerdown", startAbortLongPress\)/, "Abort should support pointer long-press");
|
|
329
|
+
assert.match(app, /abortActiveRun\(\{ source: "escape" \}\)/, "Escape should trigger the abort action when available");
|
|
219
330
|
assert.match(app, /function addAbortTranscriptNotice\(/, "abort button should render a transcript-visible aborted notice");
|
|
220
331
|
assert.match(app, /this transcript marks the run as aborted/, "abort notice should clearly mark the agent output as aborted");
|
|
221
332
|
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");
|
|
222
333
|
assert.match(app, /let runIndicatorGraceCheckTimer = null/, "local-only run indicators should have a grace-check timer");
|
|
223
334
|
assert.match(app, /const RUN_INDICATOR_STATE_RECHECK_MS = 5000/, "active run indicators should periodically re-check state");
|
|
224
335
|
assert.match(app, /function scheduleRunIndicatorGraceCheck\(/, "local-only run indicators should schedule a follow-up state check");
|
|
225
|
-
assert.match(app, /function maybeRefreshRunIndicatorState\([\s\S]*?refreshState\(\)/, "active run indicators should poll state so missed completion events do not leave stale cards");
|
|
336
|
+
assert.match(app, /function maybeRefreshRunIndicatorState\([\s\S]*?refreshState\(tabContext\)/, "active run indicators should poll state so missed completion events do not leave stale cards");
|
|
226
337
|
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");
|
|
227
|
-
assert.match(app, /scheduleRunIndicatorGraceCheck\(\)[\s\S]*?refreshState\(\)/, "stale local-only run indicators should re-check state after the start grace period");
|
|
338
|
+
assert.match(app, /scheduleRunIndicatorGraceCheck\(tabContext = activeTabContext\(\)\)[\s\S]*?refreshState\(tabContext\)/, "stale local-only run indicators should re-check state after the start grace period");
|
|
228
339
|
assert.match(app, /function scheduleAbortStateChecks\(/, "abort handling should poll state so the active indicator can clear after stop confirmation");
|
|
229
|
-
assert.match(app, /case "tool_execution_start":[\s\S]*?setRunIndicatorActivity\(`Running tool:/, "tool execution should update the active agent
|
|
340
|
+
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");
|
|
341
|
+
assert.match(app, /case "tool_execution_update":[\s\S]*?handleToolExecutionUpdate\(event\)/, "tool execution updates should be handled as transcript output");
|
|
342
|
+
assert.match(app, /case "auto_retry_start":[\s\S]*?addTransientMessage\(\{ role: "warn", title: "auto retry"/, "auto-retry starts should be transcript-visible warnings");
|
|
343
|
+
assert.match(app, /case "extension_error":[\s\S]*?addTransientMessage\(\{ role: "error", title: "extension error"/, "extension errors should be transcript-visible error cards");
|
|
230
344
|
assert.match(app, /setRunIndicatorActivity\("Requesting context compaction…"\);\n\s+scrollChatToBottom\(\{ force: true \}\);/, "manual compaction should force-follow the transcript to the bottom status card");
|
|
231
345
|
assert.match(app, /case "agent_end":[\s\S]*?clearRunIndicatorActivity\(\)/, "agent completion should remove the active agent transcript indicator");
|
|
232
346
|
assert.match(app, /case "agent_end":[\s\S]*?notifyAgentDone\(event\.tabId \|\| activeTabId/, "agent completion should trigger optional done notifications");
|
|
@@ -244,8 +358,11 @@ assert.doesNotMatch(app, /addEventListener\("mouseenter", \(\) => setActiveComma
|
|
|
244
358
|
assert.match(app, /function resizePromptInput\(\)/, "prompt textarea should auto-resize from a one-line default");
|
|
245
359
|
assert.match(app, /elements\.promptInput\.addEventListener\("input", \(\) => \{\n\s+resizePromptInput\(\);/, "prompt textarea should resize whenever the user edits it");
|
|
246
360
|
assert.match(app, /function updateComposerModeButtons\(\)/, "composer should relocate Steer and Follow-up based on run state");
|
|
247
|
-
assert.match(app, /const target = runActive \? elements\.composerRow : elements\.composerActionsPanel/, "Steer and Follow-up should
|
|
248
|
-
assert.match(app, /
|
|
361
|
+
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");
|
|
362
|
+
assert.match(app, /const before = runActive \? elements\.abortButton : null/, "active Steer and Follow-up controls should sit before Abort and Send");
|
|
363
|
+
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");
|
|
364
|
+
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");
|
|
365
|
+
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");
|
|
249
366
|
assert.match(app, /function showComposerButtonTooltip\(button\)/, "empty mode-button taps should show the usage tooltip");
|
|
250
367
|
assert.match(app, /sendPromptFromModeButton\("steer", elements\.steerButton\)/, "Steer should show tooltip instead of silently doing nothing when input is empty");
|
|
251
368
|
assert.match(app, /sendPromptFromModeButton\("follow-up", elements\.followUpButton\)/, "Follow-up should show tooltip instead of silently doing nothing when input is empty");
|
|
@@ -256,8 +373,18 @@ assert.match(app, /releaseNpmButton\.addEventListener\("click", \(\) => runPubli
|
|
|
256
373
|
assert.match(app, /releaseAurButton\.addEventListener\("click", \(\) => runPublishWorkflow\("\/release-aur"\)\)/, "Publish menu should launch /release-aur");
|
|
257
374
|
assert.match(app, /async function sendPrompt\(kind = "prompt", explicitMessage\)/, "prompt sending should accept direct messages that bypass the input field");
|
|
258
375
|
assert.match(app, /const rawMessage = usesPromptInput \? elements\.promptInput\.value : explicitMessage/, "direct prompt sends should not read the input textarea");
|
|
259
|
-
assert.match(app, /if \(usesPromptInput\) \{\
|
|
376
|
+
assert.match(app, /if \(usesPromptInput\) \{[\s\S]*?if \(targetStillActive\) \{[\s\S]*?elements\.promptInput\.value = "";/, "direct prompt sends should preserve the input textarea draft");
|
|
260
377
|
assert.match(app, /make\("button", "command-item"\)[\s\S]*?sendPrompt\("prompt", `\/\$\{command\.name\}`\)/, "side-panel command clicks should send the slash command directly");
|
|
378
|
+
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");
|
|
379
|
+
assert.match(app, /async function handleNativeSlashSelectorCommand\(message/, "frontend should intercept exact native slash commands before prompt forwarding");
|
|
380
|
+
assert.match(app, /kind === "prompt" && attachments\.length === 0 && await handleNativeSlashSelectorCommand/, "prompt sending should open native selector dialogs before marking a run active");
|
|
381
|
+
assert.match(app, /function openNativeModelSelector\(\)[\s\S]*?nativeCommandApi\("\/api\/models"\)/, "native /model selector should load models through the active tab API");
|
|
382
|
+
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");
|
|
383
|
+
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");
|
|
384
|
+
assert.match(app, /function openNativeResumeSelector\(scope = "current"\)[\s\S]*?\/api\/sessions\?scope=\$\{encodeURIComponent\(selectedScope\)\}/, "native /resume selector should list current-cwd or all sessions");
|
|
385
|
+
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");
|
|
386
|
+
assert.match(app, /Provider credential entry is intentionally not implemented in the browser yet/, "native /login should remain a safe non-secret guidance dialog");
|
|
387
|
+
assert.match(app, /const HIDDEN_COMMAND_NAMES = new Set\(\["webui-tree-navigate"\]\)/, "internal Web UI helper commands should stay out of command pickers");
|
|
261
388
|
assert.match(app, /function shouldSendPromptFromEnter\(event\)/, "prompt keyboard handling should be centralized");
|
|
262
389
|
assert.match(app, /return !isMobileView\(\);/, "plain Enter should send only outside mobile view so mobile Return can insert newlines");
|
|
263
390
|
assert.match(app, /mobile-keyboard-open/, "JS should toggle mobile keyboard mode from viewport/focus state");
|
|
@@ -278,8 +405,16 @@ assert.match(app, /function updateTerminalTabGroupOpenState\(\)/, "frontend shou
|
|
|
278
405
|
assert.match(app, /wrapper\.addEventListener\("pointerenter", \(\) => setOpenTerminalTabGroup\(group\.key\)\)/, "terminal tab groups should mark themselves open while hovered");
|
|
279
406
|
assert.match(app, /if \(openTerminalTabGroupKey\) \{\n\s+scheduleRefreshTabs\(600\);/, "tab polling should defer full tab refreshes while a group menu is open");
|
|
280
407
|
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");
|
|
281
|
-
assert.match(app, /
|
|
408
|
+
assert.match(app, /function closeTerminalTabGroup\(group\)[\s\S]*?closeTerminalTabs\(group\.tabs\.map\(\(tab\) => tab\.id\)/, "terminal tab groups should be closable as a batch");
|
|
409
|
+
assert.match(app, /function closeAllTerminalTabs\(\)[\s\S]*?closeTerminalTabs\(tabs\.map\(\(tab\) => tab\.id\)/, "tab header should close all terminal tabs as a batch");
|
|
410
|
+
assert.match(app, /WARNING: \$\{activeAgentTabs\.length\}[\s\S]*?still running or waiting for input/, "tab close confirmations should warn when agents are still running");
|
|
411
|
+
assert.match(app, /elements\.closeAllTabsButton\.addEventListener\("click", \(\) => closeAllTerminalTabs\(\)\)/, "close-all tabs button should be wired in JS");
|
|
412
|
+
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");
|
|
282
413
|
assert.match(app, /let tabSeenCompletionSerials = new Map\(\)/, "frontend should track which tab completions have been seen");
|
|
414
|
+
assert.match(app, /let activeTabGeneration = 0/, "frontend should version active-tab UI state to reject stale async work");
|
|
415
|
+
assert.match(app, /function isCurrentTabContext\(context\)/, "frontend should identify stale active-tab refresh contexts");
|
|
416
|
+
assert.match(app, /function connectEvents\(tabContext = activeTabContext\(\)\)[\s\S]*?eventSource !== source/, "frontend should ignore stale SSE messages from old active tabs");
|
|
417
|
+
assert.match(app, /async function refreshMessages\(tabContext = activeTabContext\(\)\)[\s\S]*?if \(!isCurrentTabContext\(tabContext\)\) return;/, "message refreshes should not render after the user switches tabs");
|
|
283
418
|
assert.match(app, /function tabIndicator\(tab\)/, "frontend should derive idle, working, blocked, and work-done tab indicator states");
|
|
284
419
|
assert.match(app, /pendingBlockerCount > 0[\s\S]*?state: "blocked"/, "frontend should show blocked tabs when extension UI blockers are pending");
|
|
285
420
|
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");
|
|
@@ -308,7 +443,7 @@ assert.match(app, /mobileFooterExpanded = false;[\s\S]*?document\.body\.classLis
|
|
|
308
443
|
assert.match(app, /footerMeta\("context", contextLabel, "footer-context"\)/, "footer should render context as a primary mobile meta item");
|
|
309
444
|
assert.match(app, /footerMeta\("model", modelLine, "footer-model", \{/, "footer model item should be clickable");
|
|
310
445
|
assert.match(app, /function renderFooterModelPicker\(\)/, "footer should render a scoped-model picker dropdown");
|
|
311
|
-
assert.match(app, /api\("\/api\/scoped-models"\)/, "footer model picker should load scoped models instead of all available models");
|
|
446
|
+
assert.match(app, /api\("\/api\/scoped-models", \{ tabId: tabContext\.tabId \}\)/, "footer model picker should load scoped models instead of all available models");
|
|
312
447
|
assert.match(app, /for \(const model of footerScopedModels\)/, "footer model picker should render only scoped models");
|
|
313
448
|
assert.match(app, /api\("\/api\/model", \{ method: "POST"/, "footer model picker should apply selected model through the model API");
|
|
314
449
|
assert.match(app, /footer-details-toggle/, "footer details toggle should be rendered by JS");
|
|
@@ -318,7 +453,7 @@ assert.match(app, /case "webui_tab_reloaded":/, "frontend should handle native /
|
|
|
318
453
|
assert.match(app, /addTransientMessage\(\{ role: "native", title: "\/reload"/, "native /reload should produce visible transcript output");
|
|
319
454
|
assert.match(app, /navigator\.clipboard\.writeText\(response\.data\.copyText\)/, "native /copy should use the browser clipboard when available");
|
|
320
455
|
assert.match(app, /Clipboard access failed:[\s\S]*?response\.data\.copyText/, "native /copy should show text in transcript when clipboard access fails");
|
|
321
|
-
assert.match(app, /setTimeout\(\(\) => refreshAll\(\)\.catch/, "frontend should refresh state after native /reload restarts the RPC process");
|
|
456
|
+
assert.match(app, /setTimeout\(\(\) => \{[\s\S]*?refreshAll\(tabContext\)\.catch/, "frontend should refresh state after native /reload restarts the RPC process");
|
|
322
457
|
assert.match(app, /api\("\/api\/path-fast-picks"/, "frontend should load/save fast picks through the server API");
|
|
323
458
|
assert.match(app, /loadLegacyFastPicks\(/, "frontend should migrate existing browser-local fast picks");
|
|
324
459
|
|
|
@@ -327,14 +462,18 @@ assert.equal(manifest.start_url, "/", "PWA manifest should start at the web UI r
|
|
|
327
462
|
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");
|
|
328
463
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-192.png" && icon.sizes === "192x192"), "PWA manifest should include a 192px icon");
|
|
329
464
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-512.png" && icon.sizes === "512x512"), "PWA manifest should include a 512px icon");
|
|
330
|
-
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-
|
|
465
|
+
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v16"/, "PWA service worker should define an app-shell cache");
|
|
331
466
|
assert.match(serviceWorker, /self\.addEventListener\("notificationclick"/, "PWA service worker should focus Web UI when blocked-tab notifications are clicked");
|
|
332
467
|
assert.match(serviceWorker, /event\.notification\.data\?\.url/, "blocked-tab notifications should carry a URL for service-worker click handling");
|
|
333
468
|
assert.match(serviceWorker, /"\/apple-touch-icon\.png"/, "PWA service worker should cache the apple touch icon");
|
|
469
|
+
assert.match(serviceWorker, /"\/matrix-background\.webp"/, "PWA service worker should cache the Matrix background image");
|
|
470
|
+
assert.match(serviceWorker, /"\/catppuccin-mocha-background\.png"/, "PWA service worker should cache the Catppuccin Mocha background image");
|
|
334
471
|
assert.match(serviceWorker, /url\.pathname\.startsWith\("\/api\/"\)/, "PWA service worker should not cache live API or SSE calls");
|
|
335
472
|
assert.ok(appleIcon.length > 1000, "PWA apple touch icon should be present");
|
|
336
473
|
assert.ok(icon192.length > 1000, "PWA 192px icon should be present");
|
|
337
474
|
assert.ok(icon512.length > icon192.length, "PWA 512px icon should be present and larger than 192px icon");
|
|
475
|
+
assert.ok(matrixBackground.length > 100000, "Matrix background image should be present as an optimized WebP asset");
|
|
476
|
+
assert.ok(mochaBackground.length > 8000, "Catppuccin Mocha background image should be present as a compact PNG asset");
|
|
338
477
|
|
|
339
478
|
assert.match(server, /const NATIVE_SLASH_COMMANDS = \[/, "server should define Pi native slash commands for autocomplete");
|
|
340
479
|
assert.match(server, /\{ name: "reload", description: "Reload keybindings, extensions, skills, prompts, and themes" \}/, "native /reload should be advertised for autocomplete");
|
|
@@ -364,19 +503,29 @@ assert.match(server, /async function handleNativeSlashCommand\(tab, body\)/, "se
|
|
|
364
503
|
assert.match(server, /const restoreTabs = readRestoreTabsFromEnv\(\)/, "server should accept restart tab restore descriptors from the launcher environment");
|
|
365
504
|
assert.match(server, /delete process\.env\.PI_WEBUI_RESTORE_TABS/, "server should avoid leaking restore descriptors into spawned Pi RPC processes");
|
|
366
505
|
assert.match(server, /if \(sessionFile && !options\.noSession\) piArgs\.push\("--session", sessionFile\)/, "restored tabs should resume previous session files");
|
|
367
|
-
assert.
|
|
368
|
-
assert.match(server, /
|
|
506
|
+
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");
|
|
507
|
+
assert.match(server, /const closedRestorableTabs = \[\]/, "server should track recently closed tabs separately from restart restore descriptors");
|
|
508
|
+
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");
|
|
509
|
+
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");
|
|
510
|
+
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");
|
|
369
511
|
assert.match(server, /function rememberTabState\(tab, state\)/, "server should cache last-known tab state for restart-safe session restoration");
|
|
370
512
|
assert.match(server, /sessionFile: tabRestorableSessionFile\(tab\)/, "tab metadata should expose cached session files for health/status restore descriptors");
|
|
371
|
-
assert.match(server, /restorableTabs: mergeRestorableTabDescriptors\(statusTabs
|
|
372
|
-
assert.match(server, /data\.restorableTabs = mergeRestorableTabDescriptors\(detailedTabs
|
|
513
|
+
assert.match(server, /restorableTabs: mergeRestorableTabDescriptors\(statusTabs\)/, "status should expose only currently open tabs as restart restore descriptors");
|
|
514
|
+
assert.match(server, /data\.restorableTabs = mergeRestorableTabDescriptors\(detailedTabs\)/, "detailed status should keep restart restore descriptors limited to open tabs");
|
|
515
|
+
assert.match(server, /data\.closedTabs = closedRestorableTabs\.slice\(\)/, "detailed status should expose recently closed tabs separately");
|
|
373
516
|
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");
|
|
374
517
|
assert.match(server, /const initialTabs = await createInitialTabs\(\)/, "server should recreate restored tabs before listening");
|
|
375
518
|
assert.match(extension, /api\/webui-status\?detailed=1&events=0/, "launcher should capture detailed tab status before restarting an existing Web UI");
|
|
376
|
-
assert.match(extension, /function mergeRestorableTabsFromStatusSources\(sources: unknown\[\], options: StartWebuiOptions\)/, "launcher should merge
|
|
377
|
-
assert.match(extension, /
|
|
519
|
+
assert.match(extension, /function mergeRestorableTabsFromStatusSources\(sources: unknown\[\], options: StartWebuiOptions\)/, "launcher should merge available restore sources instead of trusting only the first one");
|
|
520
|
+
assert.match(extension, /const openTabSources: unknown\[\] = \[\]/, "launcher should collect explicit open-tab sources for restart restore");
|
|
521
|
+
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");
|
|
522
|
+
assert.match(extension, /if \(openTabSources\.length > 0\) return mergeRestorableTabsFromStatusSources\(openTabSources, options\)/, "launcher should restore only open tabs when open tab lists are available");
|
|
523
|
+
assert.match(extension, /return mergeRestorableTabsFromStatusSources\(\[statusData\?\.restorableTabs, existing\.restorableTabs\], options\)/, "launcher should use restorableTabs only as a legacy fallback");
|
|
378
524
|
assert.match(extension, /env\.PI_WEBUI_RESTORE_TABS = JSON\.stringify\(restoreTabs\)/, "launcher should pass restorable tabs to the new detached server");
|
|
379
|
-
assert.match(extension, /pi\.registerCommand\("start
|
|
525
|
+
assert.match(extension, /pi\.registerCommand\("webui-start"/, "extension should expose the canonical /webui-start command");
|
|
526
|
+
assert.match(extension, /pi\.registerCommand\("webui-tree-navigate"/, "extension should expose the internal Web UI tree navigation command");
|
|
527
|
+
assert.match(extension, /ctx\.navigateTree\(payload\.entryId/, "internal Web UI tree command should call the native session tree navigation API");
|
|
528
|
+
assert.doesNotMatch(extension, /pi\.registerCommand\("start-webui"/, "extension should not expose the older /start-webui alias");
|
|
380
529
|
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");
|
|
381
530
|
assert.match(server, /case "reload": \{[\s\S]*?restartTabRpc\(tab, "slash-command"\)/, "native /reload should restart the active RPC tab");
|
|
382
531
|
assert.match(server, /message: "Reloaded keybindings, extensions, skills, prompts, and themes\."/, "native /reload should return visible command output");
|
|
@@ -387,6 +536,8 @@ assert.match(server, /case "session": \{[\s\S]*?formatSessionOutput\(tab, state\
|
|
|
387
536
|
assert.match(server, /case "copy": \{[\s\S]*?get_last_assistant_text[\s\S]*?copyText: text/, "native /copy should return text for browser clipboard handling");
|
|
388
537
|
assert.match(server, /case "hotkeys": \{[\s\S]*?webuiHotkeysOutput\(\)/, "native /hotkeys should return Web UI hotkey output");
|
|
389
538
|
assert.match(server, /url\.pathname === "\/api\/commands" && req\.method === "GET"[\s\S]*?getCommandData\(tab\)/, "GET /api/commands should merge native and RPC-visible commands");
|
|
539
|
+
assert.match(server, /function safeRpcResponse\(tab, command/, "server should provide stopped-RPC fallbacks for refresh endpoints");
|
|
540
|
+
assert.match(server, /function primeTabRpc\(tab\)/, "server should prime new terminal RPC state before returning created tabs");
|
|
390
541
|
assert.match(server, /specific Web UI action or final-output cards/, "server feedback-learning prompt should cover final outputs as well as actions");
|
|
391
542
|
assert.match(server, /function formatActionFeedbackLearningPrompt\(items\)/, "server should convert feedback into a LEARNING prompt");
|
|
392
543
|
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");
|
|
@@ -399,13 +550,28 @@ assert.match(server, /url\.pathname === "\/api\/path-suggestions" && req\.method
|
|
|
399
550
|
assert.match(server, /url\.pathname === "\/api\/path-fast-picks" && req\.method === "GET"/, "server should expose GET /api/path-fast-picks");
|
|
400
551
|
assert.match(server, /url\.pathname === "\/api\/path-fast-picks" && req\.method === "POST"/, "server should expose POST /api/path-fast-picks");
|
|
401
552
|
assert.match(server, /url\.pathname === "\/api\/scoped-models" && req\.method === "GET"/, "server should expose GET /api/scoped-models");
|
|
402
|
-
assert.match(server,
|
|
553
|
+
assert.match(server, /url\.pathname === "\/api\/fork-messages" && req\.method === "GET"/, "server should expose fork-point data for the native /fork selector");
|
|
554
|
+
assert.match(server, /url\.pathname === "\/api\/sessions" && req\.method === "GET"/, "server should expose session lists for the native /resume selector");
|
|
555
|
+
assert.match(server, /url\.pathname === "\/api\/session-tree" && req\.method === "GET"/, "server should expose session-tree data for the native /tree selector");
|
|
556
|
+
assert.match(server, /url\.pathname === "\/api\/switch-session" && req\.method === "POST"/, "server should expose session switching for the native /resume selector");
|
|
557
|
+
assert.match(server, /url\.pathname === "\/api\/tree-navigate" && req\.method === "POST"/, "server should expose tree navigation through the Web UI helper command");
|
|
558
|
+
assert.match(server, /function configuredSessionDir\(\)/, "server should honor forwarded --session-dir for session selectors");
|
|
559
|
+
assert.match(server, /SessionManager\.listAll\(sessionDir\) : await SessionManager\.list\(tab\.cwd, sessionDir\)/, "server should support current-cwd and all-session resume scopes");
|
|
560
|
+
assert.match(server, /type: "set_steering_mode"/, "server should expose steering queue-mode changes for native /settings");
|
|
561
|
+
assert.match(server, /type: "set_follow_up_mode"/, "server should expose follow-up queue-mode changes for native /settings");
|
|
562
|
+
assert.match(server, /type: "set_auto_compaction"/, "server should expose auto-compaction changes for native /settings");
|
|
563
|
+
assert.match(server, /@firstpick\/pi-themes-bundle/, "server should discover themes from the optional theme package");
|
|
564
|
+
assert.match(server, /const OPTIONAL_FEATURE_PACKAGES = new Map/, "server should whitelist optional feature packages for install actions");
|
|
565
|
+
assert.match(server, /function installOptionalFeaturePackage\(featureId\)/, "server should provide optional feature package installation helper");
|
|
566
|
+
assert.match(server, /url\.pathname === "\/api\/optional-feature-install" && req\.method === "POST"/, "server should expose optional feature install endpoint");
|
|
567
|
+
assert.match(server, /Installing optional Web UI features is only allowed from localhost/, "optional feature install endpoint should be localhost-only");
|
|
403
568
|
assert.match(server, /url\.pathname === "\/api\/themes" && req\.method === "GET"/, "server should expose GET /api/themes");
|
|
404
569
|
assert.match(server, /readBundledThemes\(\)/, "server should read bundled theme JSON files for the browser");
|
|
405
570
|
assert.match(server, /"apple-touch-icon\.png", "icon-192\.png"/, "server should serve the conventional apple touch icon path");
|
|
406
|
-
assert.match(server, /"manifest\.webmanifest", "service-worker\.js"/, "server should serve
|
|
571
|
+
assert.match(server, /"catppuccin-mocha-background\.png", "matrix-background\.webp", "manifest\.webmanifest", "service-worker\.js"/, "server should serve theme background images as static assets");
|
|
407
572
|
assert.match(server, /\["\.webmanifest", "application\/manifest\+json; charset=utf-8"\]/, "server should serve manifest with the correct MIME type");
|
|
408
573
|
assert.match(server, /\["\.png", "image\/png"\]/, "server should serve PWA PNG icons with the correct MIME type");
|
|
574
|
+
assert.match(server, /\["\.webp", "image\/webp"\]/, "server should serve Matrix WebP backgrounds with the correct MIME type");
|
|
409
575
|
assert.match(server, /function configuredScopedModelPatterns\(cwd = options\.cwd\)/, "server should read Pi configured scoped-model patterns for the active tab cwd");
|
|
410
576
|
assert.match(server, /readJsonFileIfExists\(path\.join\(cwd, "\.pi", "settings\.json"\)\)/, "server should read project-local scoped-model settings from active tab cwd");
|
|
411
577
|
assert.match(server, /resolveScopedModelsFromPatterns\(patterns, response\.data\?\.models/, "server should resolve scoped patterns against available models");
|
|
@@ -415,13 +581,42 @@ assert.match(readme, /Feedback reactions \(`👍`, `👎`, `\?`\) on final assis
|
|
|
415
581
|
assert.match(readme, /POST \/api\/action-feedback\?tab=<tabId>/, "README should document the action-feedback endpoint");
|
|
416
582
|
assert.match(readme, /`@` file\/path references with live suggestions/, "README should describe @ file/path reference autocomplete");
|
|
417
583
|
assert.match(readme, /GET \/api\/path-suggestions\?tab=<tabId>&query=<path>/, "README should document the path-suggestions endpoint");
|
|
584
|
+
assert.match(readme, /POST \/api\/optional-feature-install/, "README should document optional feature install endpoint");
|
|
418
585
|
assert.match(readme, /server-persisted fast picks/, "README should describe server-persisted fast picks");
|
|
419
586
|
assert.match(readme, /browser notifications when a tab needs an extension UI response and an optional side-panel toggle for agent-done notifications/, "README should describe blocked-tab and agent-done notifications");
|
|
420
587
|
assert.match(readme, /blocked-tab browser notifications, and optional agent-done notifications require browser service-worker\/notification support/, "README should document notification requirements");
|
|
421
|
-
assert.match(readme, /Side-panel theme picker backed by
|
|
588
|
+
assert.match(readme, /Side-panel theme picker backed by optional `@firstpick\/pi-themes-bundle` themes when loaded/, "README should describe optional theme selection");
|
|
589
|
+
assert.match(readme, /## Optional companion packages/, "README should document optional Web UI companion packages");
|
|
590
|
+
assert.match(readme, /checks loaded Pi capabilities directly through RPC-visible commands and live widget events/, "README should document capability-based startup checks");
|
|
591
|
+
assert.match(readme, /side panel shows each optional feature as enabled, disabled, or install-needed/, "README should document optional feature side-panel controls");
|
|
592
|
+
assert.match(readme, /Installing a missing feature is an explicit, warned action/, "README should document optional feature install warning behavior");
|
|
422
593
|
|
|
423
594
|
assert.equal(pkg.scripts?.test, "node tests/mobile-static.test.mjs", "package test script should run the mobile static harness");
|
|
424
|
-
|
|
595
|
+
for (const [name, range] of Object.entries(companionDependencies)) {
|
|
596
|
+
assert.equal(pkg.optionalDependencies?.[name], range, `webui package should optionally depend on ${name}`);
|
|
597
|
+
assert.equal(pkg.dependencies?.[name], undefined, `webui package should not require optional companion ${name}`);
|
|
598
|
+
}
|
|
599
|
+
assert.equal(pkg.bundledDependencies, undefined, "webui optional companion packages should not be bundled into the tarball");
|
|
600
|
+
for (const extensionPath of [
|
|
601
|
+
"../pi-extension-git-footer-status/index.ts",
|
|
602
|
+
"../pi-extension-release-aur/index.ts",
|
|
603
|
+
"../pi-extension-release-npm/index.ts",
|
|
604
|
+
"../pi-extension-stats/index.ts",
|
|
605
|
+
"../pi-extension-todo-progress/index.ts",
|
|
606
|
+
"node_modules/@firstpick/pi-extension-git-footer-status/index.ts",
|
|
607
|
+
"node_modules/@firstpick/pi-extension-release-aur/index.ts",
|
|
608
|
+
"node_modules/@firstpick/pi-extension-release-npm/index.ts",
|
|
609
|
+
"node_modules/@firstpick/pi-extension-stats/index.ts",
|
|
610
|
+
"node_modules/@firstpick/pi-extension-todo-progress/index.ts",
|
|
611
|
+
]) {
|
|
612
|
+
assert.ok(pkg.pi?.extensions?.includes(extensionPath), `webui Pi manifest should load ${extensionPath} when present`);
|
|
613
|
+
}
|
|
614
|
+
assert.ok(pkg.pi?.skills?.includes("../pi-extension-release-aur/skills"), "webui Pi manifest should load release-aur sibling skills when present");
|
|
615
|
+
assert.ok(pkg.pi?.skills?.includes("node_modules/@firstpick/pi-extension-release-aur/skills"), "webui Pi manifest should load release-aur nested skills when present");
|
|
616
|
+
assert.ok(pkg.pi?.prompts?.includes("../pi-package-prompts-git-pr/prompts"), "webui Pi manifest should load guided-git sibling prompts when present");
|
|
617
|
+
assert.ok(pkg.pi?.prompts?.includes("node_modules/@firstpick/pi-prompts-git-pr/prompts"), "webui Pi manifest should load guided-git nested prompts when present");
|
|
618
|
+
assert.ok(pkg.pi?.themes?.includes("../pi-package-themes-bundle/themes"), "webui Pi manifest should load sibling bundled themes when present");
|
|
619
|
+
assert.ok(pkg.pi?.themes?.includes("node_modules/@firstpick/pi-themes-bundle/themes"), "webui Pi manifest should load nested bundled themes when present");
|
|
425
620
|
assert.ok(pkg.scripts?.check?.includes("node --check public/app.js"), "check script should syntax-check app.js");
|
|
426
621
|
assert.ok(pkg.scripts?.check?.includes("node tests/mobile-static.test.mjs"), "check script should include mobile static assertions");
|
|
427
622
|
|