@firstpick/pi-package-webui 0.3.1 → 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.
@@ -41,6 +41,11 @@ assert.match(html, /<meta name="theme-color" content="#11111b" \/>/, "PWA should
41
41
  assert.match(html, /<link rel="manifest" href="\/manifest\.webmanifest" \/>/, "PWA should expose a web app manifest");
42
42
  assert.match(html, /<link rel="apple-touch-icon" href="\/apple-touch-icon\.png" \/>/, "PWA should expose the conventional iOS home-screen icon path");
43
43
  assert.match(html, /id="terminalTabsToggleButton"/, "mobile should expose a compact terminal-tabs toggle");
44
+ assert.match(html, /id="newTabMenu" class="terminal-new-tab-menu composer-publish-menu"/, "new-tab control should reuse the shared composer dropdown container");
45
+ assert.match(html, /id="newTabButton"[\s\S]*aria-haspopup="menu"[\s\S]*aria-controls="newTabMenuPanel"/, "new-tab control should open a dropdown menu");
46
+ assert.match(html, /id="newTabMenuPanel" class="terminal-new-tab-menu-panel composer-publish-menu-panel"/, "new-tab menu should reuse the shared composer dropdown panel");
47
+ assert.match(html, /id="newTabCurrentDirectoryButton" class="terminal-new-tab-menu-item composer-publish-menu-item"[\s\S]*<span>Current Directory<\/span>/, "new-tab menu should offer the active cwd");
48
+ assert.match(html, /id="newTabChooseDirectoryButton" class="terminal-new-tab-menu-item composer-publish-menu-item"[\s\S]*<span>Choose Directory<\/span>/, "new-tab menu should offer the cwd picker");
44
49
  assert.match(html, /id="closeAllTabsButton"[\s\S]*?>Close all Tabs<\/button>/, "tab header should expose a top-right close-all tabs action");
45
50
  assert.match(html, /id="sidePanelBackdrop"/, "mobile side panel needs an overlay/backdrop close target");
46
51
  assert.match(html, /<strong class="side-panel-title">[\s\S]*Control Deck[\s\S]*id="webuiVersionBadge"[\s\S]*id="webuiDevBadge"/, "Control Deck title should expose Web UI version and dev badges");
@@ -99,6 +104,15 @@ assert.match(html, /id="stickyUserPromptButton"/, "chat should expose a fixed la
99
104
  assert.match(html, /id="feedbackTray"/, "chat should expose a queued action-feedback tray");
100
105
  assert.match(html, /id="sendFeedbackButton"/, "action feedback should be submittable after the agent finishes");
101
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");
102
116
  assert.match(html, /id="composerActionsButton"/, "mobile composer should expose a compact actions trigger");
103
117
  assert.match(html, /id="composerActionsPanel"/, "secondary composer controls should live in a mobile actions panel");
104
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");
@@ -111,6 +125,16 @@ assert.match(html, /id="nativeCommandMenuButton"[\s\S]*?aria-controls="nativeCom
111
125
  assert.ok(html.indexOf('id="publishButton"') < html.indexOf('id="nativeCommandMenuButton"'), "skills/tools command menu should render immediately after the Publish workflow button");
112
126
  assert.match(html, /id="nativeSkillsButton"[^>]*data-command="\/skills"[\s\S]*?<span>Skills Setup<\/span>/, "skills/tools command menu should include Skills Setup");
113
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");
114
138
  assert.doesNotMatch(html, /<code>\/release-(?:npm|aur)<\/code>/, "Publish menu should not show slash command names as option labels");
115
139
  assert.doesNotMatch(html, /data-tooltip="[^"]*\/release-(?:npm|aur)/, "Publish tooltip should not show slash command names");
116
140
  assert.match(html, /id="steerButton"[\s\S]*?data-tooltip="Steer usage:/, "Steer should explain type-first usage in a tooltip");
@@ -134,6 +158,12 @@ assert.match(css, /\.path-picker-create-button:hover,[\s\S]*?var\(--ctp-blue\)/,
134
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");
135
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");
136
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");
137
167
  assert.match(css, /button, select, input \{ min-height: 44px; \}/, "base controls should meet 44px touch-target height");
138
168
  assert.match(css, /\.composer-row button[\s\S]*?min-height:\s*44px/, "mobile composer buttons should keep 44px touch targets");
139
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");
@@ -158,6 +188,8 @@ assert.match(css, /\.markdown-body \{[\s\S]*?line-height:/, "assistant Markdown
158
188
  assert.match(css, /\.markdown-table-wrapper \{[\s\S]*?overflow-x:\s*auto/, "assistant Markdown tables should be horizontally scrollable on narrow screens");
159
189
  assert.match(css, /\.tool-result-preview \{[\s\S]*?padding:/, "collapsed tool results should show a preview area by default");
160
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");
161
193
  assert.match(css, /\.run-indicator-pulse \{[\s\S]*?animation:\s*run-indicator-pulse/, "active agent run indicator should have an animated pulse");
162
194
  assert.match(css, /\.optional-features-box \{[\s\S]*?display:\s*grid/, "optional features should render as a side-panel feature list");
163
195
  assert.match(css, /\.prompt-list-controls \{[\s\S]*?display:\s*grid/, "Queue prompt-list controls should render as a side-panel control group");
@@ -185,6 +217,10 @@ assert.match(css, /\.widget-area:has\(\.release-npm-live-widget \.release-npm-ou
185
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");
186
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");
187
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");
188
224
  assert.match(css, /\.widget-area \.widget:not\(\.todo-widget\):not\(\.release-npm-widget\)/, "mobile widget filtering should keep release workflow output visible");
189
225
  assert.match(css, /\.message\.warn \.message-role \{ color: var\(--ctp-yellow\); \}/, "warning-level command output should be visually distinct");
190
226
  assert.match(css, /\.commands-box \{[\s\S]*?max-height:\s*min\(32rem, 52vh\)/, "side-panel commands should use expanded viewport-aware height");
@@ -202,6 +238,7 @@ assert.match(css, /\.action-feedback-controls:hover,[\s\S]*?\.action-feedback-co
202
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");
203
239
  assert.match(css, /\.action-feedback-button\.feedback-question\.active/, "question-mark reaction should have selected styling");
204
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");
205
242
  assert.match(css, /\.composer-row button\[data-tooltip\]\.tooltip-open::after/, "composer button tooltips should be triggerable from JS for empty mobile taps");
206
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");
207
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");
@@ -216,6 +253,8 @@ assert.match(css, /\.composer-actions-panel > \.composer-publish-menu[\s\S]*?gri
216
253
  assert.match(css, /\.composer-actions-panel[\s\S]*?bottom:\s*calc\(100% \+ 0\.42rem\)/, "mobile composer actions should open as an above-composer sheet");
217
254
  assert.match(css, /body\.composer-actions-open \.composer-actions-panel \{ display: grid; \}/, "composer actions panel should only open when toggled");
218
255
  assert.match(css, /\.terminal-tabs-toggle-button \{ display: none; \}/, "terminal tab toggle should be hidden outside mobile CSS");
256
+ assert.match(css, /\.terminal-new-tab-menu \.composer-publish-menu-panel \{[\s\S]*?inset:\s*100% 0 auto auto;[\s\S]*?padding-top:\s*0\.38rem/, "new-tab dropdown should reuse the shared composer panel and open below the tab bar");
257
+ assert.match(css, /\.terminal-new-tab-menu \.composer-publish-menu-item \{[\s\S]*?color:\s*var\(--ctp-pink\)/, "new-tab dropdown items should reuse shared composer menu items with a tab-specific color");
219
258
  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");
220
259
  assert.match(css, /\.terminal-tabs\.terminal-tabs-dense \{[\s\S]*?flex-wrap:\s*wrap/, "large terminal tab sets should wrap into a readable dense tab strip");
221
260
  assert.match(css, /\.terminal-tab-group-close \{[\s\S]*?border-left-color/, "terminal tab groups should style their close button distinctly");
@@ -238,18 +277,17 @@ assert.match(css, /\.server-offline-panel/, "PWA/offline shell should style a ba
238
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");
239
278
  assert.match(css, /button\[hidden\] \{ display: none !important; \}/, "hidden bottom-row controls should not occupy layout space");
240
279
  assert.match(css, /\.statusbar-tui-footer \{[\s\S]*?gap:\s*0/, "default TUI-like footer should reduce statusbar chrome around the compact line");
241
- assert.match(css, /\.statusbar-git-footer \{[\s\S]*?gap:\s*0\.58rem/, "enabled git-footer extension should keep the styled Web UI footer spacing");
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");
242
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");
243
- assert.match(css, /\.footer-line-meta \.footer-meta \{[\s\S]*?flex:\s*0 1 max-content/, "git-footer non-cwd boxes should shrink to content-sized chips while preferring complete text");
244
- 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");
245
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");
246
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");
247
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");
248
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");
249
- 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");
250
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");
251
289
  assert.doesNotMatch(css, /\.footer-meta-action::after/, "clickable footer boxes should not show a corner indicator dot");
252
- assert.match(css, /@media \(max-width: 1050px\)[\s\S]*?\.footer-line-meta \{[\s\S]*?display:\s*grid;[\s\S]*?\.footer-model \{ grid-column: 1; \}[\s\S]*?\.footer-thinking \{ grid-column: 2; \}/, "narrow git-footer metadata should keep model and effort on the same row");
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");
253
291
  assert.match(css, /\.footer-line-tui \{[\s\S]*?white-space:\s*nowrap/, "default Web UI footer should use a minimal TUI-like line");
254
292
  assert.match(css, /\.footer-tui-cwd[\s\S]*?max-width:\s*38%/, "TUI-like footer should keep cwd compact on desktop");
255
293
  assert.match(css, /\.footer-tui-model[\s\S]*?text-align:\s*right/, "TUI-like footer should right-align model information on desktop");
@@ -261,6 +299,7 @@ assert.match(app, /async function createPathPickerDirectory\(\)/, "cwd picker sh
261
299
  assert.match(app, /function renderPathPickerDirectoryList\(\)[\s\S]*pathPickerDirectoryMatchesSearch/, "cwd picker should filter current-directory entries in the browser");
262
300
  assert.match(app, /elements\.pathPickerSearchInput\.addEventListener\("input", renderPathPickerDirectoryList\)/, "cwd picker should update directory matches as the user types");
263
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");
264
303
  assert.match(server, /url\.pathname === "\/api\/directories" && req\.method === "POST"/, "server should expose POST /api/directories for cwd picker directory creation");
265
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");
266
305
  assert.match(css, /(?:^|\n)\s*\.side-panel-backdrop\s*\{[\s\S]*?position:\s*fixed/, "mobile side panel backdrop should be fixed overlay UI");
@@ -283,6 +322,12 @@ assert.match(css, /\.release-dialog-success \{ color: var\(--ctp-green\); \}/, "
283
322
  assert.match(css, /\.release-dialog-danger \{ color: var\(--ctp-red\); \}/, "release confirmation should color blocked/error lines as danger");
284
323
 
285
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");
286
331
  assert.match(app, /const THEME_STORAGE_KEY = "pi-webui-theme"/, "theme selection should be persisted in browser storage");
287
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");
288
333
  assert.match(app, /const CUSTOM_BACKGROUNDS_STORAGE_KEY = "pi-webui-custom-backgrounds"/, "custom backgrounds should be persisted per theme in browser storage");
@@ -316,6 +361,11 @@ assert.match(app, /case "webui_connected":[\s\S]*setWebuiVersion\(event\.version
316
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");
317
362
  assert.match(server, /webuiDev: webuiDevServer,[\s\S]*webuiMode: webuiDevServer \? "dev" : "production"/, "server status should expose Web UI dev mode");
318
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");
319
369
  assert.match(app, /serverActionSelect\.addEventListener\("change", updateServerActionButton\)/, "Server action dropdown should control the guarded run button");
320
370
  assert.match(app, /runServerActionButton\.addEventListener\("click"[\s\S]*runSelectedServerAction/, "Server action run button should execute the selected action");
321
371
  assert.match(app, /api\("\/api\/restart", \{ method: "POST", scoped: false \}\)/, "Restart Server action should call the unscoped restart endpoint");
@@ -335,11 +385,13 @@ assert.match(html, /<textarea id="promptInput"[^>]*autofocus/, "prompt composer
335
385
  assert.match(app, /function syncMobileChatToBottomForInput\(\)/, "mobile input focus should force the output view to the latest message");
336
386
  assert.match(app, /function focusPromptInput\(\{ defer = false \} = \{\}\)/, "frontend should focus the prompt composer programmatically after tab/app startup");
337
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");
338
- assert.match(app, /async function initializeTabs\(\)[\s\S]*?restoreActiveDraft\(\);\n\s+focusPromptInput\(\{ defer: true \}\);/, "starting the Web UI should focus the prompt input after restoring the active tab");
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");
339
389
  assert.match(app, /resizePromptInput\(\);\nfocusPromptInput\(\{ defer: true \}\);\nupdateComposerModeButtons\(\);/, "startup should request prompt focus before waiting for tab state refreshes");
340
390
  assert.match(app, /elements\.promptInput\.addEventListener\("focus", \(\) => \{\n\s+syncMobileChatToBottomForInput\(\);/, "focusing mobile input should scroll output to bottom");
341
391
  assert.match(app, /navigator\.serviceWorker\.register\("\/service-worker\.js"\)/, "PWA service worker should be registered by the app");
342
- assert.match(app, /function serverStartCommandText\(\)[\s\S]*pi-webui --cwd/, "PWA/offline shell should build a pi-webui --cwd recovery command");
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");
343
395
  assert.match(app, /Pi Web UI server is offline/, "PWA/offline shell should clearly report backend-down state");
344
396
  assert.match(app, /navigator\.clipboard\.writeText\(text\)/, "backend-offline recovery panel should copy the start command when possible");
345
397
  assert.match(app, /function messageCopyText\(message, body = null\)/, "frontend should derive copy text from transcript messages");
@@ -454,11 +506,45 @@ assert.match(app, /const releasePrompt = detectedReleasePrompt && isOptionalFeat
454
506
  assert.match(app, /case "webui_tab_reloaded":[\s\S]*resetOptionalFeatureAvailability\(\)/, "optional feature state should reset when the RPC tab reloads resources");
455
507
  assert.match(app, /function runPublishWorkflow\(command\)[\s\S]*hasAvailableCommand\(commandName\)/, "publish workflow launch should guard on loaded slash commands");
456
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");
457
520
  assert.match(app, /const gitWorkflowsByTab = new Map\(\)/, "guided git workflow state should be stored per terminal tab");
458
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");
459
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");
460
537
  assert.doesNotMatch(app, /gitWorkflowVisibleTabId|Workflow belongs to/, "guided git workflow should not pin or show workflows outside their owning terminal tab");
461
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");
462
548
  assert.match(app, /const releaseNpmOutputExpandedByTab = new Map\(\)/, "release-npm output collapse state should be tracked per browser tab");
463
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");
464
550
  assert.match(app, /releaseNpmStreamHeader\("Live output stream", outputLines\.length, \{ live: true \}\)/, "release-npm live output should expose a clear stream heading");
@@ -565,6 +651,9 @@ assert.match(app, /case "tool_execution_update":[\s\S]*?handleToolExecutionUpdat
565
651
  assert.match(app, /case "auto_retry_start":[\s\S]*?addTransientMessage\(\{ role: "warn", title: "auto retry"/, "auto-retry starts should be transcript-visible warnings");
566
652
  assert.match(app, /case "extension_error":[\s\S]*?addTransientMessage\(\{ role: "error", title: "extension error"/, "extension errors should be transcript-visible error cards");
567
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");
568
657
  assert.match(app, /case "agent_end":[\s\S]*?clearRunIndicatorActivity\(\)/, "agent completion should remove the active agent transcript indicator");
569
658
  assert.match(app, /case "agent_end":[\s\S]*?notifyAgentDone\(event\.tabId \|\| activeTabId/, "agent completion should trigger optional done notifications");
570
659
  assert.match(app, /function getPathTrigger\(\)/, "prompt composer should detect @ file\/path reference triggers");
@@ -723,7 +812,7 @@ assert.match(app, /api\("\/api\/thinking", \{ method: "POST", body: \{ level: ne
723
812
  assert.match(app, /function isFooterPickerOpen\(\)[\s\S]*?footerModelPickerOpen \|\| footerThinkingPickerOpen/, "footer picker overlay state should cover model and thinking pickers");
724
813
  assert.doesNotMatch(app.match(/function renderMinimalFooter\(\)[\s\S]*?\n\}/)?.[0] || "", /footer-details-toggle/, "minimal default footer should not render a details toggle chip");
725
814
  assert.match(app, /bindMobileViewChanges\(/, "side panel state should react to mobile breakpoint changes");
726
- assert.match(app, /function restoreSidePanelState\(\) \{\n\s+if \(isMobileView\(\)\)/, "mobile should start with side panel collapsed even if desktop state was expanded");
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");
727
816
  assert.match(app, /case "webui_tab_reloaded":/, "frontend should handle native /reload tab restart events");
728
817
  assert.match(app, /addTransientMessage\(\{ role: "native", title: "\/reload"/, "native /reload should produce visible transcript output");
729
818
  assert.match(app, /await copyText\(response\.data\.copyText\)/, "native /copy should use the shared browser clipboard helper when available");
@@ -737,7 +826,7 @@ assert.equal(manifest.start_url, "/", "PWA manifest should start at the web UI r
737
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");
738
827
  assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-192.png" && icon.sizes === "192x192"), "PWA manifest should include a 192px icon");
739
828
  assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-512.png" && icon.sizes === "512x512"), "PWA manifest should include a 512px icon");
740
- assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v23"/, "PWA service worker should define an app-shell cache");
829
+ assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v25"/, "PWA service worker should define an app-shell cache");
741
830
  assert.match(serviceWorker, /self\.addEventListener\("notificationclick"/, "PWA service worker should focus Web UI when blocked-tab notifications are clicked");
742
831
  assert.match(serviceWorker, /event\.notification\.data\?\.url/, "blocked-tab notifications should carry a URL for service-worker click handling");
743
832
  assert.match(serviceWorker, /"\/apple-touch-icon\.png"/, "PWA service worker should cache the apple touch icon");