@firstpick/pi-package-webui 0.3.2 → 0.3.4

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.
@@ -61,6 +61,8 @@ assert.match(html, /id="agentDoneNotificationsToggle"/, "side panel should expos
61
61
  assert.match(html, /id="agentDoneNotificationsStatus"/, "agent-done notifications toggle should expose status text");
62
62
  assert.match(html, /id="thinkingVisibilityToggle"/, "side panel should expose a thinking-output visibility toggle");
63
63
  assert.match(html, /id="thinkingVisibilityStatus"/, "thinking-output visibility toggle should expose status text");
64
+ assert.match(html, /id="terminalTabsLayoutSelect"[\s\S]*<option value="left">Left sidebar<\/option>/, "side panel controls should expose a terminal-tabs layout selector");
65
+ assert.match(html, /id="terminalTabsLayoutStatus"/, "terminal-tabs layout selector should expose status text");
64
66
  assert.match(html, /id="nativeCommandDialog"/, "native slash selector UI should have a dedicated dialog");
65
67
  assert.match(html, /id="nativeCommandSearch"[^>]*type="search"/, "native slash selector dialog should expose a filter box");
66
68
  assert.match(html, /id="pathPickerCreateNameInput"[^>]*placeholder="New directory name"/, "cwd picker should expose a new-directory name input");
@@ -104,6 +106,15 @@ assert.match(html, /id="stickyUserPromptButton"/, "chat should expose a fixed la
104
106
  assert.match(html, /id="feedbackTray"/, "chat should expose a queued action-feedback tray");
105
107
  assert.match(html, /id="sendFeedbackButton"/, "action feedback should be submittable after the agent finishes");
106
108
  assert.match(html, /<textarea id="promptInput"[^>]*rows="1"[^>]*enterkeyhint="enter"/, "prompt textarea should start at one row and hint that Return inserts a newline");
109
+ assert.match(app, /const LONG_INPUT_ATTACHMENT_LINE_THRESHOLD = 20/, "long composer text should use a 20-line threshold before becoming an attachment");
110
+ assert.match(app, /function attachLongTextAsFile\(text, source = "input text"\)/, "long composer text should be attachable as a generated text file");
111
+ 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");
112
+ assert.match(app, /promptInput\.addEventListener\("input", \(\) => \{[\s\S]*moveLongPromptInputToAttachment\(\)/, "long typed composer text should move into an attachment and clear the textarea");
113
+ assert.match(html, /id="attachmentTextDialog"[\s\S]*id="attachmentTextEditor"/, "text attachments should have an in-Web UI editing dialog");
114
+ assert.match(app, /attachment-edit-button[\s\S]*openTextAttachmentEditor\(attachment\.id\)/, "editable text attachments should expose an Edit action in the tray");
115
+ assert.match(app, /function saveTextAttachmentEdit\(\)[\s\S]*attachment\.file = nextFile/, "text attachment dialog should save edits back to the attachment file");
116
+ 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");
117
+ assert.match(css, /\.attachment-text-dialog[\s\S]*\.attachment-text-editor/, "text attachment editor should have dedicated dialog styling");
107
118
  assert.match(html, /id="composerActionsButton"/, "mobile composer should expose a compact actions trigger");
108
119
  assert.match(html, /id="composerActionsPanel"/, "secondary composer controls should live in a mobile actions panel");
109
120
  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 +127,16 @@ assert.match(html, /id="nativeCommandMenuButton"[\s\S]*?aria-controls="nativeCom
116
127
  assert.ok(html.indexOf('id="publishButton"') < html.indexOf('id="nativeCommandMenuButton"'), "skills/tools command menu should render immediately after the Publish workflow button");
117
128
  assert.match(html, /id="nativeSkillsButton"[^>]*data-command="\/skills"[\s\S]*?<span>Skills Setup<\/span>/, "skills/tools command menu should include Skills Setup");
118
129
  assert.match(html, /id="nativeToolsButton"[^>]*data-command="\/tools"[\s\S]*?<span>Tools Setup<\/span>/, "skills/tools command menu should include Tools Setup");
130
+ assert.match(html, /id="appRunnerInfoButton"[\s\S]*?aria-controls="appRunnerInfoDialog"/, "detected app-runner controls should expose an explanation popup button");
131
+ assert.match(html, /id="appRunnerMenuButton"[\s\S]*?aria-controls="appRunnerMenuPanel"/, "composer should expose a detected app-runner dropdown button");
132
+ assert.ok(html.indexOf('id="optionsMenuButton"') < html.indexOf('id="appRunnerMenuButton"'), "app-runner dropdown should render to the right of the settings/options button");
133
+ assert.match(html, /id="appRunnerMenuPanel"[^>]*aria-label="Detected app runners"/, "app-runner dropdown should render detected runner choices only from JS data");
134
+ assert.match(html, /id="appRunnerInfoDialog"[\s\S]*id="appRunnerInfoBody"/, "app-runner explanation popup should have a dynamic details body");
135
+ assert.match(app, /\.pi-webui-runners\.json/, "app-runner popup should explain the project-local custom runner config file");
136
+ assert.match(app, /appRunnerCustomPathInput[\s\S]*Browse/, "custom app-runner path should be browseable from the popup");
137
+ assert.match(server, /APP_RUNNER_CONFIG_FILE = "\.pi-webui-runners\.json"/, "server should use a project-local custom app-runner config file");
138
+ assert.match(server, /\/api\/app-runner-config/, "server should expose custom app-runner config endpoints");
139
+ assert.match(server, /\/api\/app-runner-files/, "server should expose project-scoped file browsing for custom runner paths");
119
140
  assert.doesNotMatch(html, /<code>\/release-(?:npm|aur)<\/code>/, "Publish menu should not show slash command names as option labels");
120
141
  assert.doesNotMatch(html, /data-tooltip="[^"]*\/release-(?:npm|aur)/, "Publish tooltip should not show slash command names");
121
142
  assert.match(html, /id="steerButton"[\s\S]*?data-tooltip="Steer usage:/, "Steer should explain type-first usage in a tooltip");
@@ -139,6 +160,12 @@ assert.match(css, /\.path-picker-create-button:hover,[\s\S]*?var\(--ctp-blue\)/,
139
160
  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
161
  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
162
  assert.match(css, /height:\s*var\(--visual-viewport-height, 100dvh\)/, "layout should consume visual viewport height");
163
+ 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");
164
+ 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");
165
+ 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");
166
+ 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");
167
+ 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");
168
+ 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
169
  assert.match(css, /button, select, input \{ min-height: 44px; \}/, "base controls should meet 44px touch-target height");
143
170
  assert.match(css, /\.composer-row button[\s\S]*?min-height:\s*44px/, "mobile composer buttons should keep 44px touch targets");
144
171
  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 +190,8 @@ assert.match(css, /\.markdown-body \{[\s\S]*?line-height:/, "assistant Markdown
163
190
  assert.match(css, /\.markdown-table-wrapper \{[\s\S]*?overflow-x:\s*auto/, "assistant Markdown tables should be horizontally scrollable on narrow screens");
164
191
  assert.match(css, /\.tool-result-preview \{[\s\S]*?padding:/, "collapsed tool results should show a preview area by default");
165
192
  assert.match(css, /\.message-collapse\[open\] \+ \.tool-result-preview \{[\s\S]*?display:\s*none/, "tool result preview should hide when full output is expanded");
193
+ 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");
194
+ 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
195
  assert.match(css, /\.run-indicator-pulse \{[\s\S]*?animation:\s*run-indicator-pulse/, "active agent run indicator should have an animated pulse");
167
196
  assert.match(css, /\.optional-features-box \{[\s\S]*?display:\s*grid/, "optional features should render as a side-panel feature list");
168
197
  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 +219,10 @@ assert.match(css, /\.widget-area:has\(\.release-npm-live-widget \.release-npm-ou
190
219
  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
220
  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
221
  assert.match(css, /\.release-aur-widget \{[\s\S]*?border-color/, "release-aur output should render as a specialized Web UI widget variant");
222
+ assert.match(css, /\.app-runner-widget \{[\s\S]*?border-color/, "app runner output should render as a specialized Web UI widget variant");
223
+ 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");
224
+ 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");
225
+ 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
226
  assert.match(css, /\.widget-area \.widget:not\(\.todo-widget\):not\(\.release-npm-widget\)/, "mobile widget filtering should keep release workflow output visible");
194
227
  assert.match(css, /\.message\.warn \.message-role \{ color: var\(--ctp-yellow\); \}/, "warning-level command output should be visually distinct");
195
228
  assert.match(css, /\.commands-box \{[\s\S]*?max-height:\s*min\(32rem, 52vh\)/, "side-panel commands should use expanded viewport-aware height");
@@ -207,6 +240,7 @@ assert.match(css, /\.action-feedback-controls:hover,[\s\S]*?\.action-feedback-co
207
240
  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
241
  assert.match(css, /\.action-feedback-button\.feedback-question\.active/, "question-mark reaction should have selected styling");
209
242
  assert.match(css, /\.composer-row button\[data-tooltip\]::after/, "composer button tooltips should be shared across Git, Steer, and Follow-up buttons");
243
+ 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
244
  assert.match(css, /\.composer-row button\[data-tooltip\]\.tooltip-open::after/, "composer button tooltips should be triggerable from JS for empty mobile taps");
211
245
  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
246
  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");
@@ -221,6 +255,11 @@ assert.match(css, /\.composer-actions-panel > \.composer-publish-menu[\s\S]*?gri
221
255
  assert.match(css, /\.composer-actions-panel[\s\S]*?bottom:\s*calc\(100% \+ 0\.42rem\)/, "mobile composer actions should open as an above-composer sheet");
222
256
  assert.match(css, /body\.composer-actions-open \.composer-actions-panel \{ display: grid; \}/, "composer actions panel should only open when toggled");
223
257
  assert.match(css, /\.terminal-tabs-toggle-button \{ display: none; \}/, "terminal tab toggle should be hidden outside mobile CSS");
258
+ assert.match(css, /body\.terminal-tabs-left \.chat-panel \{[\s\S]*?grid-template-columns:\s*clamp\(13rem, 18vw, 19rem\) minmax\(0, 1fr\)/, "terminal tabs left layout should split the chat panel into a sidebar and transcript area");
259
+ assert.match(css, /body\.terminal-tabs-left \.terminal-tabs-shell \{[\s\S]*?grid-column:\s*1;[\s\S]*?grid-row:\s*1 \/ -1;[\s\S]*?flex-direction:\s*column/, "terminal tabs left layout should turn the top tab strip into a vertical sidebar");
260
+ assert.match(css, /body\.terminal-tabs-left \.terminal-tabs \{[\s\S]*?flex-direction:\s*column/, "terminal tabs left layout should stack tabs vertically");
261
+ assert.match(css, /body\.terminal-tabs-left \.terminal-tab-group-menu \{[\s\S]*?inset:\s*0 auto auto 100%;[\s\S]*?padding-left:\s*var\(--terminal-left-dropdown-bridge\)/, "left-sidebar grouped tab menus should include a hover bridge so they do not vanish between button and dropdown");
262
+ assert.match(css, /body\.terminal-tabs-left \.terminal-new-tab-menu \.composer-publish-menu-panel \{[\s\S]*?inset:\s*0 auto auto 100%;[\s\S]*?padding-left:\s*var\(--terminal-left-dropdown-bridge\)/, "left-sidebar new-tab dropdown should include a hover bridge so it does not vanish between button and dropdown");
224
263
  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");
225
264
  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");
226
265
  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");
@@ -245,18 +284,17 @@ assert.match(css, /\.server-offline-panel/, "PWA/offline shell should style a ba
245
284
  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
285
  assert.match(css, /button\[hidden\] \{ display: none !important; \}/, "hidden bottom-row controls should not occupy layout space");
247
286
  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 the styled Web UI footer spacing");
287
+ 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");
288
+ 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
289
  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*0 1 max-content/, "git-footer non-cwd boxes should shrink to content-sized chips while preferring complete text");
251
- assert.match(css, /\.footer-workspace \{[\s\S]*?flex:\s*1 1 8rem/, "git-footer cwd chip should dynamically take remaining row space");
290
+ 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
291
  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
292
  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
293
  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
294
  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
295
  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
296
  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*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");
297
+ 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
298
  assert.match(css, /\.footer-line-tui \{[\s\S]*?white-space:\s*nowrap/, "default Web UI footer should use a minimal TUI-like line");
261
299
  assert.match(css, /\.footer-tui-cwd[\s\S]*?max-width:\s*38%/, "TUI-like footer should keep cwd compact on desktop");
262
300
  assert.match(css, /\.footer-tui-model[\s\S]*?text-align:\s*right/, "TUI-like footer should right-align model information on desktop");
@@ -268,6 +306,7 @@ assert.match(app, /async function createPathPickerDirectory\(\)/, "cwd picker sh
268
306
  assert.match(app, /function renderPathPickerDirectoryList\(\)[\s\S]*pathPickerDirectoryMatchesSearch/, "cwd picker should filter current-directory entries in the browser");
269
307
  assert.match(app, /elements\.pathPickerSearchInput\.addEventListener\("input", renderPathPickerDirectoryList\)/, "cwd picker should update directory matches as the user types");
270
308
  assert.match(server, /async function createDirectoryPickerDirectory\(parentPath, nameValue, activeCwd\)/, "server should implement cwd picker directory creation");
309
+ assert.match(server, /function directoryPickerActiveCwd\(req, url, body = \{\}\)/, "server should let the cwd picker run before any Pi tabs exist");
271
310
  assert.match(server, /url\.pathname === "\/api\/directories" && req\.method === "POST"/, "server should expose POST /api/directories for cwd picker directory creation");
272
311
  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
312
  assert.match(css, /(?:^|\n)\s*\.side-panel-backdrop\s*\{[\s\S]*?position:\s*fixed/, "mobile side panel backdrop should be fixed overlay UI");
@@ -290,12 +329,21 @@ assert.match(css, /\.release-dialog-success \{ color: var\(--ctp-green\); \}/, "
290
329
  assert.match(css, /\.release-dialog-danger \{ color: var\(--ctp-red\); \}/, "release confirmation should color blocked/error lines as danger");
291
330
 
292
331
  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");
332
+ 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");
333
+ assert.match(app, /function isSidePanelOverlayView\(\)[\s\S]*sidePanelOverlayMedia\?\.matches/, "side-panel overlay detection should be separate from full mobile mode");
334
+ assert.match(app, /const showBackdrop = !collapsed && isSidePanelOverlayView\(\)/, "side-panel backdrop should show for the overlay breakpoint, not only phone layouts");
335
+ assert.match(app, /function restoreSidePanelState\(\) \{\n\s+if \(isSidePanelOverlayView\(\)\) \{\n\s+setSidePanelCollapsed\(true, \{ persist: false \}\);/, "side-panel should start collapsed in narrow overlay mode");
336
+ assert.match(app, /function bindSidePanelOverlayViewChanges\(\)/, "side-panel overlay breakpoint changes should be monitored separately from full mobile changes");
337
+ assert.match(app, /if \(isSidePanelOverlayView\(\) && !document\.body\.classList\.contains\("side-panel-collapsed"\)\)/, "Escape should close the side-panel overlay at narrow widths");
293
338
  assert.match(app, /const THEME_STORAGE_KEY = "pi-webui-theme"/, "theme selection should be persisted in browser storage");
294
339
  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
340
  assert.match(app, /const CUSTOM_BACKGROUNDS_STORAGE_KEY = "pi-webui-custom-backgrounds"/, "custom backgrounds should be persisted per theme in browser storage");
296
341
  assert.match(app, /const CUSTOM_BACKGROUND_IDB_NAME = "pi-webui-custom-background"/, "custom backgrounds should prefer IndexedDB persistence for large images");
297
342
  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");
298
343
  assert.match(app, /const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications"/, "agent-done notification preference should be persisted in browser storage");
344
+ assert.match(app, /const TERMINAL_TABS_LAYOUT_STORAGE_KEY = "pi-webui-terminal-tabs-layout"/, "terminal-tabs layout preference should be persisted in browser storage");
345
+ assert.match(app, /document\.body\.classList\.toggle\("terminal-tabs-left", next === "left"\)/, "terminal-tabs layout should toggle a body class for CSS layout");
346
+ assert.match(app, /terminalTabsLayoutSelect\.addEventListener\("change"/, "terminal-tabs layout selector should update the browser layout immediately");
299
347
  assert.match(app, /async function initializeThemes\(\)/, "frontend should initialize bundled themes");
300
348
  assert.match(app, /api\("\/api\/themes", \{ scoped: false \}\)/, "theme loading should use the unscoped themes endpoint");
301
349
  assert.match(app, /function applyTheme\(theme/, "frontend should apply a selected theme to CSS variables");
@@ -323,6 +371,11 @@ assert.match(app, /case "webui_connected":[\s\S]*setWebuiVersion\(event\.version
323
371
  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
372
  assert.match(server, /webuiDev: webuiDevServer,[\s\S]*webuiMode: webuiDevServer \? "dev" : "production"/, "server status should expose Web UI dev mode");
325
373
  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");
374
+ assert.match(server, /async function validateStartupCwd\(cwd\)/, "server should validate startup cwd before spawning Pi");
375
+ assert.match(server, /--cwd does not exist:/, "server should report nonexistent startup cwd paths clearly");
376
+ assert.match(server, /options\.cwd = await validateStartupCwd\(options\.cwd\)/, "server should fail fast for invalid startup cwd paths");
377
+ assert.match(server, /cwdExplicit: false/, "server should track whether startup cwd was explicitly requested");
378
+ assert.match(server, /return options\.cwdExplicit \? \[await createTab\(\)\] : \[\]/, "server should wait for UI cwd selection when no --cwd is supplied");
326
379
  assert.match(app, /serverActionSelect\.addEventListener\("change", updateServerActionButton\)/, "Server action dropdown should control the guarded run button");
327
380
  assert.match(app, /runServerActionButton\.addEventListener\("click"[\s\S]*runSelectedServerAction/, "Server action run button should execute the selected action");
328
381
  assert.match(app, /api\("\/api\/restart", \{ method: "POST", scoped: false \}\)/, "Restart Server action should call the unscoped restart endpoint");
@@ -342,11 +395,13 @@ assert.match(html, /<textarea id="promptInput"[^>]*autofocus/, "prompt composer
342
395
  assert.match(app, /function syncMobileChatToBottomForInput\(\)/, "mobile input focus should force the output view to the latest message");
343
396
  assert.match(app, /function focusPromptInput\(\{ defer = false \} = \{\}\)/, "frontend should focus the prompt composer programmatically after tab/app startup");
344
397
  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\(\);\n\s+focusPromptInput\(\{ defer: true \}\);/, "starting the Web UI should focus the prompt input after restoring the active tab");
398
+ 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
399
  assert.match(app, /resizePromptInput\(\);\nfocusPromptInput\(\{ defer: true \}\);\nupdateComposerModeButtons\(\);/, "startup should request prompt focus before waiting for tab state refreshes");
347
400
  assert.match(app, /elements\.promptInput\.addEventListener\("focus", \(\) => \{\n\s+syncMobileChatToBottomForInput\(\);/, "focusing mobile input should scroll output to bottom");
348
401
  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 --cwd/, "PWA/offline shell should build a pi-webui --cwd recovery command");
402
+ assert.match(app, /function serverStartCommandText\(\)[\s\S]*return `pi-webui\$\{currentPortArg\(\)\}`/, "PWA/offline shell should build a pathless pi-webui recovery command");
403
+ assert.match(app, /async function createFirstTerminalTabFromChosenDirectory\(\)/, "frontend should prompt for the first terminal cwd when no tabs exist");
404
+ assert.match(app, /Choose CWD for first terminal/, "frontend should title the first-terminal cwd picker clearly");
350
405
  assert.match(app, /Pi Web UI server is offline/, "PWA/offline shell should clearly report backend-down state");
351
406
  assert.match(app, /navigator\.clipboard\.writeText\(text\)/, "backend-offline recovery panel should copy the start command when possible");
352
407
  assert.match(app, /function messageCopyText\(message, body = null\)/, "frontend should derive copy text from transcript messages");
@@ -461,11 +516,45 @@ assert.match(app, /const releasePrompt = detectedReleasePrompt && isOptionalFeat
461
516
  assert.match(app, /case "webui_tab_reloaded":[\s\S]*resetOptionalFeatureAvailability\(\)/, "optional feature state should reset when the RPC tab reloads resources");
462
517
  assert.match(app, /function runPublishWorkflow\(command\)[\s\S]*hasAvailableCommand\(commandName\)/, "publish workflow launch should guard on loaded slash commands");
463
518
  assert.match(app, /if \(!isOptionalFeatureEnabled\("gitWorkflow"\)\)/, "guided git workflow should guard on enabled /git-staged-msg feature");
519
+ assert.doesNotMatch(html, /gitWorkflowProcessSelect/, "guided git workflow should not expose process selection as a dropdown");
520
+ 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");
521
+ 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");
522
+ assert.match(app, /actionsDone: createGitWorkflowActionsDone\(\)/, "guided git workflow should track process completion separately from selected process");
523
+ 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");
524
+ 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");
525
+ 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");
526
+ 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");
527
+ 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");
528
+ assert.match(css, /\.git-workflow-step\.done::before \{[\s\S]*background:\s*var\(--ctp-green\)/, "guided git workflow completed process dots should be green");
529
+ 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
530
  assert.match(app, /const gitWorkflowsByTab = new Map\(\)/, "guided git workflow state should be stored per terminal tab");
465
531
  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
532
  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");
533
+ 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");
534
+ 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");
535
+ 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");
536
+ 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");
537
+ assert.match(app, /addGitWorkflowAction\("Manual branch", \(\) => createGitPrBranchManually\(\), "", false, GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP\)/, "Manual branch should render with its tooltip");
538
+ 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");
539
+ 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");
540
+ 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");
541
+ 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");
542
+ 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");
543
+ assert.match(app, /async function runGitPrPrompt\(tabId = gitWorkflowActionTabId\(\), \{ prefixOutput = "" \} = \{\}\)[\s\S]*body: \{ message: "\/pr" \}/, "guided git workflow should generate PR descriptions with /pr");
544
+ 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");
545
+ assert.match(server, /case "\/api\/git-workflow\/branch":[\s\S]*\["switch", "-c", branch\]/, "server should expose branch creation for the guided PR workflow");
546
+ 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
547
  assert.doesNotMatch(app, /gitWorkflowVisibleTabId|Workflow belongs to/, "guided git workflow should not pin or show workflows outside their owning terminal tab");
468
548
  assert.match(app, /function renderReleaseNpmOutputWidget\(\)/, "release-npm live output should use a specialized Web UI renderer");
549
+ assert.match(app, /async function refreshAppRunners\(tabContext = activeTabContext\(\)\)/, "frontend should load detected app runners for the active tab cwd");
550
+ assert.match(app, /function renderAppRunnerWidget\(\)/, "frontend should render app runner output in the shared top widget area");
551
+ assert.match(server, /url\.pathname === "\/api\/app-runners" && req\.method === "GET"/, "server should expose detected app runners for the active tab cwd");
552
+ assert.match(server, /url\.pathname === "\/api\/app-runner" && req\.method === "POST"/, "server should start selected app runners directly");
553
+ assert.match(server, /function addGoRunner\(runners, cwd\)[\s\S]*Go\/Golang app entry/, "server should detect Go\/Golang app runners");
554
+ 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");
555
+ 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");
556
+ assert.match(server, /function addDockerComposeRunner\(runners, cwd\)[\s\S]*docker compose up[\s\S]*docker-compose up/, "server should detect Docker Compose runners");
557
+ 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
558
  assert.match(app, /const releaseNpmOutputExpandedByTab = new Map\(\)/, "release-npm output collapse state should be tracked per browser tab");
470
559
  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
560
  assert.match(app, /releaseNpmStreamHeader\("Live output stream", outputLines\.length, \{ live: true \}\)/, "release-npm live output should expose a clear stream heading");
@@ -572,6 +661,9 @@ assert.match(app, /case "tool_execution_update":[\s\S]*?handleToolExecutionUpdat
572
661
  assert.match(app, /case "auto_retry_start":[\s\S]*?addTransientMessage\(\{ role: "warn", title: "auto retry"/, "auto-retry starts should be transcript-visible warnings");
573
662
  assert.match(app, /case "extension_error":[\s\S]*?addTransientMessage\(\{ role: "error", title: "extension error"/, "extension errors should be transcript-visible error cards");
574
663
  assert.match(app, /setRunIndicatorActivity\("Requesting context compaction…"\);\n\s+scrollChatToBottom\(\{ force: true \}\);/, "manual compaction should force-follow the transcript to the bottom status card");
664
+ assert.match(app, /function markContextUsageUnknownAfterCompaction\(/, "compaction should have a dedicated context-usage invalidation helper");
665
+ 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");
666
+ assert.match(app, /function footerStatsContextDisplay[\s\S]*?contextUsageUnknownAfterCompaction\(\)[\s\S]*?unknownFooterContextText/, "fallback footer context should show an unknown value after compaction invalidates usage");
575
667
  assert.match(app, /case "agent_end":[\s\S]*?clearRunIndicatorActivity\(\)/, "agent completion should remove the active agent transcript indicator");
576
668
  assert.match(app, /case "agent_end":[\s\S]*?notifyAgentDone\(event\.tabId \|\| activeTabId/, "agent completion should trigger optional done notifications");
577
669
  assert.match(app, /function getPathTrigger\(\)/, "prompt composer should detect @ file\/path reference triggers");
@@ -730,7 +822,7 @@ assert.match(app, /api\("\/api\/thinking", \{ method: "POST", body: \{ level: ne
730
822
  assert.match(app, /function isFooterPickerOpen\(\)[\s\S]*?footerModelPickerOpen \|\| footerThinkingPickerOpen/, "footer picker overlay state should cover model and thinking pickers");
731
823
  assert.doesNotMatch(app.match(/function renderMinimalFooter\(\)[\s\S]*?\n\}/)?.[0] || "", /footer-details-toggle/, "minimal default footer should not render a details toggle chip");
732
824
  assert.match(app, /bindMobileViewChanges\(/, "side panel state should react to mobile breakpoint changes");
733
- assert.match(app, /function restoreSidePanelState\(\) \{\n\s+if \(isMobileView\(\)\)/, "mobile should start with side panel collapsed even if desktop state was expanded");
825
+ 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
826
  assert.match(app, /case "webui_tab_reloaded":/, "frontend should handle native /reload tab restart events");
735
827
  assert.match(app, /addTransientMessage\(\{ role: "native", title: "\/reload"/, "native /reload should produce visible transcript output");
736
828
  assert.match(app, /await copyText\(response\.data\.copyText\)/, "native /copy should use the shared browser clipboard helper when available");
@@ -744,7 +836,7 @@ assert.equal(manifest.start_url, "/", "PWA manifest should start at the web UI r
744
836
  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
837
  assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-192.png" && icon.sizes === "192x192"), "PWA manifest should include a 192px icon");
746
838
  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-v24"/, "PWA service worker should define an app-shell cache");
839
+ assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v25"/, "PWA service worker should define an app-shell cache");
748
840
  assert.match(serviceWorker, /self\.addEventListener\("notificationclick"/, "PWA service worker should focus Web UI when blocked-tab notifications are clicked");
749
841
  assert.match(serviceWorker, /event\.notification\.data\?\.url/, "blocked-tab notifications should carry a URL for service-worker click handling");
750
842
  assert.match(serviceWorker, /"\/apple-touch-icon\.png"/, "PWA service worker should cache the apple touch icon");