@firstpick/pi-package-webui 0.1.4 → 0.1.5
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 +25 -6
- package/bin/pi-webui.mjs +183 -13
- package/package.json +34 -4
- package/public/app.js +567 -42
- package/public/index.html +3 -0
- package/public/service-worker.js +1 -1
- package/public/styles.css +145 -2
- package/tests/mobile-static.test.mjs +89 -8
package/public/index.html
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
<div id="tabBar" class="terminal-tabs" role="tablist" aria-label="Pi terminal tabs">
|
|
30
30
|
<button id="newTabButton" class="terminal-new-tab-button" type="button" title="Start a separate isolated Pi terminal">+ Tab</button>
|
|
31
31
|
</div>
|
|
32
|
+
<button id="closeAllTabsButton" class="terminal-close-all-button" type="button" title="Close all terminal tabs">Close all Tabs</button>
|
|
32
33
|
</header>
|
|
33
34
|
<div id="widgetArea" class="widget-area"></div>
|
|
34
35
|
<div id="chat" class="chat" aria-live="polite">
|
|
@@ -173,6 +174,8 @@
|
|
|
173
174
|
</div>
|
|
174
175
|
<button id="abortButton" type="button" class="danger">Abort</button>
|
|
175
176
|
</div>
|
|
177
|
+
<h2>Optional features</h2>
|
|
178
|
+
<div id="optionalFeaturesBox" class="optional-features-box muted">Checking optional features…</div>
|
|
176
179
|
<h2>Session</h2>
|
|
177
180
|
<dl id="stateDetails" class="details"></dl>
|
|
178
181
|
<h2>Queue</h2>
|
package/public/service-worker.js
CHANGED
package/public/styles.css
CHANGED
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
--visual-viewport-offset-top: 0px;
|
|
64
64
|
--keyboard-inset-bottom: 0px;
|
|
65
65
|
|
|
66
|
+
font-size: 80%;
|
|
66
67
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -623,6 +624,103 @@ body.side-panel-collapsed .terminal-tabs-shell {
|
|
|
623
624
|
.side-panel-controls .danger {
|
|
624
625
|
margin-top: 0.15rem;
|
|
625
626
|
}
|
|
627
|
+
.optional-features-box {
|
|
628
|
+
display: grid;
|
|
629
|
+
gap: 0.6rem;
|
|
630
|
+
padding: 0.72rem;
|
|
631
|
+
border: 1px solid rgba(180, 190, 254, 0.16);
|
|
632
|
+
border-radius: 0.85rem;
|
|
633
|
+
background: rgba(var(--ctp-crust-rgb), 0.46);
|
|
634
|
+
box-shadow: inset 0 1px 0 rgba(255,255,255,0.035), 0 0 1rem rgba(148, 226, 213, 0.05);
|
|
635
|
+
}
|
|
636
|
+
.optional-feature-row {
|
|
637
|
+
display: grid;
|
|
638
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
639
|
+
gap: 0.62rem;
|
|
640
|
+
align-items: start;
|
|
641
|
+
min-width: 0;
|
|
642
|
+
padding: 0.62rem;
|
|
643
|
+
border: 1px solid rgba(180, 190, 254, 0.14);
|
|
644
|
+
border-radius: 0.72rem;
|
|
645
|
+
background: rgba(var(--ctp-crust-rgb), 0.55);
|
|
646
|
+
}
|
|
647
|
+
.optional-feature-row.enabled {
|
|
648
|
+
border-color: rgba(166, 227, 161, 0.28);
|
|
649
|
+
}
|
|
650
|
+
.optional-feature-row.disabled {
|
|
651
|
+
border-color: rgba(249, 226, 175, 0.28);
|
|
652
|
+
}
|
|
653
|
+
.optional-feature-row.missing {
|
|
654
|
+
border-color: rgba(243, 139, 168, 0.25);
|
|
655
|
+
}
|
|
656
|
+
.optional-feature-main {
|
|
657
|
+
display: grid;
|
|
658
|
+
gap: 0.28rem;
|
|
659
|
+
min-width: 0;
|
|
660
|
+
}
|
|
661
|
+
.optional-feature-title {
|
|
662
|
+
display: flex;
|
|
663
|
+
flex-wrap: wrap;
|
|
664
|
+
gap: 0.42rem;
|
|
665
|
+
align-items: center;
|
|
666
|
+
min-width: 0;
|
|
667
|
+
}
|
|
668
|
+
.optional-feature-title strong {
|
|
669
|
+
color: rgba(var(--ctp-text-rgb), 0.94);
|
|
670
|
+
font-size: 0.82rem;
|
|
671
|
+
}
|
|
672
|
+
.optional-feature-pill {
|
|
673
|
+
display: inline-flex;
|
|
674
|
+
align-items: center;
|
|
675
|
+
min-height: 1.15rem;
|
|
676
|
+
padding: 0.12rem 0.42rem;
|
|
677
|
+
border-radius: 999px;
|
|
678
|
+
color: var(--ctp-subtext);
|
|
679
|
+
border: 1px solid rgba(180, 190, 254, 0.18);
|
|
680
|
+
background: rgba(var(--ctp-surface-rgb), 0.55);
|
|
681
|
+
font-size: 0.64rem;
|
|
682
|
+
font-weight: 900;
|
|
683
|
+
letter-spacing: 0.08em;
|
|
684
|
+
text-transform: uppercase;
|
|
685
|
+
}
|
|
686
|
+
.optional-feature-pill.enabled {
|
|
687
|
+
color: var(--ctp-green);
|
|
688
|
+
border-color: rgba(166, 227, 161, 0.32);
|
|
689
|
+
}
|
|
690
|
+
.optional-feature-pill.disabled {
|
|
691
|
+
color: var(--ctp-yellow);
|
|
692
|
+
border-color: rgba(249, 226, 175, 0.32);
|
|
693
|
+
}
|
|
694
|
+
.optional-feature-pill.missing {
|
|
695
|
+
color: var(--ctp-red);
|
|
696
|
+
border-color: rgba(243, 139, 168, 0.32);
|
|
697
|
+
}
|
|
698
|
+
.optional-feature-detail,
|
|
699
|
+
.optional-feature-description {
|
|
700
|
+
color: rgba(var(--ctp-subtext-rgb), 0.74);
|
|
701
|
+
font-size: 0.72rem;
|
|
702
|
+
line-height: 1.35;
|
|
703
|
+
}
|
|
704
|
+
.optional-feature-package {
|
|
705
|
+
width: fit-content;
|
|
706
|
+
max-width: 100%;
|
|
707
|
+
overflow-wrap: anywhere;
|
|
708
|
+
color: rgba(var(--ctp-teal-rgb), 0.86);
|
|
709
|
+
font-size: 0.68rem;
|
|
710
|
+
}
|
|
711
|
+
.optional-feature-action {
|
|
712
|
+
min-width: 5.2rem;
|
|
713
|
+
white-space: nowrap;
|
|
714
|
+
}
|
|
715
|
+
.optional-feature-action.install {
|
|
716
|
+
color: var(--ctp-yellow);
|
|
717
|
+
border-color: rgba(249, 226, 175, 0.32);
|
|
718
|
+
}
|
|
719
|
+
.optional-feature-action.install:not(:disabled):hover {
|
|
720
|
+
color: #11111b;
|
|
721
|
+
border-color: transparent;
|
|
722
|
+
background: linear-gradient(120deg, var(--ctp-yellow), var(--ctp-peach));
|
|
723
|
+
}
|
|
626
724
|
|
|
627
725
|
.terminal-tabs-shell {
|
|
628
726
|
position: relative;
|
|
@@ -640,6 +738,23 @@ body.side-panel-collapsed .terminal-tabs-shell {
|
|
|
640
738
|
box-shadow: inset 0 -1px 0 rgba(255,255,255,0.035), 0 0.45rem 1rem rgba(var(--ctp-crust-rgb), 0.20);
|
|
641
739
|
}
|
|
642
740
|
.terminal-tabs-toggle-button { display: none; }
|
|
741
|
+
.terminal-close-all-button {
|
|
742
|
+
flex: 0 0 auto;
|
|
743
|
+
min-height: 2.35rem;
|
|
744
|
+
padding: 0.38rem 0.7rem;
|
|
745
|
+
color: var(--ctp-red);
|
|
746
|
+
white-space: nowrap;
|
|
747
|
+
border-color: rgba(243, 139, 168, 0.32);
|
|
748
|
+
background:
|
|
749
|
+
linear-gradient(120deg, rgba(243, 139, 168, 0.12), rgba(250, 179, 135, 0.08)),
|
|
750
|
+
rgba(var(--ctp-crust-rgb), 0.62);
|
|
751
|
+
}
|
|
752
|
+
.terminal-close-all-button:hover,
|
|
753
|
+
.terminal-close-all-button:focus-visible {
|
|
754
|
+
color: #11111b;
|
|
755
|
+
border-color: transparent;
|
|
756
|
+
background: linear-gradient(120deg, var(--ctp-red), var(--ctp-peach));
|
|
757
|
+
}
|
|
643
758
|
.terminal-tabs {
|
|
644
759
|
display: flex;
|
|
645
760
|
align-items: center;
|
|
@@ -918,6 +1033,9 @@ body.side-panel-collapsed .terminal-tabs-shell {
|
|
|
918
1033
|
color: #11111b;
|
|
919
1034
|
background: linear-gradient(120deg, var(--ctp-red), var(--ctp-peach));
|
|
920
1035
|
}
|
|
1036
|
+
.terminal-tab-group-close {
|
|
1037
|
+
border-left-color: rgba(243, 139, 168, 0.18);
|
|
1038
|
+
}
|
|
921
1039
|
.terminal-new-tab-button {
|
|
922
1040
|
flex: 0 0 auto;
|
|
923
1041
|
padding: 0.45rem 0.72rem;
|
|
@@ -1629,6 +1747,13 @@ button.footer-meta {
|
|
|
1629
1747
|
background: linear-gradient(145deg, rgba(var(--ctp-surface-rgb), 0.66), rgba(var(--ctp-mantle-rgb), 0.7));
|
|
1630
1748
|
box-shadow: 0 0.8rem 1.8rem rgba(var(--ctp-crust-rgb), 0.34), inset 0 1px 0 rgba(255,255,255,0.045);
|
|
1631
1749
|
}
|
|
1750
|
+
.message.action-enter {
|
|
1751
|
+
animation: action-card-slide-in 180ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
|
1752
|
+
}
|
|
1753
|
+
@keyframes action-card-slide-in {
|
|
1754
|
+
from { opacity: 0; transform: translateY(0.42rem); }
|
|
1755
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1756
|
+
}
|
|
1632
1757
|
.message::before {
|
|
1633
1758
|
content: "";
|
|
1634
1759
|
position: absolute;
|
|
@@ -1779,6 +1904,18 @@ button.footer-meta {
|
|
|
1779
1904
|
padding: 0 0.9rem 0.85rem;
|
|
1780
1905
|
border-top: 1px solid rgba(249, 226, 175, 0.12);
|
|
1781
1906
|
}
|
|
1907
|
+
.tool-result-preview {
|
|
1908
|
+
padding: 0 0.9rem 0.85rem;
|
|
1909
|
+
border-top: 1px solid rgba(249, 226, 175, 0.10);
|
|
1910
|
+
}
|
|
1911
|
+
.message-collapse[open] + .tool-result-preview {
|
|
1912
|
+
display: none;
|
|
1913
|
+
}
|
|
1914
|
+
.tool-result-preview-text {
|
|
1915
|
+
max-height: none;
|
|
1916
|
+
margin: 0;
|
|
1917
|
+
color: rgba(var(--ctp-text-rgb), 0.78);
|
|
1918
|
+
}
|
|
1782
1919
|
.message.toolResult:not(.error) .message-collapse:not([open]) > summary.message-header,
|
|
1783
1920
|
.message.bashExecution .message-collapse:not([open]) > summary.message-header,
|
|
1784
1921
|
.message.compactionSummary .message-collapse:not([open]) > summary.message-header {
|
|
@@ -2537,7 +2674,7 @@ summary { cursor: pointer; color: var(--warning); }
|
|
|
2537
2674
|
body.side-panel-collapsed .terminal-tabs-shell { padding-right: 2.75rem; }
|
|
2538
2675
|
.terminal-tabs-toggle-button {
|
|
2539
2676
|
display: block;
|
|
2540
|
-
width: min(14rem, calc(100vw -
|
|
2677
|
+
width: min(14rem, calc(100vw - 8.8rem));
|
|
2541
2678
|
min-height: 28px;
|
|
2542
2679
|
padding: 0.16rem 0.58rem;
|
|
2543
2680
|
overflow: hidden;
|
|
@@ -2565,8 +2702,14 @@ summary { cursor: pointer; color: var(--warning); }
|
|
|
2565
2702
|
top: calc(0.12rem + env(safe-area-inset-top));
|
|
2566
2703
|
right: 0.42rem;
|
|
2567
2704
|
}
|
|
2705
|
+
.terminal-close-all-button {
|
|
2706
|
+
min-height: 28px;
|
|
2707
|
+
padding: 0.16rem 0.46rem;
|
|
2708
|
+
border-radius: 0.58rem;
|
|
2709
|
+
font-size: 0.72rem;
|
|
2710
|
+
line-height: 1.1;
|
|
2711
|
+
}
|
|
2568
2712
|
.terminal-tabs {
|
|
2569
|
-
position: absolute;
|
|
2570
2713
|
left: 0.55rem;
|
|
2571
2714
|
right: 0.55rem;
|
|
2572
2715
|
top: calc(100% + 0.35rem);
|
|
@@ -20,6 +20,15 @@ const [pkgRaw, html, css, app, server, extension, readme, manifestRaw, serviceWo
|
|
|
20
20
|
]);
|
|
21
21
|
const pkg = JSON.parse(pkgRaw);
|
|
22
22
|
const manifest = JSON.parse(manifestRaw);
|
|
23
|
+
const companionDependencies = {
|
|
24
|
+
"@firstpick/pi-extension-git-footer-status": "^0.2.1",
|
|
25
|
+
"@firstpick/pi-extension-release-aur": "^0.1.3",
|
|
26
|
+
"@firstpick/pi-extension-release-npm": "^0.3.3",
|
|
27
|
+
"@firstpick/pi-extension-stats": "^0.2.0",
|
|
28
|
+
"@firstpick/pi-extension-todo-progress": "^0.1.7",
|
|
29
|
+
"@firstpick/pi-prompts-git-pr": "^0.1.0",
|
|
30
|
+
"@firstpick/pi-themes-bundle": "^0.1.1",
|
|
31
|
+
};
|
|
23
32
|
|
|
24
33
|
assert.match(html, /viewport-fit=cover/, "viewport should opt into safe-area-aware full-screen layout");
|
|
25
34
|
assert.match(html, /interactive-widget=resizes-content/, "viewport should request keyboard-driven content resizing where supported");
|
|
@@ -27,11 +36,13 @@ assert.match(html, /<meta name="theme-color" content="#11111b" \/>/, "PWA should
|
|
|
27
36
|
assert.match(html, /<link rel="manifest" href="\/manifest\.webmanifest" \/>/, "PWA should expose a web app manifest");
|
|
28
37
|
assert.match(html, /<link rel="apple-touch-icon" href="\/apple-touch-icon\.png" \/>/, "PWA should expose the conventional iOS home-screen icon path");
|
|
29
38
|
assert.match(html, /id="terminalTabsToggleButton"/, "mobile should expose a compact terminal-tabs toggle");
|
|
39
|
+
assert.match(html, /id="closeAllTabsButton"[\s\S]*?>Close all Tabs<\/button>/, "tab header should expose a top-right close-all tabs action");
|
|
30
40
|
assert.match(html, /id="sidePanelBackdrop"/, "mobile side panel needs an overlay/backdrop close target");
|
|
31
41
|
assert.match(html, /id="themeSelect"/, "side panel should expose a theme selector");
|
|
32
42
|
assert.match(html, /<label for="themeSelect">Theme<\/label>/, "theme selector should be labeled in side-panel controls");
|
|
33
43
|
assert.match(html, /id="agentDoneNotificationsToggle"/, "side panel should expose an agent-done notifications toggle");
|
|
34
44
|
assert.match(html, /id="agentDoneNotificationsStatus"/, "agent-done notifications toggle should expose status text");
|
|
45
|
+
assert.match(html, /id="optionalFeaturesBox"/, "side panel should expose optional feature status and controls");
|
|
35
46
|
assert.match(html, /id="jumpToLatestButton"/, "chat should expose a jump-to-latest control for non-forced streaming");
|
|
36
47
|
assert.match(html, /id="stickyUserPromptButton"/, "chat should expose a fixed last-user-prompt jump control");
|
|
37
48
|
assert.match(html, /id="feedbackTray"/, "chat should expose a queued action-feedback tray");
|
|
@@ -54,6 +65,7 @@ assert.ok(
|
|
|
54
65
|
|
|
55
66
|
assert.match(css, /--visual-viewport-height:\s*100dvh/, "CSS should define a visual viewport height fallback");
|
|
56
67
|
assert.match(css, /color-scheme:\s*var\(--theme-color-scheme\)/, "CSS should allow JS-selected themes to update browser color-scheme");
|
|
68
|
+
assert.match(css, /font-size:\s*80%/, "Web UI should render at 80% base scale for denser layout");
|
|
57
69
|
assert.match(css, /--background-glow-pink/, "CSS should expose theme-controlled page glow colors");
|
|
58
70
|
assert.match(css, /height:\s*var\(--visual-viewport-height, 100dvh\)/, "layout should consume visual viewport height");
|
|
59
71
|
assert.match(css, /button, select, input \{ min-height: 44px; \}/, "base controls should meet 44px touch-target height");
|
|
@@ -63,9 +75,15 @@ assert.match(css, /#promptInput \{[\s\S]*?overflow-y:\s*hidden/, "prompt input s
|
|
|
63
75
|
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");
|
|
64
76
|
assert.match(css, /\.message\.extension,[\s\S]*?\.message\.native/, "extension and native command output should have visible transcript styling");
|
|
65
77
|
assert.match(css, /\.message\.run-indicator-message \{[\s\S]*?border-color/, "active agent runs should render a visible transcript indicator card");
|
|
78
|
+
assert.match(css, /\.message\.action-enter \{[\s\S]*?action-card-slide-in/, "new action cards should subtly slide in from the bottom");
|
|
79
|
+
assert.match(css, /@keyframes action-card-slide-in \{[\s\S]*?translateY\(0\.42rem\)/, "action-card entry animation should start below the final position");
|
|
66
80
|
assert.match(css, /\.message\.thinking,\n\.message\.toolCall,\n\.message\.assistantEvent/, "thinking and assistant events should have non-assistant transcript card styling");
|
|
67
81
|
assert.match(css, /\.message\.toolResult, \.message\.bashExecution, \.message\.compactionSummary/, "compaction summaries should render as compact collapsible transcript cards");
|
|
82
|
+
assert.match(css, /\.tool-result-preview \{[\s\S]*?padding:/, "collapsed tool results should show a preview area by default");
|
|
83
|
+
assert.match(css, /\.message-collapse\[open\] \+ \.tool-result-preview \{[\s\S]*?display:\s*none/, "tool result preview should hide when full output is expanded");
|
|
68
84
|
assert.match(css, /\.run-indicator-pulse \{[\s\S]*?animation:\s*run-indicator-pulse/, "active agent run indicator should have an animated pulse");
|
|
85
|
+
assert.match(css, /\.optional-features-box \{[\s\S]*?display:\s*grid/, "optional features should render as a side-panel feature list");
|
|
86
|
+
assert.match(css, /\.optional-feature-pill\.enabled/, "optional features should visually distinguish enabled state");
|
|
69
87
|
assert.match(css, /\.todo-widget \{[\s\S]*?display:\s*grid/, "todo-progress widget should render as a styled checklist card");
|
|
70
88
|
assert.match(css, /\.todo-widget-item\.partial \.todo-widget-marker/, "todo-progress partial items should have distinct styling");
|
|
71
89
|
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");
|
|
@@ -95,6 +113,8 @@ assert.match(css, /\.composer-actions-panel > \.composer-publish-menu[\s\S]*?gri
|
|
|
95
113
|
assert.match(css, /\.composer-actions-panel[\s\S]*?bottom:\s*calc\(100% \+ 0\.42rem\)/, "mobile composer actions should open as an above-composer sheet");
|
|
96
114
|
assert.match(css, /body\.composer-actions-open \.composer-actions-panel \{ display: grid; \}/, "composer actions panel should only open when toggled");
|
|
97
115
|
assert.match(css, /\.terminal-tabs-toggle-button \{ display: none; \}/, "terminal tab toggle should be hidden outside mobile CSS");
|
|
116
|
+
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");
|
|
117
|
+
assert.match(css, /\.terminal-tab-group-close \{[\s\S]*?border-left-color/, "terminal tab groups should style their close button distinctly");
|
|
98
118
|
assert.match(css, /body\.mobile-tabs-expanded \.terminal-tabs \{ display: flex; \}/, "mobile tabs should expand only when toggled");
|
|
99
119
|
assert.match(css, /\.terminal-tab-activity-indicator/, "terminal tabs should expose per-tab agent activity indicators");
|
|
100
120
|
assert.match(css, /\.terminal-tab-group-item \{[\s\S]*?background:\s*var\(--ctp-crust\)/, "grouped terminal tab items should use opaque backgrounds");
|
|
@@ -177,31 +197,50 @@ assert.match(app, /function hasQueuedDialogRequest\(id\)/, "frontend should dedu
|
|
|
177
197
|
assert.match(app, /if \(request\.replayed\) addEvent\(`recovered pending \$\{request\.method\} request`, "warn"\)/, "frontend should surface replayed extension UI blockers");
|
|
178
198
|
assert.match(app, /case "webui_extension_ui_cancelled":/, "frontend should close dialogs cancelled by backend abort handling");
|
|
179
199
|
assert.match(app, /function parseTodoProgressWidget\(lines\)/, "todo-progress widgets should be parsed from extension widget lines");
|
|
200
|
+
assert.match(app, /Optional feature detection intentionally checks loaded Pi capabilities/, "optional Web UI features should be detected through loaded capabilities, not package folders");
|
|
201
|
+
assert.match(app, /function resetOptionalFeatureAvailability\(\)/, "optional feature state should reset across active-tab and reload boundaries");
|
|
202
|
+
assert.match(app, /function renderOptionalFeaturePanel\(\)/, "side panel should render optional feature installed/enabled state");
|
|
203
|
+
assert.match(app, /OPTIONAL_FEATURES_STORAGE_KEY/, "optional feature disable toggles should persist in browser storage");
|
|
204
|
+
assert.match(app, /function installOptionalFeature\(featureId\)/, "optional features should expose an install action");
|
|
205
|
+
assert.match(app, /api\("\/api\/optional-feature-install"/, "optional feature install action should call the backend installer endpoint");
|
|
206
|
+
assert.match(app, /function updateOptionalFeatureAvailability\(\)[\s\S]*hasAvailableCommand\("git-staged-msg"\)[\s\S]*hasAvailableCommand\("release-npm"\)[\s\S]*hasAvailableCommand\("release-aur"\)[\s\S]*hasAvailableCommand\("todo-progress-status"\)/, "optional feature detection should call RPC-visible commands directly");
|
|
207
|
+
assert.match(app, /if \(!isOptionalFeatureEnabled\("todoProgressWidget"\)\) return String\(text \|\| ""\)/, "todo progress line stripping should only run when the todo feature is detected and enabled");
|
|
208
|
+
assert.match(app, /case "webui_tab_reloaded":[\s\S]*resetOptionalFeatureAvailability\(\)/, "optional feature state should reset when the RPC tab reloads resources");
|
|
209
|
+
assert.match(app, /function runPublishWorkflow\(command\)[\s\S]*hasAvailableCommand\(commandName\)/, "publish workflow launch should guard on loaded slash commands");
|
|
210
|
+
assert.match(app, /if \(!isOptionalFeatureEnabled\("gitWorkflow"\)\)/, "guided git workflow should guard on enabled /git-staged-msg feature");
|
|
180
211
|
assert.match(app, /function renderReleaseNpmOutputWidget\(\)/, "release-npm live output should use a specialized Web UI renderer");
|
|
181
212
|
assert.match(app, /function renderReleaseAurOutputWidget\(\)/, "release-aur live output should use a specialized Web UI renderer");
|
|
182
213
|
assert.match(app, /releaseNpmActionButton\("Abort", "\/release-abort", "danger"\)/, "release-npm Web UI output should expose an abort action");
|
|
183
214
|
assert.match(app, /releaseNpmActionButton\("Abort", "\/release-aur abort", "danger"\)/, "release-aur Web UI output should expose an abort action");
|
|
184
|
-
assert.match(app, /key === "todo-progress" \? renderTodoProgressWidget\(key, lines\) : null/, "todo-progress should use the specialized widget renderer");
|
|
215
|
+
assert.match(app, /key === "todo-progress" && isOptionalFeatureEnabled\("todoProgressWidget"\) \? renderTodoProgressWidget\(key, lines\) : null/, "todo-progress should use the specialized widget renderer only when enabled");
|
|
185
216
|
assert.match(app, /let transientMessages = \[\]/, "frontend should keep transient Web UI/extension output messages");
|
|
186
217
|
assert.match(app, /function orderedTranscriptItems\(\)/, "frontend should merge persisted and transient messages chronologically");
|
|
187
218
|
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
219
|
assert.match(app, /const ACTION_FEEDBACK_REACTIONS = \{/, "frontend should define direct feedback reactions");
|
|
189
220
|
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
221
|
assert.match(app, /function renderActionFeedbackControls\(/, "frontend should render per-message reaction controls");
|
|
222
|
+
assert.match(app, /function toolResultPreviewText\(message, lineLimit = 10\)/, "tool results should derive a ten-line collapsed preview");
|
|
223
|
+
assert.match(app, /appendText\(preview, toolResultPreviewText\(message, 10\), "code-block tool-result-preview-text"\)/, "collapsed tool results should render the first ten preview lines by default");
|
|
191
224
|
assert.match(app, /function assistantDisplayMessages\(message\)/, "assistant history should split thinking and tool-call parts out of the final Assistant output card");
|
|
225
|
+
assert.match(app, /function assistantHasToolCallAfter\(content, index\)/, "assistant text that precedes a tool call should be detectable and suppressible");
|
|
226
|
+
assert.match(app, /if \(!assistantHasToolCallAfter\(content, index\)\) finalParts\.push\(finalPart\);/, "assistant history should not render pre-tool-call assistant text as final output");
|
|
192
227
|
assert.match(app, /return content\.trim\(\) \? \[\{ \.\.\.message, title: "Assistant" \}\] : \[\]/, "assistant messages with stripped empty text should not render empty Assistant cards");
|
|
193
228
|
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
229
|
assert.match(app, /displayMessage\.role === "assistant" \? messageIndex : -1/, "only final Assistant output cards should keep the assistant message index for feedback");
|
|
195
230
|
assert.match(app, /function ensureStreamingThinkingBubble\(\)/, "live thinking should render in a dedicated non-assistant streaming card");
|
|
231
|
+
assert.match(app, /if \(thinkingText \|\| !streaming\) appendText\(body, thinkingText \|\| "No thinking content was exposed by the provider\.", "thinking-text"\);/, "empty live thinking cards should not render the no-thinking fallback before deltas arrive");
|
|
196
232
|
assert.match(app, /function assistantStreamingMessage\(event\)/, "live streaming should read the authoritative partial assistant message from RPC events like the TUI");
|
|
197
233
|
assert.match(app, /assistantThinkingTextFromMessage\(assistantStreamingMessage\(event\)\) \|\| thinkingDeltaText\(update\)/, "live thinking end should replace deltas with the final partial-message thinking content");
|
|
198
234
|
assert.match(app, /if \(typeof partialText === "string"\) streamRawText = partialText;/, "live assistant text should synchronize from partial messages instead of relying only on deltas");
|
|
199
235
|
assert.match(app, /const TODO_PROGRESS_LINE_REGEX = /, "frontend should recognize live todo progress lines that will be moved into the todo widget");
|
|
200
236
|
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, /
|
|
237
|
+
assert.match(app, /function renderStreamingAssistantText\(\)[\s\S]*?const assistantText = stripTodoProgressLines\(streamRawText, \{ streaming: true \}\)/, "streamed Assistant text should classify from accumulated output without flashing partial todo-progress lines");
|
|
202
238
|
assert.match(app, /const STREAM_OUTPUT_HIDE_DELAY_MS = 300/, "stream output hiding should be debounced to prevent rapid flicker");
|
|
239
|
+
assert.match(app, /const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220/, "live assistant text should be briefly guarded so pre-tool-call text can be suppressed");
|
|
203
240
|
assert.match(app, /function scheduleStreamBubbleHide\([\s\S]*?STREAM_OUTPUT_MIN_VISIBLE_MS/, "stream output cards should observe a minimum visible duration before hiding");
|
|
204
241
|
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");
|
|
242
|
+
assert.match(app, /if \(streamToolCallSeen \|\| streamBubble\) renderStreamingAssistantText\(\);\n\s+else scheduleStreamingAssistantTextRender\(\);/, "live assistant text should wait briefly before showing unless it is already visible or follows a tool call");
|
|
243
|
+
assert.match(app, /streamToolCallSeen = true;\n\s+suppressStreamingAssistantTextBeforeToolCall\(\);/, "tool-call starts should remove pending assistant text from the live transcript");
|
|
205
244
|
assert.match(app, /const created = appendMessage\(\{ role: "assistant", title: "Assistant"/, "live Assistant cards should be created only for final output text");
|
|
206
245
|
assert.match(app, /api\("\/api\/action-feedback", \{ method: "POST"/, "queued action feedback should post to the server after the run is idle");
|
|
207
246
|
assert.match(app, /function postQueuedFeedback\(tabId, items\)/, "queued feedback should have a backward-compatible submit path");
|
|
@@ -210,7 +249,7 @@ assert.match(app, /actionFeedbackSteerMessage\(item\)/, "live action feedback sh
|
|
|
210
249
|
assert.match(app, /function addTransientMessage\(\{ role = "notice"/, "frontend should render transient command output into the transcript");
|
|
211
250
|
assert.match(app, /addTransientMessage\(\{ role: "extension", title: "extension output"/, "extension notify output should appear in the transcript, not only the event log");
|
|
212
251
|
assert.match(app, /function renderRunIndicator\(/, "frontend should render a transcript-level active agent indicator");
|
|
213
|
-
assert.match(app, /return "Agent is
|
|
252
|
+
assert.match(app, /return "Agent is running: ";/, "active agent indicator should use the requested headline wording");
|
|
214
253
|
assert.doesNotMatch(app, /"agent running"/, "active agent indicator should not render a separate title/header label");
|
|
215
254
|
assert.doesNotMatch(app, /runIndicatorTimestamp/, "active agent indicator should not render a separate live timestamp header");
|
|
216
255
|
assert.match(app, /runIndicatorBubble = make\("article", "message runIndicator run-indicator-message streaming"\)/, "active agent indicator should use a dedicated streaming transcript card");
|
|
@@ -278,7 +317,11 @@ assert.match(app, /function updateTerminalTabGroupOpenState\(\)/, "frontend shou
|
|
|
278
317
|
assert.match(app, /wrapper\.addEventListener\("pointerenter", \(\) => setOpenTerminalTabGroup\(group\.key\)\)/, "terminal tab groups should mark themselves open while hovered");
|
|
279
318
|
assert.match(app, /if \(openTerminalTabGroupKey\) \{\n\s+scheduleRefreshTabs\(600\);/, "tab polling should defer full tab refreshes while a group menu is open");
|
|
280
319
|
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, /
|
|
320
|
+
assert.match(app, /function closeTerminalTabGroup\(group\)[\s\S]*?closeTerminalTabs\(group\.tabs\.map\(\(tab\) => tab\.id\)/, "terminal tab groups should be closable as a batch");
|
|
321
|
+
assert.match(app, /function closeAllTerminalTabs\(\)[\s\S]*?closeTerminalTabs\(tabs\.map\(\(tab\) => tab\.id\)/, "tab header should close all terminal tabs as a batch");
|
|
322
|
+
assert.match(app, /WARNING: \$\{activeAgentTabs\.length\}[\s\S]*?still running or waiting for input/, "tab close confirmations should warn when agents are still running");
|
|
323
|
+
assert.match(app, /elements\.closeAllTabsButton\.addEventListener\("click", \(\) => closeAllTerminalTabs\(\)\)/, "close-all tabs button should be wired in JS");
|
|
324
|
+
assert.match(app, /const groups = tabCwdGroups\(\);[\s\S]*?for \(const group of groups\) \{\n\s+if \(shouldRenderTerminalTabGroup\(group, groups\.length\)\)[\s\S]*?renderTerminalTabGroup\(group, groups\.length\)[\s\S]*?for \(const tab of group\.tabs\) elements\.tabBar\.append\(renderTerminalTab\(tab\)\);/, "terminal tabs should render groups with group count and ungrouped tabs when grouping is skipped");
|
|
282
325
|
assert.match(app, /let tabSeenCompletionSerials = new Map\(\)/, "frontend should track which tab completions have been seen");
|
|
283
326
|
assert.match(app, /function tabIndicator\(tab\)/, "frontend should derive idle, working, blocked, and work-done tab indicator states");
|
|
284
327
|
assert.match(app, /pendingBlockerCount > 0[\s\S]*?state: "blocked"/, "frontend should show blocked tabs when extension UI blockers are pending");
|
|
@@ -327,7 +370,7 @@ assert.equal(manifest.start_url, "/", "PWA manifest should start at the web UI r
|
|
|
327
370
|
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");
|
|
328
371
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-192.png" && icon.sizes === "192x192"), "PWA manifest should include a 192px icon");
|
|
329
372
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-512.png" && icon.sizes === "512x512"), "PWA manifest should include a 512px icon");
|
|
330
|
-
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-
|
|
373
|
+
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v12"/, "PWA service worker should define an app-shell cache");
|
|
331
374
|
assert.match(serviceWorker, /self\.addEventListener\("notificationclick"/, "PWA service worker should focus Web UI when blocked-tab notifications are clicked");
|
|
332
375
|
assert.match(serviceWorker, /event\.notification\.data\?\.url/, "blocked-tab notifications should carry a URL for service-worker click handling");
|
|
333
376
|
assert.match(serviceWorker, /"\/apple-touch-icon\.png"/, "PWA service worker should cache the apple touch icon");
|
|
@@ -364,8 +407,11 @@ assert.match(server, /async function handleNativeSlashCommand\(tab, body\)/, "se
|
|
|
364
407
|
assert.match(server, /const restoreTabs = readRestoreTabsFromEnv\(\)/, "server should accept restart tab restore descriptors from the launcher environment");
|
|
365
408
|
assert.match(server, /delete process\.env\.PI_WEBUI_RESTORE_TABS/, "server should avoid leaking restore descriptors into spawned Pi RPC processes");
|
|
366
409
|
assert.match(server, /if \(sessionFile && !options\.noSession\) piArgs\.push\("--session", sessionFile\)/, "restored tabs should resume previous session files");
|
|
410
|
+
assert.doesNotMatch(server, /args\.push\("--name"/, "Web UI tab titles should not be forwarded as Pi CLI --name flags because older bundled Pi CLIs reject them");
|
|
367
411
|
assert.match(server, /const closedRestorableTabs = \[\]/, "server should remember recently closed tabs for slash-command restarts");
|
|
368
412
|
assert.match(server, /async function closeTab\(id\)[\s\S]*?rememberClosedRestorableTab\(tab, restorableState\)/, "closing a tab should capture its restorable session before stopping RPC");
|
|
413
|
+
assert.match(server, /async function closeTabs\(ids\)[\s\S]*?if \(targetTabs\.length >= tabs\.size\) \{\n\s+await createTab/, "bulk tab close should create a replacement before closing every current tab");
|
|
414
|
+
assert.match(server, /url\.pathname === "\/api\/tabs\/close" && req\.method === "POST"[\s\S]*?closedIds: closed\.map\(\(tab\) => tab\.id\)/, "server should expose a bulk close-tabs endpoint");
|
|
369
415
|
assert.match(server, /function rememberTabState\(tab, state\)/, "server should cache last-known tab state for restart-safe session restoration");
|
|
370
416
|
assert.match(server, /sessionFile: tabRestorableSessionFile\(tab\)/, "tab metadata should expose cached session files for health/status restore descriptors");
|
|
371
417
|
assert.match(server, /restorableTabs: mergeRestorableTabDescriptors\(statusTabs, closedRestorableTabs\)/, "status should expose open plus recently closed restorable tabs");
|
|
@@ -387,6 +433,8 @@ assert.match(server, /case "session": \{[\s\S]*?formatSessionOutput\(tab, state\
|
|
|
387
433
|
assert.match(server, /case "copy": \{[\s\S]*?get_last_assistant_text[\s\S]*?copyText: text/, "native /copy should return text for browser clipboard handling");
|
|
388
434
|
assert.match(server, /case "hotkeys": \{[\s\S]*?webuiHotkeysOutput\(\)/, "native /hotkeys should return Web UI hotkey output");
|
|
389
435
|
assert.match(server, /url\.pathname === "\/api\/commands" && req\.method === "GET"[\s\S]*?getCommandData\(tab\)/, "GET /api/commands should merge native and RPC-visible commands");
|
|
436
|
+
assert.match(server, /function safeRpcResponse\(tab, command/, "server should provide stopped-RPC fallbacks for refresh endpoints");
|
|
437
|
+
assert.match(server, /function primeTabRpc\(tab\)/, "server should prime new terminal RPC state before returning created tabs");
|
|
390
438
|
assert.match(server, /specific Web UI action or final-output cards/, "server feedback-learning prompt should cover final outputs as well as actions");
|
|
391
439
|
assert.match(server, /function formatActionFeedbackLearningPrompt\(items\)/, "server should convert feedback into a LEARNING prompt");
|
|
392
440
|
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");
|
|
@@ -399,7 +447,11 @@ assert.match(server, /url\.pathname === "\/api\/path-suggestions" && req\.method
|
|
|
399
447
|
assert.match(server, /url\.pathname === "\/api\/path-fast-picks" && req\.method === "GET"/, "server should expose GET /api/path-fast-picks");
|
|
400
448
|
assert.match(server, /url\.pathname === "\/api\/path-fast-picks" && req\.method === "POST"/, "server should expose POST /api/path-fast-picks");
|
|
401
449
|
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
|
|
450
|
+
assert.match(server, /@firstpick\/pi-themes-bundle/, "server should discover themes from the optional theme package");
|
|
451
|
+
assert.match(server, /const OPTIONAL_FEATURE_PACKAGES = new Map/, "server should whitelist optional feature packages for install actions");
|
|
452
|
+
assert.match(server, /function installOptionalFeaturePackage\(featureId\)/, "server should provide optional feature package installation helper");
|
|
453
|
+
assert.match(server, /url\.pathname === "\/api\/optional-feature-install" && req\.method === "POST"/, "server should expose optional feature install endpoint");
|
|
454
|
+
assert.match(server, /Installing optional Web UI features is only allowed from localhost/, "optional feature install endpoint should be localhost-only");
|
|
403
455
|
assert.match(server, /url\.pathname === "\/api\/themes" && req\.method === "GET"/, "server should expose GET /api/themes");
|
|
404
456
|
assert.match(server, /readBundledThemes\(\)/, "server should read bundled theme JSON files for the browser");
|
|
405
457
|
assert.match(server, /"apple-touch-icon\.png", "icon-192\.png"/, "server should serve the conventional apple touch icon path");
|
|
@@ -415,13 +467,42 @@ assert.match(readme, /Feedback reactions \(`👍`, `👎`, `\?`\) on final assis
|
|
|
415
467
|
assert.match(readme, /POST \/api\/action-feedback\?tab=<tabId>/, "README should document the action-feedback endpoint");
|
|
416
468
|
assert.match(readme, /`@` file\/path references with live suggestions/, "README should describe @ file/path reference autocomplete");
|
|
417
469
|
assert.match(readme, /GET \/api\/path-suggestions\?tab=<tabId>&query=<path>/, "README should document the path-suggestions endpoint");
|
|
470
|
+
assert.match(readme, /POST \/api\/optional-feature-install/, "README should document optional feature install endpoint");
|
|
418
471
|
assert.match(readme, /server-persisted fast picks/, "README should describe server-persisted fast picks");
|
|
419
472
|
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
473
|
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
|
|
474
|
+
assert.match(readme, /Side-panel theme picker backed by optional `@firstpick\/pi-themes-bundle` themes when loaded/, "README should describe optional theme selection");
|
|
475
|
+
assert.match(readme, /## Optional companion packages/, "README should document optional Web UI companion packages");
|
|
476
|
+
assert.match(readme, /checks loaded Pi capabilities directly through RPC-visible commands and live widget events/, "README should document capability-based startup checks");
|
|
477
|
+
assert.match(readme, /side panel shows each optional feature as enabled, disabled, or install-needed/, "README should document optional feature side-panel controls");
|
|
478
|
+
assert.match(readme, /Installing a missing feature is an explicit, warned action/, "README should document optional feature install warning behavior");
|
|
422
479
|
|
|
423
480
|
assert.equal(pkg.scripts?.test, "node tests/mobile-static.test.mjs", "package test script should run the mobile static harness");
|
|
424
|
-
|
|
481
|
+
for (const [name, range] of Object.entries(companionDependencies)) {
|
|
482
|
+
assert.equal(pkg.optionalDependencies?.[name], range, `webui package should optionally depend on ${name}`);
|
|
483
|
+
assert.equal(pkg.dependencies?.[name], undefined, `webui package should not require optional companion ${name}`);
|
|
484
|
+
}
|
|
485
|
+
assert.equal(pkg.bundledDependencies, undefined, "webui optional companion packages should not be bundled into the tarball");
|
|
486
|
+
for (const extensionPath of [
|
|
487
|
+
"../pi-extension-git-footer-status/index.ts",
|
|
488
|
+
"../pi-extension-release-aur/index.ts",
|
|
489
|
+
"../pi-extension-release-npm/index.ts",
|
|
490
|
+
"../pi-extension-stats/index.ts",
|
|
491
|
+
"../pi-extension-todo-progress/index.ts",
|
|
492
|
+
"node_modules/@firstpick/pi-extension-git-footer-status/index.ts",
|
|
493
|
+
"node_modules/@firstpick/pi-extension-release-aur/index.ts",
|
|
494
|
+
"node_modules/@firstpick/pi-extension-release-npm/index.ts",
|
|
495
|
+
"node_modules/@firstpick/pi-extension-stats/index.ts",
|
|
496
|
+
"node_modules/@firstpick/pi-extension-todo-progress/index.ts",
|
|
497
|
+
]) {
|
|
498
|
+
assert.ok(pkg.pi?.extensions?.includes(extensionPath), `webui Pi manifest should load ${extensionPath} when present`);
|
|
499
|
+
}
|
|
500
|
+
assert.ok(pkg.pi?.skills?.includes("../pi-extension-release-aur/skills"), "webui Pi manifest should load release-aur sibling skills when present");
|
|
501
|
+
assert.ok(pkg.pi?.skills?.includes("node_modules/@firstpick/pi-extension-release-aur/skills"), "webui Pi manifest should load release-aur nested skills when present");
|
|
502
|
+
assert.ok(pkg.pi?.prompts?.includes("../pi-package-prompts-git-pr/prompts"), "webui Pi manifest should load guided-git sibling prompts when present");
|
|
503
|
+
assert.ok(pkg.pi?.prompts?.includes("node_modules/@firstpick/pi-prompts-git-pr/prompts"), "webui Pi manifest should load guided-git nested prompts when present");
|
|
504
|
+
assert.ok(pkg.pi?.themes?.includes("../pi-package-themes-bundle/themes"), "webui Pi manifest should load sibling bundled themes when present");
|
|
505
|
+
assert.ok(pkg.pi?.themes?.includes("node_modules/@firstpick/pi-themes-bundle/themes"), "webui Pi manifest should load nested bundled themes when present");
|
|
425
506
|
assert.ok(pkg.scripts?.check?.includes("node --check public/app.js"), "check script should syntax-check app.js");
|
|
426
507
|
assert.ok(pkg.scripts?.check?.includes("node tests/mobile-static.test.mjs"), "check script should include mobile static assertions");
|
|
427
508
|
|