@firstpick/pi-package-webui 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -3
- package/bin/pi-webui.mjs +1261 -16
- package/package.json +2 -1
- package/public/app.js +1482 -44
- package/public/index.html +71 -3
- package/public/service-worker.js +1 -1
- package/public/styles.css +592 -27
- package/tests/mobile-static.test.mjs +91 -9
|
@@ -104,6 +104,15 @@ assert.match(html, /id="stickyUserPromptButton"/, "chat should expose a fixed la
|
|
|
104
104
|
assert.match(html, /id="feedbackTray"/, "chat should expose a queued action-feedback tray");
|
|
105
105
|
assert.match(html, /id="sendFeedbackButton"/, "action feedback should be submittable after the agent finishes");
|
|
106
106
|
assert.match(html, /<textarea id="promptInput"[^>]*rows="1"[^>]*enterkeyhint="enter"/, "prompt textarea should start at one row and hint that Return inserts a newline");
|
|
107
|
+
assert.match(app, /const LONG_INPUT_ATTACHMENT_LINE_THRESHOLD = 20/, "long composer text should use a 20-line threshold before becoming an attachment");
|
|
108
|
+
assert.match(app, /function attachLongTextAsFile\(text, source = "input text"\)/, "long composer text should be attachable as a generated text file");
|
|
109
|
+
assert.match(app, /function handleAttachmentPaste\(event\)[\s\S]*attachLongTextAsFile\(text, "clipboard text"\)/, "long pasted text should be attached instead of inserted into the prompt textarea");
|
|
110
|
+
assert.match(app, /promptInput\.addEventListener\("input", \(\) => \{[\s\S]*moveLongPromptInputToAttachment\(\)/, "long typed composer text should move into an attachment and clear the textarea");
|
|
111
|
+
assert.match(html, /id="attachmentTextDialog"[\s\S]*id="attachmentTextEditor"/, "text attachments should have an in-Web UI editing dialog");
|
|
112
|
+
assert.match(app, /attachment-edit-button[\s\S]*openTextAttachmentEditor\(attachment\.id\)/, "editable text attachments should expose an Edit action in the tray");
|
|
113
|
+
assert.match(app, /function saveTextAttachmentEdit\(\)[\s\S]*attachment\.file = nextFile/, "text attachment dialog should save edits back to the attachment file");
|
|
114
|
+
assert.match(app, /attachmentTextDialog\?\.addEventListener\("keydown"[\s\S]*event\.key\.toLowerCase\(\) !== "s"[\s\S]*saveTextAttachmentEdit\(\)/, "text attachment dialog should save with Ctrl+S or Cmd+S");
|
|
115
|
+
assert.match(css, /\.attachment-text-dialog[\s\S]*\.attachment-text-editor/, "text attachment editor should have dedicated dialog styling");
|
|
107
116
|
assert.match(html, /id="composerActionsButton"/, "mobile composer should expose a compact actions trigger");
|
|
108
117
|
assert.match(html, /id="composerActionsPanel"/, "secondary composer controls should live in a mobile actions panel");
|
|
109
118
|
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");
|
|
@@ -116,6 +125,16 @@ assert.match(html, /id="nativeCommandMenuButton"[\s\S]*?aria-controls="nativeCom
|
|
|
116
125
|
assert.ok(html.indexOf('id="publishButton"') < html.indexOf('id="nativeCommandMenuButton"'), "skills/tools command menu should render immediately after the Publish workflow button");
|
|
117
126
|
assert.match(html, /id="nativeSkillsButton"[^>]*data-command="\/skills"[\s\S]*?<span>Skills Setup<\/span>/, "skills/tools command menu should include Skills Setup");
|
|
118
127
|
assert.match(html, /id="nativeToolsButton"[^>]*data-command="\/tools"[\s\S]*?<span>Tools Setup<\/span>/, "skills/tools command menu should include Tools Setup");
|
|
128
|
+
assert.match(html, /id="appRunnerInfoButton"[\s\S]*?aria-controls="appRunnerInfoDialog"/, "detected app-runner controls should expose an explanation popup button");
|
|
129
|
+
assert.match(html, /id="appRunnerMenuButton"[\s\S]*?aria-controls="appRunnerMenuPanel"/, "composer should expose a detected app-runner dropdown button");
|
|
130
|
+
assert.ok(html.indexOf('id="optionsMenuButton"') < html.indexOf('id="appRunnerMenuButton"'), "app-runner dropdown should render to the right of the settings/options button");
|
|
131
|
+
assert.match(html, /id="appRunnerMenuPanel"[^>]*aria-label="Detected app runners"/, "app-runner dropdown should render detected runner choices only from JS data");
|
|
132
|
+
assert.match(html, /id="appRunnerInfoDialog"[\s\S]*id="appRunnerInfoBody"/, "app-runner explanation popup should have a dynamic details body");
|
|
133
|
+
assert.match(app, /\.pi-webui-runners\.json/, "app-runner popup should explain the project-local custom runner config file");
|
|
134
|
+
assert.match(app, /appRunnerCustomPathInput[\s\S]*Browse/, "custom app-runner path should be browseable from the popup");
|
|
135
|
+
assert.match(server, /APP_RUNNER_CONFIG_FILE = "\.pi-webui-runners\.json"/, "server should use a project-local custom app-runner config file");
|
|
136
|
+
assert.match(server, /\/api\/app-runner-config/, "server should expose custom app-runner config endpoints");
|
|
137
|
+
assert.match(server, /\/api\/app-runner-files/, "server should expose project-scoped file browsing for custom runner paths");
|
|
119
138
|
assert.doesNotMatch(html, /<code>\/release-(?:npm|aur)<\/code>/, "Publish menu should not show slash command names as option labels");
|
|
120
139
|
assert.doesNotMatch(html, /data-tooltip="[^"]*\/release-(?:npm|aur)/, "Publish tooltip should not show slash command names");
|
|
121
140
|
assert.match(html, /id="steerButton"[\s\S]*?data-tooltip="Steer usage:/, "Steer should explain type-first usage in a tooltip");
|
|
@@ -139,6 +158,12 @@ assert.match(css, /\.path-picker-create-button:hover,[\s\S]*?var\(--ctp-blue\)/,
|
|
|
139
158
|
assert.match(css, /\.path-picker-search-row \{[\s\S]*?grid-template-columns:\s*minmax\(0, 1fr\) auto/, "cwd picker search controls should sit in a responsive row");
|
|
140
159
|
assert.match(css, /\.path-picker-clear-search-button:hover,[\s\S]*?var\(--ctp-mauve\)/, "cwd picker clear-search action should have a distinct hover style");
|
|
141
160
|
assert.match(css, /height:\s*var\(--visual-viewport-height, 100dvh\)/, "layout should consume visual viewport height");
|
|
161
|
+
assert.match(css, /@media \(max-width: 1050px\)[\s\S]*?\.chat-panel \{[\s\S]*?height:\s*calc\(var\(--visual-viewport-height, 100dvh\) - 2rem\);[\s\S]*?max-height:\s*calc\(var\(--visual-viewport-height, 100dvh\) - 2rem\);[\s\S]*?\.chat \{ flex-basis:\s*0; \}/, "narrow stacked layout should bound the transcript so terminal tabs and bottom controls stay visible");
|
|
162
|
+
assert.match(css, /@media \(max-width: 1050px\)[\s\S]*?\.side-panel-backdrop \{[\s\S]*?position:\s*fixed[\s\S]*?\.side-panel \{[\s\S]*?position:\s*fixed/, "narrow stacked layout should use the mobile-style side-panel overlay drawer");
|
|
163
|
+
assert.match(css, /@media \(max-width: 1050px\)[\s\S]*?body:not\(\.side-panel-collapsed\) \{ overflow:\s*hidden; \}/, "narrow side-panel overlay should prevent background page scrolling while open");
|
|
164
|
+
assert.match(css, /@media \(max-width: 1050px\)[\s\S]*?body:not\(\.side-panel-collapsed\) \.chat-panel \{[\s\S]*?visibility:\s*hidden;[\s\S]*?pointer-events:\s*none;/, "narrow side-panel overlay should suppress the underlying terminal header so it cannot cover side-panel controls");
|
|
165
|
+
assert.match(css, /@media \(max-width: 1050px\)[\s\S]*?\.side-panel-backdrop \{[\s\S]*?z-index:\s*110;[\s\S]*?\.side-panel \{[\s\S]*?z-index:\s*111;[\s\S]*?body\.side-panel-collapsed \.terminal-tabs-shell \{[\s\S]*?padding-right:\s*4\.85rem;[\s\S]*?\.side-panel-expand-button \{[\s\S]*?z-index:\s*120/, "narrow side-panel overlay and expand button should stay above and reserve space from terminal header controls");
|
|
166
|
+
assert.match(css, /@media \(max-width: 720px\), \(max-device-width: 720px\), \(pointer: coarse\) and \(hover: none\)[\s\S]*?body\.side-panel-collapsed \.terminal-tabs-shell \{ padding-right:\s*calc\(44px \+ 0\.8rem\); \}[\s\S]*?\.side-panel-expand-button \{[\s\S]*?z-index:\s*120[\s\S]*?\.side-panel-backdrop \{[\s\S]*?z-index:\s*110;[\s\S]*?\.side-panel \{[\s\S]*?z-index:\s*111;/, "mobile side-panel controls should not hide behind terminal header buttons");
|
|
142
167
|
assert.match(css, /button, select, input \{ min-height: 44px; \}/, "base controls should meet 44px touch-target height");
|
|
143
168
|
assert.match(css, /\.composer-row button[\s\S]*?min-height:\s*44px/, "mobile composer buttons should keep 44px touch targets");
|
|
144
169
|
assert.match(css, /\.composer-abort-button,\n\.composer-row button\.primary \{[\s\S]*?min-width:/, "Abort and Send should share stable bottom-row sizing");
|
|
@@ -163,6 +188,8 @@ assert.match(css, /\.markdown-body \{[\s\S]*?line-height:/, "assistant Markdown
|
|
|
163
188
|
assert.match(css, /\.markdown-table-wrapper \{[\s\S]*?overflow-x:\s*auto/, "assistant Markdown tables should be horizontally scrollable on narrow screens");
|
|
164
189
|
assert.match(css, /\.tool-result-preview \{[\s\S]*?padding:/, "collapsed tool results should show a preview area by default");
|
|
165
190
|
assert.match(css, /\.message-collapse\[open\] \+ \.tool-result-preview \{[\s\S]*?display:\s*none/, "tool result preview should hide when full output is expanded");
|
|
191
|
+
assert.match(css, /\.message\.toolResult \.message-collapse\[open\] > \.message-body,[\s\S]*?\.message\.bashExecution \.message-collapse\[open\] > \.message-body,[\s\S]*?max-height:\s*min\(42rem, 62dvh\);[\s\S]*?overflow:\s*auto/, "expanded transcript tool and bash output should scroll inside their cards");
|
|
192
|
+
assert.match(css, /\.tool-output-details\[open\] > \.tool-output-code \{[\s\S]*?max-height:\s*min\(34rem, 52dvh\);[\s\S]*?overflow:\s*auto/, "expanded live tool output should get an internal scrollbar");
|
|
166
193
|
assert.match(css, /\.run-indicator-pulse \{[\s\S]*?animation:\s*run-indicator-pulse/, "active agent run indicator should have an animated pulse");
|
|
167
194
|
assert.match(css, /\.optional-features-box \{[\s\S]*?display:\s*grid/, "optional features should render as a side-panel feature list");
|
|
168
195
|
assert.match(css, /\.prompt-list-controls \{[\s\S]*?display:\s*grid/, "Queue prompt-list controls should render as a side-panel control group");
|
|
@@ -190,6 +217,10 @@ assert.match(css, /\.widget-area:has\(\.release-npm-live-widget \.release-npm-ou
|
|
|
190
217
|
assert.match(css, /\.release-npm-live-widget \.release-npm-output-details\[open\] \.release-npm-terminal,[\s\S]*?height:\s*clamp\(15rem, 42dvh, 31rem\)/, "live release terminals should keep a fixed viewport height while output streams");
|
|
191
218
|
assert.match(css, /\.release-npm-terminal \{[\s\S]*?rgba\(3, 4, 10, 0\.98\)/, "release-npm terminal should use a high-contrast stream panel");
|
|
192
219
|
assert.match(css, /\.release-aur-widget \{[\s\S]*?border-color/, "release-aur output should render as a specialized Web UI widget variant");
|
|
220
|
+
assert.match(css, /\.app-runner-widget \{[\s\S]*?border-color/, "app runner output should render as a specialized Web UI widget variant");
|
|
221
|
+
assert.match(css, /\.composer-app-runner-info-button \{[\s\S]*?position:\s*absolute;[\s\S]*?right:\s*-0\.48rem;[\s\S]*?flex:\s*none;[\s\S]*?pointer-events:\s*none/, "app runner info help should float over Run without consuming toolbar width");
|
|
222
|
+
assert.match(css, /\.composer-app-runner-menu\.has-runners:hover \.composer-app-runner-info-button,[\s\S]*?\.composer-app-runner-info-button:focus-visible \{[\s\S]*?opacity:\s*1;[\s\S]*?pointer-events:\s*auto;[\s\S]*?transform:\s*translateY\(0\) scale\(1\)/, "app runner info help should reveal without reflowing neighboring composer buttons");
|
|
223
|
+
assert.match(css, /\.widget-area:has\(\.app-runner-live-widget \.release-npm-output-details\[open\]\)/, "live app runner output should reserve the same fixed top widget slot as release output");
|
|
193
224
|
assert.match(css, /\.widget-area \.widget:not\(\.todo-widget\):not\(\.release-npm-widget\)/, "mobile widget filtering should keep release workflow output visible");
|
|
194
225
|
assert.match(css, /\.message\.warn \.message-role \{ color: var\(--ctp-yellow\); \}/, "warning-level command output should be visually distinct");
|
|
195
226
|
assert.match(css, /\.commands-box \{[\s\S]*?max-height:\s*min\(32rem, 52vh\)/, "side-panel commands should use expanded viewport-aware height");
|
|
@@ -207,6 +238,7 @@ assert.match(css, /\.action-feedback-controls:hover,[\s\S]*?\.action-feedback-co
|
|
|
207
238
|
assert.match(css, /\.action-feedback-controls:not\(:hover\):not\(:focus-within\) \.action-feedback-button/, "hidden action reactions should not expose button hit targets until the hover area is reached");
|
|
208
239
|
assert.match(css, /\.action-feedback-button\.feedback-question\.active/, "question-mark reaction should have selected styling");
|
|
209
240
|
assert.match(css, /\.composer-row button\[data-tooltip\]::after/, "composer button tooltips should be shared across Git, Steer, and Follow-up buttons");
|
|
241
|
+
assert.match(css, /\.composer-row \.composer-git-button\[data-tooltip\]::after \{[\s\S]*?left:\s*0;[\s\S]*?right:\s*auto;/, "Git workflow tooltip should open rightward so it is not clipped off the left edge");
|
|
210
242
|
assert.match(css, /\.composer-row button\[data-tooltip\]\.tooltip-open::after/, "composer button tooltips should be triggerable from JS for empty mobile taps");
|
|
211
243
|
assert.match(css, /\.footer-floating-tooltip \{[\s\S]*?position:\s*fixed;[\s\S]*?z-index:\s*1000/, "git footer extension boxes should use one viewport-positioned styled tooltip");
|
|
212
244
|
assert.match(css, /\.footer-floating-tooltip \{[\s\S]*?overflow-wrap:\s*anywhere;[\s\S]*?white-space:\s*pre-wrap;/, "git footer tooltips should wrap long paths instead of clipping them");
|
|
@@ -245,18 +277,17 @@ assert.match(css, /\.server-offline-panel/, "PWA/offline shell should style a ba
|
|
|
245
277
|
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");
|
|
246
278
|
assert.match(css, /button\[hidden\] \{ display: none !important; \}/, "hidden bottom-row controls should not occupy layout space");
|
|
247
279
|
assert.match(css, /\.statusbar-tui-footer \{[\s\S]*?gap:\s*0/, "default TUI-like footer should reduce statusbar chrome around the compact line");
|
|
248
|
-
assert.match(css, /\.statusbar-git-footer \{[\s\S]*?gap:\s*0\.58rem/, "enabled git-footer extension should keep
|
|
280
|
+
assert.match(css, /\.statusbar-git-footer \{[\s\S]*?--footer-chip-min-width:\s*7\.6rem;[\s\S]*?gap:\s*0\.58rem/, "enabled git-footer extension should keep styled spacing and one shared minimal chip width token");
|
|
281
|
+
assert.match(css, /\.footer-line-main \.footer-metric \{[\s\S]*?flex:\s*1 1 var\(--footer-chip-min-width\);[\s\S]*?width:\s*auto;[\s\S]*?min-width:\s*0/, "git-footer metrics should use a shared preferred minimum and distribute spare row space equally");
|
|
249
282
|
assert.match(css, /\.footer-line-meta \{[\s\S]*?display:\s*flex;[\s\S]*?flex-wrap:\s*nowrap/, "git-footer metadata should keep cwd, git chips, model, and effort on one row when space allows");
|
|
250
|
-
assert.match(css, /\.footer-line-meta \.footer-meta \{[\s\S]*?flex:\s*
|
|
251
|
-
assert.match(css, /\.footer-workspace \{[\s\S]*?flex:\s*1 1 8rem/, "git-footer cwd chip should dynamically take remaining row space");
|
|
283
|
+
assert.match(css, /\.footer-line-meta \.footer-meta \{[\s\S]*?flex:\s*1 1 var\(--footer-chip-min-width\);[\s\S]*?width:\s*auto;[\s\S]*?min-width:\s*0/, "git-footer metadata chips should use a shared preferred minimum and distribute spare row space equally");
|
|
252
284
|
assert.match(css, /\.footer-thinking \.footer-meta-value \{[\s\S]*?color:\s*var\(--ctp-mauve\)/, "git-footer effort chip should have its own styling");
|
|
253
285
|
assert.match(css, /\.footer-changes \{[\s\S]*?border-color:\s*rgba\(249, 226, 175, 0\.36\)/, "git-footer changes chip should use a higher-contrast warning tint");
|
|
254
286
|
assert.match(css, /\.footer-changes \.footer-meta-value \{[\s\S]*?color:\s*var\(--ctp-yellow\)[\s\S]*?font-weight:\s*950/, "git-footer changes value should be bright and bold");
|
|
255
287
|
assert.match(css, /\.footer-git-extra \.footer-meta-value \{[\s\S]*?color:\s*var\(--ctp-sky\)[\s\S]*?font-weight:\s*900/, "git-footer extras value should be bright enough to read at footer size");
|
|
256
|
-
assert.match(css, /\.footer-changes,[\s\S]*?\.footer-git-extra \{[\s\S]*?flex:\s*0 0 auto/, "git-footer changes and extras chips should resist truncating short status values");
|
|
257
288
|
assert.match(css, /\.footer-meta-action \{[\s\S]*?position:\s*relative;[\s\S]*?border-color:\s*rgba\(148, 226, 213, 0\.26\)/, "clickable footer boxes should have a subtle always-visible highlight");
|
|
258
289
|
assert.doesNotMatch(css, /\.footer-meta-action::after/, "clickable footer boxes should not show a corner indicator dot");
|
|
259
|
-
assert.match(css, /@media \(max-width: 1050px\)[\s\S]*?\.footer-line-meta \{[\s\S]*?display:\s*
|
|
290
|
+
assert.match(css, /@media \(max-width: 1050px\)[\s\S]*?\.footer-line-meta \{[\s\S]*?display:\s*flex;[\s\S]*?flex-wrap:\s*wrap;[\s\S]*?\.footer-line-meta \.footer-meta \{[\s\S]*?flex:\s*1 1 var\(--footer-chip-min-width\);[\s\S]*?width:\s*auto;[\s\S]*?\.footer-workspace,\n\s+\.footer-model,\n\s+\.footer-thinking \{ grid-column:\s*auto; \}/, "narrow git-footer metadata should wrap like the top metric row instead of forcing a two-column grid");
|
|
260
291
|
assert.match(css, /\.footer-line-tui \{[\s\S]*?white-space:\s*nowrap/, "default Web UI footer should use a minimal TUI-like line");
|
|
261
292
|
assert.match(css, /\.footer-tui-cwd[\s\S]*?max-width:\s*38%/, "TUI-like footer should keep cwd compact on desktop");
|
|
262
293
|
assert.match(css, /\.footer-tui-model[\s\S]*?text-align:\s*right/, "TUI-like footer should right-align model information on desktop");
|
|
@@ -268,6 +299,7 @@ assert.match(app, /async function createPathPickerDirectory\(\)/, "cwd picker sh
|
|
|
268
299
|
assert.match(app, /function renderPathPickerDirectoryList\(\)[\s\S]*pathPickerDirectoryMatchesSearch/, "cwd picker should filter current-directory entries in the browser");
|
|
269
300
|
assert.match(app, /elements\.pathPickerSearchInput\.addEventListener\("input", renderPathPickerDirectoryList\)/, "cwd picker should update directory matches as the user types");
|
|
270
301
|
assert.match(server, /async function createDirectoryPickerDirectory\(parentPath, nameValue, activeCwd\)/, "server should implement cwd picker directory creation");
|
|
302
|
+
assert.match(server, /function directoryPickerActiveCwd\(req, url, body = \{\}\)/, "server should let the cwd picker run before any Pi tabs exist");
|
|
271
303
|
assert.match(server, /url\.pathname === "\/api\/directories" && req\.method === "POST"/, "server should expose POST /api/directories for cwd picker directory creation");
|
|
272
304
|
assert.match(css, /@media \(max-width: 720px\), \(max-device-width: 720px\), \(pointer: coarse\) and \(hover: none\)[\s\S]*?\.footer-line-tui \{[\s\S]*?flex-wrap:\s*wrap/, "mobile footer should wrap the minimal TUI-like line instead of using expanded metadata chips");
|
|
273
305
|
assert.match(css, /(?:^|\n)\s*\.side-panel-backdrop\s*\{[\s\S]*?position:\s*fixed/, "mobile side panel backdrop should be fixed overlay UI");
|
|
@@ -290,6 +322,12 @@ assert.match(css, /\.release-dialog-success \{ color: var\(--ctp-green\); \}/, "
|
|
|
290
322
|
assert.match(css, /\.release-dialog-danger \{ color: var\(--ctp-red\); \}/, "release confirmation should color blocked/error lines as danger");
|
|
291
323
|
|
|
292
324
|
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");
|
|
325
|
+
assert.match(app, /const SIDE_PANEL_OVERLAY_QUERY = "\(max-width: 1050px\), \(max-device-width: 720px\), \(pointer: coarse\) and \(hover: none\)"/, "side-panel overlay mode should also activate at the stacked narrow layout breakpoint");
|
|
326
|
+
assert.match(app, /function isSidePanelOverlayView\(\)[\s\S]*sidePanelOverlayMedia\?\.matches/, "side-panel overlay detection should be separate from full mobile mode");
|
|
327
|
+
assert.match(app, /const showBackdrop = !collapsed && isSidePanelOverlayView\(\)/, "side-panel backdrop should show for the overlay breakpoint, not only phone layouts");
|
|
328
|
+
assert.match(app, /function restoreSidePanelState\(\) \{\n\s+if \(isSidePanelOverlayView\(\)\) \{\n\s+setSidePanelCollapsed\(true, \{ persist: false \}\);/, "side-panel should start collapsed in narrow overlay mode");
|
|
329
|
+
assert.match(app, /function bindSidePanelOverlayViewChanges\(\)/, "side-panel overlay breakpoint changes should be monitored separately from full mobile changes");
|
|
330
|
+
assert.match(app, /if \(isSidePanelOverlayView\(\) && !document\.body\.classList\.contains\("side-panel-collapsed"\)\)/, "Escape should close the side-panel overlay at narrow widths");
|
|
293
331
|
assert.match(app, /const THEME_STORAGE_KEY = "pi-webui-theme"/, "theme selection should be persisted in browser storage");
|
|
294
332
|
assert.match(app, /const CUSTOM_BACKGROUND_STORAGE_KEY = "pi-webui-custom-background"/, "custom backgrounds should keep a legacy persistent browser storage key for migration");
|
|
295
333
|
assert.match(app, /const CUSTOM_BACKGROUNDS_STORAGE_KEY = "pi-webui-custom-backgrounds"/, "custom backgrounds should be persisted per theme in browser storage");
|
|
@@ -323,6 +361,11 @@ assert.match(app, /case "webui_connected":[\s\S]*setWebuiVersion\(event\.version
|
|
|
323
361
|
assert.match(server, /const webuiDevServer = isTruthyEnv\(process\.env\.PI_WEBUI_DEV\) \|\| isSourceCheckout\(packageRoot\)/, "server should derive dev mode from PI_WEBUI_DEV or a source checkout");
|
|
324
362
|
assert.match(server, /webuiDev: webuiDevServer,[\s\S]*webuiMode: webuiDevServer \? "dev" : "production"/, "server status should expose Web UI dev mode");
|
|
325
363
|
assert.match(server, /type: "webui_connected",[\s\S]*webuiDev: webuiDevServer,[\s\S]*webuiMode: webuiDevServer \? "dev" : "production"/, "SSE connect event should expose Web UI dev mode");
|
|
364
|
+
assert.match(server, /async function validateStartupCwd\(cwd\)/, "server should validate startup cwd before spawning Pi");
|
|
365
|
+
assert.match(server, /--cwd does not exist:/, "server should report nonexistent startup cwd paths clearly");
|
|
366
|
+
assert.match(server, /options\.cwd = await validateStartupCwd\(options\.cwd\)/, "server should fail fast for invalid startup cwd paths");
|
|
367
|
+
assert.match(server, /cwdExplicit: false/, "server should track whether startup cwd was explicitly requested");
|
|
368
|
+
assert.match(server, /return options\.cwdExplicit \? \[await createTab\(\)\] : \[\]/, "server should wait for UI cwd selection when no --cwd is supplied");
|
|
326
369
|
assert.match(app, /serverActionSelect\.addEventListener\("change", updateServerActionButton\)/, "Server action dropdown should control the guarded run button");
|
|
327
370
|
assert.match(app, /runServerActionButton\.addEventListener\("click"[\s\S]*runSelectedServerAction/, "Server action run button should execute the selected action");
|
|
328
371
|
assert.match(app, /api\("\/api\/restart", \{ method: "POST", scoped: false \}\)/, "Restart Server action should call the unscoped restart endpoint");
|
|
@@ -342,11 +385,13 @@ assert.match(html, /<textarea id="promptInput"[^>]*autofocus/, "prompt composer
|
|
|
342
385
|
assert.match(app, /function syncMobileChatToBottomForInput\(\)/, "mobile input focus should force the output view to the latest message");
|
|
343
386
|
assert.match(app, /function focusPromptInput\(\{ defer = false \} = \{\}\)/, "frontend should focus the prompt composer programmatically after tab/app startup");
|
|
344
387
|
assert.match(app, /async function switchTab\(tabId\)[\s\S]*?restoreActiveDraft\(\);\n\s+focusPromptInput\(\{ defer: true \}\);/, "switching to a newly opened tab should focus the prompt input immediately");
|
|
345
|
-
assert.match(app, /async function initializeTabs\(\)[\s\S]*?restoreActiveDraft\(\)
|
|
388
|
+
assert.match(app, /async function initializeTabs\(\)[\s\S]*?restoreActiveDraft\(\);[\s\S]*if \(!loadedTabs\.length\)[\s\S]*focusPromptInput\(\{ defer: true \}\);/, "starting the Web UI should prompt for cwd when needed and focus active tabs");
|
|
346
389
|
assert.match(app, /resizePromptInput\(\);\nfocusPromptInput\(\{ defer: true \}\);\nupdateComposerModeButtons\(\);/, "startup should request prompt focus before waiting for tab state refreshes");
|
|
347
390
|
assert.match(app, /elements\.promptInput\.addEventListener\("focus", \(\) => \{\n\s+syncMobileChatToBottomForInput\(\);/, "focusing mobile input should scroll output to bottom");
|
|
348
391
|
assert.match(app, /navigator\.serviceWorker\.register\("\/service-worker\.js"\)/, "PWA service worker should be registered by the app");
|
|
349
|
-
assert.match(app, /function serverStartCommandText\(\)[\s\S]*pi-webui
|
|
392
|
+
assert.match(app, /function serverStartCommandText\(\)[\s\S]*return `pi-webui\$\{currentPortArg\(\)\}`/, "PWA/offline shell should build a pathless pi-webui recovery command");
|
|
393
|
+
assert.match(app, /async function createFirstTerminalTabFromChosenDirectory\(\)/, "frontend should prompt for the first terminal cwd when no tabs exist");
|
|
394
|
+
assert.match(app, /Choose CWD for first terminal/, "frontend should title the first-terminal cwd picker clearly");
|
|
350
395
|
assert.match(app, /Pi Web UI server is offline/, "PWA/offline shell should clearly report backend-down state");
|
|
351
396
|
assert.match(app, /navigator\.clipboard\.writeText\(text\)/, "backend-offline recovery panel should copy the start command when possible");
|
|
352
397
|
assert.match(app, /function messageCopyText\(message, body = null\)/, "frontend should derive copy text from transcript messages");
|
|
@@ -461,11 +506,45 @@ assert.match(app, /const releasePrompt = detectedReleasePrompt && isOptionalFeat
|
|
|
461
506
|
assert.match(app, /case "webui_tab_reloaded":[\s\S]*resetOptionalFeatureAvailability\(\)/, "optional feature state should reset when the RPC tab reloads resources");
|
|
462
507
|
assert.match(app, /function runPublishWorkflow\(command\)[\s\S]*hasAvailableCommand\(commandName\)/, "publish workflow launch should guard on loaded slash commands");
|
|
463
508
|
assert.match(app, /if \(!isOptionalFeatureEnabled\("gitWorkflow"\)\)/, "guided git workflow should guard on enabled /git-staged-msg feature");
|
|
509
|
+
assert.doesNotMatch(html, /gitWorkflowProcessSelect/, "guided git workflow should not expose process selection as a dropdown");
|
|
510
|
+
assert.match(app, /const GIT_WORKFLOW_PROCESSES = \[[\s\S]*value: "stage", label: "Stage"[\s\S]*value: "message", label: "Message"[\s\S]*value: "commit", label: "Commit"[\s\S]*value: "push", label: "Push"/, "guided git workflow should define Stage/Message/Commit/Push process buttons");
|
|
511
|
+
assert.match(app, /function selectGitWorkflowProcess\(processValue, tabId = gitWorkflowActionTabId\(\)\)[\s\S]*process === "commit"[\s\S]*loadGitWorkflowMessage\(\{ requireFresh: false, runId, tabId \}\)/, "guided git workflow process buttons should jump to Commit by loading current generated message files");
|
|
512
|
+
assert.match(app, /actionsDone: createGitWorkflowActionsDone\(\)/, "guided git workflow should track process completion separately from selected process");
|
|
513
|
+
assert.match(app, /if \(gitWorkflowActionDone\(gitWorkflow, process\.value\)\) item\.classList\.add\("done"\)/, "guided git workflow step pills should turn green only after their action is done");
|
|
514
|
+
assert.doesNotMatch(app, /index < activeIndex\) item\.classList\.add\("done"\)/, "guided git workflow step pills should not turn green merely because a later process was selected");
|
|
515
|
+
assert.match(app, /make\("button", "git-workflow-step", process\.label\)[\s\S]*item\.dataset\.gitWorkflowProcess = process\.value/, "guided git workflow step pills should render as clickable buttons");
|
|
516
|
+
assert.match(app, /gitWorkflowSteps\.addEventListener\("click"[\s\S]*data-git-workflow-process[\s\S]*selectGitWorkflowProcess\(button\.dataset\.gitWorkflowProcess\)/, "guided git workflow process buttons should select processes directly");
|
|
517
|
+
assert.match(css, /\.git-workflow-step::before \{[\s\S]*background:\s*var\(--ctp-yellow\)/, "guided git workflow step dots should stay yellow until that process action is done");
|
|
518
|
+
assert.match(css, /\.git-workflow-step\.done::before \{[\s\S]*background:\s*var\(--ctp-green\)/, "guided git workflow completed process dots should be green");
|
|
519
|
+
assert.match(css, /button\.git-workflow-step \{[\s\S]*min-height:\s*2\.15rem[\s\S]*box-shadow:/, "guided git workflow step buttons should keep pill button styling");
|
|
464
520
|
assert.match(app, /const gitWorkflowsByTab = new Map\(\)/, "guided git workflow state should be stored per terminal tab");
|
|
465
521
|
assert.match(app, /function bindGitWorkflowToActiveTab\(\) \{\n\s+gitWorkflow = gitWorkflowForTab\(activeTabId\) \|\| createGitWorkflowState\(\);/, "guided git workflow should render only the active terminal tab's workflow state");
|
|
466
522
|
assert.match(app, /function setGitWorkflow\(patch, \{ tabId = activeTabId \} = \{\}\)[\s\S]*if \(tabId === activeTabId\) \{[\s\S]*renderGitWorkflow\(\);/, "guided git workflow should not render inactive terminal workflows globally");
|
|
523
|
+
assert.match(html, /id="gitPrDialog"[\s\S]*id="gitPrTitleInput"[\s\S]*id="gitPrBodyEditor"[\s\S]*id="gitPrCreateButton"/, "guided git workflow should expose a PR review dialog with title and body editing");
|
|
524
|
+
assert.match(app, /addGitWorkflowAction\("Create PR", \(\) => createGitPrBranch\(\), "primary", false, GIT_WORKFLOW_CREATE_PR_TOOLTIP\)/, "guided git workflow should offer a Create PR branch action after message generation");
|
|
525
|
+
assert.match(app, /const GIT_WORKFLOW_CREATE_PR_TOOLTIP = \[[\s\S]*"1\. Ask Pi to generate a type\/feature-name branch from staged changes\."[\s\S]*"5\. Return here so you can choose Commit short or Commit long on that branch\."/, "Create PR should have a step-by-step tooltip");
|
|
526
|
+
assert.match(app, /const GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP = \[[\s\S]*"1\. Skip agent branch-name generation\."[\s\S]*"5\. Return here so you can choose Commit short or Commit long on that branch\."/, "Manual branch should have a step-by-step tooltip");
|
|
527
|
+
assert.match(app, /addGitWorkflowAction\("Manual branch", \(\) => createGitPrBranchManually\(\), "", false, GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP\)/, "Manual branch should render with its tooltip");
|
|
528
|
+
assert.match(css, /\.git-workflow-actions button\[data-tooltip\]::after \{[\s\S]*content:\s*attr\(data-tooltip\)[\s\S]*white-space:\s*pre-line/, "guided git workflow action tooltips should render multiline step lists");
|
|
529
|
+
assert.match(app, /function gitBranchNamePromptMessage\(\)[\s\S]*hasAvailableCommand\("git-branch-name"\)[\s\S]*return "\/git-branch-name"/, "guided git workflow should ask the agent to generate PR branch names when the prompt is available");
|
|
530
|
+
assert.match(app, /async function loadGitWorkflowBranchName\([\s\S]*gitWorkflowRequest\("\/api\/git-workflow\/branch-name"/, "guided git workflow should load generated agent branch names before branch creation");
|
|
531
|
+
assert.match(app, /async function createGitPrBranchWithSuggestion\([\s\S]*gitWorkflowRequest\("\/api\/git-workflow\/branch"/, "guided git workflow should confirm an agent-suggested branch before creating it");
|
|
532
|
+
assert.match(app, /addGitWorkflowAction\("Push and Create PR", \(\) => pushAndCreatePrGitWorkflow\(\), "primary", false\)/, "guided git workflow should replace push with Push and Create PR in PR mode");
|
|
533
|
+
assert.match(app, /async function runGitPrPrompt\(tabId = gitWorkflowActionTabId\(\), \{ prefixOutput = "" \} = \{\}\)[\s\S]*body: \{ message: "\/pr" \}/, "guided git workflow should generate PR descriptions with /pr");
|
|
534
|
+
assert.match(server, /case "\/api\/git-workflow\/branch-name":[\s\S]*readGitWorkflowBranchName\(cwd\)/, "server should expose generated branch-name file loading for the guided PR workflow");
|
|
535
|
+
assert.match(server, /case "\/api\/git-workflow\/branch":[\s\S]*\["switch", "-c", branch\]/, "server should expose branch creation for the guided PR workflow");
|
|
536
|
+
assert.match(server, /case "\/api\/git-workflow\/create-pr":[\s\S]*runGitHubWorkflowCommand\(\["pr", "create"/, "server should create PRs with the GitHub CLI after confirmation");
|
|
467
537
|
assert.doesNotMatch(app, /gitWorkflowVisibleTabId|Workflow belongs to/, "guided git workflow should not pin or show workflows outside their owning terminal tab");
|
|
468
538
|
assert.match(app, /function renderReleaseNpmOutputWidget\(\)/, "release-npm live output should use a specialized Web UI renderer");
|
|
539
|
+
assert.match(app, /async function refreshAppRunners\(tabContext = activeTabContext\(\)\)/, "frontend should load detected app runners for the active tab cwd");
|
|
540
|
+
assert.match(app, /function renderAppRunnerWidget\(\)/, "frontend should render app runner output in the shared top widget area");
|
|
541
|
+
assert.match(server, /url\.pathname === "\/api\/app-runners" && req\.method === "GET"/, "server should expose detected app runners for the active tab cwd");
|
|
542
|
+
assert.match(server, /url\.pathname === "\/api\/app-runner" && req\.method === "POST"/, "server should start selected app runners directly");
|
|
543
|
+
assert.match(server, /function addGoRunner\(runners, cwd\)[\s\S]*Go\/Golang app entry/, "server should detect Go\/Golang app runners");
|
|
544
|
+
assert.match(server, /function addZigRunner\(runners, cwd\)[\s\S]*zig build run[\s\S]*zig run/, "server should detect Zig build and entry-file runners");
|
|
545
|
+
assert.match(server, /function addCppRunners\(runners, cwd\)[\s\S]*C\/C\+\+ CMake executable target[\s\S]*language: "C\+\+"/, "server should detect C\/C++ CMake and entry-file runners");
|
|
546
|
+
assert.match(server, /function addDockerComposeRunner\(runners, cwd\)[\s\S]*docker compose up[\s\S]*docker-compose up/, "server should detect Docker Compose runners");
|
|
547
|
+
assert.match(server, /APP_RUNNER_SHELL_SCRIPT_DIRS = \["", "dev", "scripts", "dev\/scripts"\][\s\S]*function addShellScriptRunners\(runners, cwd\)/, "server should detect bash\/zsh\/fish scripts in root, dev, scripts, and dev\/scripts");
|
|
469
548
|
assert.match(app, /const releaseNpmOutputExpandedByTab = new Map\(\)/, "release-npm output collapse state should be tracked per browser tab");
|
|
470
549
|
assert.match(app, /function renderReleaseNpmOutputDetails\(key, streamHeader, terminal, controls = null\)[\s\S]*node\.open = releaseNpmOutputExpandedByTab\.get\(stateKey\) !== false[\s\S]*release-npm-output-toggle/, "release-npm output should render as a browser-side details expander");
|
|
471
550
|
assert.match(app, /releaseNpmStreamHeader\("Live output stream", outputLines\.length, \{ live: true \}\)/, "release-npm live output should expose a clear stream heading");
|
|
@@ -572,6 +651,9 @@ assert.match(app, /case "tool_execution_update":[\s\S]*?handleToolExecutionUpdat
|
|
|
572
651
|
assert.match(app, /case "auto_retry_start":[\s\S]*?addTransientMessage\(\{ role: "warn", title: "auto retry"/, "auto-retry starts should be transcript-visible warnings");
|
|
573
652
|
assert.match(app, /case "extension_error":[\s\S]*?addTransientMessage\(\{ role: "error", title: "extension error"/, "extension errors should be transcript-visible error cards");
|
|
574
653
|
assert.match(app, /setRunIndicatorActivity\("Requesting context compaction…"\);\n\s+scrollChatToBottom\(\{ force: true \}\);/, "manual compaction should force-follow the transcript to the bottom status card");
|
|
654
|
+
assert.match(app, /function markContextUsageUnknownAfterCompaction\(/, "compaction should have a dedicated context-usage invalidation helper");
|
|
655
|
+
assert.match(app, /case "compaction_end":[\s\S]*?markContextUsageUnknownAfterCompaction\(event\.tabId \|\| activeTabId\)/, "finished compaction should make footer context usage unknown instead of showing stale pressure");
|
|
656
|
+
assert.match(app, /function footerStatsContextDisplay[\s\S]*?contextUsageUnknownAfterCompaction\(\)[\s\S]*?unknownFooterContextText/, "fallback footer context should show an unknown value after compaction invalidates usage");
|
|
575
657
|
assert.match(app, /case "agent_end":[\s\S]*?clearRunIndicatorActivity\(\)/, "agent completion should remove the active agent transcript indicator");
|
|
576
658
|
assert.match(app, /case "agent_end":[\s\S]*?notifyAgentDone\(event\.tabId \|\| activeTabId/, "agent completion should trigger optional done notifications");
|
|
577
659
|
assert.match(app, /function getPathTrigger\(\)/, "prompt composer should detect @ file\/path reference triggers");
|
|
@@ -730,7 +812,7 @@ assert.match(app, /api\("\/api\/thinking", \{ method: "POST", body: \{ level: ne
|
|
|
730
812
|
assert.match(app, /function isFooterPickerOpen\(\)[\s\S]*?footerModelPickerOpen \|\| footerThinkingPickerOpen/, "footer picker overlay state should cover model and thinking pickers");
|
|
731
813
|
assert.doesNotMatch(app.match(/function renderMinimalFooter\(\)[\s\S]*?\n\}/)?.[0] || "", /footer-details-toggle/, "minimal default footer should not render a details toggle chip");
|
|
732
814
|
assert.match(app, /bindMobileViewChanges\(/, "side panel state should react to mobile breakpoint changes");
|
|
733
|
-
assert.match(app, /function restoreSidePanelState\(\) \{\n\s+if \(
|
|
815
|
+
assert.match(app, /function restoreSidePanelState\(\) \{\n\s+if \(isSidePanelOverlayView\(\)\)/, "mobile and narrow overlay layouts should start with side panel collapsed even if desktop state was expanded");
|
|
734
816
|
assert.match(app, /case "webui_tab_reloaded":/, "frontend should handle native /reload tab restart events");
|
|
735
817
|
assert.match(app, /addTransientMessage\(\{ role: "native", title: "\/reload"/, "native /reload should produce visible transcript output");
|
|
736
818
|
assert.match(app, /await copyText\(response\.data\.copyText\)/, "native /copy should use the shared browser clipboard helper when available");
|
|
@@ -744,7 +826,7 @@ assert.equal(manifest.start_url, "/", "PWA manifest should start at the web UI r
|
|
|
744
826
|
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");
|
|
745
827
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-192.png" && icon.sizes === "192x192"), "PWA manifest should include a 192px icon");
|
|
746
828
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-512.png" && icon.sizes === "512x512"), "PWA manifest should include a 512px icon");
|
|
747
|
-
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-
|
|
829
|
+
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v25"/, "PWA service worker should define an app-shell cache");
|
|
748
830
|
assert.match(serviceWorker, /self\.addEventListener\("notificationclick"/, "PWA service worker should focus Web UI when blocked-tab notifications are clicked");
|
|
749
831
|
assert.match(serviceWorker, /event\.notification\.data\?\.url/, "blocked-tab notifications should carry a URL for service-worker click handling");
|
|
750
832
|
assert.match(serviceWorker, /"\/apple-touch-icon\.png"/, "PWA service worker should cache the apple touch icon");
|