@firstpick/pi-package-webui 0.2.0 → 0.2.2
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 +80 -136
- package/bin/pi-webui.mjs +67 -1
- package/package.json +1 -1
- package/public/app.js +219 -29
- package/public/index.html +24 -4
- package/public/service-worker.js +1 -1
- package/public/styles.css +200 -7
- package/start-webui.ps1 +45 -0
- package/start-webui.sh +59 -49
- package/tests/mobile-static.test.mjs +34 -3
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-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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);
|
package/start-webui.ps1
CHANGED
|
@@ -32,7 +32,52 @@ function Get-LaunchCwd {
|
|
|
32
32
|
return $cwd
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function Get-PiManagedPiWebui {
|
|
36
|
+
$node = Get-Command "node" -ErrorAction SilentlyContinue
|
|
37
|
+
if (-not $node) {
|
|
38
|
+
return $null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
$script = @'
|
|
42
|
+
const { homedir } = require("node:os");
|
|
43
|
+
const { join } = require("node:path");
|
|
44
|
+
|
|
45
|
+
let agentDir = process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
|
|
46
|
+
if (agentDir === "~") {
|
|
47
|
+
agentDir = homedir();
|
|
48
|
+
} else if (agentDir.startsWith("~/") || (process.platform === "win32" && agentDir.startsWith("~\\"))) {
|
|
49
|
+
agentDir = join(homedir(), agentDir.slice(2));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const binName = process.platform === "win32" ? "pi-webui.cmd" : "pi-webui";
|
|
53
|
+
for (const candidate of [
|
|
54
|
+
join(agentDir, "npm", "node_modules", ".bin", binName),
|
|
55
|
+
join(agentDir, "npm", "node_modules", ".bin", "pi-webui"),
|
|
56
|
+
]) {
|
|
57
|
+
process.stdout.write(`${candidate}\n`);
|
|
58
|
+
}
|
|
59
|
+
'@
|
|
60
|
+
|
|
61
|
+
$candidates = @(& $node.Source -e $script 2>$null)
|
|
62
|
+
if ($LASTEXITCODE -ne 0) {
|
|
63
|
+
return $null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
foreach ($candidate in $candidates) {
|
|
67
|
+
if (-not [string]::IsNullOrWhiteSpace($candidate) -and (Test-Path -LiteralPath $candidate -PathType Leaf)) {
|
|
68
|
+
return $candidate
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return $null
|
|
73
|
+
}
|
|
74
|
+
|
|
35
75
|
function Ensure-PiWebui {
|
|
76
|
+
$managed = Get-PiManagedPiWebui
|
|
77
|
+
if ($managed) {
|
|
78
|
+
return $managed
|
|
79
|
+
}
|
|
80
|
+
|
|
36
81
|
$command = Get-Command "pi-webui" -ErrorAction SilentlyContinue
|
|
37
82
|
if ($command) {
|
|
38
83
|
return $command.Source
|
package/start-webui.sh
CHANGED
|
@@ -6,6 +6,7 @@ PACKAGE_NAME="@firstpick/pi-package-webui"
|
|
|
6
6
|
DEFAULT_HOST="127.0.0.1"
|
|
7
7
|
DEFAULT_PORT="31415"
|
|
8
8
|
SERVER_PID=""
|
|
9
|
+
PI_WEBUI_COMMAND=""
|
|
9
10
|
|
|
10
11
|
script_dir() {
|
|
11
12
|
local source dir
|
|
@@ -37,6 +38,44 @@ local_pi_webui_bin() {
|
|
|
37
38
|
printf '%s\n' "$candidate"
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
pi_managed_pi_webui_bin() {
|
|
42
|
+
local candidates candidate
|
|
43
|
+
|
|
44
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
45
|
+
return 1
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
candidates="$(node <<'NODE'
|
|
49
|
+
const { homedir } = require("node:os");
|
|
50
|
+
const { join } = require("node:path");
|
|
51
|
+
|
|
52
|
+
let agentDir = process.env.PI_CODING_AGENT_DIR || join(homedir(), ".pi", "agent");
|
|
53
|
+
if (agentDir === "~") {
|
|
54
|
+
agentDir = homedir();
|
|
55
|
+
} else if (agentDir.startsWith("~/") || (process.platform === "win32" && agentDir.startsWith("~\\"))) {
|
|
56
|
+
agentDir = join(homedir(), agentDir.slice(2));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const binName = process.platform === "win32" ? "pi-webui.cmd" : "pi-webui";
|
|
60
|
+
for (const candidate of [
|
|
61
|
+
join(agentDir, "npm", "node_modules", ".bin", "pi-webui"),
|
|
62
|
+
join(agentDir, "npm", "node_modules", ".bin", binName),
|
|
63
|
+
]) {
|
|
64
|
+
process.stdout.write(`${candidate.replace(/\\/g, "/")}\n`);
|
|
65
|
+
}
|
|
66
|
+
NODE
|
|
67
|
+
)"
|
|
68
|
+
|
|
69
|
+
while IFS= read -r candidate; do
|
|
70
|
+
if [[ -n "$candidate" && -f "$candidate" ]]; then
|
|
71
|
+
printf '%s\n' "$candidate"
|
|
72
|
+
return 0
|
|
73
|
+
fi
|
|
74
|
+
done <<< "$candidates"
|
|
75
|
+
|
|
76
|
+
return 1
|
|
77
|
+
}
|
|
78
|
+
|
|
40
79
|
cleanup() {
|
|
41
80
|
if [[ -n "${SERVER_PID:-}" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then
|
|
42
81
|
kill "$SERVER_PID" 2>/dev/null || true
|
|
@@ -66,7 +105,15 @@ choose_cwd() {
|
|
|
66
105
|
}
|
|
67
106
|
|
|
68
107
|
ensure_pi_webui() {
|
|
108
|
+
local managed_bin
|
|
109
|
+
|
|
110
|
+
if managed_bin="$(pi_managed_pi_webui_bin 2>/dev/null)" && [[ -n "$managed_bin" ]]; then
|
|
111
|
+
PI_WEBUI_COMMAND="$managed_bin"
|
|
112
|
+
return 0
|
|
113
|
+
fi
|
|
114
|
+
|
|
69
115
|
if command -v pi-webui >/dev/null 2>&1; then
|
|
116
|
+
PI_WEBUI_COMMAND="$(command -v pi-webui)"
|
|
70
117
|
return 0
|
|
71
118
|
fi
|
|
72
119
|
|
|
@@ -105,6 +152,8 @@ ensure_pi_webui() {
|
|
|
105
152
|
echo "Installed, but pi-webui is still not on PATH. Check your npm global bin directory." >&2
|
|
106
153
|
return 1
|
|
107
154
|
fi
|
|
155
|
+
|
|
156
|
+
PI_WEBUI_COMMAND="$(command -v pi-webui)"
|
|
108
157
|
}
|
|
109
158
|
|
|
110
159
|
browser_host_for_url() {
|
|
@@ -134,51 +183,10 @@ connect_host_for_port() {
|
|
|
134
183
|
esac
|
|
135
184
|
}
|
|
136
185
|
|
|
137
|
-
|
|
186
|
+
print_manual_url() {
|
|
138
187
|
local url="$1"
|
|
139
|
-
local platform=""
|
|
140
|
-
platform="$(uname -s 2>/dev/null || true)"
|
|
141
|
-
|
|
142
|
-
case "$platform" in
|
|
143
|
-
MINGW*|MSYS*|CYGWIN*)
|
|
144
|
-
if command -v cmd.exe >/dev/null 2>&1; then
|
|
145
|
-
cmd.exe /c start "" "$url" </dev/null >/dev/null 2>&1 &
|
|
146
|
-
return 0
|
|
147
|
-
fi
|
|
148
|
-
if command -v powershell.exe >/dev/null 2>&1; then
|
|
149
|
-
powershell.exe -NoProfile -Command 'Start-Process -FilePath $args[0]' "$url" </dev/null >/dev/null 2>&1 &
|
|
150
|
-
return 0
|
|
151
|
-
fi
|
|
152
|
-
;;
|
|
153
|
-
Linux*)
|
|
154
|
-
if grep -qi microsoft /proc/version 2>/dev/null; then
|
|
155
|
-
if command -v wslview >/dev/null 2>&1; then
|
|
156
|
-
wslview "$url" </dev/null >/dev/null 2>&1 &
|
|
157
|
-
return 0
|
|
158
|
-
fi
|
|
159
|
-
if command -v cmd.exe >/dev/null 2>&1; then
|
|
160
|
-
cmd.exe /c start "" "$url" </dev/null >/dev/null 2>&1 &
|
|
161
|
-
return 0
|
|
162
|
-
fi
|
|
163
|
-
fi
|
|
164
|
-
;;
|
|
165
|
-
esac
|
|
166
188
|
|
|
167
|
-
|
|
168
|
-
xdg-open "$url" </dev/null >/dev/null 2>&1 &
|
|
169
|
-
elif command -v open >/dev/null 2>&1; then
|
|
170
|
-
open "$url" </dev/null >/dev/null 2>&1 &
|
|
171
|
-
elif command -v wslview >/dev/null 2>&1; then
|
|
172
|
-
wslview "$url" </dev/null >/dev/null 2>&1 &
|
|
173
|
-
elif command -v cmd.exe >/dev/null 2>&1; then
|
|
174
|
-
cmd.exe /c start "" "$url" </dev/null >/dev/null 2>&1 &
|
|
175
|
-
elif command -v powershell.exe >/dev/null 2>&1; then
|
|
176
|
-
powershell.exe -NoProfile -Command 'Start-Process -FilePath $args[0]' "$url" </dev/null >/dev/null 2>&1 &
|
|
177
|
-
else
|
|
178
|
-
echo "Could not find a browser opener. Open manually:" >&2
|
|
179
|
-
echo " $url" >&2
|
|
180
|
-
return 1
|
|
181
|
-
fi
|
|
189
|
+
echo "Open manually: $url"
|
|
182
190
|
}
|
|
183
191
|
|
|
184
192
|
http_ok() {
|
|
@@ -407,8 +415,7 @@ main() {
|
|
|
407
415
|
if [[ "$dev_mode" -eq 1 ]]; then
|
|
408
416
|
echo "--dev only affects newly started servers; stop the existing server first to run this checkout."
|
|
409
417
|
fi
|
|
410
|
-
|
|
411
|
-
open_url "$target_url" || true
|
|
418
|
+
print_manual_url "$target_url"
|
|
412
419
|
exit 0
|
|
413
420
|
fi
|
|
414
421
|
|
|
@@ -425,10 +432,12 @@ main() {
|
|
|
425
432
|
if [[ "$dev_mode" -eq 1 ]]; then
|
|
426
433
|
local_webui_bin="$(local_pi_webui_bin)"
|
|
427
434
|
webui_cmd=(node "$local_webui_bin")
|
|
435
|
+
export PI_WEBUI_DEV=1
|
|
428
436
|
echo "Dev mode: using local Pi Web UI server: $local_webui_bin"
|
|
429
437
|
else
|
|
430
438
|
ensure_pi_webui
|
|
431
|
-
webui_cmd=(
|
|
439
|
+
webui_cmd=("$PI_WEBUI_COMMAND")
|
|
440
|
+
unset PI_WEBUI_DEV
|
|
432
441
|
fi
|
|
433
442
|
|
|
434
443
|
echo "Starting Pi Web UI in: $cwd"
|
|
@@ -442,7 +451,8 @@ main() {
|
|
|
442
451
|
trap 'cleanup; exit 143' TERM
|
|
443
452
|
|
|
444
453
|
if wait_until_ready "$url" "$SERVER_PID"; then
|
|
445
|
-
|
|
454
|
+
echo "Pi Web UI is ready."
|
|
455
|
+
print_manual_url "$url"
|
|
446
456
|
else
|
|
447
457
|
ready_status="$?"
|
|
448
458
|
if [[ "$ready_status" -eq 2 ]]; then
|
|
@@ -451,8 +461,8 @@ main() {
|
|
|
451
461
|
exit $?
|
|
452
462
|
fi
|
|
453
463
|
|
|
454
|
-
echo "Server did not respond yet; opening
|
|
455
|
-
|
|
464
|
+
echo "Server did not respond yet; not opening a browser automatically." >&2
|
|
465
|
+
print_manual_url "$url"
|
|
456
466
|
fi
|
|
457
467
|
|
|
458
468
|
wait "$SERVER_PID"
|
|
@@ -41,11 +41,15 @@ assert.match(html, /<link rel="apple-touch-icon" href="\/apple-touch-icon\.png"
|
|
|
41
41
|
assert.match(html, /id="terminalTabsToggleButton"/, "mobile should expose a compact terminal-tabs toggle");
|
|
42
42
|
assert.match(html, /id="closeAllTabsButton"[\s\S]*?>Close all Tabs<\/button>/, "tab header should expose a top-right close-all tabs action");
|
|
43
43
|
assert.match(html, /id="sidePanelBackdrop"/, "mobile side panel needs an overlay/backdrop close target");
|
|
44
|
+
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");
|
|
45
|
+
assert.doesNotMatch(html, /id="sessionLine"/, "Control Deck title should not show verbose session status metadata");
|
|
44
46
|
assert.match(html, /id="themeSelect"/, "side panel should expose a theme selector");
|
|
45
47
|
assert.match(html, /<label for="themeSelect">Theme<\/label>/, "theme selector should be labeled in side-panel controls");
|
|
46
48
|
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
49
|
assert.match(html, /id="backgroundClearButton"[\s\S]*?>×<\/button>/, "side-panel background control should expose an X remove button");
|
|
48
|
-
assert.match(html, /id="
|
|
50
|
+
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");
|
|
51
|
+
assert.match(html, /id="runServerActionButton"[^>]*disabled[^>]*>Run<\/button>/, "side panel should expose a guarded button for selected server actions");
|
|
52
|
+
assert.match(html, /id="serverActionStatus"[^>]*aria-live="polite"/, "server actions should expose visible restart feedback");
|
|
49
53
|
assert.match(html, /id="agentDoneNotificationsToggle"/, "side panel should expose an agent-done notifications toggle");
|
|
50
54
|
assert.match(html, /id="agentDoneNotificationsStatus"/, "agent-done notifications toggle should expose status text");
|
|
51
55
|
assert.match(html, /id="thinkingVisibilityToggle"/, "side panel should expose a thinking-output visibility toggle");
|
|
@@ -56,6 +60,7 @@ assert.match(html, /id="optionalFeaturesBox"/, "side panel should expose optiona
|
|
|
56
60
|
assert.match(html, /id="codexUsageBox"/, "side panel should expose Codex subscription usage status");
|
|
57
61
|
assert.match(html, /data-side-panel-section="codex-usage"/, "Codex usage should live in a collapsible side-panel section");
|
|
58
62
|
assert.match(html, /id="serverOfflinePanel"/, "PWA/offline shell should expose a backend-offline recovery panel");
|
|
63
|
+
assert.match(html, /id="serverRestartPanel"[\s\S]*id="serverRestartMessage"/, "server restart should expose a loading overlay instead of the generic offline shell");
|
|
59
64
|
assert.match(html, /id="copyServerCommandButton"/, "backend-offline recovery panel should expose a start-command copy button");
|
|
60
65
|
assert.match(html, /id="retryServerConnectionButton"/, "backend-offline recovery panel should expose a retry button");
|
|
61
66
|
assert.match(html, /data-side-panel-section="controls"/, "side panel controls should live in a collapsible section");
|
|
@@ -135,6 +140,10 @@ assert.match(css, /\.message-collapse\[open\] \+ \.tool-result-preview \{[\s\S]*
|
|
|
135
140
|
assert.match(css, /\.run-indicator-pulse \{[\s\S]*?animation:\s*run-indicator-pulse/, "active agent run indicator should have an animated pulse");
|
|
136
141
|
assert.match(css, /\.optional-features-box \{[\s\S]*?display:\s*grid/, "optional features should render as a side-panel feature list");
|
|
137
142
|
assert.match(css, /\.side-panel-section-toggle \{[\s\S]*?justify-content:\s*space-between/, "side panel section toggles should align labels and chevrons");
|
|
143
|
+
assert.match(css, /\.server-restart-panel \{[\s\S]*?z-index:\s*62/, "server restart overlay should render above the offline shell");
|
|
144
|
+
assert.match(css, /@keyframes server-restart-spin/, "server restart overlay should show a loading spinner");
|
|
145
|
+
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");
|
|
146
|
+
assert.match(css, /\.webui-dev-badge \{[\s\S]*?color:\s*var\(--ctp-yellow\)/, "Web UI dev indicator should have distinct warning styling");
|
|
138
147
|
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
148
|
assert.match(css, /\.side-panel-section:not\(\.collapsed\) \.side-panel-section-chevron/, "expanded side panel sections should rotate the chevron");
|
|
140
149
|
assert.match(css, /\.optional-feature-pill\.enabled/, "optional features should visually distinguish enabled state");
|
|
@@ -145,6 +154,10 @@ assert.match(css, /\.todo-widget-item\.partial \.todo-widget-marker/, "todo-prog
|
|
|
145
154
|
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
155
|
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
156
|
assert.match(css, /\.release-npm-stream-header \{[\s\S]*?text-transform:\s*uppercase/, "release-npm output should label the output stream clearly");
|
|
157
|
+
assert.match(css, /\.release-npm-output-summary \{[\s\S]*?cursor:\s*pointer/, "release-npm output should expose a local expand/collapse summary");
|
|
158
|
+
assert.match(css, /\.release-npm-output-details\[open\] \.release-npm-output-toggle/, "release-npm expanded output should rotate the summary chevron");
|
|
159
|
+
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");
|
|
160
|
+
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
161
|
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
162
|
assert.match(css, /\.release-aur-widget \{[\s\S]*?border-color/, "release-aur output should render as a specialized Web UI widget variant");
|
|
150
163
|
assert.match(css, /\.widget-area \.widget:not\(\.todo-widget\):not\(\.release-npm-widget\)/, "mobile widget filtering should keep release workflow output visible");
|
|
@@ -239,8 +252,22 @@ assert.match(app, /Restart Web UI to load themes/, "frontend should explain when
|
|
|
239
252
|
assert.match(app, /themeSelect\.addEventListener\("change"/, "side-panel theme selector should switch themes immediately");
|
|
240
253
|
assert.match(app, /open \? "Close for network" : "Open to network"/, "network button should toggle from open to close action");
|
|
241
254
|
assert.match(app, /api\("\/api\/network\/close", \{ method: "POST"/, "network close action should call the close endpoint");
|
|
242
|
-
assert.match(app, /
|
|
255
|
+
assert.match(app, /webuiVersionBadge: \$\("#webuiVersionBadge"\)/, "frontend should bind the Control Deck version badge");
|
|
256
|
+
assert.match(app, /webuiDevBadge: \$\("#webuiDevBadge"\)/, "frontend should bind the Control Deck dev badge");
|
|
257
|
+
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");
|
|
258
|
+
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");
|
|
259
|
+
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");
|
|
260
|
+
assert.match(server, /webuiDev: webuiDevServer,[\s\S]*webuiMode: webuiDevServer \? "dev" : "production"/, "server status should expose Web UI dev mode");
|
|
261
|
+
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");
|
|
262
|
+
assert.match(app, /serverActionSelect\.addEventListener\("change", updateServerActionButton\)/, "Server action dropdown should control the guarded run button");
|
|
263
|
+
assert.match(app, /runServerActionButton\.addEventListener\("click"[\s\S]*runSelectedServerAction/, "Server action run button should execute the selected action");
|
|
264
|
+
assert.match(app, /api\("\/api\/restart", \{ method: "POST", scoped: false \}\)/, "Restart Server action should call the unscoped restart endpoint");
|
|
265
|
+
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");
|
|
266
|
+
assert.match(app, /const showOfflinePanel = backendOffline && !serverRestartInProgress/, "intentional restart should suppress the generic offline shell while reconnecting");
|
|
243
267
|
assert.match(app, /api\("\/api\/shutdown", \{ method: "POST", scoped: false \}\)/, "Stop Server action should call the unscoped shutdown endpoint");
|
|
268
|
+
assert.match(server, /url\.pathname === "\/api\/restart" && req\.method === "POST"/, "server should expose restart endpoint");
|
|
269
|
+
assert.match(server, /PI_WEBUI_RESTORE_TABS: JSON\.stringify\(restorableTabs \|\| \[\]\)/, "server restart should preserve restorable tab metadata");
|
|
270
|
+
assert.match(server, /if \(webuiDevServer\) env\.PI_WEBUI_DEV = "1";/, "server restart should explicitly preserve dev mode");
|
|
244
271
|
assert.match(server, /async function closeNetworkAccess\(\)/, "server should expose a local-only rebind helper for closing network access");
|
|
245
272
|
assert.match(server, /url\.pathname === "\/api\/network\/close" && req\.method === "POST"/, "server should route network close requests");
|
|
246
273
|
assert.match(server, /server\.closeAllConnections\?\.\(\)/, "network rebind should force-close long-lived clients so close-to-localhost can complete");
|
|
@@ -324,7 +351,10 @@ assert.match(app, /function bindGitWorkflowToActiveTab\(\) \{\n\s+gitWorkflow =
|
|
|
324
351
|
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
352
|
assert.doesNotMatch(app, /gitWorkflowVisibleTabId|Workflow belongs to/, "guided git workflow should not pin or show workflows outside their owning terminal tab");
|
|
326
353
|
assert.match(app, /function renderReleaseNpmOutputWidget\(\)/, "release-npm live output should use a specialized Web UI renderer");
|
|
354
|
+
assert.match(app, /const releaseNpmOutputExpandedByTab = new Map\(\)/, "release-npm output collapse state should be tracked per browser tab");
|
|
355
|
+
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
356
|
assert.match(app, /releaseNpmStreamHeader\("Live output stream", outputLines\.length, \{ live: true \}\)/, "release-npm live output should expose a clear stream heading");
|
|
357
|
+
assert.match(app, /renderReleaseNpmOutputDetails\("release-npm:output", streamHeader, terminal, controls\)/, "release-npm live stream should be wrapped in the local expander");
|
|
328
358
|
assert.match(app, /function renderReleaseAurOutputWidget\(\)/, "release-aur live output should use a specialized Web UI renderer");
|
|
329
359
|
assert.match(app, /releaseNpmActionButton\("Abort", "\/release-abort", "danger"\)/, "release-npm Web UI output should expose an abort action");
|
|
330
360
|
assert.match(app, /releaseNpmActionButton\("Abort", "\/release-aur abort", "danger"\)/, "release-aur Web UI output should expose an abort action");
|
|
@@ -566,7 +596,7 @@ assert.equal(manifest.start_url, "/", "PWA manifest should start at the web UI r
|
|
|
566
596
|
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
597
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-192.png" && icon.sizes === "192x192"), "PWA manifest should include a 192px icon");
|
|
568
598
|
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-
|
|
599
|
+
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v18"/, "PWA service worker should define an app-shell cache");
|
|
570
600
|
assert.match(serviceWorker, /self\.addEventListener\("notificationclick"/, "PWA service worker should focus Web UI when blocked-tab notifications are clicked");
|
|
571
601
|
assert.match(serviceWorker, /event\.notification\.data\?\.url/, "blocked-tab notifications should carry a URL for service-worker click handling");
|
|
572
602
|
assert.match(serviceWorker, /"\/apple-touch-icon\.png"/, "PWA service worker should cache the apple touch icon");
|
|
@@ -715,6 +745,7 @@ assert.match(readme, /\.\/start-webui\.sh --dev --cwd \/path\/to\/project/, "REA
|
|
|
715
745
|
assert.match(startScript, /--dev\)/, "start-webui.sh should accept a --dev flag");
|
|
716
746
|
assert.match(startScript, /local_pi_webui_bin\(\)/, "start-webui.sh should resolve this checkout's local server entrypoint");
|
|
717
747
|
assert.match(startScript, /webui_cmd=\(node "\$local_webui_bin"\)/, "start-webui.sh --dev should run the local bin with node");
|
|
748
|
+
assert.match(startScript, /export PI_WEBUI_DEV=1/, "start-webui.sh --dev should mark the Web UI server as dev mode");
|
|
718
749
|
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
750
|
|
|
720
751
|
assert.match(pkg.scripts?.test || "", /node tests\/mobile-static\.test\.mjs/, "package test script should run the mobile static harness");
|