@firstpick/pi-package-webui 0.2.4 → 0.2.6
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 +1 -1
- package/bin/pi-webui.mjs +46 -26
- package/package.json +1 -1
- package/public/app.js +521 -140
- package/public/index.html +23 -2
- package/public/service-worker.js +1 -1
- package/public/styles.css +179 -3
- package/tests/mobile-static.test.mjs +67 -14
package/public/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<link rel="manifest" href="/manifest.webmanifest" />
|
|
13
13
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
14
14
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
15
|
-
<link rel="stylesheet" href="/styles.css?v=
|
|
15
|
+
<link rel="stylesheet" href="/styles.css?v=23" />
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<button id="sidePanelExpandButton" class="side-panel-expand-button" type="button" aria-controls="sidePanel" aria-expanded="false" aria-label="Expand side panel" title="Expand side panel">
|
|
@@ -133,6 +133,27 @@
|
|
|
133
133
|
</button>
|
|
134
134
|
</div>
|
|
135
135
|
</div>
|
|
136
|
+
<div class="composer-publish-menu composer-native-command-menu">
|
|
137
|
+
<button
|
|
138
|
+
id="nativeCommandMenuButton"
|
|
139
|
+
class="composer-icon-button composer-publish-button composer-native-command-button"
|
|
140
|
+
type="button"
|
|
141
|
+
title="Open skills and tools commands"
|
|
142
|
+
aria-label="Open /skills and /tools commands"
|
|
143
|
+
aria-haspopup="menu"
|
|
144
|
+
aria-expanded="false"
|
|
145
|
+
aria-controls="nativeCommandMenu"
|
|
146
|
+
data-tooltip="Skills/tools setup: open skill or tool setup."
|
|
147
|
+
><svg class="composer-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 5h16v14H4z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="m7 10 2.5 2L7 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 15h5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button>
|
|
148
|
+
<div id="nativeCommandMenu" class="composer-publish-menu-panel composer-native-command-menu-panel" role="menu" aria-label="Skills and tools setup">
|
|
149
|
+
<button id="nativeSkillsButton" class="composer-publish-menu-item composer-native-command-menu-item" type="button" role="menuitem" data-command="/skills">
|
|
150
|
+
<span>Skills Setup</span>
|
|
151
|
+
</button>
|
|
152
|
+
<button id="nativeToolsButton" class="composer-publish-menu-item composer-native-command-menu-item" type="button" role="menuitem" data-command="/tools">
|
|
153
|
+
<span>Tools Setup</span>
|
|
154
|
+
</button>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
136
157
|
</div>
|
|
137
158
|
<div class="spacer"></div>
|
|
138
159
|
<button
|
|
@@ -364,6 +385,6 @@
|
|
|
364
385
|
</form>
|
|
365
386
|
</dialog>
|
|
366
387
|
|
|
367
|
-
<script type="module" src="/app.js?v=
|
|
388
|
+
<script type="module" src="/app.js?v=23"></script>
|
|
368
389
|
</body>
|
|
369
390
|
</html>
|
package/public/service-worker.js
CHANGED
package/public/styles.css
CHANGED
|
@@ -1458,6 +1458,17 @@ body.side-panel-collapsed .terminal-tabs-shell {
|
|
|
1458
1458
|
opacity: 0.62;
|
|
1459
1459
|
box-shadow: 0 0 1rem var(--glow-mauve);
|
|
1460
1460
|
}
|
|
1461
|
+
.statusbar-tui-footer {
|
|
1462
|
+
gap: 0;
|
|
1463
|
+
padding-block: 0.58rem;
|
|
1464
|
+
}
|
|
1465
|
+
.statusbar-tui-footer::before {
|
|
1466
|
+
opacity: 0.32;
|
|
1467
|
+
box-shadow: none;
|
|
1468
|
+
}
|
|
1469
|
+
.statusbar-git-footer {
|
|
1470
|
+
gap: 0.58rem;
|
|
1471
|
+
}
|
|
1461
1472
|
.footer-line {
|
|
1462
1473
|
position: relative;
|
|
1463
1474
|
z-index: 1;
|
|
@@ -1475,6 +1486,51 @@ body.side-panel-collapsed .terminal-tabs-shell {
|
|
|
1475
1486
|
gap: 0.5rem;
|
|
1476
1487
|
color: rgba(var(--ctp-subtext-rgb), 0.76);
|
|
1477
1488
|
}
|
|
1489
|
+
.footer-line-tui {
|
|
1490
|
+
align-items: center;
|
|
1491
|
+
gap: 0.5rem;
|
|
1492
|
+
overflow: hidden;
|
|
1493
|
+
color: rgba(var(--ctp-subtext-rgb), 0.78);
|
|
1494
|
+
white-space: nowrap;
|
|
1495
|
+
}
|
|
1496
|
+
.footer-tui-item {
|
|
1497
|
+
min-width: 0;
|
|
1498
|
+
overflow: hidden;
|
|
1499
|
+
padding: 0;
|
|
1500
|
+
color: inherit;
|
|
1501
|
+
font: inherit;
|
|
1502
|
+
font-weight: 700;
|
|
1503
|
+
text-overflow: ellipsis;
|
|
1504
|
+
white-space: nowrap;
|
|
1505
|
+
}
|
|
1506
|
+
button.footer-tui-item {
|
|
1507
|
+
appearance: none;
|
|
1508
|
+
border: 0;
|
|
1509
|
+
background: transparent;
|
|
1510
|
+
cursor: pointer;
|
|
1511
|
+
}
|
|
1512
|
+
.footer-tui-action:hover,
|
|
1513
|
+
.footer-tui-action:focus-visible {
|
|
1514
|
+
color: var(--ctp-teal);
|
|
1515
|
+
outline: none;
|
|
1516
|
+
}
|
|
1517
|
+
.footer-tui-cwd {
|
|
1518
|
+
flex: 0 1 auto;
|
|
1519
|
+
max-width: 38%;
|
|
1520
|
+
color: rgba(var(--ctp-text-rgb), 0.86);
|
|
1521
|
+
}
|
|
1522
|
+
.footer-tui-status { color: var(--ctp-yellow); }
|
|
1523
|
+
.footer-tui-stat { flex: 0 0 auto; }
|
|
1524
|
+
.footer-tui-spacer {
|
|
1525
|
+
flex: 1 1 auto;
|
|
1526
|
+
min-width: 1.2rem;
|
|
1527
|
+
}
|
|
1528
|
+
.footer-tui-model {
|
|
1529
|
+
flex: 0 1 auto;
|
|
1530
|
+
max-width: 46%;
|
|
1531
|
+
color: rgba(var(--ctp-text-rgb), 0.86);
|
|
1532
|
+
text-align: right;
|
|
1533
|
+
}
|
|
1478
1534
|
.footer-details-toggle { display: none; }
|
|
1479
1535
|
.footer-metric,
|
|
1480
1536
|
.footer-meta {
|
|
@@ -2423,6 +2479,49 @@ button.footer-meta {
|
|
|
2423
2479
|
color: rgba(var(--ctp-subtext-rgb), 0.78);
|
|
2424
2480
|
font-size: 0.78rem;
|
|
2425
2481
|
}
|
|
2482
|
+
.message.has-copy-action:not(.toolResult):not(.bashExecution):not(.compactionSummary) > .message-header,
|
|
2483
|
+
.message.has-copy-action:not(.toolResult):not(.bashExecution):not(.compactionSummary) > .message-body {
|
|
2484
|
+
padding-right: 3.1rem;
|
|
2485
|
+
}
|
|
2486
|
+
.message.has-copy-action > .message-collapse > summary.message-header {
|
|
2487
|
+
padding-right: 3.4rem;
|
|
2488
|
+
}
|
|
2489
|
+
.message-copy-button {
|
|
2490
|
+
position: absolute;
|
|
2491
|
+
top: 0.48rem;
|
|
2492
|
+
right: 0.54rem;
|
|
2493
|
+
z-index: 9;
|
|
2494
|
+
display: inline-flex;
|
|
2495
|
+
align-items: center;
|
|
2496
|
+
justify-content: center;
|
|
2497
|
+
width: 2.15rem;
|
|
2498
|
+
min-width: 2.15rem;
|
|
2499
|
+
min-height: 2.05rem;
|
|
2500
|
+
padding: 0;
|
|
2501
|
+
border-radius: 0.64rem;
|
|
2502
|
+
color: rgba(var(--ctp-text-rgb), 0.82);
|
|
2503
|
+
background: linear-gradient(180deg, rgba(var(--ctp-surface-rgb), 0.82), rgba(var(--ctp-crust-rgb), 0.90));
|
|
2504
|
+
border-color: rgba(148, 226, 213, 0.26);
|
|
2505
|
+
box-shadow: 0 0.45rem 1rem rgba(var(--ctp-crust-rgb), 0.30), inset 0 1px 0 rgba(255,255,255,0.055);
|
|
2506
|
+
line-height: 1;
|
|
2507
|
+
opacity: 0.72;
|
|
2508
|
+
}
|
|
2509
|
+
.message-copy-button:hover,
|
|
2510
|
+
.message-copy-button:focus-visible,
|
|
2511
|
+
.message-copy-button.copied {
|
|
2512
|
+
color: #11111b;
|
|
2513
|
+
border-color: transparent;
|
|
2514
|
+
background: linear-gradient(120deg, var(--ctp-teal), var(--ctp-blue));
|
|
2515
|
+
box-shadow: 0 0 0.9rem rgba(148, 226, 213, 0.28), 0 0.45rem 1rem rgba(var(--ctp-crust-rgb), 0.34);
|
|
2516
|
+
opacity: 1;
|
|
2517
|
+
}
|
|
2518
|
+
.message-copy-button.copied {
|
|
2519
|
+
background: linear-gradient(120deg, var(--ctp-green), var(--ctp-teal));
|
|
2520
|
+
}
|
|
2521
|
+
.message-copy-icon {
|
|
2522
|
+
font-size: 1.18rem;
|
|
2523
|
+
line-height: 1;
|
|
2524
|
+
}
|
|
2426
2525
|
.message-collapse {
|
|
2427
2526
|
margin: 0;
|
|
2428
2527
|
padding: 0;
|
|
@@ -2597,7 +2696,7 @@ button.footer-meta {
|
|
|
2597
2696
|
}
|
|
2598
2697
|
.action-feedback-controls {
|
|
2599
2698
|
position: absolute;
|
|
2600
|
-
|
|
2699
|
+
bottom: 0.48rem;
|
|
2601
2700
|
right: 0.55rem;
|
|
2602
2701
|
z-index: 8;
|
|
2603
2702
|
display: flex;
|
|
@@ -2605,7 +2704,7 @@ button.footer-meta {
|
|
|
2605
2704
|
gap: 0.35rem;
|
|
2606
2705
|
opacity: 0;
|
|
2607
2706
|
pointer-events: auto;
|
|
2608
|
-
transform: translateY(
|
|
2707
|
+
transform: translateY(0.12rem) scale(0.98);
|
|
2609
2708
|
transition: opacity 140ms ease, transform 140ms ease;
|
|
2610
2709
|
}
|
|
2611
2710
|
.action-feedback-controls:hover,
|
|
@@ -3092,6 +3191,22 @@ summary { cursor: pointer; color: var(--warning); }
|
|
|
3092
3191
|
.composer-publish-button.menu-open {
|
|
3093
3192
|
background: linear-gradient(120deg, var(--ctp-peach), var(--ctp-yellow), var(--ctp-mauve));
|
|
3094
3193
|
}
|
|
3194
|
+
.composer-native-command-button {
|
|
3195
|
+
color: var(--ctp-mauve);
|
|
3196
|
+
border-color: rgba(203, 166, 247, 0.40);
|
|
3197
|
+
background:
|
|
3198
|
+
linear-gradient(120deg, rgba(203, 166, 247, 0.15), rgba(137, 180, 250, 0.10)),
|
|
3199
|
+
linear-gradient(180deg, rgba(var(--ctp-surface-rgb), 0.88), rgba(var(--ctp-crust-rgb), 0.88));
|
|
3200
|
+
}
|
|
3201
|
+
.composer-native-command-button:hover,
|
|
3202
|
+
.composer-native-command-button.menu-open {
|
|
3203
|
+
color: #11111b;
|
|
3204
|
+
background: linear-gradient(120deg, var(--ctp-mauve), var(--ctp-blue), var(--ctp-teal));
|
|
3205
|
+
border-color: transparent;
|
|
3206
|
+
}
|
|
3207
|
+
.composer-native-command-menu.open .composer-native-command-button {
|
|
3208
|
+
border-color: rgba(203, 166, 247, 0.62);
|
|
3209
|
+
}
|
|
3095
3210
|
.composer-publish-menu-panel {
|
|
3096
3211
|
position: absolute;
|
|
3097
3212
|
z-index: 100;
|
|
@@ -3141,6 +3256,20 @@ summary { cursor: pointer; color: var(--warning); }
|
|
|
3141
3256
|
background: linear-gradient(120deg, var(--ctp-peach), var(--ctp-yellow));
|
|
3142
3257
|
box-shadow: 0 0 1rem rgba(250, 179, 135, 0.20);
|
|
3143
3258
|
}
|
|
3259
|
+
.composer-native-command-menu-item {
|
|
3260
|
+
color: var(--ctp-mauve);
|
|
3261
|
+
border-color: rgba(203, 166, 247, 0.32);
|
|
3262
|
+
background:
|
|
3263
|
+
linear-gradient(120deg, rgba(203, 166, 247, 0.12), rgba(137, 180, 250, 0.08)),
|
|
3264
|
+
var(--ctp-crust);
|
|
3265
|
+
}
|
|
3266
|
+
.composer-native-command-menu-item:hover,
|
|
3267
|
+
.composer-native-command-menu-item:focus-visible {
|
|
3268
|
+
color: #11111b;
|
|
3269
|
+
border-color: transparent;
|
|
3270
|
+
background: linear-gradient(120deg, var(--ctp-mauve), var(--ctp-blue));
|
|
3271
|
+
box-shadow: 0 0 1rem rgba(203, 166, 247, 0.20);
|
|
3272
|
+
}
|
|
3144
3273
|
.composer button[data-tooltip] {
|
|
3145
3274
|
position: relative;
|
|
3146
3275
|
}
|
|
@@ -3213,6 +3342,15 @@ summary { cursor: pointer; color: var(--warning); }
|
|
|
3213
3342
|
.composer-input-row button[data-tooltip].tooltip-open::before {
|
|
3214
3343
|
transform: translate(-1.2rem, 0) rotate(45deg);
|
|
3215
3344
|
}
|
|
3345
|
+
.composer-publish-menu:hover > .composer-publish-button[data-tooltip]::before,
|
|
3346
|
+
.composer-publish-menu:hover > .composer-publish-button[data-tooltip]::after,
|
|
3347
|
+
.composer-publish-menu:focus-within > .composer-publish-button[data-tooltip]::before,
|
|
3348
|
+
.composer-publish-menu:focus-within > .composer-publish-button[data-tooltip]::after,
|
|
3349
|
+
.composer-publish-menu.open > .composer-publish-button[data-tooltip]::before,
|
|
3350
|
+
.composer-publish-menu.open > .composer-publish-button[data-tooltip]::after {
|
|
3351
|
+
display: none !important;
|
|
3352
|
+
opacity: 0 !important;
|
|
3353
|
+
}
|
|
3216
3354
|
|
|
3217
3355
|
.details {
|
|
3218
3356
|
display: grid;
|
|
@@ -3489,6 +3627,18 @@ summary { cursor: pointer; color: var(--warning); }
|
|
|
3489
3627
|
color: #ff9f43 !important;
|
|
3490
3628
|
background: rgba(255, 159, 67, 0.10);
|
|
3491
3629
|
}
|
|
3630
|
+
.native-selector-badge.native-selector-badge-pi-native,
|
|
3631
|
+
.native-command-body:has(.native-resource-summary) .native-selector-badge.native-selector-badge-pi-native {
|
|
3632
|
+
border-color: rgba(137, 180, 250, 0.48);
|
|
3633
|
+
color: var(--ctp-blue);
|
|
3634
|
+
background: rgba(137, 180, 250, 0.10);
|
|
3635
|
+
}
|
|
3636
|
+
.native-selector-badge.native-selector-badge-external,
|
|
3637
|
+
.native-command-body:has(.native-resource-summary) .native-selector-badge.native-selector-badge-external {
|
|
3638
|
+
border-color: rgba(203, 166, 247, 0.46);
|
|
3639
|
+
color: var(--ctp-mauve);
|
|
3640
|
+
background: rgba(203, 166, 247, 0.10);
|
|
3641
|
+
}
|
|
3492
3642
|
.native-selector-detail,
|
|
3493
3643
|
.native-selector-meta,
|
|
3494
3644
|
.native-settings-hint {
|
|
@@ -3943,6 +4093,19 @@ summary { cursor: pointer; color: var(--warning); }
|
|
|
3943
4093
|
padding: 0.68rem 0.7rem;
|
|
3944
4094
|
border-radius: 0.82rem;
|
|
3945
4095
|
}
|
|
4096
|
+
.message-copy-button {
|
|
4097
|
+
top: 0.38rem;
|
|
4098
|
+
right: 0.42rem;
|
|
4099
|
+
width: 2rem;
|
|
4100
|
+
min-width: 2rem;
|
|
4101
|
+
min-height: 1.92rem;
|
|
4102
|
+
}
|
|
4103
|
+
.message-copy-icon { font-size: 1.12rem; }
|
|
4104
|
+
.message.has-copy-action:not(.toolResult):not(.bashExecution):not(.compactionSummary) > .message-header,
|
|
4105
|
+
.message.has-copy-action:not(.toolResult):not(.bashExecution):not(.compactionSummary) > .message-body,
|
|
4106
|
+
.message.has-copy-action > .message-collapse > summary.message-header {
|
|
4107
|
+
padding-right: 2.55rem;
|
|
4108
|
+
}
|
|
3946
4109
|
.message-header {
|
|
3947
4110
|
gap: 0.5rem;
|
|
3948
4111
|
margin-bottom: 0.38rem;
|
|
@@ -3956,7 +4119,7 @@ summary { cursor: pointer; color: var(--warning); }
|
|
|
3956
4119
|
}
|
|
3957
4120
|
.feedback-tray button { width: 100%; }
|
|
3958
4121
|
.action-feedback-controls {
|
|
3959
|
-
|
|
4122
|
+
bottom: 0.38rem;
|
|
3960
4123
|
right: 0.34rem;
|
|
3961
4124
|
gap: 0.28rem;
|
|
3962
4125
|
}
|
|
@@ -3981,6 +4144,19 @@ summary { cursor: pointer; color: var(--warning); }
|
|
|
3981
4144
|
overflow: visible;
|
|
3982
4145
|
}
|
|
3983
4146
|
body.footer-model-picker-open .footer-line { z-index: 1; }
|
|
4147
|
+
.footer-line-tui {
|
|
4148
|
+
flex-wrap: wrap;
|
|
4149
|
+
gap: 0.22rem 0.48rem;
|
|
4150
|
+
white-space: normal;
|
|
4151
|
+
}
|
|
4152
|
+
.footer-tui-item { white-space: nowrap; }
|
|
4153
|
+
.footer-tui-cwd,
|
|
4154
|
+
.footer-tui-model {
|
|
4155
|
+
flex-basis: 100%;
|
|
4156
|
+
max-width: 100%;
|
|
4157
|
+
text-align: left;
|
|
4158
|
+
}
|
|
4159
|
+
.footer-tui-spacer { display: none; }
|
|
3984
4160
|
.footer-model-picker {
|
|
3985
4161
|
position: fixed;
|
|
3986
4162
|
left: max(0.55rem, env(safe-area-inset-left));
|
|
@@ -98,6 +98,10 @@ assert.doesNotMatch(html, /class="side-panel-controls"[\s\S]*id="abortButton"/,
|
|
|
98
98
|
assert.match(html, /id="publishButton"[\s\S]*?aria-controls="publishMenu"/, "composer should expose a Publish workflow menu button");
|
|
99
99
|
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");
|
|
100
100
|
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");
|
|
101
|
+
assert.match(html, /id="nativeCommandMenuButton"[\s\S]*?aria-controls="nativeCommandMenu"/, "composer should expose a /skills and /tools command menu button");
|
|
102
|
+
assert.ok(html.indexOf('id="publishButton"') < html.indexOf('id="nativeCommandMenuButton"'), "skills/tools command menu should render immediately after the Publish workflow button");
|
|
103
|
+
assert.match(html, /id="nativeSkillsButton"[^>]*data-command="\/skills"[\s\S]*?<span>Skills Setup<\/span>/, "skills/tools command menu should include Skills Setup");
|
|
104
|
+
assert.match(html, /id="nativeToolsButton"[^>]*data-command="\/tools"[\s\S]*?<span>Tools Setup<\/span>/, "skills/tools command menu should include Tools Setup");
|
|
101
105
|
assert.doesNotMatch(html, /<code>\/release-(?:npm|aur)<\/code>/, "Publish menu should not show slash command names as option labels");
|
|
102
106
|
assert.doesNotMatch(html, /data-tooltip="[^"]*\/release-(?:npm|aur)/, "Publish tooltip should not show slash command names");
|
|
103
107
|
assert.match(html, /id="steerButton"[\s\S]*?data-tooltip="Steer usage:/, "Steer should explain type-first usage in a tooltip");
|
|
@@ -127,6 +131,8 @@ assert.match(css, /#promptInput \{[\s\S]*?overflow-y:\s*hidden/, "prompt input s
|
|
|
127
131
|
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");
|
|
128
132
|
assert.match(css, /\.message\.extension,[\s\S]*?\.message\.native/, "extension and native command output should have visible transcript styling");
|
|
129
133
|
assert.match(css, /\.message\.run-indicator-message \{[\s\S]*?border-color/, "active agent runs should render a visible transcript indicator card");
|
|
134
|
+
assert.match(css, /\.message-copy-button \{[\s\S]*?position:\s*absolute/, "transcript messages should expose a top-right copy button");
|
|
135
|
+
assert.match(css, /\.message\.has-copy-action[\s\S]*?padding-right:\s*3\.1rem/, "copy buttons should reserve space in message cards");
|
|
130
136
|
assert.match(css, /\.message\.action-enter \{[\s\S]*?action-card-slide-in 340ms/, "new action cards should visibly slide in from the bottom");
|
|
131
137
|
assert.match(css, /@keyframes action-card-slide-in \{[\s\S]*?translate3d\(0, 1\.45rem, 0\)/, "action-card entry animation should start well below the final position");
|
|
132
138
|
assert.match(css, /\.message\.thinking,\n\.message\.toolCall,\n\.message\.assistantEvent/, "thinking and assistant events should have non-assistant transcript card styling");
|
|
@@ -173,16 +179,19 @@ assert.match(css, /\.command-suggest-item:hover \{\n\s+box-shadow: none;\n\s+tra
|
|
|
173
179
|
assert.doesNotMatch(css, /\.command-suggest-item:hover,\n\.command-suggest-item\.active/, "autocomplete hover and active selection styles should stay separate");
|
|
174
180
|
assert.match(css, /\.feedback-tray\[hidden\] \{ display: none; \}/, "queued action-feedback tray should hide when empty");
|
|
175
181
|
assert.match(css, /\.action-feedback-controls \{[\s\S]*?position:\s*absolute/, "action reactions should be absolutely positioned so they do not expand cards");
|
|
176
|
-
assert.match(css, /\.action-feedback-controls \{[\s\S]*?
|
|
182
|
+
assert.match(css, /\.action-feedback-controls \{[\s\S]*?bottom:\s*0\.48rem/, "action reactions should sit inside the message box by default");
|
|
177
183
|
assert.match(css, /\.action-feedback-controls \{[\s\S]*?opacity:\s*0/, "action reactions should stay hidden until hovered or focused");
|
|
178
184
|
assert.match(css, /\.action-feedback-controls:hover,[\s\S]*?\.action-feedback-controls:focus-within/, "action reactions should reveal on hover or keyboard focus");
|
|
179
185
|
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");
|
|
180
186
|
assert.match(css, /\.action-feedback-button\.feedback-question\.active/, "question-mark reaction should have selected styling");
|
|
181
187
|
assert.match(css, /\.composer-row button\[data-tooltip\]::after/, "composer button tooltips should be shared across Git, Steer, and Follow-up buttons");
|
|
182
188
|
assert.match(css, /\.composer-row button\[data-tooltip\]\.tooltip-open::after/, "composer button tooltips should be triggerable from JS for empty mobile taps");
|
|
189
|
+
assert.match(css, /\.composer-publish-menu:hover > \.composer-publish-button\[data-tooltip\]::before,[\s\S]*?\.composer-publish-menu\.open > \.composer-publish-button\[data-tooltip\]::after \{[\s\S]*?display:\s*none !important;[\s\S]*?opacity:\s*0 !important;/, "dropdown button tooltips should hide while publish or setup menus are open");
|
|
183
190
|
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");
|
|
184
191
|
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");
|
|
185
|
-
assert.match(css, /\.composer-
|
|
192
|
+
assert.match(css, /\.composer-native-command-button \{[\s\S]*?color:\s*var\(--ctp-mauve\)/, "skills/tools command menu should have a distinct slash-command button style");
|
|
193
|
+
assert.match(css, /\.composer-native-command-menu-item \{[\s\S]*?color:\s*var\(--ctp-mauve\)/, "skills/tools command menu items should be styled separately from publish actions");
|
|
194
|
+
assert.match(css, /\.composer-actions-panel > \.composer-publish-menu[\s\S]*?grid-column: span 1/, "Publish and command menu buttons should fit beside Git workflow in mobile actions");
|
|
186
195
|
assert.match(css, /\.composer-actions-panel[\s\S]*?bottom:\s*calc\(100% \+ 0\.42rem\)/, "mobile composer actions should open as an above-composer sheet");
|
|
187
196
|
assert.match(css, /body\.composer-actions-open \.composer-actions-panel \{ display: grid; \}/, "composer actions panel should only open when toggled");
|
|
188
197
|
assert.match(css, /\.terminal-tabs-toggle-button \{ display: none; \}/, "terminal tab toggle should be hidden outside mobile CSS");
|
|
@@ -205,14 +214,16 @@ assert.match(css, /body\.mobile-keyboard-open \.composer-actions-button,[\s\S]*?
|
|
|
205
214
|
assert.match(css, /\.server-offline-panel/, "PWA/offline shell should style a backend-offline recovery panel");
|
|
206
215
|
assert.match(css, /body:not\(\.pi-run-active\):not\(\.mobile-keyboard-open\) \.composer-row button\.primary \{ grid-column: span 4; \}/, "idle mobile composer should keep Actions and Send on one compact row");
|
|
207
216
|
assert.match(css, /button\[hidden\] \{ display: none !important; \}/, "hidden bottom-row controls should not occupy layout space");
|
|
208
|
-
assert.match(css, /\.
|
|
209
|
-
assert.match(css, /\.
|
|
210
|
-
assert.match(css, /\.footer-
|
|
217
|
+
assert.match(css, /\.statusbar-tui-footer \{[\s\S]*?gap:\s*0/, "default TUI-like footer should reduce statusbar chrome around the compact line");
|
|
218
|
+
assert.match(css, /\.statusbar-git-footer \{[\s\S]*?gap:\s*0\.58rem/, "enabled git-footer extension should keep the styled Web UI footer spacing");
|
|
219
|
+
assert.match(css, /\.footer-line-tui \{[\s\S]*?white-space:\s*nowrap/, "default Web UI footer should use a minimal TUI-like line");
|
|
220
|
+
assert.match(css, /\.footer-tui-cwd[\s\S]*?max-width:\s*38%/, "TUI-like footer should keep cwd compact on desktop");
|
|
221
|
+
assert.match(css, /\.footer-tui-model[\s\S]*?text-align:\s*right/, "TUI-like footer should right-align model information on desktop");
|
|
211
222
|
assert.match(css, /\.footer-model-picker[\s\S]*?position:\s*absolute/, "footer model picker should render as a dropdown/popover");
|
|
212
223
|
assert.match(css, /@media \(max-width: 720px\), \(max-device-width: 720px\), \(pointer: coarse\) and \(hover: none\)[\s\S]*?\.footer-model-picker \{[\s\S]*?position:\s*fixed/, "mobile footer model picker should escape footer-details stacking as a fixed overlay on narrow, device-width-narrow, or touch-only devices");
|
|
213
224
|
assert.match(css, /bottom:\s*var\(--footer-model-picker-bottom/, "mobile footer model picker should be anchored by a JS-computed viewport offset");
|
|
214
225
|
assert.match(css, /\.footer-model-option\.active/, "footer model picker should style the selected scoped model");
|
|
215
|
-
assert.match(css,
|
|
226
|
+
assert.match(css, /@media \(max-width: 720px\), \(max-device-width: 720px\), \(pointer: coarse\) and \(hover: none\)[\s\S]*?\.footer-line-tui \{[\s\S]*?flex-wrap:\s*wrap/, "mobile footer should wrap the minimal TUI-like line instead of using expanded metadata chips");
|
|
216
227
|
assert.match(css, /(?:^|\n)\s*\.side-panel-backdrop\s*\{[\s\S]*?position:\s*fixed/, "mobile side panel backdrop should be fixed overlay UI");
|
|
217
228
|
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");
|
|
218
229
|
assert.match(css, /\.extension-dialog[\s\S]*?max-height:\s*calc\(var\(--visual-viewport-height/, "dialogs should fit the visual viewport on mobile");
|
|
@@ -220,6 +231,8 @@ assert.match(css, /\.extension-dialog[\s\S]*?inset:\s*auto 0 0 0/, "mobile dialo
|
|
|
220
231
|
assert.match(css, /#dialogMessage \{[\s\S]*?white-space:\s*pre-wrap/, "extension dialog messages should preserve multiline prompts");
|
|
221
232
|
assert.match(css, /\.native-command-dialog \{[\s\S]*?width:\s*min\(56rem/, "native slash selector dialog should have roomy desktop layout");
|
|
222
233
|
assert.match(css, /\.native-selector-item \{[\s\S]*?--tree-depth/, "native slash selector choices should support tree indentation");
|
|
234
|
+
assert.match(css, /\.native-selector-badge\.native-selector-badge-pi-native[\s\S]*?color:\s*var\(--ctp-blue\)/, "Tools Setup should distinguish Pi native tools with a Pi Native tag");
|
|
235
|
+
assert.match(css, /\.native-selector-badge\.native-selector-badge-external[\s\S]*?color:\s*var\(--ctp-mauve\)/, "Tools Setup should distinguish external tools with an External tag");
|
|
223
236
|
assert.match(css, /\.native-settings-grid,[\s\S]*?\.native-tree-options \{[\s\S]*?grid-template-columns:/, "native settings and tree selector options should use responsive grids");
|
|
224
237
|
assert.match(css, /\.extension-dialog\.guardrail-dialog[\s\S]*?border-color:\s*rgba\(249, 226, 175/, "guardrail dialogs should have warning-specific styling");
|
|
225
238
|
assert.match(css, /\.extension-dialog\.release-dialog[\s\S]*?width:\s*min\(64rem/, "release confirmation dialogs should have more horizontal room");
|
|
@@ -287,6 +300,10 @@ assert.match(app, /navigator\.serviceWorker\.register\("\/service-worker\.js"\)/
|
|
|
287
300
|
assert.match(app, /function serverStartCommandText\(\)[\s\S]*pi-webui --cwd/, "PWA/offline shell should build a pi-webui --cwd recovery command");
|
|
288
301
|
assert.match(app, /Pi Web UI server is offline/, "PWA/offline shell should clearly report backend-down state");
|
|
289
302
|
assert.match(app, /navigator\.clipboard\.writeText\(text\)/, "backend-offline recovery panel should copy the start command when possible");
|
|
303
|
+
assert.match(app, /function messageCopyText\(message, body = null\)/, "frontend should derive copy text from transcript messages");
|
|
304
|
+
assert.match(app, /function attachMessageCopyButton\(bubble, message, body\)/, "frontend should add copy controls to rendered transcript cards");
|
|
305
|
+
assert.match(app, /button\.append\(make\("span", "message-copy-icon", "⧉"\)\)/, "message copy buttons should render as icon-only controls");
|
|
306
|
+
assert.match(app, /copyMessageBubble\(button\)/, "message copy buttons should copy through the shared clipboard helper");
|
|
290
307
|
assert.match(app, /retryServerConnectionButton.*retryServerConnection/s, "backend-offline recovery panel should wire a retry action");
|
|
291
308
|
assert.match(app, /function isChatNearBottom\(/, "chat should detect whether the user is reading above the bottom");
|
|
292
309
|
assert.match(app, /function scheduleChatFollowScroll\(/, "chat auto-follow should retry after layout settles during fast streaming");
|
|
@@ -336,9 +353,33 @@ assert.match(app, /function renderCodexUsage\(\)/, "frontend should render Codex
|
|
|
336
353
|
assert.match(app, /api\(`\/api\/codex-usage\$\{suffix\}`, \{ scoped: false \}\)/, "Codex usage should load through a server endpoint without browser credentials");
|
|
337
354
|
assert.match(app, /restoreSidePanelSectionState\(\);\nbindSidePanelSectionToggles\(\);/, "side panel section state should restore before toggles are bound");
|
|
338
355
|
assert.match(app, /OPTIONAL_FEATURES_STORAGE_KEY/, "optional feature disable toggles should persist in browser storage");
|
|
356
|
+
assert.match(app, /GIT_FOOTER_WEBUI_STATUS_KEY = "git-footer-webui"/, "git footer Web UI data should be received as an extension-owned status payload");
|
|
357
|
+
assert.match(app, /function parseGitFooterWebuiPayloadRaw\(raw\)[\s\S]*GIT_FOOTER_WEBUI_PAYLOAD_TYPE[\s\S]*GIT_FOOTER_WEBUI_PAYLOAD_VERSION/, "Web UI footer should parse the structured payload emitted by git-footer-status");
|
|
358
|
+
assert.match(app, /function renderFooter\(\)[\s\S]*parseGitFooterWebuiPayload\(\)[\s\S]*renderGitFooterPayload\(gitFooterPayload\)/, "detailed footer rendering should prefer the git-footer-status extension payload");
|
|
359
|
+
assert.match(app, /function renderGitFooterPayload\(payload\)[\s\S]*classList\.remove\("statusbar-tui-footer"\)[\s\S]*classList\.add\("statusbar-git-footer"\)[\s\S]*payload\.main\.map\(renderGitFooterPayloadMetric\)[\s\S]*payload\.meta\.map/, "enabled git footer payload should use the styled extension chip renderer, not the default TUI line");
|
|
360
|
+
assert.match(app, /function renderGitFooterPayloadMetric\(chip\)[\s\S]*footerMetric\(chip\.icon/, "git footer main payload chips should render as styled metrics");
|
|
361
|
+
assert.match(app, /function renderGitFooterPayloadMeta\(chip, tab\)[\s\S]*footerMeta\(chip\.label, chip\.value, footerMetaClassForPayload\(chip\)/, "git footer meta payload chips should render as styled metadata");
|
|
362
|
+
assert.match(app, /let latestStats = null/, "default footer should retain session stats for token and context display");
|
|
363
|
+
assert.match(app, /async function refreshStats\(tabContext = activeTabContext\(\)\)[\s\S]*api\("\/api\/stats"/, "default footer should fetch session stats");
|
|
364
|
+
assert.match(app, /function renderMinimalFooter\(\)[\s\S]*stats: fallbackFooterStats\(\)/, "minimal default footer should include token, cost, and context stats");
|
|
365
|
+
assert.match(app, /function footerStatsTokensDisplay\(stats = latestStats\)[\s\S]*`↑\$\{formatFooterTokenCount\(tokens\.input\)\} ↓\$\{formatFooterTokenCount\(tokens\.output\)\}`/, "fallback footer stats should include input/output tokens");
|
|
366
|
+
assert.match(app, /function footerStatsCostDisplay\(stats = latestStats\)[\s\S]*footerCostAuthLabel\(\)/, "fallback footer stats should include api\/sub cost mode");
|
|
367
|
+
assert.doesNotMatch(app, /Git footer status disabled/, "disabled git footer should show only the minimal footer metadata");
|
|
368
|
+
assert.doesNotMatch(app, /footerMeta\("runtime"/, "minimal Web UI footer should not render runtime metadata");
|
|
369
|
+
assert.match(app, /statusEntries\.has\(GIT_FOOTER_WEBUI_STATUS_KEY\)/, "optional feature detection should recognize the git-footer-status Web UI payload");
|
|
370
|
+
assert.match(app, /\/git-footer-refresh --webui-silent/, "Web UI should quietly request the extension-owned footer payload when idle and missing");
|
|
371
|
+
assert.match(app, /Loading git footer status…/, "missing git footer payload should show a loading state before declaring the extension unavailable");
|
|
372
|
+
assert.match(app, /GIT_FOOTER_WEBUI_PAYLOAD_CACHE_KEY/, "git footer payloads should be cached across Web UI reloads");
|
|
373
|
+
assert.match(app, /function setOptionalFeatureDisabled\(featureId, disabled\)[\s\S]*clearGitFooterWebuiPayloadCache\(\)/, "changing the git footer feature toggle should invalidate the cached footer payload");
|
|
374
|
+
const workspaceInfoSource = server.match(/async function getWorkspaceInfo[\s\S]*?\n}\n\nlet activeGitWorkflowProcess/)?.[0] || "";
|
|
375
|
+
assert.ok(workspaceInfoSource, "server workspace info source should be inspectable");
|
|
376
|
+
assert.doesNotMatch(workspaceInfoSource, /runCommand\("git"|branchStatus|isRepo/, "Web UI workspace endpoint should not duplicate git footer status collection");
|
|
339
377
|
assert.match(app, /function renderOptionalFeatureDependentDisplays\(\)[\s\S]*renderOptionalFeatureControls\(\);[\s\S]*renderThemeSelect\(\);[\s\S]*renderWidgets\(\);[\s\S]*renderStatus\(\);[\s\S]*renderCommands\(\);[\s\S]*renderAllMessages\(\{ preserveScroll: true \}\);[\s\S]*if \(streamRawText\) renderStreamingAssistantText\(\);/, "optional feature toggles should immediately refresh visible controls, commands, transcript, and live stream displays");
|
|
340
378
|
assert.match(app, /function setOptionalFeatureDisabled\(featureId, disabled\)[\s\S]*renderOptionalFeatureDependentDisplays\(\);[\s\S]*const tabContext = activeTabContext\(\);[\s\S]*refreshCommands\(tabContext\)/, "optional feature enable/disable should re-render the GUI and then refresh command capabilities");
|
|
341
379
|
assert.match(app, /function setOptionalControlState\(button, available, unavailableTitle\)[\s\S]*setAttribute\("aria-label", nextAriaLabel\)[\s\S]*setAttribute\("data-tooltip", nextTooltip\)/, "optional feature button disabled state should update accessible labels and visible tooltips");
|
|
380
|
+
assert.match(app, /\["skills", "tuiSkillsCommand"\][\s\S]*\["tools", "tuiToolsCommand"\]/, "optional feature toggles should gate /skills and /tools command surfaces");
|
|
381
|
+
assert.match(app, /function setNativeCommandMenuOpen\(open\)/, "frontend should track the skills/tools command menu open state separately from Publish");
|
|
382
|
+
assert.match(app, /nativeSkillsButton\.hidden = !isOptionalFeatureEnabled\("tuiSkillsCommand"\)[\s\S]*nativeToolsButton\.hidden = !isOptionalFeatureEnabled\("tuiToolsCommand"\)/, "skills/tools menu items should be hidden by their optional feature toggles");
|
|
342
383
|
assert.match(app, /function renderCommands\(\)/, "side-panel commands should be re-renderable from current optional feature state");
|
|
343
384
|
assert.match(app, /function installOptionalFeature\(featureId\)/, "optional features should expose an install action");
|
|
344
385
|
assert.match(app, /api\("\/api\/optional-feature-install"/, "optional feature install action should call the backend installer endpoint");
|
|
@@ -486,10 +527,18 @@ assert.match(app, /function showComposerButtonTooltip\(button\)/, "empty mode-bu
|
|
|
486
527
|
assert.match(app, /sendPromptFromModeButton\("steer", elements\.steerButton\)/, "Steer should show tooltip instead of silently doing nothing when input is empty");
|
|
487
528
|
assert.match(app, /sendPromptFromModeButton\("follow-up", elements\.followUpButton\)/, "Follow-up should show tooltip instead of silently doing nothing when input is empty");
|
|
488
529
|
assert.match(app, /function runPublishWorkflow\(command\)[\s\S]*?sendPrompt\("prompt", command\)/, "Publish workflows should send slash commands directly without replacing the draft");
|
|
489
|
-
assert.match(app, /
|
|
530
|
+
assert.match(app, /async function runNativeCommandMenu\(command\)[\s\S]*?await handleNativeSlashSelectorCommand\(command\)/, "skills/tools command menu should open native selector dialogs directly");
|
|
531
|
+
assert.match(app, /function nativeToolOriginTag\(resource\)[\s\S]*?sourceInfo\?\.source === "builtin"[\s\S]*?label: "Pi Native"[\s\S]*?label: "External"/, "Tools Setup should classify built-in Pi tools separately from external tools");
|
|
532
|
+
assert.match(app, /renderNativeResourceToggles\(tools, \{[\s\S]*?getResourceTag: nativeToolOriginTag/, "Tools Setup should render Pi Native\/External tags");
|
|
533
|
+
assert.match(app, /const tags = Array\.isArray\(item\.tags\)[\s\S]*?item\.badge, \.\.\.tags/, "native selector filtering should include extra resource tags");
|
|
534
|
+
assert.match(app, /publishMenuContainer\?\.addEventListener\("pointerenter", \(\) => \{[\s\S]*?setPublishMenuOpen\(true\);[\s\S]*?\}\)/, "Publish menu should expand on hover");
|
|
490
535
|
assert.match(app, /publishMenuContainer\?\.addEventListener\("pointerleave", \(\) => setPublishMenuOpen\(false\)\)/, "Publish menu should collapse after hover leaves");
|
|
536
|
+
assert.match(app, /nativeCommandMenuContainer\?\.addEventListener\("pointerenter", \(\) => \{[\s\S]*?setNativeCommandMenuOpen\(true\);[\s\S]*?\}\)/, "skills/tools command menu should expand on hover");
|
|
537
|
+
assert.match(app, /nativeCommandMenuContainer\?\.addEventListener\("pointerleave", \(\) => setNativeCommandMenuOpen\(false\)\)/, "skills/tools command menu should collapse after hover leaves");
|
|
491
538
|
assert.match(app, /releaseNpmButton\.addEventListener\("click", \(\) => runPublishWorkflow\("\/release-npm"\)\)/, "Publish menu should launch /release-npm");
|
|
492
539
|
assert.match(app, /releaseAurButton\.addEventListener\("click", \(\) => runPublishWorkflow\("\/release-aur"\)\)/, "Publish menu should launch /release-aur");
|
|
540
|
+
assert.match(app, /nativeSkillsButton\.addEventListener\("click", \(\) => runNativeCommandMenu\("\/skills"\)\)/, "skills/tools command menu should launch /skills");
|
|
541
|
+
assert.match(app, /nativeToolsButton\.addEventListener\("click", \(\) => runNativeCommandMenu\("\/tools"\)\)/, "skills/tools command menu should launch /tools");
|
|
493
542
|
assert.match(app, /async function sendPrompt\(kind = "prompt", explicitMessage\)/, "prompt sending should accept direct messages that bypass the input field");
|
|
494
543
|
assert.match(app, /const rawMessage = usesPromptInput \? elements\.promptInput\.value : explicitMessage/, "direct prompt sends should not read the input textarea");
|
|
495
544
|
assert.match(app, /if \(usesPromptInput\) \{[\s\S]*?if \(targetStillActive\) \{[\s\S]*?elements\.promptInput\.value = "";/, "direct prompt sends should preserve the input textarea draft");
|
|
@@ -575,16 +624,16 @@ assert.match(app, /function applyResponseTab\(response\)/, "frontend should merg
|
|
|
575
624
|
assert.match(app, /case "webui_tab_renamed":/, "frontend should update tab labels from backend rename events");
|
|
576
625
|
assert.match(app, /terminalTabsToggleButton\.addEventListener\("click"/, "terminal tabs trigger should be wired in JS");
|
|
577
626
|
assert.match(app, /composerActionsButton\.addEventListener\("click"/, "composer actions trigger should be wired in JS");
|
|
578
|
-
assert.match(app, /function setMobileFooterExpanded\(/, "mobile footer should
|
|
627
|
+
assert.match(app, /function setMobileFooterExpanded\(/, "mobile footer should preserve expansion state for compatibility");
|
|
579
628
|
assert.match(app, /function updateFooterModelPickerPosition\(\)/, "mobile model picker should compute a fixed overlay position above the footer");
|
|
580
|
-
assert.match(app, /mobileFooterExpanded = false;[\s\S]*?document\.body\.classList\.remove\("footer-details-expanded"\)/, "opening mobile model picker should collapse footer details so
|
|
581
|
-
assert.match(app, /
|
|
582
|
-
assert.match(app, /
|
|
629
|
+
assert.match(app, /mobileFooterExpanded = false;[\s\S]*?document\.body\.classList\.remove\("footer-details-expanded"\)/, "opening mobile model picker should collapse legacy footer details so they cannot cover the dropdown");
|
|
630
|
+
assert.match(app, /function renderTuiFooterLine\([\s\S]*footer-line footer-line-tui/, "footer should render a minimal TUI-like line instead of metadata chips");
|
|
631
|
+
assert.match(app, /footerTuiItem\(model, "footer-tui-model", \{[\s\S]*setFooterModelPickerOpen\(!footerModelPickerOpen\)/, "footer model item should be clickable");
|
|
583
632
|
assert.match(app, /function renderFooterModelPicker\(\)/, "footer should render a scoped-model picker dropdown");
|
|
584
633
|
assert.match(app, /api\("\/api\/scoped-models", \{ tabId: tabContext\.tabId \}\)/, "footer model picker should load scoped models instead of all available models");
|
|
585
634
|
assert.match(app, /for \(const model of footerScopedModels\)/, "footer model picker should render only scoped models");
|
|
586
635
|
assert.match(app, /api\("\/api\/model", \{ method: "POST"/, "footer model picker should apply selected model through the model API");
|
|
587
|
-
assert.match(
|
|
636
|
+
assert.doesNotMatch(app.match(/function renderMinimalFooter\(\)[\s\S]*?\n\}/)?.[0] || "", /footer-details-toggle/, "minimal default footer should not render a details toggle chip");
|
|
588
637
|
assert.match(app, /bindMobileViewChanges\(/, "side panel state should react to mobile breakpoint changes");
|
|
589
638
|
assert.match(app, /function restoreSidePanelState\(\) \{\n\s+if \(isMobileView\(\)\)/, "mobile should start with side panel collapsed even if desktop state was expanded");
|
|
590
639
|
assert.match(app, /case "webui_tab_reloaded":/, "frontend should handle native /reload tab restart events");
|
|
@@ -600,7 +649,7 @@ assert.equal(manifest.start_url, "/", "PWA manifest should start at the web UI r
|
|
|
600
649
|
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");
|
|
601
650
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-192.png" && icon.sizes === "192x192"), "PWA manifest should include a 192px icon");
|
|
602
651
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-512.png" && icon.sizes === "512x512"), "PWA manifest should include a 512px icon");
|
|
603
|
-
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-
|
|
652
|
+
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v23"/, "PWA service worker should define an app-shell cache");
|
|
604
653
|
assert.match(serviceWorker, /self\.addEventListener\("notificationclick"/, "PWA service worker should focus Web UI when blocked-tab notifications are clicked");
|
|
605
654
|
assert.match(serviceWorker, /event\.notification\.data\?\.url/, "blocked-tab notifications should carry a URL for service-worker click handling");
|
|
606
655
|
assert.match(serviceWorker, /"\/apple-touch-icon\.png"/, "PWA service worker should cache the apple touch icon");
|
|
@@ -632,10 +681,14 @@ assert.match(server, /tabActivity: tabActivitySnapshot\(tab\)/, "server should e
|
|
|
632
681
|
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");
|
|
633
682
|
assert.match(server, /function trackPendingExtensionUiRequest\(tab, event\)/, "server should track blocking extension UI requests per tab");
|
|
634
683
|
assert.match(server, /pendingExtensionUiRequests: new Map\(\)/, "new tabs should initialize pending extension UI request storage");
|
|
684
|
+
assert.match(server, /extensionStatuses: new Map\(\)/, "new tabs should initialize replayable extension status storage");
|
|
685
|
+
assert.match(server, /function rememberExtensionStatusEvent\(tab, event\)[\s\S]*event\.method !== "setStatus"[\s\S]*statuses\.set\(String\(event\.statusKey\), String\(event\.statusText\)\)/, "server should retain extension status events for reconnects");
|
|
686
|
+
assert.match(server, /rememberExtensionStatusEvent\(tab, scopedEvent\)[\s\S]*trackPendingExtensionUiRequest\(tab, scopedEvent\)/, "RPC events should retain extension statuses before broadcasting");
|
|
635
687
|
assert.match(server, /trackPendingExtensionUiRequest\(tab, scopedEvent\)/, "RPC events should populate pending extension UI storage before broadcasting");
|
|
636
688
|
assert.match(server, /scopedEvent = \{ \.\.\.scopedEvent,[\s\S]*?pendingExtensionUiRequestCount: pendingExtensionUiRequests\(tab\)\.length \}/, "RPC events should broadcast pending blocker counts for tab indicators");
|
|
689
|
+
assert.match(server, /function replayExtensionStatuses\(tab, res\)[\s\S]*method: "setStatus"/, "server should replay latest extension statuses on SSE reconnect");
|
|
637
690
|
assert.match(server, /function replayPendingExtensionUiRequests\(tab, res\)/, "server should be able to replay missed extension UI requests on SSE reconnect");
|
|
638
|
-
assert.match(server, /replayPendingExtensionUiRequests\(tab, res\)/, "SSE connections should replay
|
|
691
|
+
assert.match(server, /replayExtensionStatuses\(tab, res\);\n\s+replayPendingExtensionUiRequests\(tab, res\)/, "SSE connections should replay extension statuses before pending blockers");
|
|
639
692
|
assert.match(server, /pendingExtensionUiRequests: pendingExtensionUiRequestSummaries\(tab\)/, "detailed Web UI status should expose pending extension UI blockers");
|
|
640
693
|
assert.match(server, /resolvePendingExtensionUiRequest\(tab, payload\.id\)/, "extension UI responses should clear the pending blocker cache");
|
|
641
694
|
assert.match(server, /type: "webui_extension_ui_resolved"[\s\S]*?pendingExtensionUiRequestCount/, "extension UI responses should notify clients that a blocker resolved");
|