@firstpick/pi-package-webui 0.5.3 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/pi-webui.mjs +12 -3
- package/package.json +3 -2
- package/public/app.js +849 -45
- package/public/index.html +2 -2
- package/public/styles.css +307 -1
- package/tests/http-endpoints-harness.test.mjs +15 -1
- package/tests/mobile-static.test.mjs +38 -5
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=59" />
|
|
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">
|
|
@@ -740,6 +740,6 @@
|
|
|
740
740
|
</form>
|
|
741
741
|
</dialog>
|
|
742
742
|
|
|
743
|
-
<script type="module" src="/app.js?v=
|
|
743
|
+
<script type="module" src="/app.js?v=57"></script>
|
|
744
744
|
</body>
|
|
745
745
|
</html>
|
package/public/styles.css
CHANGED
|
@@ -74,7 +74,10 @@
|
|
|
74
74
|
|
|
75
75
|
* { box-sizing: border-box; }
|
|
76
76
|
html, body { height: 100%; min-height: 100%; }
|
|
77
|
-
html {
|
|
77
|
+
html {
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
scrollbar-gutter: auto;
|
|
80
|
+
}
|
|
78
81
|
body {
|
|
79
82
|
margin: 0;
|
|
80
83
|
min-height: 0;
|
|
@@ -2187,6 +2190,10 @@ button.footer-meta {
|
|
|
2187
2190
|
max-height: none;
|
|
2188
2191
|
overflow: visible;
|
|
2189
2192
|
}
|
|
2193
|
+
.footer-model-picker.footer-branch-picker {
|
|
2194
|
+
overflow: visible;
|
|
2195
|
+
max-height: none;
|
|
2196
|
+
}
|
|
2190
2197
|
.footer-branch-picker {
|
|
2191
2198
|
width: min(28rem, calc(100vw - 2rem));
|
|
2192
2199
|
border-color: rgba(245, 194, 231, 0.30);
|
|
@@ -2210,6 +2217,35 @@ button.footer-meta {
|
|
|
2210
2217
|
border-color: rgba(166, 227, 161, 0.46);
|
|
2211
2218
|
box-shadow: inset 3px 0 0 var(--ctp-green), 0 0 1rem rgba(166, 227, 161, 0.14);
|
|
2212
2219
|
}
|
|
2220
|
+
.footer-model-option[data-footer-model-key] {
|
|
2221
|
+
position: relative;
|
|
2222
|
+
cursor: grab;
|
|
2223
|
+
touch-action: none;
|
|
2224
|
+
user-select: none;
|
|
2225
|
+
}
|
|
2226
|
+
.footer-model-option.dragging {
|
|
2227
|
+
cursor: grabbing;
|
|
2228
|
+
border-color: rgba(148, 226, 213, 0.72);
|
|
2229
|
+
box-shadow: inset 0 0 0 1px rgba(148, 226, 213, 0.46), inset 4px 0 0 var(--ctp-teal), 0 0 1.1rem rgba(148, 226, 213, 0.24);
|
|
2230
|
+
}
|
|
2231
|
+
.footer-model-option.drag-over-before::before,
|
|
2232
|
+
.footer-model-option.drag-over-after::after {
|
|
2233
|
+
content: "";
|
|
2234
|
+
position: absolute;
|
|
2235
|
+
left: 0.62rem;
|
|
2236
|
+
right: 0.62rem;
|
|
2237
|
+
height: 2px;
|
|
2238
|
+
border-radius: 999px;
|
|
2239
|
+
background: var(--ctp-teal);
|
|
2240
|
+
box-shadow: 0 0 0.7rem rgba(148, 226, 213, 0.72);
|
|
2241
|
+
pointer-events: none;
|
|
2242
|
+
}
|
|
2243
|
+
.footer-model-option.drag-over-before::before {
|
|
2244
|
+
top: -0.28rem;
|
|
2245
|
+
}
|
|
2246
|
+
.footer-model-option.drag-over-after::after {
|
|
2247
|
+
bottom: -0.28rem;
|
|
2248
|
+
}
|
|
2213
2249
|
.footer-branch-option.active {
|
|
2214
2250
|
border-color: rgba(245, 194, 231, 0.36);
|
|
2215
2251
|
box-shadow: inset 3px 0 0 var(--ctp-pink), 0 0 1rem rgba(245, 194, 231, 0.12);
|
|
@@ -2225,6 +2261,192 @@ button.footer-meta {
|
|
|
2225
2261
|
.footer-branch-create-option .footer-model-option-main {
|
|
2226
2262
|
color: var(--ctp-green);
|
|
2227
2263
|
}
|
|
2264
|
+
.footer-branch-create-form {
|
|
2265
|
+
display: grid;
|
|
2266
|
+
gap: 0.44rem;
|
|
2267
|
+
padding: 0.6rem 0.64rem;
|
|
2268
|
+
border: 1px solid rgba(166, 227, 161, 0.28);
|
|
2269
|
+
border-radius: 0.78rem;
|
|
2270
|
+
background: rgba(166, 227, 161, 0.06);
|
|
2271
|
+
box-shadow: inset 3px 0 0 rgba(166, 227, 161, 0.72);
|
|
2272
|
+
}
|
|
2273
|
+
.footer-branch-create-header {
|
|
2274
|
+
display: flex;
|
|
2275
|
+
align-items: center;
|
|
2276
|
+
justify-content: space-between;
|
|
2277
|
+
gap: 0.6rem;
|
|
2278
|
+
}
|
|
2279
|
+
.footer-branch-create-title {
|
|
2280
|
+
color: var(--ctp-green);
|
|
2281
|
+
font-size: 0.78rem;
|
|
2282
|
+
font-weight: 900;
|
|
2283
|
+
}
|
|
2284
|
+
.footer-branch-create-preview {
|
|
2285
|
+
color: rgba(var(--ctp-subtext-rgb), 0.72);
|
|
2286
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
2287
|
+
font-size: 0.7rem;
|
|
2288
|
+
}
|
|
2289
|
+
.footer-branch-create-fields {
|
|
2290
|
+
display: grid;
|
|
2291
|
+
grid-template-columns: minmax(5.4rem, 0.34fr) auto minmax(0, 1fr) auto;
|
|
2292
|
+
align-items: center;
|
|
2293
|
+
gap: 0.4rem;
|
|
2294
|
+
}
|
|
2295
|
+
.footer-branch-create-type-field {
|
|
2296
|
+
position: relative;
|
|
2297
|
+
min-width: 0;
|
|
2298
|
+
}
|
|
2299
|
+
.footer-branch-create-type-field::after {
|
|
2300
|
+
content: "▾";
|
|
2301
|
+
position: absolute;
|
|
2302
|
+
top: 50%;
|
|
2303
|
+
right: 0.5rem;
|
|
2304
|
+
transform: translateY(-50%);
|
|
2305
|
+
color: var(--ctp-green);
|
|
2306
|
+
font-size: 0.72rem;
|
|
2307
|
+
pointer-events: none;
|
|
2308
|
+
opacity: 0.86;
|
|
2309
|
+
}
|
|
2310
|
+
.footer-branch-create-dropdown-inputfield,
|
|
2311
|
+
.footer-branch-create-input-field {
|
|
2312
|
+
min-width: 0;
|
|
2313
|
+
min-height: 2.28rem;
|
|
2314
|
+
border: 1px solid rgba(var(--ctp-overlay-rgb), 0.38);
|
|
2315
|
+
border-radius: 0.62rem;
|
|
2316
|
+
background: rgba(var(--ctp-crust-rgb), 0.56);
|
|
2317
|
+
color: var(--ctp-text);
|
|
2318
|
+
font: inherit;
|
|
2319
|
+
}
|
|
2320
|
+
.footer-branch-create-dropdown-inputfield {
|
|
2321
|
+
width: 100%;
|
|
2322
|
+
padding: 0 1.35rem 0 0.48rem;
|
|
2323
|
+
}
|
|
2324
|
+
.footer-branch-create-slash {
|
|
2325
|
+
color: var(--ctp-green);
|
|
2326
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
2327
|
+
font-size: 1rem;
|
|
2328
|
+
font-weight: 900;
|
|
2329
|
+
opacity: 0.9;
|
|
2330
|
+
}
|
|
2331
|
+
.footer-branch-type-suggestions {
|
|
2332
|
+
position: absolute;
|
|
2333
|
+
left: 0;
|
|
2334
|
+
top: calc(100% + 0.26rem);
|
|
2335
|
+
z-index: 60;
|
|
2336
|
+
display: grid;
|
|
2337
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
2338
|
+
gap: 0.18rem;
|
|
2339
|
+
width: min(20rem, calc(100vw - 2rem));
|
|
2340
|
+
max-height: none;
|
|
2341
|
+
overflow: visible;
|
|
2342
|
+
padding: 0.32rem;
|
|
2343
|
+
border: 1px solid rgba(166, 227, 161, 0.32);
|
|
2344
|
+
border-radius: 0.66rem;
|
|
2345
|
+
background: linear-gradient(145deg, rgba(var(--ctp-crust-rgb), 0.99), rgba(var(--ctp-base-rgb), 0.98));
|
|
2346
|
+
box-shadow: 0 0.9rem 1.8rem rgba(var(--ctp-crust-rgb), 0.58), 0 0 1rem rgba(166, 227, 161, 0.12);
|
|
2347
|
+
}
|
|
2348
|
+
.footer-branch-type-suggestions[hidden] {
|
|
2349
|
+
display: none !important;
|
|
2350
|
+
}
|
|
2351
|
+
.footer-branch-type-suggestion {
|
|
2352
|
+
min-height: 1.85rem;
|
|
2353
|
+
padding: 0.28rem 0.48rem;
|
|
2354
|
+
border-color: rgba(var(--ctp-overlay-rgb), 0.22);
|
|
2355
|
+
border-radius: 0.48rem;
|
|
2356
|
+
color: var(--ctp-text);
|
|
2357
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
2358
|
+
font-size: 0.76rem;
|
|
2359
|
+
text-align: left;
|
|
2360
|
+
}
|
|
2361
|
+
.footer-branch-type-suggestion:hover,
|
|
2362
|
+
.footer-branch-type-suggestion:focus,
|
|
2363
|
+
.footer-branch-type-suggestion.active {
|
|
2364
|
+
border-color: rgba(166, 227, 161, 0.48);
|
|
2365
|
+
color: var(--ctp-green);
|
|
2366
|
+
background: rgba(166, 227, 161, 0.10);
|
|
2367
|
+
}
|
|
2368
|
+
.footer-branch-create-input-field {
|
|
2369
|
+
padding: 0 0.68rem;
|
|
2370
|
+
}
|
|
2371
|
+
.footer-branch-create-submit {
|
|
2372
|
+
position: relative;
|
|
2373
|
+
min-height: 2.28rem;
|
|
2374
|
+
padding: 0 0.72rem;
|
|
2375
|
+
border-color: rgba(166, 227, 161, 0.42);
|
|
2376
|
+
color: var(--ctp-green);
|
|
2377
|
+
white-space: nowrap;
|
|
2378
|
+
}
|
|
2379
|
+
.footer-branch-create-submit-disabled {
|
|
2380
|
+
cursor: not-allowed;
|
|
2381
|
+
border-color: rgba(166, 227, 161, 0.24);
|
|
2382
|
+
color: rgba(166, 227, 161, 0.58);
|
|
2383
|
+
background: rgba(var(--ctp-crust-rgb), 0.42);
|
|
2384
|
+
}
|
|
2385
|
+
.footer-branch-create-submit[data-tooltip]::before,
|
|
2386
|
+
.footer-branch-create-submit[data-tooltip]::after {
|
|
2387
|
+
position: absolute;
|
|
2388
|
+
z-index: 80;
|
|
2389
|
+
opacity: 0;
|
|
2390
|
+
pointer-events: none;
|
|
2391
|
+
transition: opacity 0.14s ease, transform 0.14s ease;
|
|
2392
|
+
}
|
|
2393
|
+
.footer-branch-create-submit[data-tooltip]::before {
|
|
2394
|
+
content: "";
|
|
2395
|
+
right: 1.2rem;
|
|
2396
|
+
bottom: calc(100% + 0.38rem);
|
|
2397
|
+
width: 0.72rem;
|
|
2398
|
+
height: 0.72rem;
|
|
2399
|
+
border: 1px solid rgba(166, 227, 161, 0.26);
|
|
2400
|
+
border-top: 0;
|
|
2401
|
+
border-left: 0;
|
|
2402
|
+
background: rgba(var(--ctp-crust-rgb), 0.99);
|
|
2403
|
+
transform: translateY(0.24rem) rotate(45deg);
|
|
2404
|
+
}
|
|
2405
|
+
.footer-branch-create-submit[data-tooltip]::after {
|
|
2406
|
+
content: attr(data-tooltip);
|
|
2407
|
+
right: 0;
|
|
2408
|
+
bottom: calc(100% + 0.7rem);
|
|
2409
|
+
width: min(24rem, calc(100vw - 2.4rem));
|
|
2410
|
+
padding: 0.72rem 0.78rem;
|
|
2411
|
+
border: 1px solid rgba(166, 227, 161, 0.30);
|
|
2412
|
+
border-radius: 0.78rem;
|
|
2413
|
+
background:
|
|
2414
|
+
radial-gradient(circle at 10% 0%, rgba(166, 227, 161, 0.13), transparent 10rem),
|
|
2415
|
+
linear-gradient(145deg, rgba(var(--ctp-crust-rgb), 0.99), rgba(var(--ctp-base-rgb), 0.98));
|
|
2416
|
+
box-shadow: 0 1rem 2rem rgba(var(--ctp-crust-rgb), 0.62), 0 0 1.2rem rgba(166, 227, 161, 0.14);
|
|
2417
|
+
color: var(--ctp-text);
|
|
2418
|
+
font-size: 0.76rem;
|
|
2419
|
+
font-weight: 650;
|
|
2420
|
+
line-height: 1.42;
|
|
2421
|
+
text-align: left;
|
|
2422
|
+
white-space: pre-wrap;
|
|
2423
|
+
overflow-wrap: anywhere;
|
|
2424
|
+
transform: translateY(0.24rem);
|
|
2425
|
+
}
|
|
2426
|
+
.footer-branch-create-submit[data-tooltip]:hover::before,
|
|
2427
|
+
.footer-branch-create-submit[data-tooltip]:hover::after,
|
|
2428
|
+
.footer-branch-create-submit[data-tooltip]:focus-visible::before,
|
|
2429
|
+
.footer-branch-create-submit[data-tooltip]:focus-visible::after {
|
|
2430
|
+
opacity: 1;
|
|
2431
|
+
transform: translateY(0);
|
|
2432
|
+
}
|
|
2433
|
+
.footer-branch-create-submit[data-tooltip]:hover::before,
|
|
2434
|
+
.footer-branch-create-submit[data-tooltip]:focus-visible::before {
|
|
2435
|
+
transform: translateY(0) rotate(45deg);
|
|
2436
|
+
}
|
|
2437
|
+
@media (max-width: 520px) {
|
|
2438
|
+
.footer-branch-create-fields {
|
|
2439
|
+
grid-template-columns: 1fr auto 1fr;
|
|
2440
|
+
}
|
|
2441
|
+
.footer-branch-type-suggestions {
|
|
2442
|
+
width: calc(100vw - 3rem);
|
|
2443
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
2444
|
+
}
|
|
2445
|
+
.footer-branch-create-submit {
|
|
2446
|
+
grid-column: 1 / -1;
|
|
2447
|
+
width: 100%;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2228
2450
|
.footer-model-option-main,
|
|
2229
2451
|
.footer-model-option-name {
|
|
2230
2452
|
min-width: 0;
|
|
@@ -3410,6 +3632,14 @@ button.git-workflow-step:hover:not(:disabled) {
|
|
|
3410
3632
|
list-style: none;
|
|
3411
3633
|
}
|
|
3412
3634
|
.todo-widget-summary::-webkit-details-marker { display: none; }
|
|
3635
|
+
.todo-widget-goal {
|
|
3636
|
+
min-width: 0;
|
|
3637
|
+
color: rgba(var(--ctp-text-rgb), 0.92);
|
|
3638
|
+
font-size: 0.78rem;
|
|
3639
|
+
font-weight: 800;
|
|
3640
|
+
line-height: 1.3;
|
|
3641
|
+
overflow-wrap: anywhere;
|
|
3642
|
+
}
|
|
3413
3643
|
.todo-widget-header {
|
|
3414
3644
|
display: flex;
|
|
3415
3645
|
align-items: center;
|
|
@@ -4126,6 +4356,9 @@ button.git-workflow-step:hover:not(:disabled) {
|
|
|
4126
4356
|
position: relative;
|
|
4127
4357
|
margin: 0.55rem 0;
|
|
4128
4358
|
}
|
|
4359
|
+
.markdown-code-block.has-code-copy-action > .code-block {
|
|
4360
|
+
padding-top: 2.05rem;
|
|
4361
|
+
}
|
|
4129
4362
|
.markdown-code-language {
|
|
4130
4363
|
position: absolute;
|
|
4131
4364
|
top: 0.42rem;
|
|
@@ -4138,10 +4371,82 @@ button.git-workflow-step:hover:not(:disabled) {
|
|
|
4138
4371
|
letter-spacing: 0.08em;
|
|
4139
4372
|
text-transform: uppercase;
|
|
4140
4373
|
}
|
|
4374
|
+
.markdown-code-block.has-code-copy-action > .markdown-code-language {
|
|
4375
|
+
right: 4.65rem;
|
|
4376
|
+
}
|
|
4377
|
+
.markdown-code-copy-button {
|
|
4378
|
+
position: absolute;
|
|
4379
|
+
top: 0.36rem;
|
|
4380
|
+
right: 0.42rem;
|
|
4381
|
+
z-index: 2;
|
|
4382
|
+
min-width: 3.45rem;
|
|
4383
|
+
min-height: 1.52rem;
|
|
4384
|
+
padding: 0.16rem 0.45rem;
|
|
4385
|
+
border: 1px solid rgba(148, 226, 213, 0.28);
|
|
4386
|
+
border-radius: 999px;
|
|
4387
|
+
color: rgba(var(--ctp-text-rgb), 0.82);
|
|
4388
|
+
background: rgba(var(--ctp-crust-rgb), 0.78);
|
|
4389
|
+
box-shadow: 0 0 0.7rem rgba(var(--ctp-crust-rgb), 0.28);
|
|
4390
|
+
font-size: 0.68rem;
|
|
4391
|
+
font-weight: 900;
|
|
4392
|
+
letter-spacing: 0.05em;
|
|
4393
|
+
text-transform: uppercase;
|
|
4394
|
+
opacity: 0.78;
|
|
4395
|
+
}
|
|
4396
|
+
.markdown-code-copy-button:hover,
|
|
4397
|
+
.markdown-code-copy-button:focus-visible,
|
|
4398
|
+
.markdown-code-copy-button.copied {
|
|
4399
|
+
color: #11111b;
|
|
4400
|
+
border-color: transparent;
|
|
4401
|
+
background: linear-gradient(120deg, var(--ctp-teal), var(--ctp-blue));
|
|
4402
|
+
opacity: 1;
|
|
4403
|
+
}
|
|
4404
|
+
.markdown-code-copy-button.copied {
|
|
4405
|
+
background: linear-gradient(120deg, var(--ctp-green), var(--ctp-teal));
|
|
4406
|
+
}
|
|
4141
4407
|
.markdown-code code {
|
|
4142
4408
|
display: block;
|
|
4143
4409
|
min-width: max-content;
|
|
4144
4410
|
}
|
|
4411
|
+
.markdown-mermaid-block {
|
|
4412
|
+
overflow-x: auto;
|
|
4413
|
+
padding: 0.72rem;
|
|
4414
|
+
border: 1px solid rgba(148, 226, 213, 0.22);
|
|
4415
|
+
border-radius: 0.9rem;
|
|
4416
|
+
background:
|
|
4417
|
+
radial-gradient(circle at 0 0, rgba(148, 226, 213, 0.10), transparent 18rem),
|
|
4418
|
+
rgba(var(--ctp-crust-rgb), 0.58);
|
|
4419
|
+
box-shadow: inset 0 0 1.3rem rgba(var(--ctp-crust-rgb), 0.24), 0 0 1rem rgba(148, 226, 213, 0.06);
|
|
4420
|
+
}
|
|
4421
|
+
.markdown-mermaid-diagram {
|
|
4422
|
+
display: flex;
|
|
4423
|
+
align-items: center;
|
|
4424
|
+
justify-content: center;
|
|
4425
|
+
min-width: min-content;
|
|
4426
|
+
min-height: 3.5rem;
|
|
4427
|
+
padding: 0.95rem 0.35rem 0.35rem;
|
|
4428
|
+
}
|
|
4429
|
+
.markdown-mermaid-diagram svg {
|
|
4430
|
+
display: block;
|
|
4431
|
+
max-width: 100%;
|
|
4432
|
+
height: auto;
|
|
4433
|
+
}
|
|
4434
|
+
.markdown-mermaid-status {
|
|
4435
|
+
margin: 0.35rem 0 0;
|
|
4436
|
+
font-size: 0.82rem;
|
|
4437
|
+
}
|
|
4438
|
+
.markdown-mermaid-status.error {
|
|
4439
|
+
color: var(--danger);
|
|
4440
|
+
}
|
|
4441
|
+
.markdown-mermaid-source {
|
|
4442
|
+
margin: 0.55rem 0 0;
|
|
4443
|
+
padding: 0.5rem;
|
|
4444
|
+
border-color: rgba(249, 226, 175, 0.18);
|
|
4445
|
+
background: rgba(var(--ctp-crust-rgb), 0.50);
|
|
4446
|
+
}
|
|
4447
|
+
.markdown-mermaid-source .code-block {
|
|
4448
|
+
margin-bottom: 0;
|
|
4449
|
+
}
|
|
4145
4450
|
.markdown-list {
|
|
4146
4451
|
margin: 0.45rem 0 0.55rem 1.2rem;
|
|
4147
4452
|
padding-left: 1rem;
|
|
@@ -7070,6 +7375,7 @@ button.composer-skill-tag:focus-visible {
|
|
|
7070
7375
|
padding-bottom: 0.42rem;
|
|
7071
7376
|
}
|
|
7072
7377
|
.todo-widget-summary { gap: 0.24rem; }
|
|
7378
|
+
.todo-widget-goal { font-size: 0.72rem; }
|
|
7073
7379
|
.todo-widget-header { gap: 0.26rem; }
|
|
7074
7380
|
.todo-widget-toggle {
|
|
7075
7381
|
width: 0.9rem;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
|
-
import { chmod, mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { chmod, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { networkInterfaces, tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { setTimeout as delay } from "node:timers/promises";
|
|
@@ -125,6 +125,20 @@ try {
|
|
|
125
125
|
assert.equal(gzipResponse.headers.get("content-encoding"), "gzip", "styles.css should fall back to gzip");
|
|
126
126
|
await gzipResponse.arrayBuffer();
|
|
127
127
|
|
|
128
|
+
const mermaidModuleResponse = await fetch(`http://127.0.0.1:${port}/vendor/mermaid/mermaid.esm.min.mjs`, {
|
|
129
|
+
signal: AbortSignal.timeout(5_000),
|
|
130
|
+
});
|
|
131
|
+
assert.equal(mermaidModuleResponse.status, 200, "Mermaid ESM module should be served from the vendored dependency path");
|
|
132
|
+
assert.match(mermaidModuleResponse.headers.get("content-type") || "", /text\/javascript/, "Mermaid ESM module should use a JavaScript MIME type");
|
|
133
|
+
const mermaidModuleText = await mermaidModuleResponse.text();
|
|
134
|
+
const mermaidChunkPath = mermaidModuleText.match(/\.\/(chunks\/mermaid\.esm\.min\/[A-Za-z0-9._-]+\.mjs)/)?.[1];
|
|
135
|
+
assert.ok(mermaidChunkPath, "Mermaid ESM module should reference same-directory chunks");
|
|
136
|
+
const mermaidChunkResponse = await fetch(`http://127.0.0.1:${port}/vendor/mermaid/${mermaidChunkPath}`, {
|
|
137
|
+
signal: AbortSignal.timeout(5_000),
|
|
138
|
+
});
|
|
139
|
+
assert.equal(mermaidChunkResponse.status, 200, "Mermaid ESM chunks should be served for dynamic imports");
|
|
140
|
+
assert.equal(await mermaidChunkResponse.text(), await readFile(join(root, "node_modules", "mermaid", "dist", mermaidChunkPath), "utf8"), "served Mermaid chunks should match the dependency files");
|
|
141
|
+
|
|
128
142
|
const tabsResponse = await request("127.0.0.1", "/api/tabs");
|
|
129
143
|
assert.equal(tabsResponse.status, 200);
|
|
130
144
|
const tabList = tabsResponse.body?.data?.tabs || tabsResponse.body?.tabs || [];
|
|
@@ -214,7 +214,7 @@ assert.match(app, /function handleRemoteWebuiStatus\(statusText\)[\s\S]*?opening
|
|
|
214
214
|
assert.match(app, /case "confirm":[\s\S]*?if \(isRemoteWebuiQrPopupLoading\(\)\) closeRemoteWebuiQrPopup\(\)/, "blocking extension dialogs should close the QR loading popup before opening");
|
|
215
215
|
assert.match(app, /function showRemoteWebuiQrPopup\(widgetKey, lines = \[\], request = \{\}\)[\s\S]*?widgetKey !== "pi-remote-webui"[\s\S]*?openRemoteWebuiQrPopup\(lines\)/, "remote WebUI QR widget events should open the QR popup");
|
|
216
216
|
assert.match(app, /function mirrorRemoteWebuiWidgetToTranscript\(widgetKey, lines = \[\], request = \{\}\)[\s\S]*?widgetKey !== "pi-remote-webui"[\s\S]*?addTransientMessage\(\{ role: "extension", title: "\/remote"/, "remote WebUI QR widget events should still mirror into the active tab transcript");
|
|
217
|
-
assert.match(app, /if \(widgetKey === "pi-remote-webui"\) \{[\s\S]*?
|
|
217
|
+
assert.match(app, /if \(widgetKey === "pi-remote-webui"\) \{[\s\S]*?setWidgetForTab\(requestTabId, widgetKey, \{ \.\.\.request, widgetLines: undefined \}\);[\s\S]*?showRemoteWebuiQrPopup\(widgetKey, request\.widgetLines, request\)/, "remote WebUI QR widget events should not render in the generic widget area");
|
|
218
218
|
assert.doesNotMatch(app, /function renderRemoteWebuiWidget/, "remote WebUI QR should not render through the generic widget renderer");
|
|
219
219
|
assert.match(css, /\.message\.run-indicator-message \{[\s\S]*?border-color/, "active agent runs should render a visible transcript indicator card");
|
|
220
220
|
assert.match(css, /\.message-copy-button \{[\s\S]*?position:\s*absolute/, "transcript messages should expose a top-right copy button");
|
|
@@ -250,6 +250,7 @@ assert.match(css, /\.side-panel-section:not\(\.collapsed\) \.side-panel-section-
|
|
|
250
250
|
assert.match(css, /\.optional-feature-pill\.enabled/, "optional features should visually distinguish enabled state");
|
|
251
251
|
assert.match(css, /\.todo-widget \{[\s\S]*?display:\s*grid/, "todo-progress widget should render as a styled checklist card");
|
|
252
252
|
assert.match(css, /\.todo-widget-summary \{[\s\S]*?cursor:\s*pointer/, "todo-progress widget should expose a compact expandable summary");
|
|
253
|
+
assert.match(css, /\.todo-widget-goal \{[\s\S]*?overflow-wrap:\s*anywhere/, "todo-progress widget should show long goals above progress without layout overflow");
|
|
253
254
|
assert.match(css, /\.todo-widget-body \{[\s\S]*?max-height:/, "expanded todo-progress details should be height-limited");
|
|
254
255
|
assert.match(css, /\.todo-widget-item\.partial \.todo-widget-marker/, "todo-progress partial items should have distinct styling");
|
|
255
256
|
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");
|
|
@@ -519,6 +520,12 @@ assert.match(app, /function messageCopyText\(message, body = null\)/, "frontend
|
|
|
519
520
|
assert.match(app, /function attachMessageCopyButton\(bubble, message, body\)/, "frontend should add copy controls to rendered transcript cards");
|
|
520
521
|
assert.match(app, /button\.append\(make\("span", "message-copy-icon", "⧉"\)\)/, "message copy buttons should render as icon-only controls");
|
|
521
522
|
assert.match(app, /copyMessageBubble\(button\)/, "message copy buttons should copy through the shared clipboard helper");
|
|
523
|
+
assert.match(app, /function attachMarkdownCodeCopyButton\(wrapper, label = "Copy"\)/, "frontend should add copy controls to markdown code blocks");
|
|
524
|
+
assert.match(app, /function copyMarkdownCodeBlock\(button\)[\s\S]*?await copyText\(text\)/, "markdown code block copy controls should use the shared clipboard helper");
|
|
525
|
+
assert.match(app, /attachMarkdownCodeCopyButton\(wrapper\);/, "normal fenced code blocks should get copy buttons");
|
|
526
|
+
assert.match(app, /attachMarkdownCodeCopyButton\(wrapper, "Copy source"\);/, "rendered Mermaid blocks should expose source copy buttons");
|
|
527
|
+
assert.match(css, /\.markdown-code-copy-button \{[\s\S]*?position:\s*absolute/, "markdown code blocks should expose positioned copy buttons");
|
|
528
|
+
assert.match(css, /\.markdown-code-block\.has-code-copy-action > \.code-block \{[\s\S]*?padding-top:/, "code block copy buttons should reserve vertical space above source text");
|
|
522
529
|
assert.match(app, /retryServerConnectionButton.*retryServerConnection/s, "backend-offline recovery panel should wire a retry action");
|
|
523
530
|
assert.match(app, /function isChatNearBottom\(/, "chat should detect whether the user is reading above the bottom");
|
|
524
531
|
assert.match(app, /function scheduleChatFollowScroll\(/, "chat auto-follow should retry after layout settles during fast streaming");
|
|
@@ -545,6 +552,8 @@ assert.match(app, /case "webui_extension_ui_cancelled":/, "frontend should close
|
|
|
545
552
|
assert.match(app, /case "webui_extension_ui_resolved":[\s\S]*?removeQueuedDialogRequests\(\[event\.id\]\)/, "frontend should close dialogs resolved by another connected browser");
|
|
546
553
|
assert.match(app, /if \(responseId && activeDialog && String\(activeDialog\.id \|\| ""\) !== responseId\) return;/, "dialog response cleanup should not close the next queued dialog after a resolve-event race");
|
|
547
554
|
assert.match(app, /function parseTodoProgressWidget\(lines\)/, "todo-progress widgets should be parsed from extension widget lines");
|
|
555
|
+
assert.ok(app.includes("const goalLine = cleanLines.find((line) => /^Goal\\s*[::]/i.test(line));"), "todo-progress parser should preserve an optional Goal line from extension widget lines");
|
|
556
|
+
assert.ok(app.includes("if (todo.goal) summary.append(make(\"div\", \"todo-widget-goal\", `Goal: ${todo.goal}`));"), "todo-progress widget should display the goal above the progress header");
|
|
548
557
|
assert.match(app, /const todoProgressWidgetExpandedByTab = new Map\(\)/, "todo-progress expansion state should survive widget re-renders per tab");
|
|
549
558
|
assert.match(app, /const node = make\("details", "widget todo-widget"\)/, "todo-progress widget should render collapsed by default as expandable details");
|
|
550
559
|
assert.match(app, /Optional feature detection intentionally checks loaded Pi capabilities/, "optional Web UI features should be detected through loaded capabilities, not package folders");
|
|
@@ -599,8 +608,22 @@ assert.match(app, /function insertChangedFilePathReference\(path\)[\s\S]*formatP
|
|
|
599
608
|
assert.match(app, /function renderGitFooterPayloadMeta\(chip, tab\)[\s\S]*options\.title = gitFooterPayloadTooltip\(chip, \{ action \}\)[\s\S]*footerMeta\(chip\.label, chip\.value, footerMetaClassForPayload\(chip\), options\)[\s\S]*applyFooterChangedFilesDropdown\(node, chip\)/, "git footer meta payload chips should render as styled metadata with explanatory tooltips and changes popovers");
|
|
600
609
|
assert.match(app, /chip\.key === "git"[\s\S]*setFooterBranchPickerOpen\(!footerBranchPickerOpen\)[\s\S]*Click to switch to another local branch/, "git branch footer chip should open the branch picker");
|
|
601
610
|
assert.match(app, /function renderFooterBranchPicker\(\)[\s\S]*Git branches[\s\S]*applyFooterGitBranch\(branch\.name\)/, "git branch picker should render available branches and switch on selection");
|
|
602
|
-
assert.match(app, /
|
|
603
|
-
assert.match(app, /
|
|
611
|
+
assert.match(app, /function renderFooterBranchPicker\(\)[\s\S]*renderFooterBranchCreateForm\(state\)[\s\S]*for \(const branch of state\.branches\)/, "git branch picker should always offer inline branch creation before local branch choices");
|
|
612
|
+
assert.match(app, /GIT_BRANCH_TYPE_SUGGESTIONS = \["feat", "fix", "change", "perf", "test", "chore", "refactor", "docs", "style", "build", "ci", "revert"\]/, "new branch creation should reuse the conventional type suggestions from /git-staged-msg");
|
|
613
|
+
assert.match(app, /function renderFooterBranchCreateForm\(state = footerBranchPickerState\)[\s\S]*footer-branch-create-dropdown-inputfield[\s\S]*footer-branch-type-suggestions[\s\S]*footer-branch-create-input-field[\s\S]*Create new branch/, "new branch creation should use a styled editable suggestions dropdown plus input field instead of a browser prompt");
|
|
614
|
+
assert.match(app, /let footerBranchCreateDraft = \{ type: "", name: "" \}/, "new branch creation should not default the editable type prefix to feat");
|
|
615
|
+
assert.match(app, /function footerBranchCreateType\(value = footerBranchCreateDraft\.type\) \{\n\s+return slugifyGitBranchPart\(value\);\n\}/, "branch type suggestions should not restrict or default custom user-entered prefixes");
|
|
616
|
+
assert.match(app, /const slash = make\("span", "footer-branch-create-slash", "\/"\)/, "branch creation should visibly separate the prefix and user input with a slash");
|
|
617
|
+
assert.match(app, /function gitSwitchCreateCommandDisplay\(branch\)[\s\S]*`git switch -c \$\{quoteGitBranchForDisplay\(branch\)\}`[\s\S]*preview\.textContent = gitSwitchCreateCommandDisplay\(branchName \|\| footerBranchCreatePreviewName\(\)\)/, "branch creation should show the live complete quoted git switch -c command");
|
|
618
|
+
assert.match(app, /function footerBranchCreateTooltip\(branchName = footerBranchCreateName\(\)\)[\s\S]*A branch is a safe workspace for your changes\.[\s\S]*does not commit, push, or delete anything[\s\S]*Tip: use short lowercase words/, "create branch button should have a detailed non-technical explanatory tooltip");
|
|
619
|
+
assert.match(app, /submitButton\.dataset\.tooltip = footerBranchCreateTooltip\(branchName\);[\s\S]*submitButton\.removeAttribute\("title"\)/, "create branch button should use the styled tooltip instead of a native title tooltip");
|
|
620
|
+
assert.match(css, /\.footer-branch-create-submit\[data-tooltip\]::after \{[\s\S]*?content:\s*attr\(data-tooltip\);[\s\S]*?white-space:\s*pre-wrap;[\s\S]*?overflow-wrap:\s*anywhere;/, "create branch button tooltip should be styled and support detailed multiline copy");
|
|
621
|
+
assert.match(app, /submitButton\.disabled = false;[\s\S]*submitButton\.classList\.toggle\("footer-branch-create-submit-disabled", submitDisabled\);[\s\S]*submitButton\.setAttribute\("aria-disabled", submitDisabled \? "true" : "false"\)/, "branch creation should use aria-disabled styling so the tooltip is not dimmed by disabled button opacity");
|
|
622
|
+
assert.match(css, /\.footer-branch-create-submit-disabled \{[\s\S]*?cursor:\s*not-allowed;[\s\S]*?color:\s*rgba\(166, 227, 161, 0\.58\);/, "greyed branch create button should use a class instead of disabled opacity");
|
|
623
|
+
assert.match(app, /Loading existing local branches… New branch creation is available\./, "branch picker loading copy should make clear that creation is still available");
|
|
624
|
+
assert.match(css, /\.footer-model-picker\.footer-branch-picker \{[\s\S]*?overflow:\s*visible;[\s\S]*?max-height:\s*none;/, "branch picker should not force scrolling just to see the open type suggestions dropdown");
|
|
625
|
+
assert.match(css, /\.footer-branch-type-suggestions \{[\s\S]*?grid-template-columns:\s*repeat\(3,[\s\S]*?max-height:\s*none;[\s\S]*?overflow:\s*visible;/, "branch type suggestions should render as a styled multi-column dropdown without internal scrolling");
|
|
626
|
+
assert.match(app, /async function createFooterGitBranch\(branch = footerBranchCreateName\(\)\)[\s\S]*confirmFooterGitBranchAction\(branchName, \{ create: true, requireConfirm: true/, "new branch creation should require confirmation before running git switch -c");
|
|
604
627
|
assert.match(app, /function footerBranchAgentWarningLines[\s\S]*WARNING:[\s\S]*still running or waiting for input in this Git working tree/, "branch create/switch confirmation should warn when an agent is active in the current git working tree");
|
|
605
628
|
assert.match(app, /if \(footerBranchPickerOpen\) elements\.statusBar\.append\(renderFooterBranchPicker\(\)\)/, "footer should append the branch picker above the status bar when open");
|
|
606
629
|
assert.match(server, /url\.pathname === "\/api\/git-branches"[\s\S]*readGitBranches\(tab\.cwd\)/, "server should expose local git branch listing for the footer picker");
|
|
@@ -747,6 +770,8 @@ assert.match(app, /appendText\(preview, toolResultPreviewText\(message, 10\), "c
|
|
|
747
770
|
assert.match(app, /function assistantDisplayMessages\(message\)/, "assistant history should split thinking and tool-call parts out of the final Assistant output card");
|
|
748
771
|
assert.match(app, /function assistantHasToolCallAfter\(content, index\)/, "assistant text that precedes a tool call should be detectable and suppressible");
|
|
749
772
|
assert.match(app, /if \(!assistantHasToolCallAfter\(content, index\)\) finalParts\.push\(finalPart\);/, "assistant history should not render pre-tool-call assistant text as final output");
|
|
773
|
+
assert.match(app, /typeof content === "string"[\s\S]*?splitThinkingFormatText\(content\)[\s\S]*?content: parsed\.finalText/, "assistant string messages with tagged <think> output should render final text separately");
|
|
774
|
+
assert.match(app, /const textForThinkingFormat[\s\S]*?splitThinkingFormatText\(textForThinkingFormat\)[\s\S]*?appendThinkingFormatDisplayMessages\(displayMessages, base, parsed\)[\s\S]*?finalParts\.push/, "assistant text parts with tagged <think> output should split into thinking and final-output cards");
|
|
750
775
|
assert.match(app, /return content\.trim\(\) \? \[\{ \.\.\.message, title: "final output" \}\] : \[\]/, "assistant messages with stripped empty text should not render empty final-output cards");
|
|
751
776
|
assert.match(app, /function isEmptyAssistantTextPart\(part\)[\s\S]*?part\.type === "text"[\s\S]*?!assistantTextPartText\(part\)\.trim\(\)/, "empty assistant text parts should be recognized as skippable provider metadata");
|
|
752
777
|
assert.match(app, /if \(isEmptyAssistantTextPart\(part\)\) continue;/, "empty assistant text parts should not render as assistant-event cards");
|
|
@@ -754,6 +779,10 @@ assert.match(app, /function assistantFinalOutputPart\(part\)[\s\S]*?if \(part\.t
|
|
|
754
779
|
assert.match(app, /\["assistant", "toolExecution"\]\.includes\(transcriptMessage\.role\) \? messageIndex : -1/, "final Assistant output and paired tool action cards should keep the source message index for feedback");
|
|
755
780
|
assert.match(app, /function ensureStreamingThinkingBubble\(\)[\s\S]*if \(!thinkingOutputVisible\) return false/, "live thinking should respect the show/hide thinking-output toggle");
|
|
756
781
|
assert.match(app, /const UNEXPOSED_THINKING_TEXT = "No thinking content was exposed by the provider\."/, "frontend should name the provider no-thinking placeholder for suppression");
|
|
782
|
+
assert.match(app, /THINKING_FORMAT_OPEN_TAG_REGEX/, "frontend should recognize tagged <think> provider output");
|
|
783
|
+
assert.match(app, /CHANNEL_THINKING_FORMAT_OPEN_TAG_REGEX = \/\^<\\\|\(\[a-z\]\[\\w-\]\*\)>\/i/, "frontend should recognize tagged <|channel> provider output");
|
|
784
|
+
assert.match(app, /function thinkingFormatOpenMatch\(text\)[\s\S]*?CHANNEL_THINKING_FORMAT_OPEN_TAG_REGEX[\s\S]*?closeRegex: new RegExp\(`<\$\{escapeRegExp\(name\)\}\\\\\|>`/, "channel-style tagged output should create a matching <channel|> close delimiter");
|
|
785
|
+
assert.match(app, /function splitThinkingFormatText\(text, \{ streaming = false \} = \{\}\)[\s\S]*?thinkingFormatOpenMatch\(rest\)[\s\S]*?finalText: stripThinkingFormatOutputSeparator\(rest\)/, "tagged thinking output should split thinking text from final response text");
|
|
757
786
|
assert.match(app, /function visibleThinkingText\(text\)[\s\S]*?trimmed === UNEXPOSED_THINKING_TEXT[\s\S]*?return "";/, "provider no-thinking placeholders should normalize to empty thinking output");
|
|
758
787
|
assert.match(app, /if \(isThinkingPart\) \{[\s\S]*?visibleThinkingText\(assistantThinkingText\(part\)\)[\s\S]*?if \(thinking\) displayMessages\.push/, "assistant transcript splitting should skip empty or unexposed thinking parts");
|
|
759
788
|
assert.match(app, /message\.role === "thinking"[\s\S]*?visibleThinkingText\(message\.thinking \|\| textFromContent\(message\.content\)\)[\s\S]*?if \(thinkingOutputVisible && thinkingText\) appendText\(body, thinkingText, "thinking-text"\);/, "thinking cards should suppress empty and provider no-thinking placeholder output");
|
|
@@ -766,15 +795,19 @@ assert.match(app, /function thinkingDeltaText\(update\) \{[\s\S]*?return visible
|
|
|
766
795
|
assert.match(app, /const THINKING_VISIBILITY_STORAGE_KEY = "pi-webui-thinking-visible"/, "thinking visibility should persist in browser storage");
|
|
767
796
|
assert.match(app, /function setThinkingOutputVisible\(visible[\s\S]*renderAllMessages\(\{ preserveScroll: true \}\)/, "thinking visibility changes should immediately re-render the transcript");
|
|
768
797
|
assert.match(app, /function assistantStreamingMessage\(event\)/, "live streaming should read the authoritative partial assistant message from RPC events like the TUI");
|
|
769
|
-
assert.match(app, /assistantThinkingTextFromMessage\(assistantStreamingMessage\(event\)\) \|\| thinkingDeltaText\(update\)/, "live thinking end should replace deltas with the final partial-message thinking content");
|
|
798
|
+
assert.match(app, /assistantThinkingTextFromMessage\(assistantStreamingMessage\(event\), \{ streaming: true \}\) \|\| thinkingDeltaText\(update\)/, "live thinking end should replace deltas with the final partial-message thinking content");
|
|
770
799
|
assert.match(app, /if \(typeof partialText === "string"\) streamRawText = partialText;/, "live assistant text should synchronize from partial messages instead of relying only on deltas");
|
|
771
800
|
assert.match(app, /const TODO_PROGRESS_LINE_REGEX = /, "frontend should recognize live todo progress lines that will be moved into the todo widget");
|
|
772
801
|
assert.match(app, /function stripTodoProgressLines\(text, \{ streaming = false \} = \{\}\)/, "live Assistant output should strip todo-progress lines before rendering final-output text");
|
|
802
|
+
assert.match(app, /function syncLiveTodoProgressWidgetFromText\(text, tabId = activeTabId\)/, "live Assistant checklist text should update the todo-progress widget before tool execution events");
|
|
803
|
+
assert.match(app, /syncLiveTodoProgressWidgetFromText\(streamRawText, event\.tabId \|\| activeTabId\)/, "streaming assistant text should feed the live todo-progress widget immediately");
|
|
773
804
|
assert.match(app, /function renderStreamingAssistantText\(\)[\s\S]*?const assistantText = stripTodoProgressLines\(streamRawText, \{ streaming: true \}\)/, "streamed Assistant text should classify from accumulated output without flashing partial todo-progress lines");
|
|
805
|
+
assert.match(app, /function syncStreamingThinkingFormat\(assistantText\)[\s\S]*?splitThinkingFormatText\(assistantText, \{ streaming: true \}\)[\s\S]*?setStreamingThinkingText\(thinking\)/, "tagged <think> streaming output should update the live thinking card instead of flashing raw tags");
|
|
806
|
+
assert.match(app, /const finalText = thinkingFormat\?\.hasThinkingFormat \? stripTodoProgressLines\(thinkingFormat\.finalText, \{ streaming: true \}\) : assistantText;/, "tagged <think> streaming output should render only final response text in the Assistant card");
|
|
774
807
|
assert.match(app, /const STREAM_OUTPUT_HIDE_DELAY_MS = 300/, "stream output hiding should be debounced to prevent rapid flicker");
|
|
775
808
|
assert.match(app, /const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220/, "live assistant text should be briefly guarded so pre-tool-call text can be suppressed");
|
|
776
809
|
assert.match(app, /function scheduleStreamBubbleHide\([\s\S]*?STREAM_OUTPUT_MIN_VISIBLE_MS/, "stream output cards should observe a minimum visible duration before hiding");
|
|
777
|
-
assert.match(app, /if \(
|
|
810
|
+
assert.match(app, /if \(finalText\) \{[\s\S]*?renderStreamingMarkdown\(streamText, finalText\);[\s\S]*?\} else \{\n\s+scheduleStreamBubbleHide\(\);/, "empty filtered stream output should schedule hide while visible stream output renders as Markdown");
|
|
778
811
|
assert.match(app, /if \(streamToolCallSeen \|\| streamBubble\) renderStreamingAssistantText\(\);\n\s+else scheduleStreamingAssistantTextRender\(\);/, "live assistant text should wait briefly before showing unless it is already visible or follows a tool call");
|
|
779
812
|
assert.match(app, /streamToolCallSeen = true;\n\s+suppressStreamingAssistantTextBeforeToolCall\(\);/, "tool-call starts should remove pending assistant text from the live transcript");
|
|
780
813
|
assert.match(app, /const created = appendMessage\(\{ role: "assistant", title: "final output"/, "live Assistant cards should be created only for final output text without a noisy Assistant label");
|