@firstpick/pi-package-webui 0.1.3 → 0.1.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.
@@ -30,6 +30,8 @@ assert.match(html, /id="terminalTabsToggleButton"/, "mobile should expose a comp
30
30
  assert.match(html, /id="sidePanelBackdrop"/, "mobile side panel needs an overlay/backdrop close target");
31
31
  assert.match(html, /id="themeSelect"/, "side panel should expose a theme selector");
32
32
  assert.match(html, /<label for="themeSelect">Theme<\/label>/, "theme selector should be labeled in side-panel controls");
33
+ assert.match(html, /id="agentDoneNotificationsToggle"/, "side panel should expose an agent-done notifications toggle");
34
+ assert.match(html, /id="agentDoneNotificationsStatus"/, "agent-done notifications toggle should expose status text");
33
35
  assert.match(html, /id="jumpToLatestButton"/, "chat should expose a jump-to-latest control for non-forced streaming");
34
36
  assert.match(html, /id="stickyUserPromptButton"/, "chat should expose a fixed last-user-prompt jump control");
35
37
  assert.match(html, /id="feedbackTray"/, "chat should expose a queued action-feedback tray");
@@ -37,6 +39,11 @@ assert.match(html, /id="sendFeedbackButton"/, "action feedback should be submitt
37
39
  assert.match(html, /<textarea id="promptInput"[^>]*rows="1"[^>]*enterkeyhint="enter"/, "prompt textarea should start at one row and hint that Return inserts a newline");
38
40
  assert.match(html, /id="composerActionsButton"/, "mobile composer should expose a compact actions trigger");
39
41
  assert.match(html, /id="composerActionsPanel"/, "secondary composer controls should live in a mobile actions panel");
42
+ assert.match(html, /id="publishButton"[\s\S]*?aria-controls="publishMenu"/, "composer should expose a Publish workflow menu button");
43
+ assert.match(html, /id="releaseNpmButton"[^>]*data-command="\/release-npm"[\s\S]*?<span>NPM Release<\/span>/, "Publish menu should include the npm release workflow by label");
44
+ assert.match(html, /id="releaseAurButton"[^>]*data-command="\/release-aur"[\s\S]*?<span>AUR Release<\/span>/, "Publish menu should include the AUR release workflow by label");
45
+ assert.doesNotMatch(html, /<code>\/release-(?:npm|aur)<\/code>/, "Publish menu should not show slash command names as option labels");
46
+ assert.doesNotMatch(html, /data-tooltip="[^"]*\/release-(?:npm|aur)/, "Publish tooltip should not show slash command names");
40
47
  assert.match(html, /id="steerButton"[\s\S]*?data-tooltip="Steer usage:/, "Steer should explain type-first usage in a tooltip");
41
48
  assert.match(html, /id="followUpButton"[\s\S]*?data-tooltip="Follow-up usage:/, "Follow-up should explain type-first usage in a tooltip");
42
49
  assert.ok(
@@ -62,10 +69,17 @@ assert.match(css, /\.run-indicator-pulse \{[\s\S]*?animation:\s*run-indicator-pu
62
69
  assert.match(css, /\.todo-widget \{[\s\S]*?display:\s*grid/, "todo-progress widget should render as a styled checklist card");
63
70
  assert.match(css, /\.todo-widget-item\.partial \.todo-widget-marker/, "todo-progress partial items should have distinct styling");
64
71
  assert.match(css, /\.todo-widget-item\.done \.todo-widget-text[\s\S]*?text-decoration:\s*line-through/, "todo-progress completed items should be visually crossed out");
72
+ assert.match(css, /\.release-npm-widget \{[\s\S]*?display:\s*grid/, "release-npm output should render as a specialized Web UI widget");
73
+ assert.match(css, /\.release-aur-widget \{[\s\S]*?border-color/, "release-aur output should render as a specialized Web UI widget variant");
74
+ assert.match(css, /\.widget-area \.widget:not\(\.todo-widget\):not\(\.release-npm-widget\)/, "mobile widget filtering should keep release workflow output visible");
65
75
  assert.match(css, /\.message\.warn \.message-role \{ color: var\(--ctp-yellow\); \}/, "warning-level command output should be visually distinct");
66
76
  assert.match(css, /\.commands-box \{[\s\S]*?max-height:\s*min\(32rem, 52vh\)/, "side-panel commands should use expanded viewport-aware height");
67
77
  assert.match(css, /\.command-item \{[\s\S]*?width:\s*100%/, "side-panel commands should render as full-width click targets");
78
+ assert.match(css, /\.toggle-control \{[\s\S]*?grid-template-columns:\s*auto minmax\(0, 1fr\)/, "side-panel notification toggle should align checkbox and label text");
79
+ assert.match(css, /\.toggle-control:has\(input:checked\)/, "side-panel notification toggle should style the enabled state");
68
80
  assert.match(css, /\.command-item:hover,[\s\S]*?\.command-item:focus-visible/, "side-panel commands should have hover and keyboard focus affordances");
81
+ assert.match(css, /\.command-suggest-item:hover \{\n\s+box-shadow: none;\n\s+transform: none;\n\}\n\.command-suggest-item\.active \{/, "autocomplete hover should not render as the selected suggestion unless JS marks it active");
82
+ assert.doesNotMatch(css, /\.command-suggest-item:hover,\n\.command-suggest-item\.active/, "autocomplete hover and active selection styles should stay separate");
69
83
  assert.match(css, /\.feedback-tray\[hidden\] \{ display: none; \}/, "queued action-feedback tray should hide when empty");
70
84
  assert.match(css, /\.action-feedback-controls \{[\s\S]*?position:\s*absolute/, "action reactions should be absolutely positioned so they do not expand cards");
71
85
  assert.match(css, /\.action-feedback-controls \{[\s\S]*?top:\s*calc\(100% - 0\.18rem\)/, "action reactions should sit outside the message box by default");
@@ -75,6 +89,9 @@ assert.match(css, /\.action-feedback-controls:not\(:hover\):not\(:focus-within\)
75
89
  assert.match(css, /\.action-feedback-button\.feedback-question\.active/, "question-mark reaction should have selected styling");
76
90
  assert.match(css, /\.composer-row button\[data-tooltip\]::after/, "composer button tooltips should be shared across Git, Steer, and Follow-up buttons");
77
91
  assert.match(css, /\.composer-row button\[data-tooltip\]\.tooltip-open::after/, "composer button tooltips should be triggerable from JS for empty mobile taps");
92
+ assert.match(css, /\.composer-publish-menu-panel \{[\s\S]*?display:\s*none;[\s\S]*?flex-direction:\s*column/, "Publish workflow menu should hide when closed and expand like grouped tabs");
93
+ assert.match(css, /\.composer-publish-menu:hover \.composer-publish-menu-panel,[\s\S]*?\.composer-publish-menu:focus-within \.composer-publish-menu-panel,[\s\S]*?\.composer-publish-menu\.open \.composer-publish-menu-panel \{\n\s+display:\s*flex;/, "Publish workflow menu should open on hover, focus, or explicit open state");
94
+ assert.match(css, /\.composer-actions-panel > \.composer-publish-menu[\s\S]*?grid-column: span 1/, "Publish workflow button should fit beside Git workflow in mobile actions");
78
95
  assert.match(css, /\.composer-actions-panel[\s\S]*?bottom:\s*calc\(100% \+ 0\.42rem\)/, "mobile composer actions should open as an above-composer sheet");
79
96
  assert.match(css, /body\.composer-actions-open \.composer-actions-panel \{ display: grid; \}/, "composer actions panel should only open when toggled");
80
97
  assert.match(css, /\.terminal-tabs-toggle-button \{ display: none; \}/, "terminal tab toggle should be hidden outside mobile CSS");
@@ -108,9 +125,14 @@ assert.match(css, /\.extension-dialog[\s\S]*?max-height:\s*calc\(var\(--visual-v
108
125
  assert.match(css, /\.extension-dialog[\s\S]*?inset:\s*auto 0 0 0/, "mobile dialogs should behave like bottom sheets");
109
126
  assert.match(css, /#dialogMessage \{[\s\S]*?white-space:\s*pre-wrap/, "extension dialog messages should preserve multiline prompts");
110
127
  assert.match(css, /\.extension-dialog\.guardrail-dialog[\s\S]*?border-color:\s*rgba\(249, 226, 175/, "guardrail dialogs should have warning-specific styling");
128
+ assert.match(css, /\.extension-dialog\.release-dialog[\s\S]*?width:\s*min\(64rem/, "release confirmation dialogs should have more horizontal room");
129
+ assert.match(css, /\.extension-dialog\.release-dialog #dialogMessage[\s\S]*?max-height:\s*min\(56vh, 34rem\)/, "release confirmation summaries should scroll in a roomy panel");
130
+ assert.match(css, /\.release-dialog-success \{ color: var\(--ctp-green\); \}/, "release confirmation should color publish/update lines as success");
131
+ assert.match(css, /\.release-dialog-danger \{ color: var\(--ctp-red\); \}/, "release confirmation should color blocked/error lines as danger");
111
132
 
112
133
  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");
113
134
  assert.match(app, /const THEME_STORAGE_KEY = "pi-webui-theme"/, "theme selection should be persisted in browser storage");
135
+ assert.match(app, /const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications"/, "agent-done notification preference should be persisted in browser storage");
114
136
  assert.match(app, /async function initializeThemes\(\)/, "frontend should initialize bundled themes");
115
137
  assert.match(app, /api\("\/api\/themes", \{ scoped: false \}\)/, "theme loading should use the unscoped themes endpoint");
116
138
  assert.match(app, /function applyTheme\(theme/, "frontend should apply a selected theme to CSS variables");
@@ -143,15 +165,26 @@ assert.match(app, /function renderAnsiText\(parent, text\)/, "extension dialogs
143
165
  assert.match(app, /function applyAnsiSgr\(codes, state\)/, "ANSI SGR color state should be parsed for dialog rendering");
144
166
  assert.match(app, /function normalizeDialogPrompt\(request\)/, "extension dialogs should split multiline prompts into title and body");
145
167
  assert.match(app, /plainMessage: stripAnsi\(message\)/, "dialog prompt parsing should keep a plain-text copy for detection and visibility");
146
- assert.match(app, /renderAnsiText\(elements\.dialogMessage, prompt\.message\)/, "dialog prompts should preserve TUI highlight colors in the browser");
168
+ assert.match(app, /function releaseDialogPromptParts\(prompt\)/, "release confirmation dialogs should promote the publish question into the dialog title");
169
+ assert.match(app, /Publish to AUR/, "release confirmation dialogs should also recognize AUR publish prompts");
170
+ assert.match(app, /function renderReleaseDialogMessage\(parent, text\)/, "release confirmation dialogs should semantically color summary lines");
171
+ assert.match(app, /else renderAnsiText\(elements\.dialogMessage, displayPrompt\.message\)/, "non-release dialog prompts should preserve TUI highlight colors in the browser");
147
172
  assert.match(app, /elements\.dialog\.classList\.toggle\("guardrail-dialog", isGuardrailDialog\)/, "guardrail extension dialogs should get dedicated styling");
173
+ assert.match(app, /elements\.dialog\.classList\.toggle\("release-dialog", isReleaseDialog\)/, "release extension dialogs should get dedicated roomy styling");
174
+ assert.match(app, /release-publish-action/, "release dialogs should distinguish the publish confirmation button");
148
175
  assert.match(app, /guardrail-safe-action/, "guardrail dialogs should distinguish safe and allow actions");
149
176
  assert.match(app, /function hasQueuedDialogRequest\(id\)/, "frontend should deduplicate replayed extension UI dialogs by request id");
150
177
  assert.match(app, /if \(request\.replayed\) addEvent\(`recovered pending \$\{request\.method\} request`, "warn"\)/, "frontend should surface replayed extension UI blockers");
151
178
  assert.match(app, /case "webui_extension_ui_cancelled":/, "frontend should close dialogs cancelled by backend abort handling");
152
179
  assert.match(app, /function parseTodoProgressWidget\(lines\)/, "todo-progress widgets should be parsed from extension widget lines");
180
+ assert.match(app, /function renderReleaseNpmOutputWidget\(\)/, "release-npm live output should use a specialized Web UI renderer");
181
+ assert.match(app, /function renderReleaseAurOutputWidget\(\)/, "release-aur live output should use a specialized Web UI renderer");
182
+ assert.match(app, /releaseNpmActionButton\("Abort", "\/release-abort", "danger"\)/, "release-npm Web UI output should expose an abort action");
183
+ assert.match(app, /releaseNpmActionButton\("Abort", "\/release-aur abort", "danger"\)/, "release-aur Web UI output should expose an abort action");
153
184
  assert.match(app, /key === "todo-progress" \? renderTodoProgressWidget\(key, lines\) : null/, "todo-progress should use the specialized widget renderer");
154
185
  assert.match(app, /let transientMessages = \[\]/, "frontend should keep transient Web UI/extension output messages");
186
+ assert.match(app, /function orderedTranscriptItems\(\)/, "frontend should merge persisted and transient messages chronologically");
187
+ assert.match(app, /items\.sort\(\(a, b\) => a\.timestampMs - b\.timestampMs \|\| a\.order - b\.order\)/, "transient extension output should not pin itself below newer persisted messages");
155
188
  assert.match(app, /const ACTION_FEEDBACK_REACTIONS = \{/, "frontend should define direct feedback reactions");
156
189
  assert.match(app, /message\?\.role === "assistant" \|\| message\?\.role === "toolResult" \|\| message\?\.role === "bashExecution"/, "frontend should allow reactions on final assistant output as well as actions");
157
190
  assert.match(app, /function renderActionFeedbackControls\(/, "frontend should render per-message reaction controls");
@@ -166,6 +199,9 @@ assert.match(app, /if \(typeof partialText === "string"\) streamRawText = partia
166
199
  assert.match(app, /const TODO_PROGRESS_LINE_REGEX = /, "frontend should recognize live todo progress lines that will be moved into the todo widget");
167
200
  assert.match(app, /function stripTodoProgressLines\(text, \{ streaming = false \} = \{\}\)/, "live Assistant output should strip todo-progress lines before rendering final-output text");
168
201
  assert.match(app, /streamRawText \+= delta;[\s\S]*?const assistantText = stripTodoProgressLines\(streamRawText, \{ streaming: true \}\)/, "streamed Assistant text should classify from accumulated output without flashing partial todo-progress lines");
202
+ assert.match(app, /const STREAM_OUTPUT_HIDE_DELAY_MS = 300/, "stream output hiding should be debounced to prevent rapid flicker");
203
+ assert.match(app, /function scheduleStreamBubbleHide\([\s\S]*?STREAM_OUTPUT_MIN_VISIBLE_MS/, "stream output cards should observe a minimum visible duration before hiding");
204
+ assert.match(app, /if \(assistantText\) \{[\s\S]*?streamText\.textContent = assistantText;[\s\S]*?\} else \{\n\s+scheduleStreamBubbleHide\(\);/, "empty filtered stream output should schedule hide instead of immediately removing the card");
169
205
  assert.match(app, /const created = appendMessage\(\{ role: "assistant", title: "Assistant"/, "live Assistant cards should be created only for final output text");
170
206
  assert.match(app, /api\("\/api\/action-feedback", \{ method: "POST"/, "queued action feedback should post to the server after the run is idle");
171
207
  assert.match(app, /function postQueuedFeedback\(tabId, items\)/, "queued feedback should have a backward-compatible submit path");
@@ -180,6 +216,9 @@ assert.doesNotMatch(app, /runIndicatorTimestamp/, "active agent indicator should
180
216
  assert.match(app, /runIndicatorBubble = make\("article", "message runIndicator run-indicator-message streaming"\)/, "active agent indicator should use a dedicated streaming transcript card");
181
217
  assert.match(app, /runIndicatorShowsElapsed\(\) \? `\$\{detail\} · run time/, "active agent indicator should label elapsed run time instead of showing a bare counter");
182
218
  assert.match(app, /Abort requested; checking whether Pi stopped/, "abort feedback should clarify that Web UI is checking stop status");
219
+ assert.match(app, /function addAbortTranscriptNotice\(/, "abort button should render a transcript-visible aborted notice");
220
+ assert.match(app, /this transcript marks the run as aborted/, "abort notice should clearly mark the agent output as aborted");
221
+ assert.match(app, /await api\("\/api\/abort"[\s\S]*?addAbortTranscriptNotice\(\{ activeRun: hadActiveRun \}\)/, "abort button should add the aborted transcript notice after the abort request succeeds");
183
222
  assert.match(app, /let runIndicatorGraceCheckTimer = null/, "local-only run indicators should have a grace-check timer");
184
223
  assert.match(app, /const RUN_INDICATOR_STATE_RECHECK_MS = 5000/, "active run indicators should periodically re-check state");
185
224
  assert.match(app, /function scheduleRunIndicatorGraceCheck\(/, "local-only run indicators should schedule a follow-up state check");
@@ -190,9 +229,18 @@ assert.match(app, /function scheduleAbortStateChecks\(/, "abort handling should
190
229
  assert.match(app, /case "tool_execution_start":[\s\S]*?setRunIndicatorActivity\(`Running tool:/, "tool execution should update the active agent transcript indicator");
191
230
  assert.match(app, /setRunIndicatorActivity\("Requesting context compaction…"\);\n\s+scrollChatToBottom\(\{ force: true \}\);/, "manual compaction should force-follow the transcript to the bottom status card");
192
231
  assert.match(app, /case "agent_end":[\s\S]*?clearRunIndicatorActivity\(\)/, "agent completion should remove the active agent transcript indicator");
232
+ assert.match(app, /case "agent_end":[\s\S]*?notifyAgentDone\(event\.tabId \|\| activeTabId/, "agent completion should trigger optional done notifications");
193
233
  assert.match(app, /function getPathTrigger\(\)/, "prompt composer should detect @ file\/path reference triggers");
194
234
  assert.match(app, /api\(`\/api\/path-suggestions\?query=\$\{encodeURIComponent\(trigger\.query\)\}`/, "@ reference suggestions should load from the server as the user types");
195
235
  assert.match(app, /formatPathReference\(suggestion\.path, trigger\.quoted\)/, "accepting a path suggestion should insert an @ reference into the prompt");
236
+ assert.match(app, /let pathSuggestActiveQuery = null/, "@ autocomplete should track the active path query to avoid duplicate refresh flicker");
237
+ assert.match(app, /pathSuggestActiveQuery === trigger\.query[\s\S]*?return;/, "@ autocomplete should skip duplicate same-query fetches from input and keyup events");
238
+ assert.match(app, /const keepExistingPathMenu = suggestionMode === "path"[\s\S]*?if \(!keepExistingPathMenu\) \{[\s\S]*?Finding paths…/, "@ autocomplete should keep the existing menu visible while refreshing a new path query");
239
+ assert.match(app, /elements\.commandSuggest\.setAttribute\("aria-busy", "true"\)/, "@ autocomplete should mark async path refreshes busy without clearing rendered suggestions");
240
+ assert.match(app, /function setActiveCommandSuggestionFromPointerMove\(index, event\)/, "command and path autocomplete should route pointer selection through movement detection");
241
+ assert.match(app, /item\.addEventListener\("pointermove", \(event\) => setActiveCommandSuggestionFromPointerMove\(index, event\)\);[\s\S]*?item\.addEventListener\("click", \(\) => insertCommandSuggestion\(index\)\);/, "slash command autocomplete should only follow pointer movement before click insertion");
242
+ assert.match(app, /item\.addEventListener\("pointermove", \(event\) => setActiveCommandSuggestionFromPointerMove\(index, event\)\);[\s\S]*?item\.addEventListener\("click", \(\) => insertPathSuggestion\(index\)\);/, "path autocomplete should only follow pointer movement before click insertion");
243
+ assert.doesNotMatch(app, /addEventListener\("mouseenter", \(\) => setActiveCommandSuggestion\(index\)\)/, "autocomplete should not change active selection on stationary mouseenter");
196
244
  assert.match(app, /function resizePromptInput\(\)/, "prompt textarea should auto-resize from a one-line default");
197
245
  assert.match(app, /elements\.promptInput\.addEventListener\("input", \(\) => \{\n\s+resizePromptInput\(\);/, "prompt textarea should resize whenever the user edits it");
198
246
  assert.match(app, /function updateComposerModeButtons\(\)/, "composer should relocate Steer and Follow-up based on run state");
@@ -201,6 +249,11 @@ assert.match(app, /document\.body\.classList\.toggle\("pi-run-active", runActive
201
249
  assert.match(app, /function showComposerButtonTooltip\(button\)/, "empty mode-button taps should show the usage tooltip");
202
250
  assert.match(app, /sendPromptFromModeButton\("steer", elements\.steerButton\)/, "Steer should show tooltip instead of silently doing nothing when input is empty");
203
251
  assert.match(app, /sendPromptFromModeButton\("follow-up", elements\.followUpButton\)/, "Follow-up should show tooltip instead of silently doing nothing when input is empty");
252
+ assert.match(app, /function runPublishWorkflow\(command\)[\s\S]*?sendPrompt\("prompt", command\)/, "Publish workflows should send slash commands directly without replacing the draft");
253
+ assert.match(app, /publishMenuContainer\?\.addEventListener\("pointerenter", \(\) => setPublishMenuOpen\(true\)\)/, "Publish menu should expand on hover");
254
+ assert.match(app, /publishMenuContainer\?\.addEventListener\("pointerleave", \(\) => setPublishMenuOpen\(false\)\)/, "Publish menu should collapse after hover leaves");
255
+ assert.match(app, /releaseNpmButton\.addEventListener\("click", \(\) => runPublishWorkflow\("\/release-npm"\)\)/, "Publish menu should launch /release-npm");
256
+ assert.match(app, /releaseAurButton\.addEventListener\("click", \(\) => runPublishWorkflow\("\/release-aur"\)\)/, "Publish menu should launch /release-aur");
204
257
  assert.match(app, /async function sendPrompt\(kind = "prompt", explicitMessage\)/, "prompt sending should accept direct messages that bypass the input field");
205
258
  assert.match(app, /const rawMessage = usesPromptInput \? elements\.promptInput\.value : explicitMessage/, "direct prompt sends should not read the input textarea");
206
259
  assert.match(app, /if \(usesPromptInput\) \{\n\s+elements\.promptInput\.value = "";/, "direct prompt sends should preserve the input textarea draft");
@@ -232,8 +285,13 @@ assert.match(app, /pendingBlockerCount > 0[\s\S]*?state: "blocked"/, "frontend s
232
285
  assert.match(app, /const EXTENSION_UI_BLOCKING_METHODS = new Set\(\["select", "confirm", "input", "editor"\]\)/, "frontend should share blocking extension UI method detection for dialogs and notifications");
233
286
  assert.match(app, /function notifyBlockedTab\(/, "frontend should send blocked-tab notifications when extension UI blocks a run");
234
287
  assert.match(app, /function showBlockedTabBrowserNotification\(/, "frontend should use browser notifications for blocked tabs when permission allows");
235
- assert.match(app, /Notification\.requestPermission\(\)/, "frontend should request notification permission before blocked-tab browser alerts");
288
+ assert.match(app, /function setAgentDoneNotificationsEnabled\(/, "frontend should manage the side-panel agent-done notification toggle");
289
+ assert.match(app, /agentDoneNotificationsToggle\.addEventListener\("change"/, "agent-done notification toggle should be wired to user changes");
290
+ assert.match(app, /function notifyAgentDone\(/, "frontend should send optional browser notifications when agent work completes");
291
+ assert.match(app, /ensureAgentDoneNotificationPermission\(\)/, "agent-done notifications should request browser notification permission from the toggle flow");
292
+ assert.match(app, /Notification\.requestPermission\(\)/, "frontend should request notification permission before browser alerts");
236
293
  assert.match(app, /syncBlockedTabNotificationsFromTabs\(tabs, previousTabs\)/, "tab refreshes should notify when a background tab becomes blocked");
294
+ assert.match(app, /syncAgentDoneNotificationsFromTabs\(tabs, previousTabs\)/, "tab refreshes should notify when background tab work completes");
237
295
  assert.match(app, /notifyBlockedTab\(request\.tabId, \{ request, count: request\.pendingExtensionUiRequestCount \}\)/, "extension UI requests should trigger blocked-tab notifications");
238
296
  assert.match(app, /pendingExtensionUiRequestCount[\s\S]*?setTabPendingBlockerCount/, "frontend should ingest pending blocker counts from tab events");
239
297
  assert.match(app, /function markTabOutputSeen\(/, "frontend should clear work-done indicators once output is seen");
@@ -269,7 +327,7 @@ assert.equal(manifest.start_url, "/", "PWA manifest should start at the web UI r
269
327
  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");
270
328
  assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-192.png" && icon.sizes === "192x192"), "PWA manifest should include a 192px icon");
271
329
  assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-512.png" && icon.sizes === "512x512"), "PWA manifest should include a 512px icon");
272
- assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v6"/, "PWA service worker should define an app-shell cache");
330
+ assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v10"/, "PWA service worker should define an app-shell cache");
273
331
  assert.match(serviceWorker, /self\.addEventListener\("notificationclick"/, "PWA service worker should focus Web UI when blocked-tab notifications are clicked");
274
332
  assert.match(serviceWorker, /event\.notification\.data\?\.url/, "blocked-tab notifications should carry a URL for service-worker click handling");
275
333
  assert.match(serviceWorker, /"\/apple-touch-icon\.png"/, "PWA service worker should cache the apple touch icon");
@@ -358,8 +416,8 @@ assert.match(readme, /POST \/api\/action-feedback\?tab=<tabId>/, "README should
358
416
  assert.match(readme, /`@` file\/path references with live suggestions/, "README should describe @ file/path reference autocomplete");
359
417
  assert.match(readme, /GET \/api\/path-suggestions\?tab=<tabId>&query=<path>/, "README should document the path-suggestions endpoint");
360
418
  assert.match(readme, /server-persisted fast picks/, "README should describe server-persisted fast picks");
361
- assert.match(readme, /browser notifications when a tab needs an extension UI response/, "README should describe blocked-tab notifications");
362
- assert.match(readme, /blocked-tab browser notifications require browser service-worker\/notification support/, "README should document blocked-tab notification requirements");
419
+ assert.match(readme, /browser notifications when a tab needs an extension UI response and an optional side-panel toggle for agent-done notifications/, "README should describe blocked-tab and agent-done notifications");
420
+ assert.match(readme, /blocked-tab browser notifications, and optional agent-done notifications require browser service-worker\/notification support/, "README should document notification requirements");
363
421
  assert.match(readme, /Side-panel theme picker backed by the bundled `@firstpick\/pi-themes-bundle` themes/, "README should describe bundled theme selection");
364
422
 
365
423
  assert.equal(pkg.scripts?.test, "node tests/mobile-static.test.mjs", "package test script should run the mobile static harness");