@firstpick/pi-package-webui 0.1.2 → 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.
- package/README.md +18 -9
- package/bin/pi-webui.mjs +1039 -33
- package/index.ts +136 -29
- package/package.json +3 -2
- package/public/app.js +3025 -137
- package/public/index.html +43 -3
- package/public/service-worker.js +13 -1
- package/public/styles.css +1031 -131
- package/tests/mobile-static.test.mjs +260 -2
|
@@ -4,12 +4,13 @@ import { dirname, join } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
-
const [pkgRaw, html, css, app, server, readme, manifestRaw, serviceWorker, appleIcon, icon192, icon512] = await Promise.all([
|
|
7
|
+
const [pkgRaw, html, css, app, server, extension, readme, manifestRaw, serviceWorker, appleIcon, icon192, icon512] = await Promise.all([
|
|
8
8
|
readFile(join(root, "package.json"), "utf8"),
|
|
9
9
|
readFile(join(root, "public", "index.html"), "utf8"),
|
|
10
10
|
readFile(join(root, "public", "styles.css"), "utf8"),
|
|
11
11
|
readFile(join(root, "public", "app.js"), "utf8"),
|
|
12
12
|
readFile(join(root, "bin", "pi-webui.mjs"), "utf8"),
|
|
13
|
+
readFile(join(root, "index.ts"), "utf8"),
|
|
13
14
|
readFile(join(root, "README.md"), "utf8"),
|
|
14
15
|
readFile(join(root, "public", "manifest.webmanifest"), "utf8"),
|
|
15
16
|
readFile(join(root, "public", "service-worker.js"), "utf8"),
|
|
@@ -27,10 +28,22 @@ assert.match(html, /<link rel="manifest" href="\/manifest\.webmanifest" \/>/, "P
|
|
|
27
28
|
assert.match(html, /<link rel="apple-touch-icon" href="\/apple-touch-icon\.png" \/>/, "PWA should expose the conventional iOS home-screen icon path");
|
|
28
29
|
assert.match(html, /id="terminalTabsToggleButton"/, "mobile should expose a compact terminal-tabs toggle");
|
|
29
30
|
assert.match(html, /id="sidePanelBackdrop"/, "mobile side panel needs an overlay/backdrop close target");
|
|
31
|
+
assert.match(html, /id="themeSelect"/, "side panel should expose a theme selector");
|
|
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");
|
|
30
35
|
assert.match(html, /id="jumpToLatestButton"/, "chat should expose a jump-to-latest control for non-forced streaming");
|
|
36
|
+
assert.match(html, /id="stickyUserPromptButton"/, "chat should expose a fixed last-user-prompt jump control");
|
|
37
|
+
assert.match(html, /id="feedbackTray"/, "chat should expose a queued action-feedback tray");
|
|
38
|
+
assert.match(html, /id="sendFeedbackButton"/, "action feedback should be submittable after the agent finishes");
|
|
31
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");
|
|
32
40
|
assert.match(html, /id="composerActionsButton"/, "mobile composer should expose a compact actions trigger");
|
|
33
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");
|
|
34
47
|
assert.match(html, /id="steerButton"[\s\S]*?data-tooltip="Steer usage:/, "Steer should explain type-first usage in a tooltip");
|
|
35
48
|
assert.match(html, /id="followUpButton"[\s\S]*?data-tooltip="Follow-up usage:/, "Follow-up should explain type-first usage in a tooltip");
|
|
36
49
|
assert.ok(
|
|
@@ -40,22 +53,59 @@ assert.ok(
|
|
|
40
53
|
);
|
|
41
54
|
|
|
42
55
|
assert.match(css, /--visual-viewport-height:\s*100dvh/, "CSS should define a visual viewport height fallback");
|
|
56
|
+
assert.match(css, /color-scheme:\s*var\(--theme-color-scheme\)/, "CSS should allow JS-selected themes to update browser color-scheme");
|
|
57
|
+
assert.match(css, /--background-glow-pink/, "CSS should expose theme-controlled page glow colors");
|
|
43
58
|
assert.match(css, /height:\s*var\(--visual-viewport-height, 100dvh\)/, "layout should consume visual viewport height");
|
|
44
59
|
assert.match(css, /button, select, input \{ min-height: 44px; \}/, "base controls should meet 44px touch-target height");
|
|
45
60
|
assert.match(css, /\.composer-row button[\s\S]*?min-height:\s*44px/, "mobile composer buttons should keep 44px touch targets");
|
|
46
61
|
assert.match(css, /#promptInput \{[\s\S]*?min-height:\s*calc\(1\.5em \+ 1\.8rem\)/, "prompt input should default to a compact single-line height");
|
|
47
62
|
assert.match(css, /#promptInput \{[\s\S]*?overflow-y:\s*hidden/, "prompt input should be JS-resized instead of showing a scrollbar by default");
|
|
63
|
+
assert.match(css, /\.sticky-user-prompt-button \{[\s\S]*?grid-template-columns:\s*auto minmax\(0, 1fr\) auto/, "last-user-prompt jump control should render as a fixed transcript header");
|
|
48
64
|
assert.match(css, /\.message\.extension,[\s\S]*?\.message\.native/, "extension and native command output should have visible transcript styling");
|
|
65
|
+
assert.match(css, /\.message\.run-indicator-message \{[\s\S]*?border-color/, "active agent runs should render a visible transcript indicator card");
|
|
66
|
+
assert.match(css, /\.message\.thinking,\n\.message\.toolCall,\n\.message\.assistantEvent/, "thinking and assistant events should have non-assistant transcript card styling");
|
|
67
|
+
assert.match(css, /\.message\.toolResult, \.message\.bashExecution, \.message\.compactionSummary/, "compaction summaries should render as compact collapsible transcript cards");
|
|
68
|
+
assert.match(css, /\.run-indicator-pulse \{[\s\S]*?animation:\s*run-indicator-pulse/, "active agent run indicator should have an animated pulse");
|
|
49
69
|
assert.match(css, /\.todo-widget \{[\s\S]*?display:\s*grid/, "todo-progress widget should render as a styled checklist card");
|
|
50
70
|
assert.match(css, /\.todo-widget-item\.partial \.todo-widget-marker/, "todo-progress partial items should have distinct styling");
|
|
51
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");
|
|
52
75
|
assert.match(css, /\.message\.warn \.message-role \{ color: var\(--ctp-yellow\); \}/, "warning-level command output should be visually distinct");
|
|
76
|
+
assert.match(css, /\.commands-box \{[\s\S]*?max-height:\s*min\(32rem, 52vh\)/, "side-panel commands should use expanded viewport-aware height");
|
|
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");
|
|
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");
|
|
83
|
+
assert.match(css, /\.feedback-tray\[hidden\] \{ display: none; \}/, "queued action-feedback tray should hide when empty");
|
|
84
|
+
assert.match(css, /\.action-feedback-controls \{[\s\S]*?position:\s*absolute/, "action reactions should be absolutely positioned so they do not expand cards");
|
|
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");
|
|
86
|
+
assert.match(css, /\.action-feedback-controls \{[\s\S]*?opacity:\s*0/, "action reactions should stay hidden until hovered or focused");
|
|
87
|
+
assert.match(css, /\.action-feedback-controls:hover,[\s\S]*?\.action-feedback-controls:focus-within/, "action reactions should reveal on hover or keyboard focus");
|
|
88
|
+
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");
|
|
89
|
+
assert.match(css, /\.action-feedback-button\.feedback-question\.active/, "question-mark reaction should have selected styling");
|
|
53
90
|
assert.match(css, /\.composer-row button\[data-tooltip\]::after/, "composer button tooltips should be shared across Git, Steer, and Follow-up buttons");
|
|
54
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");
|
|
55
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");
|
|
56
96
|
assert.match(css, /body\.composer-actions-open \.composer-actions-panel \{ display: grid; \}/, "composer actions panel should only open when toggled");
|
|
57
97
|
assert.match(css, /\.terminal-tabs-toggle-button \{ display: none; \}/, "terminal tab toggle should be hidden outside mobile CSS");
|
|
58
98
|
assert.match(css, /body\.mobile-tabs-expanded \.terminal-tabs \{ display: flex; \}/, "mobile tabs should expand only when toggled");
|
|
99
|
+
assert.match(css, /\.terminal-tab-activity-indicator/, "terminal tabs should expose per-tab agent activity indicators");
|
|
100
|
+
assert.match(css, /\.terminal-tab-group-item \{[\s\S]*?background:\s*var\(--ctp-crust\)/, "grouped terminal tab items should use opaque backgrounds");
|
|
101
|
+
assert.match(css, /\.terminal-tab-group\.active \{[\s\S]*?background:[\s\S]*?var\(--ctp-crust\)/, "active terminal tab groups should keep opaque backgrounds");
|
|
102
|
+
assert.match(css, /\.terminal-tab-group\.stopped \{[\s\S]*?opacity:\s*1/, "stopped terminal tab groups should not become transparent");
|
|
103
|
+
assert.match(css, /\.terminal-tabs:has\(\.terminal-tab-group\.menu-open\)/, "open terminal tab groups should keep the tab strip usable across rerenders");
|
|
104
|
+
assert.match(css, /\.terminal-tab-group\.menu-open \.terminal-tab-group-menu \{[\s\S]*?display:\s*flex/, "open terminal tab group menus should remain visible without hover");
|
|
105
|
+
assert.match(css, /\.terminal-tab\.activity-working[\s\S]*?terminal-tab-working-pulse/, "working tab indicators should be visibly animated");
|
|
106
|
+
assert.match(css, /\.terminal-tab\.activity-blocked[\s\S]*?rgba\(250, 179, 135/, "blocked tab indicators should use orange styling");
|
|
107
|
+
assert.match(css, /\.terminal-tab\.activity-blocked \.terminal-tab-activity-indicator[\s\S]*?background:\s*var\(--ctp-peach\)/, "blocked tab indicator dots should be orange");
|
|
108
|
+
assert.match(css, /\.terminal-tab\.activity-done/, "completed unseen work should have a distinct tab style");
|
|
59
109
|
assert.match(css, /\.terminal-tabs[\s\S]*?position:\s*absolute/, "expanded mobile tabs should overlay instead of consuming transcript space");
|
|
60
110
|
assert.match(css, /body\.mobile-keyboard-open \.terminal-tabs-shell,[\s\S]*?body\.mobile-keyboard-open \.widget-area,[\s\S]*?body\.mobile-keyboard-open \.statusbar/, "mobile keyboard mode should hide header, widgets, and footer");
|
|
61
111
|
assert.match(css, /body\.mobile-keyboard-open \.composer-actions-button,[\s\S]*?body\.mobile-keyboard-open #steerButton,[\s\S]*?body\.mobile-keyboard-open #followUpButton/, "mobile keyboard mode should hide secondary composer buttons");
|
|
@@ -73,19 +123,124 @@ assert.match(css, /(?:^|\n)\s*\.side-panel-backdrop\s*\{[\s\S]*?position:\s*fixe
|
|
|
73
123
|
assert.match(css, /(?:^|\n)\s*\.side-panel\s*\{[\s\S]*?position:\s*fixed/, "mobile side panel should be an overlay drawer instead of stacked content");
|
|
74
124
|
assert.match(css, /\.extension-dialog[\s\S]*?max-height:\s*calc\(var\(--visual-viewport-height/, "dialogs should fit the visual viewport on mobile");
|
|
75
125
|
assert.match(css, /\.extension-dialog[\s\S]*?inset:\s*auto 0 0 0/, "mobile dialogs should behave like bottom sheets");
|
|
126
|
+
assert.match(css, /#dialogMessage \{[\s\S]*?white-space:\s*pre-wrap/, "extension dialog messages should preserve multiline prompts");
|
|
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");
|
|
76
132
|
|
|
77
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");
|
|
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");
|
|
136
|
+
assert.match(app, /async function initializeThemes\(\)/, "frontend should initialize bundled themes");
|
|
137
|
+
assert.match(app, /api\("\/api\/themes", \{ scoped: false \}\)/, "theme loading should use the unscoped themes endpoint");
|
|
138
|
+
assert.match(app, /function applyTheme\(theme/, "frontend should apply a selected theme to CSS variables");
|
|
139
|
+
assert.match(app, /Restart Web UI to load themes/, "frontend should explain when a stale server cannot serve the themes endpoint");
|
|
140
|
+
assert.match(app, /themeSelect\.addEventListener\("change"/, "side-panel theme selector should switch themes immediately");
|
|
141
|
+
assert.match(app, /open \? "Close for network" : "Open to network"/, "network button should toggle from open to close action");
|
|
142
|
+
assert.match(app, /api\("\/api\/network\/close", \{ method: "POST"/, "network close action should call the close endpoint");
|
|
143
|
+
assert.match(server, /async function closeNetworkAccess\(\)/, "server should expose a local-only rebind helper for closing network access");
|
|
144
|
+
assert.match(server, /url\.pathname === "\/api\/network\/close" && req\.method === "POST"/, "server should route network close requests");
|
|
145
|
+
assert.match(server, /server\.closeAllConnections\?\.\(\)/, "network rebind should force-close long-lived clients so close-to-localhost can complete");
|
|
146
|
+
assert.match(server, /connection: "close"/, "network rebind API responses should not hold keep-alive sockets open");
|
|
147
|
+
assert.match(readme, /toggles to "Close for network"/, "README should document the close-network toggle");
|
|
78
148
|
assert.match(app, /window\.visualViewport/, "app should listen to VisualViewport for keyboard/viewport updates");
|
|
149
|
+
assert.match(html, /<textarea id="promptInput"[^>]*autofocus/, "prompt composer should autofocus for new Web UI/app launches");
|
|
79
150
|
assert.match(app, /function syncMobileChatToBottomForInput\(\)/, "mobile input focus should force the output view to the latest message");
|
|
151
|
+
assert.match(app, /function focusPromptInput\(\{ defer = false \} = \{\}\)/, "frontend should focus the prompt composer programmatically after tab/app startup");
|
|
152
|
+
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");
|
|
153
|
+
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");
|
|
154
|
+
assert.match(app, /resizePromptInput\(\);\nfocusPromptInput\(\{ defer: true \}\);\nupdateComposerModeButtons\(\);/, "startup should request prompt focus before waiting for tab state refreshes");
|
|
80
155
|
assert.match(app, /elements\.promptInput\.addEventListener\("focus", \(\) => \{\n\s+syncMobileChatToBottomForInput\(\);/, "focusing mobile input should scroll output to bottom");
|
|
81
156
|
assert.match(app, /navigator\.serviceWorker\.register\("\/service-worker\.js"\)/, "PWA service worker should be registered by the app");
|
|
82
157
|
assert.match(app, /function isChatNearBottom\(/, "chat should detect whether the user is reading above the bottom");
|
|
158
|
+
assert.match(app, /function scheduleChatFollowScroll\(/, "chat auto-follow should retry after layout settles during fast streaming");
|
|
159
|
+
assert.match(app, /function setChatScrollTopInstant\(top\)[\s\S]*?scrollBehavior = "auto"/, "chat auto-follow should bypass smooth scrolling while chasing fast output");
|
|
160
|
+
assert.match(app, /function syncAutoFollowFromChatScroll\(/, "programmatic scroll events should not accidentally disable auto-follow");
|
|
161
|
+
assert.match(app, /elements\.chat\.addEventListener\("wheel", noteChatUserScrollIntent/, "manual wheel scrolling should still be able to pause auto-follow");
|
|
83
162
|
assert.match(app, /function stripAnsi\(text\)/, "widget rendering should strip ANSI color escapes before display");
|
|
163
|
+
assert.match(app, /\(\?:\\x1B\|\\u241B\)/, "ANSI stripping should handle literal escape characters and visible escape glyphs");
|
|
164
|
+
assert.match(app, /function renderAnsiText\(parent, text\)/, "extension dialogs should render ANSI-colored TUI text as browser spans");
|
|
165
|
+
assert.match(app, /function applyAnsiSgr\(codes, state\)/, "ANSI SGR color state should be parsed for dialog rendering");
|
|
166
|
+
assert.match(app, /function normalizeDialogPrompt\(request\)/, "extension dialogs should split multiline prompts into title and body");
|
|
167
|
+
assert.match(app, /plainMessage: stripAnsi\(message\)/, "dialog prompt parsing should keep a plain-text copy for detection and visibility");
|
|
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");
|
|
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");
|
|
175
|
+
assert.match(app, /guardrail-safe-action/, "guardrail dialogs should distinguish safe and allow actions");
|
|
176
|
+
assert.match(app, /function hasQueuedDialogRequest\(id\)/, "frontend should deduplicate replayed extension UI dialogs by request id");
|
|
177
|
+
assert.match(app, /if \(request\.replayed\) addEvent\(`recovered pending \$\{request\.method\} request`, "warn"\)/, "frontend should surface replayed extension UI blockers");
|
|
178
|
+
assert.match(app, /case "webui_extension_ui_cancelled":/, "frontend should close dialogs cancelled by backend abort handling");
|
|
84
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");
|
|
85
184
|
assert.match(app, /key === "todo-progress" \? renderTodoProgressWidget\(key, lines\) : null/, "todo-progress should use the specialized widget renderer");
|
|
86
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");
|
|
188
|
+
assert.match(app, /const ACTION_FEEDBACK_REACTIONS = \{/, "frontend should define direct feedback reactions");
|
|
189
|
+
assert.match(app, /message\?\.role === "assistant" \|\| message\?\.role === "toolResult" \|\| message\?\.role === "bashExecution"/, "frontend should allow reactions on final assistant output as well as actions");
|
|
190
|
+
assert.match(app, /function renderActionFeedbackControls\(/, "frontend should render per-message reaction controls");
|
|
191
|
+
assert.match(app, /function assistantDisplayMessages\(message\)/, "assistant history should split thinking and tool-call parts out of the final Assistant output card");
|
|
192
|
+
assert.match(app, /return content\.trim\(\) \? \[\{ \.\.\.message, title: "Assistant" \}\] : \[\]/, "assistant messages with stripped empty text should not render empty Assistant cards");
|
|
193
|
+
assert.match(app, /part\.type === "text"\) return typeof part\.text === "string" && part\.text\.trim\(\) \? part : null/, "empty assistant text parts should be filtered after todo/widget extraction");
|
|
194
|
+
assert.match(app, /displayMessage\.role === "assistant" \? messageIndex : -1/, "only final Assistant output cards should keep the assistant message index for feedback");
|
|
195
|
+
assert.match(app, /function ensureStreamingThinkingBubble\(\)/, "live thinking should render in a dedicated non-assistant streaming card");
|
|
196
|
+
assert.match(app, /function assistantStreamingMessage\(event\)/, "live streaming should read the authoritative partial assistant message from RPC events like the TUI");
|
|
197
|
+
assert.match(app, /assistantThinkingTextFromMessage\(assistantStreamingMessage\(event\)\) \|\| thinkingDeltaText\(update\)/, "live thinking end should replace deltas with the final partial-message thinking content");
|
|
198
|
+
assert.match(app, /if \(typeof partialText === "string"\) streamRawText = partialText;/, "live assistant text should synchronize from partial messages instead of relying only on deltas");
|
|
199
|
+
assert.match(app, /const TODO_PROGRESS_LINE_REGEX = /, "frontend should recognize live todo progress lines that will be moved into the todo widget");
|
|
200
|
+
assert.match(app, /function stripTodoProgressLines\(text, \{ streaming = false \} = \{\}\)/, "live Assistant output should strip todo-progress lines before rendering final-output text");
|
|
201
|
+
assert.match(app, /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");
|
|
205
|
+
assert.match(app, /const created = appendMessage\(\{ role: "assistant", title: "Assistant"/, "live Assistant cards should be created only for final output text");
|
|
206
|
+
assert.match(app, /api\("\/api\/action-feedback", \{ method: "POST"/, "queued action feedback should post to the server after the run is idle");
|
|
207
|
+
assert.match(app, /function postQueuedFeedback\(tabId, items\)/, "queued feedback should have a backward-compatible submit path");
|
|
208
|
+
assert.match(app, /\/api\/action-feedback not found; falling back to a normal prompt/, "new frontend should gracefully handle older running Web UI servers");
|
|
209
|
+
assert.match(app, /actionFeedbackSteerMessage\(item\)/, "live action feedback should be sent as steering while the agent is running");
|
|
87
210
|
assert.match(app, /function addTransientMessage\(\{ role = "notice"/, "frontend should render transient command output into the transcript");
|
|
88
211
|
assert.match(app, /addTransientMessage\(\{ role: "extension", title: "extension output"/, "extension notify output should appear in the transcript, not only the event log");
|
|
212
|
+
assert.match(app, /function renderRunIndicator\(/, "frontend should render a transcript-level active agent indicator");
|
|
213
|
+
assert.match(app, /return "Agent is still runing: ";/, "active agent indicator should use the requested headline wording");
|
|
214
|
+
assert.doesNotMatch(app, /"agent running"/, "active agent indicator should not render a separate title/header label");
|
|
215
|
+
assert.doesNotMatch(app, /runIndicatorTimestamp/, "active agent indicator should not render a separate live timestamp header");
|
|
216
|
+
assert.match(app, /runIndicatorBubble = make\("article", "message runIndicator run-indicator-message streaming"\)/, "active agent indicator should use a dedicated streaming transcript card");
|
|
217
|
+
assert.match(app, /runIndicatorShowsElapsed\(\) \? `\$\{detail\} · run time/, "active agent indicator should label elapsed run time instead of showing a bare counter");
|
|
218
|
+
assert.match(app, /Abort requested; 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");
|
|
222
|
+
assert.match(app, /let runIndicatorGraceCheckTimer = null/, "local-only run indicators should have a grace-check timer");
|
|
223
|
+
assert.match(app, /const RUN_INDICATOR_STATE_RECHECK_MS = 5000/, "active run indicators should periodically re-check state");
|
|
224
|
+
assert.match(app, /function scheduleRunIndicatorGraceCheck\(/, "local-only run indicators should schedule a follow-up state check");
|
|
225
|
+
assert.match(app, /function maybeRefreshRunIndicatorState\([\s\S]*?refreshState\(\)/, "active run indicators should poll state so missed completion events do not leave stale cards");
|
|
226
|
+
assert.match(app, /function clearRunIndicatorActivity[\s\S]*?clearRunIndicatorGraceCheck\(\)[\s\S]*?runIndicatorLastStateCheckAt = 0/, "clearing the active agent indicator should cancel pending grace checks and reset state polling");
|
|
227
|
+
assert.match(app, /scheduleRunIndicatorGraceCheck\(\)[\s\S]*?refreshState\(\)/, "stale local-only run indicators should re-check state after the start grace period");
|
|
228
|
+
assert.match(app, /function scheduleAbortStateChecks\(/, "abort handling should poll state so the active indicator can clear after stop confirmation");
|
|
229
|
+
assert.match(app, /case "tool_execution_start":[\s\S]*?setRunIndicatorActivity\(`Running tool:/, "tool execution should update the active agent transcript indicator");
|
|
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");
|
|
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");
|
|
233
|
+
assert.match(app, /function getPathTrigger\(\)/, "prompt composer should detect @ file\/path reference triggers");
|
|
234
|
+
assert.match(app, /api\(`\/api\/path-suggestions\?query=\$\{encodeURIComponent\(trigger\.query\)\}`/, "@ reference suggestions should load from the server as the user types");
|
|
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");
|
|
89
244
|
assert.match(app, /function resizePromptInput\(\)/, "prompt textarea should auto-resize from a one-line default");
|
|
90
245
|
assert.match(app, /elements\.promptInput\.addEventListener\("input", \(\) => \{\n\s+resizePromptInput\(\);/, "prompt textarea should resize whenever the user edits it");
|
|
91
246
|
assert.match(app, /function updateComposerModeButtons\(\)/, "composer should relocate Steer and Follow-up based on run state");
|
|
@@ -94,13 +249,57 @@ assert.match(app, /document\.body\.classList\.toggle\("pi-run-active", runActive
|
|
|
94
249
|
assert.match(app, /function showComposerButtonTooltip\(button\)/, "empty mode-button taps should show the usage tooltip");
|
|
95
250
|
assert.match(app, /sendPromptFromModeButton\("steer", elements\.steerButton\)/, "Steer should show tooltip instead of silently doing nothing when input is empty");
|
|
96
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");
|
|
257
|
+
assert.match(app, /async function sendPrompt\(kind = "prompt", explicitMessage\)/, "prompt sending should accept direct messages that bypass the input field");
|
|
258
|
+
assert.match(app, /const rawMessage = usesPromptInput \? elements\.promptInput\.value : explicitMessage/, "direct prompt sends should not read the input textarea");
|
|
259
|
+
assert.match(app, /if \(usesPromptInput\) \{\n\s+elements\.promptInput\.value = "";/, "direct prompt sends should preserve the input textarea draft");
|
|
260
|
+
assert.match(app, /make\("button", "command-item"\)[\s\S]*?sendPrompt\("prompt", `\/\$\{command\.name\}`\)/, "side-panel command clicks should send the slash command directly");
|
|
97
261
|
assert.match(app, /function shouldSendPromptFromEnter\(event\)/, "prompt keyboard handling should be centralized");
|
|
98
262
|
assert.match(app, /return !isMobileView\(\);/, "plain Enter should send only outside mobile view so mobile Return can insert newlines");
|
|
99
263
|
assert.match(app, /mobile-keyboard-open/, "JS should toggle mobile keyboard mode from viewport/focus state");
|
|
100
264
|
assert.match(app, /maxVisualViewportHeight - viewportHeight > 120/, "keyboard mode should detect viewport shrink even when keyboard inset is unavailable");
|
|
101
265
|
assert.match(app, /jumpToLatestButton/, "jump-to-latest button should be wired in JS");
|
|
266
|
+
assert.match(app, /function updateStickyUserPromptButton\(/, "last user prompt header should update from scroll position");
|
|
267
|
+
assert.match(app, /function jumpToStickyUserPrompt\(/, "last user prompt header should jump to its source message");
|
|
268
|
+
assert.match(app, /data-user-prompt/, "user prompt messages should be marked for sticky prompt navigation");
|
|
269
|
+
assert.match(app, /function resetChatOutput\(\)[\s\S]*?stickyUserPromptButton/, "chat rerenders should preserve the sticky user prompt control inside the transcript scroller");
|
|
270
|
+
assert.match(app, /LAST_USER_PROMPT_STORAGE_KEY/, "last user prompt should be cached so compaction cannot remove the sticky prompt preview");
|
|
271
|
+
assert.match(app, /function syncLastUserPromptFromMessages\(messages = latestMessages\)/, "message refresh should preserve the latest user prompt across compacted transcripts");
|
|
272
|
+
assert.match(app, /dataset\.compacted/, "sticky prompt should expose a compacted fallback state when its source message was summarized away");
|
|
273
|
+
assert.match(app, /stickyUserPromptButton\?\.addEventListener\("click", jumpToStickyUserPrompt\)/, "last user prompt header should be clickable without breaking stale cached HTML");
|
|
102
274
|
assert.match(app, /function setComposerActionsOpen\(/, "mobile composer actions panel should be JS-toggleable");
|
|
103
275
|
assert.match(app, /function setMobileTabsExpanded\(/, "mobile tab strip should be JS-toggleable");
|
|
276
|
+
assert.match(app, /let openTerminalTabGroupKey = null/, "frontend should track the open terminal tab group across tab bar rerenders");
|
|
277
|
+
assert.match(app, /function updateTerminalTabGroupOpenState\(\)/, "frontend should be able to reapply open terminal tab group state after rerenders");
|
|
278
|
+
assert.match(app, /wrapper\.addEventListener\("pointerenter", \(\) => setOpenTerminalTabGroup\(group\.key\)\)/, "terminal tab groups should mark themselves open while hovered");
|
|
279
|
+
assert.match(app, /if \(openTerminalTabGroupKey\) \{\n\s+scheduleRefreshTabs\(600\);/, "tab polling should defer full tab refreshes while a group menu is open");
|
|
280
|
+
assert.match(app, /function shouldRenderTerminalTabGroup\(group, groupCount\) \{\n\s+return groupCount > 1 && group\.tabs\.length > 1 && Boolean\(group\.cwd\);\n\}/, "terminal tabs should only collapse cwd groups when multiple groups are available");
|
|
281
|
+
assert.match(app, /const groups = tabCwdGroups\(\);[\s\S]*?for \(const group of groups\) \{\n\s+if \(shouldRenderTerminalTabGroup\(group, groups\.length\)\)[\s\S]*?for \(const tab of group\.tabs\) elements\.tabBar\.append\(renderTerminalTab\(tab\)\);/, "terminal tabs should render every tab ungrouped when grouping is skipped");
|
|
282
|
+
assert.match(app, /let tabSeenCompletionSerials = new Map\(\)/, "frontend should track which tab completions have been seen");
|
|
283
|
+
assert.match(app, /function tabIndicator\(tab\)/, "frontend should derive idle, working, blocked, and work-done tab indicator states");
|
|
284
|
+
assert.match(app, /pendingBlockerCount > 0[\s\S]*?state: "blocked"/, "frontend should show blocked tabs when extension UI blockers are pending");
|
|
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");
|
|
286
|
+
assert.match(app, /function notifyBlockedTab\(/, "frontend should send blocked-tab notifications when extension UI blocks a run");
|
|
287
|
+
assert.match(app, /function showBlockedTabBrowserNotification\(/, "frontend should use browser notifications for blocked tabs when permission allows");
|
|
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");
|
|
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");
|
|
295
|
+
assert.match(app, /notifyBlockedTab\(request\.tabId, \{ request, count: request\.pendingExtensionUiRequestCount \}\)/, "extension UI requests should trigger blocked-tab notifications");
|
|
296
|
+
assert.match(app, /pendingExtensionUiRequestCount[\s\S]*?setTabPendingBlockerCount/, "frontend should ingest pending blocker counts from tab events");
|
|
297
|
+
assert.match(app, /function markTabOutputSeen\(/, "frontend should clear work-done indicators once output is seen");
|
|
298
|
+
assert.match(app, /function markTabDoneLocally\(/, "frontend should locally recover tabs that were left working after idle state refreshes");
|
|
299
|
+
assert.match(app, /function syncActiveTabActivityFromState\(state = currentState\)/, "frontend should reconcile active-tab indicators from authoritative state snapshots");
|
|
300
|
+
assert.match(app, /event\.command === "get_state" && event\.tabId === activeTabId[\s\S]*?syncActiveTabActivityFromState\(currentState\)/, "get_state response events should update stale active-tab activity");
|
|
301
|
+
assert.match(app, /function applyResponseTab\(response\)/, "frontend should merge tab metadata returned by prompt responses");
|
|
302
|
+
assert.match(app, /case "webui_tab_renamed":/, "frontend should update tab labels from backend rename events");
|
|
104
303
|
assert.match(app, /terminalTabsToggleButton\.addEventListener\("click"/, "terminal tabs trigger should be wired in JS");
|
|
105
304
|
assert.match(app, /composerActionsButton\.addEventListener\("click"/, "composer actions trigger should be wired in JS");
|
|
106
305
|
assert.match(app, /function setMobileFooterExpanded\(/, "mobile footer should have an expandable details state");
|
|
@@ -128,7 +327,9 @@ assert.equal(manifest.start_url, "/", "PWA manifest should start at the web UI r
|
|
|
128
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");
|
|
129
328
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-192.png" && icon.sizes === "192x192"), "PWA manifest should include a 192px icon");
|
|
130
329
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-512.png" && icon.sizes === "512x512"), "PWA manifest should include a 512px icon");
|
|
131
|
-
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-
|
|
330
|
+
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v10"/, "PWA service worker should define an app-shell cache");
|
|
331
|
+
assert.match(serviceWorker, /self\.addEventListener\("notificationclick"/, "PWA service worker should focus Web UI when blocked-tab notifications are clicked");
|
|
332
|
+
assert.match(serviceWorker, /event\.notification\.data\?\.url/, "blocked-tab notifications should carry a URL for service-worker click handling");
|
|
132
333
|
assert.match(serviceWorker, /"\/apple-touch-icon\.png"/, "PWA service worker should cache the apple touch icon");
|
|
133
334
|
assert.match(serviceWorker, /url\.pathname\.startsWith\("\/api\/"\)/, "PWA service worker should not cache live API or SSE calls");
|
|
134
335
|
assert.ok(appleIcon.length > 1000, "PWA apple touch icon should be present");
|
|
@@ -138,21 +339,69 @@ assert.ok(icon512.length > icon192.length, "PWA 512px icon should be present and
|
|
|
138
339
|
assert.match(server, /const NATIVE_SLASH_COMMANDS = \[/, "server should define Pi native slash commands for autocomplete");
|
|
139
340
|
assert.match(server, /\{ name: "reload", description: "Reload keybindings, extensions, skills, prompts, and themes" \}/, "native /reload should be advertised for autocomplete");
|
|
140
341
|
assert.match(server, /function parseSlashCommand\(message\)/, "server should parse native slash commands before prompt forwarding");
|
|
342
|
+
assert.match(server, /function generatedTabTitleFromPrompt\(message\)/, "server should derive concise automatic tab titles from first prompts");
|
|
343
|
+
assert.match(server, /function maybeNameTabForConversation\(tab, command\)/, "server should auto-name default tabs when a conversation starts");
|
|
344
|
+
assert.match(server, /function createTabActivity\(/, "server should track per-tab activity for idle, working, and completed work");
|
|
345
|
+
assert.match(server, /function reconcileTabActivityFromState\(tab, state/, "server should recover stale working tab activity from get_state snapshots");
|
|
346
|
+
assert.match(server, /pendingExtensionUiRequests\(tab\)\.length > 0[\s\S]*?markTabWorking\(tab, timestamp\)/, "server should keep tabs with pending blockers in working activity until the blocker resolves");
|
|
347
|
+
assert.match(server, /case "response":[\s\S]*?event\.command === "get_state"[\s\S]*?reconcileTabActivityFromState\(tab, event\.data/, "server should reconcile tab activity when get_state responses flow through RPC events");
|
|
348
|
+
assert.match(server, /async function listTabsWithReconciledActivity\(\)/, "server tab listing should reconcile stale working tabs before returning metadata");
|
|
349
|
+
assert.match(server, /tabs: await listTabsWithReconciledActivity\(\)/, "GET /api/tabs should use reconciled tab activity metadata");
|
|
350
|
+
assert.match(server, /tabActivity: tabActivitySnapshot\(tab\)/, "server should expose tab activity over tab metadata and events");
|
|
351
|
+
assert.match(server, /const EXTENSION_UI_BLOCKING_METHODS = new Set\(\["select", "confirm", "input", "editor"\]\)/, "server should know which extension UI requests can block Pi runs");
|
|
352
|
+
assert.match(server, /function trackPendingExtensionUiRequest\(tab, event\)/, "server should track blocking extension UI requests per tab");
|
|
353
|
+
assert.match(server, /pendingExtensionUiRequests: new Map\(\)/, "new tabs should initialize pending extension UI request storage");
|
|
354
|
+
assert.match(server, /trackPendingExtensionUiRequest\(tab, scopedEvent\)/, "RPC events should populate pending extension UI storage before broadcasting");
|
|
355
|
+
assert.match(server, /scopedEvent = \{ \.\.\.scopedEvent,[\s\S]*?pendingExtensionUiRequestCount: pendingExtensionUiRequests\(tab\)\.length \}/, "RPC events should broadcast pending blocker counts for tab indicators");
|
|
356
|
+
assert.match(server, /function replayPendingExtensionUiRequests\(tab, res\)/, "server should be able to replay missed extension UI requests on SSE reconnect");
|
|
357
|
+
assert.match(server, /replayPendingExtensionUiRequests\(tab, res\)/, "SSE connections should replay pending extension UI blockers");
|
|
358
|
+
assert.match(server, /pendingExtensionUiRequests: pendingExtensionUiRequestSummaries\(tab\)/, "detailed Web UI status should expose pending extension UI blockers");
|
|
359
|
+
assert.match(server, /resolvePendingExtensionUiRequest\(tab, payload\.id\)/, "extension UI responses should clear the pending blocker cache");
|
|
360
|
+
assert.match(server, /type: "webui_extension_ui_resolved"[\s\S]*?pendingExtensionUiRequestCount/, "extension UI responses should notify clients that a blocker resolved");
|
|
361
|
+
assert.match(server, /command\.type === "abort"[\s\S]*?cancelPendingExtensionUiRequests\(tab\)/, "abort should cancel hidden pending extension UI requests");
|
|
362
|
+
assert.match(server, /type: "webui_extension_ui_cancelled"/, "server should notify browsers when pending extension UI requests are cancelled");
|
|
141
363
|
assert.match(server, /async function handleNativeSlashCommand\(tab, body\)/, "server should intercept supported native slash commands");
|
|
364
|
+
assert.match(server, /const restoreTabs = readRestoreTabsFromEnv\(\)/, "server should accept restart tab restore descriptors from the launcher environment");
|
|
365
|
+
assert.match(server, /delete process\.env\.PI_WEBUI_RESTORE_TABS/, "server should avoid leaking restore descriptors into spawned Pi RPC processes");
|
|
366
|
+
assert.match(server, /if \(sessionFile && !options\.noSession\) piArgs\.push\("--session", sessionFile\)/, "restored tabs should resume previous session files");
|
|
367
|
+
assert.match(server, /const closedRestorableTabs = \[\]/, "server should remember recently closed tabs for slash-command restarts");
|
|
368
|
+
assert.match(server, /async function closeTab\(id\)[\s\S]*?rememberClosedRestorableTab\(tab, restorableState\)/, "closing a tab should capture its restorable session before stopping RPC");
|
|
369
|
+
assert.match(server, /function rememberTabState\(tab, state\)/, "server should cache last-known tab state for restart-safe session restoration");
|
|
370
|
+
assert.match(server, /sessionFile: tabRestorableSessionFile\(tab\)/, "tab metadata should expose cached session files for health/status restore descriptors");
|
|
371
|
+
assert.match(server, /restorableTabs: mergeRestorableTabDescriptors\(statusTabs, closedRestorableTabs\)/, "status should expose open plus recently closed restorable tabs");
|
|
372
|
+
assert.match(server, /data\.restorableTabs = mergeRestorableTabDescriptors\(detailedTabs, closedRestorableTabs\)/, "detailed status should include session-aware closed tab restore descriptors");
|
|
373
|
+
assert.match(server, /const stateData = stateResult\.ok \? stateResult\.data : tab\.lastState \|\| null/, "detailed status should fall back to cached state when live RPC state is temporarily unavailable");
|
|
374
|
+
assert.match(server, /const initialTabs = await createInitialTabs\(\)/, "server should recreate restored tabs before listening");
|
|
375
|
+
assert.match(extension, /api\/webui-status\?detailed=1&events=0/, "launcher should capture detailed tab status before restarting an existing Web UI");
|
|
376
|
+
assert.match(extension, /function mergeRestorableTabsFromStatusSources\(sources: unknown\[\], options: StartWebuiOptions\)/, "launcher should merge all available restore sources instead of trusting only the first one");
|
|
377
|
+
assert.match(extension, /statusData\?\.restorableTabs, statusData\?\.tabs, existing\.restorableTabs, existing\.tabs/, "launcher should combine detailed, health, open, and closed restore descriptors");
|
|
378
|
+
assert.match(extension, /env\.PI_WEBUI_RESTORE_TABS = JSON\.stringify\(restoreTabs\)/, "launcher should pass restorable tabs to the new detached server");
|
|
379
|
+
assert.match(extension, /pi\.registerCommand\("start-webui"/, "extension should expose /start-webui as an alias for users who invoke that command name");
|
|
142
380
|
assert.match(server, /if \(state\.data\?\.sessionFile && !options\.noSession\) piArgs\.push\("--session", state\.data\.sessionFile\)/, "native /reload should resume the same session file when restarting the RPC tab");
|
|
143
381
|
assert.match(server, /case "reload": \{[\s\S]*?restartTabRpc\(tab, "slash-command"\)/, "native /reload should restart the active RPC tab");
|
|
144
382
|
assert.match(server, /message: "Reloaded keybindings, extensions, skills, prompts, and themes\."/, "native /reload should return visible command output");
|
|
383
|
+
assert.match(server, /case "name": \{[\s\S]*?renameTab\(tab, parsed\.args, \{ source: "explicit" \}\)/, "native /name should also rename the browser tab");
|
|
384
|
+
assert.match(server, /maybeNameTabForConversation\(tab, command\);[\s\S]*?markTabWorking\(tab\)/, "server should auto-name tabs before starting visible prompt work");
|
|
145
385
|
assert.match(server, /function formatSessionOutput\(tab, state, stats\)/, "native /session should have visible Web UI output");
|
|
146
386
|
assert.match(server, /case "session": \{[\s\S]*?formatSessionOutput\(tab, state\.data \|\| \{\}, stats\.success === false \? null : stats\.data\)/, "native /session should render state and stats through Web UI");
|
|
147
387
|
assert.match(server, /case "copy": \{[\s\S]*?get_last_assistant_text[\s\S]*?copyText: text/, "native /copy should return text for browser clipboard handling");
|
|
148
388
|
assert.match(server, /case "hotkeys": \{[\s\S]*?webuiHotkeysOutput\(\)/, "native /hotkeys should return Web UI hotkey output");
|
|
149
389
|
assert.match(server, /url\.pathname === "\/api\/commands" && req\.method === "GET"[\s\S]*?getCommandData\(tab\)/, "GET /api/commands should merge native and RPC-visible commands");
|
|
390
|
+
assert.match(server, /specific Web UI action or final-output cards/, "server feedback-learning prompt should cover final outputs as well as actions");
|
|
391
|
+
assert.match(server, /function formatActionFeedbackLearningPrompt\(items\)/, "server should convert feedback into a LEARNING prompt");
|
|
392
|
+
assert.match(server, /url\.pathname === "\/api\/action-feedback" && req\.method === "POST"[\s\S]*?handleActionFeedback\(tab, body\)/, "POST /api/action-feedback should trigger the feedback-learning prompt");
|
|
393
|
+
assert.match(server, /Wait for the current agent run or compaction to finish before sending feedback\./, "server should only accept post-run feedback submissions");
|
|
150
394
|
assert.match(server, /url\.pathname === "\/api\/prompt" && req\.method === "POST"[\s\S]*?handleNativeSlashCommand\(tab, body\)/, "POST /api/prompt should intercept native slash commands before normal prompt forwarding");
|
|
151
395
|
assert.match(server, /function fastPicksStorageFile\(/, "server should define a persistent fast-picks storage file");
|
|
152
396
|
assert.match(server, /PI_WEBUI_FAST_PICKS_FILE/, "server should allow overriding the fast-picks storage path");
|
|
397
|
+
assert.match(server, /async function getPathSuggestionData\(tab, rawQuery\)/, "server should compute @ file\/path reference suggestions for the active tab cwd");
|
|
398
|
+
assert.match(server, /url\.pathname === "\/api\/path-suggestions" && req\.method === "GET"/, "server should expose GET /api/path-suggestions for @ reference autocomplete");
|
|
153
399
|
assert.match(server, /url\.pathname === "\/api\/path-fast-picks" && req\.method === "GET"/, "server should expose GET /api/path-fast-picks");
|
|
154
400
|
assert.match(server, /url\.pathname === "\/api\/path-fast-picks" && req\.method === "POST"/, "server should expose POST /api/path-fast-picks");
|
|
155
401
|
assert.match(server, /url\.pathname === "\/api\/scoped-models" && req\.method === "GET"/, "server should expose GET /api/scoped-models");
|
|
402
|
+
assert.match(server, /@firstpick\/pi-themes-bundle/, "server should discover themes from the bundled theme package");
|
|
403
|
+
assert.match(server, /url\.pathname === "\/api\/themes" && req\.method === "GET"/, "server should expose GET /api/themes");
|
|
404
|
+
assert.match(server, /readBundledThemes\(\)/, "server should read bundled theme JSON files for the browser");
|
|
156
405
|
assert.match(server, /"apple-touch-icon\.png", "icon-192\.png"/, "server should serve the conventional apple touch icon path");
|
|
157
406
|
assert.match(server, /"manifest\.webmanifest", "service-worker\.js"/, "server should serve PWA manifest and service worker as static assets");
|
|
158
407
|
assert.match(server, /\["\.webmanifest", "application\/manifest\+json; charset=utf-8"\]/, "server should serve manifest with the correct MIME type");
|
|
@@ -161,9 +410,18 @@ assert.match(server, /function configuredScopedModelPatterns\(cwd = options\.cwd
|
|
|
161
410
|
assert.match(server, /readJsonFileIfExists\(path\.join\(cwd, "\.pi", "settings\.json"\)\)/, "server should read project-local scoped-model settings from active tab cwd");
|
|
162
411
|
assert.match(server, /resolveScopedModelsFromPatterns\(patterns, response\.data\?\.models/, "server should resolve scoped patterns against available models");
|
|
163
412
|
assert.match(server, /writeFile\(tmpFile[\s\S]*?rename\(tmpFile, storageFile\)/, "server should persist fast picks with an atomic temp-file rename");
|
|
413
|
+
assert.match(readme, /Automatic tab naming from the first prompt/, "README should describe automatic terminal-tab naming");
|
|
414
|
+
assert.match(readme, /Feedback reactions \(`👍`, `👎`, `\?`\) on final assistant output plus tool\/bash action cards/, "README should describe final-output and action feedback reactions");
|
|
415
|
+
assert.match(readme, /POST \/api\/action-feedback\?tab=<tabId>/, "README should document the action-feedback endpoint");
|
|
416
|
+
assert.match(readme, /`@` file\/path references with live suggestions/, "README should describe @ file/path reference autocomplete");
|
|
417
|
+
assert.match(readme, /GET \/api\/path-suggestions\?tab=<tabId>&query=<path>/, "README should document the path-suggestions endpoint");
|
|
164
418
|
assert.match(readme, /server-persisted fast picks/, "README should describe server-persisted fast picks");
|
|
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");
|
|
421
|
+
assert.match(readme, /Side-panel theme picker backed by the bundled `@firstpick\/pi-themes-bundle` themes/, "README should describe bundled theme selection");
|
|
165
422
|
|
|
166
423
|
assert.equal(pkg.scripts?.test, "node tests/mobile-static.test.mjs", "package test script should run the mobile static harness");
|
|
424
|
+
assert.equal(pkg.dependencies?.["@firstpick/pi-themes-bundle"], "^0.1.0", "webui package should depend on the bundled theme package");
|
|
167
425
|
assert.ok(pkg.scripts?.check?.includes("node --check public/app.js"), "check script should syntax-check app.js");
|
|
168
426
|
assert.ok(pkg.scripts?.check?.includes("node tests/mobile-static.test.mjs"), "check script should include mobile static assertions");
|
|
169
427
|
|