@firstpick/pi-package-webui 0.2.1 → 0.2.3

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/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" />
15
+ <link rel="stylesheet" href="/styles.css?v=20" />
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">
@@ -36,6 +36,15 @@
36
36
  </div>
37
37
  </section>
38
38
 
39
+ <section id="serverRestartPanel" class="server-restart-panel" aria-live="assertive" hidden>
40
+ <div class="server-restart-card" role="status">
41
+ <div class="server-restart-spinner" aria-hidden="true"></div>
42
+ <span class="server-restart-kicker">Restarting</span>
43
+ <h1>Restarting Pi Web UI server</h1>
44
+ <p id="serverRestartMessage">Waiting for the server to come back…</p>
45
+ </div>
46
+ </section>
47
+
39
48
  <main class="layout">
40
49
  <section class="chat-panel">
41
50
  <header class="terminal-tabs-shell">
@@ -151,8 +160,11 @@
151
160
  <div class="side-panel-header">
152
161
  <div>
153
162
  <span class="side-panel-kicker">Pi Web UI</span>
154
- <strong>Control Deck</strong>
155
- <p id="sessionLine" class="side-panel-session-line muted">Connecting…</p>
163
+ <strong class="side-panel-title">
164
+ <span>Control Deck</span>
165
+ <span id="webuiVersionBadge" class="webui-version-badge" aria-label="Pi Web UI version" hidden></span>
166
+ <span id="webuiDevBadge" class="webui-dev-badge" aria-label="Pi Web UI dev server" hidden>DEV</span>
167
+ </strong>
156
168
  </div>
157
169
  <button id="toggleSidePanelButton" class="side-panel-toggle-button" type="button" aria-controls="sidePanel" aria-expanded="true" aria-label="Collapse side panel" title="Collapse side panel">
158
170
  <span class="side-panel-button-icon" aria-hidden="true">
@@ -217,8 +229,16 @@
217
229
  <button id="openNetworkButton" type="button">Open to network</button>
218
230
  </div>
219
231
  <div class="control-field server-control-field">
220
- <label>Server</label>
221
- <button id="stopServerButton" class="danger" type="button" title="Stop the Pi Web UI server and disconnect all browser clients" aria-label="Stop the Pi Web UI server">Stop Server</button>
232
+ <label for="serverActionSelect">Server</label>
233
+ <div class="server-action-row">
234
+ <select id="serverActionSelect" title="Server action" aria-label="Server action">
235
+ <option value="" selected>Choose action…</option>
236
+ <option value="restart">Restart Server</option>
237
+ <option value="stop">Stop Server</option>
238
+ </select>
239
+ <button id="runServerActionButton" type="button" disabled>Run</button>
240
+ </div>
241
+ <div id="serverActionStatus" class="server-action-status muted" role="status" aria-live="polite"></div>
222
242
  </div>
223
243
  <div class="control-field notification-control-field">
224
244
  <span class="control-label">Notifications</span>
@@ -344,6 +364,6 @@
344
364
  </form>
345
365
  </dialog>
346
366
 
347
- <script type="module" src="/app.js"></script>
367
+ <script type="module" src="/app.js?v=20"></script>
348
368
  </body>
349
369
  </html>
@@ -1,4 +1,4 @@
1
- const CACHE_NAME = "pi-webui-pwa-v16";
1
+ const CACHE_NAME = "pi-webui-pwa-v20";
2
2
  const APP_SHELL = [
3
3
  "/",
4
4
  "/index.html",
package/public/styles.css CHANGED
@@ -375,6 +375,65 @@ body.server-offline .layout {
375
375
  filter: blur(2px);
376
376
  pointer-events: none;
377
377
  }
378
+ .server-restart-panel[hidden] {
379
+ display: none !important;
380
+ }
381
+ .server-restart-panel {
382
+ position: fixed;
383
+ inset: 1rem;
384
+ z-index: 62;
385
+ display: grid;
386
+ place-items: center;
387
+ padding: 1rem;
388
+ pointer-events: none;
389
+ }
390
+ .server-restart-card {
391
+ position: relative;
392
+ display: grid;
393
+ justify-items: center;
394
+ width: min(34rem, 100%);
395
+ pointer-events: auto;
396
+ padding: clamp(1.35rem, 4vw, 2.2rem);
397
+ text-align: center;
398
+ border: 1px solid rgba(148, 226, 213, 0.34);
399
+ border-radius: 1.2rem;
400
+ background:
401
+ radial-gradient(circle at 50% 0, rgba(148, 226, 213, 0.18), transparent 18rem),
402
+ linear-gradient(145deg, rgba(var(--ctp-base-rgb), 0.96), rgba(var(--ctp-crust-rgb), 0.98));
403
+ box-shadow: 0 1.2rem 4rem rgba(var(--ctp-crust-rgb), 0.74), 0 0 2rem rgba(148, 226, 213, 0.14), inset 0 1px 0 rgba(255,255,255,0.07);
404
+ }
405
+ .server-restart-spinner {
406
+ width: 2.8rem;
407
+ height: 2.8rem;
408
+ margin-bottom: 0.9rem;
409
+ border: 0.22rem solid rgba(148, 226, 213, 0.18);
410
+ border-top-color: var(--ctp-teal);
411
+ border-radius: 999px;
412
+ animation: server-restart-spin 900ms linear infinite;
413
+ }
414
+ .server-restart-kicker {
415
+ color: var(--ctp-teal);
416
+ font-size: 0.76rem;
417
+ font-weight: 900;
418
+ letter-spacing: 0.12em;
419
+ text-transform: uppercase;
420
+ }
421
+ .server-restart-card h1 {
422
+ margin: 0.35rem 0 0.45rem;
423
+ font-size: clamp(1.35rem, 4vw, 2rem);
424
+ }
425
+ .server-restart-card p {
426
+ margin: 0;
427
+ color: rgba(var(--ctp-subtext-rgb), 0.9);
428
+ }
429
+ body.server-restarting .layout {
430
+ opacity: 0.56;
431
+ filter: blur(1.5px);
432
+ pointer-events: none;
433
+ }
434
+ @keyframes server-restart-spin {
435
+ to { transform: rotate(360deg); }
436
+ }
378
437
  .side-panel-expand-button {
379
438
  position: fixed;
380
439
  top: 1rem;
@@ -504,13 +563,36 @@ body.side-panel-collapsed .terminal-tabs-shell {
504
563
  color: var(--ctp-text);
505
564
  letter-spacing: 0.03em;
506
565
  }
507
- .side-panel-session-line {
508
- margin: 0.34rem 0 0;
509
- max-width: 17rem;
510
- overflow: hidden;
511
- text-overflow: ellipsis;
512
- white-space: nowrap;
513
- font-size: 0.76rem;
566
+ .side-panel-header .side-panel-title {
567
+ display: flex;
568
+ align-items: center;
569
+ gap: 0.46rem;
570
+ flex-wrap: wrap;
571
+ }
572
+ .webui-version-badge,
573
+ .webui-dev-badge {
574
+ display: inline-flex;
575
+ align-items: center;
576
+ min-height: 1.18rem;
577
+ padding: 0.05rem 0.44rem;
578
+ border: 1px solid rgba(180, 190, 254, 0.24);
579
+ border-radius: 999px;
580
+ color: var(--ctp-subtext);
581
+ background: rgba(var(--ctp-surface-rgb), 0.74);
582
+ font-size: 0.68rem;
583
+ font-weight: 800;
584
+ letter-spacing: 0.04em;
585
+ line-height: 1;
586
+ }
587
+ .webui-dev-badge {
588
+ border-color: rgba(249, 226, 175, 0.38);
589
+ color: var(--ctp-yellow);
590
+ background: rgba(249, 226, 175, 0.12);
591
+ box-shadow: 0 0 0.7rem rgba(249, 226, 175, 0.1);
592
+ }
593
+ .webui-version-badge[hidden],
594
+ .webui-dev-badge[hidden] {
595
+ display: none;
514
596
  }
515
597
  .side-panel-kicker {
516
598
  display: block;
@@ -647,6 +729,35 @@ body.side-panel-collapsed .terminal-tabs-shell {
647
729
  gap: 0.42rem;
648
730
  align-items: center;
649
731
  }
732
+ .server-action-row {
733
+ display: grid;
734
+ grid-template-columns: minmax(0, 1fr) auto;
735
+ gap: 0.42rem;
736
+ align-items: center;
737
+ }
738
+ .server-action-row button {
739
+ width: auto;
740
+ min-width: 4.4rem;
741
+ }
742
+ .server-action-status {
743
+ min-height: 1.05rem;
744
+ color: rgba(var(--ctp-subtext-rgb), 0.82);
745
+ font-size: 0.72rem;
746
+ font-weight: 750;
747
+ line-height: 1.35;
748
+ }
749
+ .server-action-status.warn {
750
+ color: var(--ctp-yellow);
751
+ }
752
+ .server-action-status.error {
753
+ color: var(--ctp-red);
754
+ }
755
+ .server-action-status.success {
756
+ color: var(--ctp-green);
757
+ }
758
+ .server-action-status[hidden] {
759
+ display: none;
760
+ }
650
761
  .background-clear-button {
651
762
  width: 44px !important;
652
763
  min-width: 44px !important;
@@ -1311,6 +1422,15 @@ body.side-panel-collapsed .terminal-tabs-shell {
1311
1422
  color: rgba(var(--ctp-text-rgb), 0.78);
1312
1423
  background: linear-gradient(90deg, rgba(203, 166, 247, 0.10), rgba(137, 180, 250, 0.05), rgba(148, 226, 213, 0.08));
1313
1424
  }
1425
+ .widget-area:has(.release-npm-live-widget .release-npm-output-details[open]),
1426
+ .widget-area:has(.release-aur-live-widget .release-npm-output-details[open]) {
1427
+ flex: 0 0 min(44rem, 68dvh);
1428
+ min-height: 0;
1429
+ overflow: auto;
1430
+ overscroll-behavior: contain;
1431
+ scrollbar-gutter: stable;
1432
+ overflow-anchor: none;
1433
+ }
1314
1434
  .statusbar {
1315
1435
  position: relative;
1316
1436
  flex: 0 0 auto;
@@ -1806,6 +1926,52 @@ button.footer-meta {
1806
1926
  text-transform: none;
1807
1927
  white-space: nowrap;
1808
1928
  }
1929
+ .release-npm-output-details {
1930
+ display: grid;
1931
+ gap: 0.58rem;
1932
+ min-width: 0;
1933
+ }
1934
+ .release-npm-output-summary {
1935
+ display: grid;
1936
+ grid-template-columns: auto minmax(0, 1fr);
1937
+ align-items: center;
1938
+ gap: 0.38rem;
1939
+ min-width: 0;
1940
+ color: rgba(var(--ctp-text-rgb), 0.92);
1941
+ cursor: pointer;
1942
+ list-style: none;
1943
+ }
1944
+ .release-npm-output-summary::-webkit-details-marker { display: none; }
1945
+ .release-npm-output-summary:focus-visible {
1946
+ outline: 2px solid rgba(137, 180, 250, 0.72);
1947
+ outline-offset: 0.18rem;
1948
+ border-radius: 0.72rem;
1949
+ }
1950
+ .release-npm-output-summary:hover .release-npm-stream-header,
1951
+ .release-npm-output-summary:focus-visible .release-npm-stream-header {
1952
+ border-color: rgba(137, 180, 250, 0.54);
1953
+ box-shadow: inset 0 0 0 1px rgba(137, 180, 250, 0.08), 0 0 0.8rem rgba(137, 180, 250, 0.08);
1954
+ }
1955
+ .release-npm-output-toggle {
1956
+ display: inline-grid;
1957
+ place-items: center;
1958
+ width: 1rem;
1959
+ height: 1rem;
1960
+ flex: 0 0 auto;
1961
+ color: rgba(var(--ctp-subtext-rgb), 0.78);
1962
+ font-size: 1rem;
1963
+ font-weight: 950;
1964
+ line-height: 1;
1965
+ transition: transform 0.16s ease, color 0.16s ease;
1966
+ }
1967
+ .release-npm-output-details[open] .release-npm-output-toggle {
1968
+ color: var(--ctp-blue);
1969
+ transform: rotate(90deg);
1970
+ }
1971
+ .release-npm-output-summary .release-npm-stream-header {
1972
+ min-width: 0;
1973
+ width: 100%;
1974
+ }
1809
1975
  .release-npm-terminal {
1810
1976
  max-height: min(34rem, 42vh);
1811
1977
  min-height: 5.25rem;
@@ -1825,6 +1991,11 @@ button.footer-meta {
1825
1991
  line-height: 1.5;
1826
1992
  overscroll-behavior: contain;
1827
1993
  }
1994
+ .release-npm-live-widget .release-npm-output-details[open] .release-npm-terminal,
1995
+ .release-aur-live-widget .release-npm-output-details[open] .release-npm-terminal {
1996
+ height: clamp(15rem, 42dvh, 31rem);
1997
+ min-height: 0;
1998
+ }
1828
1999
  .release-npm-line {
1829
2000
  display: block;
1830
2001
  width: max-content;
@@ -3189,6 +3360,28 @@ summary { cursor: pointer; color: var(--warning); }
3189
3360
  text-align: center;
3190
3361
  font-weight: 800;
3191
3362
  }
3363
+ .extension-dialog.release-dialog .dialog-options button.release-publish-disabled-action {
3364
+ color: rgba(var(--ctp-subtext-rgb), 0.72);
3365
+ border-color: rgba(var(--ctp-overlay-rgb), 0.32);
3366
+ background: linear-gradient(180deg, rgba(var(--ctp-surface-rgb), 0.58), rgba(var(--ctp-crust-rgb), 0.72));
3367
+ }
3368
+ .extension-dialog.release-dialog .dialog-options button.release-target-option {
3369
+ text-align: left;
3370
+ border-color: rgba(137, 180, 250, 0.34);
3371
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
3372
+ font-size: 0.78rem;
3373
+ line-height: 1.35;
3374
+ overflow-wrap: anywhere;
3375
+ white-space: normal;
3376
+ }
3377
+ .extension-dialog.release-dialog .dialog-options button.release-target-selected {
3378
+ color: var(--ctp-green);
3379
+ border-color: rgba(166, 227, 161, 0.58);
3380
+ background:
3381
+ linear-gradient(120deg, rgba(166, 227, 161, 0.16), rgba(137, 180, 250, 0.08)),
3382
+ linear-gradient(180deg, rgba(var(--ctp-surface-rgb), 0.80), rgba(var(--ctp-crust-rgb), 0.78));
3383
+ box-shadow: 0 0 1rem rgba(166, 227, 161, 0.14);
3384
+ }
3192
3385
  .extension-dialog.release-dialog .dialog-options button.release-cancel-action {
3193
3386
  border-color: rgba(249, 226, 175, 0.38);
3194
3387
  color: var(--ctp-yellow);
@@ -3271,6 +3464,31 @@ summary { cursor: pointer; color: var(--warning); }
3271
3464
  text-transform: uppercase;
3272
3465
  letter-spacing: 0.07em;
3273
3466
  }
3467
+ .native-command-body:has(.native-resource-summary) .native-selector-badge {
3468
+ border-color: rgba(255, 159, 67, 0.62);
3469
+ color: #ff9f43;
3470
+ background: rgba(255, 159, 67, 0.10);
3471
+ }
3472
+ .native-selector-badge.enabled,
3473
+ .native-selector-badge.native-selector-badge-enabled,
3474
+ .native-selector-badge[data-badge-state="enabled"],
3475
+ .native-command-body:has(.native-resource-summary) .native-selector-badge.enabled,
3476
+ .native-command-body:has(.native-resource-summary) .native-selector-badge.native-selector-badge-enabled,
3477
+ .native-command-body:has(.native-resource-summary) .native-selector-badge[data-badge-state="enabled"] {
3478
+ border-color: rgba(166, 227, 161, 0.32);
3479
+ color: var(--ctp-green);
3480
+ background: transparent;
3481
+ }
3482
+ .native-selector-badge.disabled,
3483
+ .native-selector-badge.native-selector-badge-disabled,
3484
+ .native-selector-badge[data-badge-state="disabled"],
3485
+ .native-command-body:has(.native-resource-summary) .native-selector-badge.disabled,
3486
+ .native-command-body:has(.native-resource-summary) .native-selector-badge.native-selector-badge-disabled,
3487
+ .native-command-body:has(.native-resource-summary) .native-selector-badge[data-badge-state="disabled"] {
3488
+ border-color: rgba(255, 159, 67, 0.62) !important;
3489
+ color: #ff9f43 !important;
3490
+ background: rgba(255, 159, 67, 0.10);
3491
+ }
3274
3492
  .native-selector-detail,
3275
3493
  .native-selector-meta,
3276
3494
  .native-settings-hint {
package/start-webui.sh CHANGED
@@ -183,51 +183,10 @@ connect_host_for_port() {
183
183
  esac
184
184
  }
185
185
 
186
- open_url() {
186
+ print_manual_url() {
187
187
  local url="$1"
188
- local platform=""
189
- platform="$(uname -s 2>/dev/null || true)"
190
188
 
191
- case "$platform" in
192
- MINGW*|MSYS*|CYGWIN*)
193
- if command -v cmd.exe >/dev/null 2>&1; then
194
- cmd.exe /c start "" "$url" </dev/null >/dev/null 2>&1 &
195
- return 0
196
- fi
197
- if command -v powershell.exe >/dev/null 2>&1; then
198
- powershell.exe -NoProfile -Command 'Start-Process -FilePath $args[0]' "$url" </dev/null >/dev/null 2>&1 &
199
- return 0
200
- fi
201
- ;;
202
- Linux*)
203
- if grep -qi microsoft /proc/version 2>/dev/null; then
204
- if command -v wslview >/dev/null 2>&1; then
205
- wslview "$url" </dev/null >/dev/null 2>&1 &
206
- return 0
207
- fi
208
- if command -v cmd.exe >/dev/null 2>&1; then
209
- cmd.exe /c start "" "$url" </dev/null >/dev/null 2>&1 &
210
- return 0
211
- fi
212
- fi
213
- ;;
214
- esac
215
-
216
- if command -v xdg-open >/dev/null 2>&1; then
217
- xdg-open "$url" </dev/null >/dev/null 2>&1 &
218
- elif command -v open >/dev/null 2>&1; then
219
- open "$url" </dev/null >/dev/null 2>&1 &
220
- elif command -v wslview >/dev/null 2>&1; then
221
- wslview "$url" </dev/null >/dev/null 2>&1 &
222
- elif command -v cmd.exe >/dev/null 2>&1; then
223
- cmd.exe /c start "" "$url" </dev/null >/dev/null 2>&1 &
224
- elif command -v powershell.exe >/dev/null 2>&1; then
225
- powershell.exe -NoProfile -Command 'Start-Process -FilePath $args[0]' "$url" </dev/null >/dev/null 2>&1 &
226
- else
227
- echo "Could not find a browser opener. Open manually:" >&2
228
- echo " $url" >&2
229
- return 1
230
- fi
189
+ echo "Open manually: $url"
231
190
  }
232
191
 
233
192
  http_ok() {
@@ -456,8 +415,7 @@ main() {
456
415
  if [[ "$dev_mode" -eq 1 ]]; then
457
416
  echo "--dev only affects newly started servers; stop the existing server first to run this checkout."
458
417
  fi
459
- echo "Opening: $target_url"
460
- open_url "$target_url" || true
418
+ print_manual_url "$target_url"
461
419
  exit 0
462
420
  fi
463
421
 
@@ -474,10 +432,12 @@ main() {
474
432
  if [[ "$dev_mode" -eq 1 ]]; then
475
433
  local_webui_bin="$(local_pi_webui_bin)"
476
434
  webui_cmd=(node "$local_webui_bin")
435
+ export PI_WEBUI_DEV=1
477
436
  echo "Dev mode: using local Pi Web UI server: $local_webui_bin"
478
437
  else
479
438
  ensure_pi_webui
480
439
  webui_cmd=("$PI_WEBUI_COMMAND")
440
+ unset PI_WEBUI_DEV
481
441
  fi
482
442
 
483
443
  echo "Starting Pi Web UI in: $cwd"
@@ -491,7 +451,8 @@ main() {
491
451
  trap 'cleanup; exit 143' TERM
492
452
 
493
453
  if wait_until_ready "$url" "$SERVER_PID"; then
494
- open_url "$url" || true
454
+ echo "Pi Web UI is ready."
455
+ print_manual_url "$url"
495
456
  else
496
457
  ready_status="$?"
497
458
  if [[ "$ready_status" -eq 2 ]]; then
@@ -500,8 +461,8 @@ main() {
500
461
  exit $?
501
462
  fi
502
463
 
503
- echo "Server did not respond yet; opening the URL anyway." >&2
504
- open_url "$url" || true
464
+ echo "Server did not respond yet; not opening a browser automatically." >&2
465
+ print_manual_url "$url"
505
466
  fi
506
467
 
507
468
  wait "$SERVER_PID"
@@ -27,8 +27,10 @@ const companionDependencies = {
27
27
  "@firstpick/pi-extension-git-footer-status": "^0.2.1",
28
28
  "@firstpick/pi-extension-release-aur": "^0.1.3",
29
29
  "@firstpick/pi-extension-release-npm": "^0.3.3",
30
+ "@firstpick/pi-extension-setup-skills": "^0.1.5",
30
31
  "@firstpick/pi-extension-stats": "^0.2.0",
31
32
  "@firstpick/pi-extension-todo-progress": "^0.1.7",
33
+ "@firstpick/pi-extension-tools": "^0.1.4",
32
34
  "@firstpick/pi-prompts-git-pr": "^0.1.0",
33
35
  "@firstpick/pi-themes-bundle": "^0.1.1",
34
36
  };
@@ -41,11 +43,15 @@ assert.match(html, /<link rel="apple-touch-icon" href="\/apple-touch-icon\.png"
41
43
  assert.match(html, /id="terminalTabsToggleButton"/, "mobile should expose a compact terminal-tabs toggle");
42
44
  assert.match(html, /id="closeAllTabsButton"[\s\S]*?>Close all Tabs<\/button>/, "tab header should expose a top-right close-all tabs action");
43
45
  assert.match(html, /id="sidePanelBackdrop"/, "mobile side panel needs an overlay/backdrop close target");
46
+ assert.match(html, /<strong class="side-panel-title">[\s\S]*Control Deck[\s\S]*id="webuiVersionBadge"[\s\S]*id="webuiDevBadge"/, "Control Deck title should expose Web UI version and dev badges");
47
+ assert.doesNotMatch(html, /id="sessionLine"/, "Control Deck title should not show verbose session status metadata");
44
48
  assert.match(html, /id="themeSelect"/, "side panel should expose a theme selector");
45
49
  assert.match(html, /<label for="themeSelect">Theme<\/label>/, "theme selector should be labeled in side-panel controls");
46
50
  assert.match(html, /id="backgroundInput"[^>]*type="file"[^>]*accept="image\/png,image\/jpeg,image\/webp,image\/gif"/, "side panel should expose an image picker for custom backgrounds");
47
51
  assert.match(html, /id="backgroundClearButton"[\s\S]*?>×<\/button>/, "side-panel background control should expose an X remove button");
48
- assert.match(html, /id="stopServerButton"[^>]*?>Stop Server<\/button>/, "side panel should expose a Stop Server button");
52
+ assert.match(html, /id="serverActionSelect"[\s\S]*<option value="restart">Restart Server<\/option>[\s\S]*<option value="stop">Stop Server<\/option>/, "side panel should expose restart and stop server actions in a dropdown");
53
+ assert.match(html, /id="runServerActionButton"[^>]*disabled[^>]*>Run<\/button>/, "side panel should expose a guarded button for selected server actions");
54
+ assert.match(html, /id="serverActionStatus"[^>]*aria-live="polite"/, "server actions should expose visible restart feedback");
49
55
  assert.match(html, /id="agentDoneNotificationsToggle"/, "side panel should expose an agent-done notifications toggle");
50
56
  assert.match(html, /id="agentDoneNotificationsStatus"/, "agent-done notifications toggle should expose status text");
51
57
  assert.match(html, /id="thinkingVisibilityToggle"/, "side panel should expose a thinking-output visibility toggle");
@@ -56,6 +62,7 @@ assert.match(html, /id="optionalFeaturesBox"/, "side panel should expose optiona
56
62
  assert.match(html, /id="codexUsageBox"/, "side panel should expose Codex subscription usage status");
57
63
  assert.match(html, /data-side-panel-section="codex-usage"/, "Codex usage should live in a collapsible side-panel section");
58
64
  assert.match(html, /id="serverOfflinePanel"/, "PWA/offline shell should expose a backend-offline recovery panel");
65
+ assert.match(html, /id="serverRestartPanel"[\s\S]*id="serverRestartMessage"/, "server restart should expose a loading overlay instead of the generic offline shell");
59
66
  assert.match(html, /id="copyServerCommandButton"/, "backend-offline recovery panel should expose a start-command copy button");
60
67
  assert.match(html, /id="retryServerConnectionButton"/, "backend-offline recovery panel should expose a retry button");
61
68
  assert.match(html, /data-side-panel-section="controls"/, "side panel controls should live in a collapsible section");
@@ -135,6 +142,10 @@ assert.match(css, /\.message-collapse\[open\] \+ \.tool-result-preview \{[\s\S]*
135
142
  assert.match(css, /\.run-indicator-pulse \{[\s\S]*?animation:\s*run-indicator-pulse/, "active agent run indicator should have an animated pulse");
136
143
  assert.match(css, /\.optional-features-box \{[\s\S]*?display:\s*grid/, "optional features should render as a side-panel feature list");
137
144
  assert.match(css, /\.side-panel-section-toggle \{[\s\S]*?justify-content:\s*space-between/, "side panel section toggles should align labels and chevrons");
145
+ assert.match(css, /\.server-restart-panel \{[\s\S]*?z-index:\s*62/, "server restart overlay should render above the offline shell");
146
+ assert.match(css, /@keyframes server-restart-spin/, "server restart overlay should show a loading spinner");
147
+ assert.match(css, /\.webui-version-badge,\n\.webui-dev-badge \{[\s\S]*?border-radius:\s*999px/, "Web UI version and dev indicators should render as compact title badges");
148
+ assert.match(css, /\.webui-dev-badge \{[\s\S]*?color:\s*var\(--ctp-yellow\)/, "Web UI dev indicator should have distinct warning styling");
138
149
  assert.match(css, /\.side-panel-section\.collapsed \.side-panel-section-content,\n\.side-panel-section-content\[hidden\] \{\n\s+display:\s*none;/, "collapsed side panel section content should be hidden");
139
150
  assert.match(css, /\.side-panel-section:not\(\.collapsed\) \.side-panel-section-chevron/, "expanded side panel sections should rotate the chevron");
140
151
  assert.match(css, /\.optional-feature-pill\.enabled/, "optional features should visually distinguish enabled state");
@@ -145,6 +156,10 @@ assert.match(css, /\.todo-widget-item\.partial \.todo-widget-marker/, "todo-prog
145
156
  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");
146
157
  assert.match(css, /\.release-npm-widget \{[\s\S]*?border-left:\s*0\.28rem solid/, "release-npm output should stand apart from the page background");
147
158
  assert.match(css, /\.release-npm-stream-header \{[\s\S]*?text-transform:\s*uppercase/, "release-npm output should label the output stream clearly");
159
+ assert.match(css, /\.release-npm-output-summary \{[\s\S]*?cursor:\s*pointer/, "release-npm output should expose a local expand/collapse summary");
160
+ assert.match(css, /\.release-npm-output-details\[open\] \.release-npm-output-toggle/, "release-npm expanded output should rotate the summary chevron");
161
+ assert.match(css, /\.widget-area:has\(\.release-npm-live-widget \.release-npm-output-details\[open\]\)[\s\S]*?flex:\s*0 0 min\(44rem, 68dvh\)/, "live release output should reserve a stable widget slot instead of resizing the transcript while streaming");
162
+ assert.match(css, /\.release-npm-live-widget \.release-npm-output-details\[open\] \.release-npm-terminal,[\s\S]*?height:\s*clamp\(15rem, 42dvh, 31rem\)/, "live release terminals should keep a fixed viewport height while output streams");
148
163
  assert.match(css, /\.release-npm-terminal \{[\s\S]*?rgba\(3, 4, 10, 0\.98\)/, "release-npm terminal should use a high-contrast stream panel");
149
164
  assert.match(css, /\.release-aur-widget \{[\s\S]*?border-color/, "release-aur output should render as a specialized Web UI widget variant");
150
165
  assert.match(css, /\.widget-area \.widget:not\(\.todo-widget\):not\(\.release-npm-widget\)/, "mobile widget filtering should keep release workflow output visible");
@@ -239,8 +254,22 @@ assert.match(app, /Restart Web UI to load themes/, "frontend should explain when
239
254
  assert.match(app, /themeSelect\.addEventListener\("change"/, "side-panel theme selector should switch themes immediately");
240
255
  assert.match(app, /open \? "Close for network" : "Open to network"/, "network button should toggle from open to close action");
241
256
  assert.match(app, /api\("\/api\/network\/close", \{ method: "POST"/, "network close action should call the close endpoint");
242
- assert.match(app, /stopServerButton\.addEventListener\("click", stopServer\)/, "Stop Server button should be wired to the shutdown handler");
257
+ assert.match(app, /webuiVersionBadge: \$\("#webuiVersionBadge"\)/, "frontend should bind the Control Deck version badge");
258
+ assert.match(app, /webuiDevBadge: \$\("#webuiDevBadge"\)/, "frontend should bind the Control Deck dev badge");
259
+ assert.match(app, /function refreshWebuiVersion\(\)[\s\S]*api\("\/api\/health", \{ scoped: false \}\)[\s\S]*setWebuiVersion\(health\.webuiVersion\)[\s\S]*setWebuiDevServer\(isWebuiDevMetadata\(health\)\)/, "frontend should load Web UI version and dev mode from health metadata");
260
+ assert.match(app, /case "webui_connected":[\s\S]*setWebuiVersion\(event\.version\)[\s\S]*setWebuiDevServer\(isWebuiDevMetadata\(event\)\)/, "frontend should refresh Web UI version and dev mode from reconnect events");
261
+ assert.match(server, /const webuiDevServer = isTruthyEnv\(process\.env\.PI_WEBUI_DEV\) \|\| isSourceCheckout\(packageRoot\)/, "server should derive dev mode from PI_WEBUI_DEV or a source checkout");
262
+ assert.match(server, /webuiDev: webuiDevServer,[\s\S]*webuiMode: webuiDevServer \? "dev" : "production"/, "server status should expose Web UI dev mode");
263
+ assert.match(server, /type: "webui_connected",[\s\S]*webuiDev: webuiDevServer,[\s\S]*webuiMode: webuiDevServer \? "dev" : "production"/, "SSE connect event should expose Web UI dev mode");
264
+ assert.match(app, /serverActionSelect\.addEventListener\("change", updateServerActionButton\)/, "Server action dropdown should control the guarded run button");
265
+ assert.match(app, /runServerActionButton\.addEventListener\("click"[\s\S]*runSelectedServerAction/, "Server action run button should execute the selected action");
266
+ assert.match(app, /api\("\/api\/restart", \{ method: "POST", scoped: false \}\)/, "Restart Server action should call the unscoped restart endpoint");
267
+ assert.match(app, /setServerActionStatus\(message, "warn"\);\n\s+setServerRestartOverlay\(true, message\)/, "Restart Server action should show reconnect progress in the side panel and loading overlay");
268
+ assert.match(app, /const showOfflinePanel = backendOffline && !serverRestartInProgress/, "intentional restart should suppress the generic offline shell while reconnecting");
243
269
  assert.match(app, /api\("\/api\/shutdown", \{ method: "POST", scoped: false \}\)/, "Stop Server action should call the unscoped shutdown endpoint");
270
+ assert.match(server, /url\.pathname === "\/api\/restart" && req\.method === "POST"/, "server should expose restart endpoint");
271
+ assert.match(server, /PI_WEBUI_RESTORE_TABS: JSON\.stringify\(restorableTabs \|\| \[\]\)/, "server restart should preserve restorable tab metadata");
272
+ assert.match(server, /if \(webuiDevServer\) env\.PI_WEBUI_DEV = "1";/, "server restart should explicitly preserve dev mode");
244
273
  assert.match(server, /async function closeNetworkAccess\(\)/, "server should expose a local-only rebind helper for closing network access");
245
274
  assert.match(server, /url\.pathname === "\/api\/network\/close" && req\.method === "POST"/, "server should route network close requests");
246
275
  assert.match(server, /server\.closeAllConnections\?\.\(\)/, "network rebind should force-close long-lived clients so close-to-localhost can complete");
@@ -313,7 +342,9 @@ assert.match(app, /function setOptionalControlState\(button, available, unavaila
313
342
  assert.match(app, /function renderCommands\(\)/, "side-panel commands should be re-renderable from current optional feature state");
314
343
  assert.match(app, /function installOptionalFeature\(featureId\)/, "optional features should expose an install action");
315
344
  assert.match(app, /api\("\/api\/optional-feature-install"/, "optional feature install action should call the backend installer endpoint");
316
- 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");
345
+ assert.match(app, /id: "tuiSkillsCommand"[\s\S]*?@firstpick\/pi-extension-setup-skills/, "optional features should include the TUI skills command companion");
346
+ assert.match(app, /id: "tuiToolsCommand"[\s\S]*?@firstpick\/pi-extension-tools/, "optional features should include the TUI tools command companion");
347
+ assert.match(app, /function updateOptionalFeatureAvailability\(\)[\s\S]*hasAvailableCommand\("git-staged-msg"\)[\s\S]*hasAvailableCommand\("release-npm"\)[\s\S]*hasAvailableCommand\("release-aur"\)[\s\S]*hasLoadedRpcCommand\("skills"\)[\s\S]*hasAvailableCommand\("todo-progress-status"\)[\s\S]*hasLoadedRpcCommand\("tools"\)/, "optional feature detection should call RPC-visible commands directly and distinguish native resource selectors from TUI companions");
317
348
  assert.match(app, /if \(!isOptionalFeatureEnabled\("todoProgressWidget"\)\) return String\(text \|\| ""\)/, "todo progress line stripping should only run when the todo feature is detected and enabled");
318
349
  assert.match(app, /const releasePrompt = detectedReleasePrompt && isOptionalFeatureEnabled\(detectedReleasePrompt\.featureId\) \? detectedReleasePrompt : null/, "release confirmation dialogs should use specialized rendering only when their release optional feature is enabled");
319
350
  assert.match(app, /case "webui_tab_reloaded":[\s\S]*resetOptionalFeatureAvailability\(\)/, "optional feature state should reset when the RPC tab reloads resources");
@@ -324,7 +355,10 @@ assert.match(app, /function bindGitWorkflowToActiveTab\(\) \{\n\s+gitWorkflow =
324
355
  assert.match(app, /function setGitWorkflow\(patch, \{ tabId = activeTabId \} = \{\}\)[\s\S]*if \(tabId === activeTabId\) \{[\s\S]*renderGitWorkflow\(\);/, "guided git workflow should not render inactive terminal workflows globally");
325
356
  assert.doesNotMatch(app, /gitWorkflowVisibleTabId|Workflow belongs to/, "guided git workflow should not pin or show workflows outside their owning terminal tab");
326
357
  assert.match(app, /function renderReleaseNpmOutputWidget\(\)/, "release-npm live output should use a specialized Web UI renderer");
358
+ assert.match(app, /const releaseNpmOutputExpandedByTab = new Map\(\)/, "release-npm output collapse state should be tracked per browser tab");
359
+ assert.match(app, /function renderReleaseNpmOutputDetails\(key, streamHeader, terminal, controls = null\)[\s\S]*node\.open = releaseNpmOutputExpandedByTab\.get\(stateKey\) !== false[\s\S]*release-npm-output-toggle/, "release-npm output should render as a browser-side details expander");
327
360
  assert.match(app, /releaseNpmStreamHeader\("Live output stream", outputLines\.length, \{ live: true \}\)/, "release-npm live output should expose a clear stream heading");
361
+ assert.match(app, /renderReleaseNpmOutputDetails\("release-npm:output", streamHeader, terminal, controls\)/, "release-npm live stream should be wrapped in the local expander");
328
362
  assert.match(app, /function renderReleaseAurOutputWidget\(\)/, "release-aur live output should use a specialized Web UI renderer");
329
363
  assert.match(app, /releaseNpmActionButton\("Abort", "\/release-abort", "danger"\)/, "release-npm Web UI output should expose an abort action");
330
364
  assert.match(app, /releaseNpmActionButton\("Abort", "\/release-aur abort", "danger"\)/, "release-aur Web UI output should expose an abort action");
@@ -460,7 +494,7 @@ assert.match(app, /async function sendPrompt\(kind = "prompt", explicitMessage\)
460
494
  assert.match(app, /const rawMessage = usesPromptInput \? elements\.promptInput\.value : explicitMessage/, "direct prompt sends should not read the input textarea");
461
495
  assert.match(app, /if \(usesPromptInput\) \{[\s\S]*?if \(targetStillActive\) \{[\s\S]*?elements\.promptInput\.value = "";/, "direct prompt sends should preserve the input textarea draft");
462
496
  assert.match(app, /make\("button", "command-item"\)[\s\S]*?sendPrompt\("prompt", `\/\$\{command\.name\}`\)/, "side-panel command clicks should send the slash command directly");
463
- assert.match(app, /const NATIVE_SELECTOR_COMMANDS = new Set\(\["model", "settings", "theme", "fork", "clone", "resume", "tree", "login", "logout", "scoped-models"\]\)/, "frontend should route native slash commands into selector UIs");
497
+ assert.match(app, /const NATIVE_SELECTOR_COMMANDS = new Set\(\["model", "settings", "theme", "fork", "clone", "resume", "tree", "login", "logout", "scoped-models", "tools", "skills"\]\)/, "frontend should route native slash commands into selector UIs");
464
498
  assert.match(app, /async function handleNativeSlashSelectorCommand\(message/, "frontend should intercept exact native slash commands before prompt forwarding");
465
499
  assert.match(app, /kind === "prompt" && attachments\.length === 0 && await handleNativeSlashSelectorCommand/, "prompt sending should open native selector dialogs before marking a run active");
466
500
  assert.match(app, /function openNativeModelSelector\(\)[\s\S]*?nativeCommandApi\("\/api\/models"\)/, "native /model selector should load models through the active tab API");
@@ -469,7 +503,7 @@ assert.match(app, /function openNativeForkSelector\(\)[\s\S]*?\/api\/fork-messag
469
503
  assert.match(app, /function openNativeResumeSelector\(scope = "current"\)[\s\S]*?\/api\/sessions\?scope=\$\{encodeURIComponent\(selectedScope\)\}/, "native /resume selector should list current-cwd or all sessions");
470
504
  assert.match(app, /function openNativeTreeSelector\(\)[\s\S]*?\/api\/session-tree[\s\S]*?\/api\/tree-navigate/, "native /tree selector should list tree entries and navigate through the backend helper");
471
505
  assert.match(app, /Provider credential entry is intentionally not implemented in the browser yet/, "native /login should remain a safe non-secret guidance dialog");
472
- assert.match(app, /const HIDDEN_COMMAND_NAMES = new Set\(\["webui-tree-navigate"\]\)/, "internal Web UI helper commands should stay out of command pickers");
506
+ assert.match(app, /const HIDDEN_COMMAND_NAMES = new Set\(\["webui-tree-navigate", "webui-helper"\]\)/, "internal Web UI helper commands should stay out of command pickers");
473
507
  assert.match(app, /function shouldSendPromptFromEnter\(event\)/, "prompt keyboard handling should be centralized");
474
508
  assert.match(app, /const PROMPT_HISTORY_STORAGE_KEY = "pi-webui-prompt-history"/, "prompt history should be persisted per browser for keyboard recall");
475
509
  assert.match(app, /function recallPreviousPromptFromHistory\(\)/, "prompt history should support recalling older prompts from the textarea");
@@ -566,7 +600,7 @@ assert.equal(manifest.start_url, "/", "PWA manifest should start at the web UI r
566
600
  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");
567
601
  assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-192.png" && icon.sizes === "192x192"), "PWA manifest should include a 192px icon");
568
602
  assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-512.png" && icon.sizes === "512x512"), "PWA manifest should include a 512px icon");
569
- assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v16"/, "PWA service worker should define an app-shell cache");
603
+ assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v20"/, "PWA service worker should define an app-shell cache");
570
604
  assert.match(serviceWorker, /self\.addEventListener\("notificationclick"/, "PWA service worker should focus Web UI when blocked-tab notifications are clicked");
571
605
  assert.match(serviceWorker, /event\.notification\.data\?\.url/, "blocked-tab notifications should carry a URL for service-worker click handling");
572
606
  assert.match(serviceWorker, /"\/apple-touch-icon\.png"/, "PWA service worker should cache the apple touch icon");
@@ -683,6 +717,8 @@ assert.match(server, /type: "set_follow_up_mode"/, "server should expose follow-
683
717
  assert.match(server, /type: "set_auto_compaction"/, "server should expose auto-compaction changes for native /settings");
684
718
  assert.match(server, /@firstpick\/pi-themes-bundle/, "server should discover themes from the optional theme package");
685
719
  assert.match(server, /const OPTIONAL_FEATURE_PACKAGES = new Map/, "server should whitelist optional feature packages for install actions");
720
+ assert.match(server, /\["tuiSkillsCommand", "@firstpick\/pi-extension-setup-skills"\]/, "server should allow installing the TUI skills optional feature");
721
+ assert.match(server, /\["tuiToolsCommand", "@firstpick\/pi-extension-tools"\]/, "server should allow installing the TUI tools optional feature");
686
722
  assert.match(server, /function installOptionalFeaturePackage\(featureId\)/, "server should provide optional feature package installation helper");
687
723
  assert.match(server, /url\.pathname === "\/api\/optional-feature-install" && req\.method === "POST"/, "server should expose optional feature install endpoint");
688
724
  assert.match(server, /Installing optional Web UI features is only allowed from localhost/, "optional feature install endpoint should be localhost-only");
@@ -715,6 +751,7 @@ assert.match(readme, /\.\/start-webui\.sh --dev --cwd \/path\/to\/project/, "REA
715
751
  assert.match(startScript, /--dev\)/, "start-webui.sh should accept a --dev flag");
716
752
  assert.match(startScript, /local_pi_webui_bin\(\)/, "start-webui.sh should resolve this checkout's local server entrypoint");
717
753
  assert.match(startScript, /webui_cmd=\(node "\$local_webui_bin"\)/, "start-webui.sh --dev should run the local bin with node");
754
+ assert.match(startScript, /export PI_WEBUI_DEV=1/, "start-webui.sh --dev should mark the Web UI server as dev mode");
718
755
  assert.match(startScript, /"\$\{webui_cmd\[@\]\}" --cwd "\$cwd" --host "\$host" --port "\$port" "\$\{pass_args\[@\]\}"/, "start-webui.sh should launch through the selected server command without forwarding --dev");
719
756
 
720
757
  assert.match(pkg.scripts?.test || "", /node tests\/mobile-static\.test\.mjs/, "package test script should run the mobile static harness");
@@ -730,13 +767,17 @@ for (const extensionPath of [
730
767
  "../pi-extension-git-footer-status/index.ts",
731
768
  "../pi-extension-release-aur/index.ts",
732
769
  "../pi-extension-release-npm/index.ts",
770
+ "../pi-extension-setup-skills/index.ts",
733
771
  "../pi-extension-stats/index.ts",
734
772
  "../pi-extension-todo-progress/index.ts",
773
+ "../pi-extension-tools/index.ts",
735
774
  "node_modules/@firstpick/pi-extension-git-footer-status/index.ts",
736
775
  "node_modules/@firstpick/pi-extension-release-aur/index.ts",
737
776
  "node_modules/@firstpick/pi-extension-release-npm/index.ts",
777
+ "node_modules/@firstpick/pi-extension-setup-skills/index.ts",
738
778
  "node_modules/@firstpick/pi-extension-stats/index.ts",
739
779
  "node_modules/@firstpick/pi-extension-todo-progress/index.ts",
780
+ "node_modules/@firstpick/pi-extension-tools/index.ts",
740
781
  ]) {
741
782
  assert.ok(pkg.pi?.extensions?.includes(extensionPath), `webui Pi manifest should load ${extensionPath} when present`);
742
783
  }