@firstpick/pi-package-webui 0.5.4 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/pi-webui.mjs +55 -3
- package/package.json +4 -3
- package/public/app.js +1155 -100
- 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 +49 -14
- package/tests/streaming-ui-coupling.test.mjs +175 -0
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 || [];
|
|
@@ -31,7 +31,7 @@ const companionDependencies = {
|
|
|
31
31
|
"@firstpick/pi-extension-safety-guard": "^0.2.3",
|
|
32
32
|
"@firstpick/pi-extension-setup-skills": "^0.1.8",
|
|
33
33
|
"@firstpick/pi-extension-stats": "^0.2.6",
|
|
34
|
-
"@firstpick/pi-extension-todo-progress": "^0.2.
|
|
34
|
+
"@firstpick/pi-extension-todo-progress": "^0.2.5",
|
|
35
35
|
"@firstpick/pi-extension-tools": "^0.1.6",
|
|
36
36
|
"@firstpick/pi-prompts-git-pr": "^0.1.2",
|
|
37
37
|
"@firstpick/pi-themes-bundle": "^0.1.4",
|
|
@@ -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,7 +552,12 @@ 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");
|
|
558
|
+
assert.match(app, /const todoProgressSignatureByTab = new Map\(\)/, "todo-progress should track per-tab signatures to avoid unchanged re-renders");
|
|
559
|
+
assert.match(app, /function widgetRequestEquivalent\(a, b\)[\s\S]*?return a\.widgetLines\.every/, "todo-progress and generic widgets should no-op identical widget payloads");
|
|
560
|
+
assert.match(app, /todoProgressSignatureByTab\.get\(tabId\) === signature\) return false/, "live todo-progress sync should skip unchanged checklist signatures");
|
|
549
561
|
assert.match(app, /const node = make\("details", "widget todo-widget"\)/, "todo-progress widget should render collapsed by default as expandable details");
|
|
550
562
|
assert.match(app, /Optional feature detection intentionally checks loaded Pi capabilities/, "optional Web UI features should be detected through loaded capabilities, not package folders");
|
|
551
563
|
assert.match(app, /function resetOptionalFeatureAvailability\(\)/, "optional feature state should reset across active-tab and reload boundaries");
|
|
@@ -599,8 +611,22 @@ assert.match(app, /function insertChangedFilePathReference\(path\)[\s\S]*formatP
|
|
|
599
611
|
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
612
|
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
613
|
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, /
|
|
614
|
+
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");
|
|
615
|
+
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");
|
|
616
|
+
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");
|
|
617
|
+
assert.match(app, /let footerBranchCreateDraft = \{ type: "", name: "" \}/, "new branch creation should not default the editable type prefix to feat");
|
|
618
|
+
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");
|
|
619
|
+
assert.match(app, /const slash = make\("span", "footer-branch-create-slash", "\/"\)/, "branch creation should visibly separate the prefix and user input with a slash");
|
|
620
|
+
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");
|
|
621
|
+
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");
|
|
622
|
+
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");
|
|
623
|
+
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");
|
|
624
|
+
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");
|
|
625
|
+
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");
|
|
626
|
+
assert.match(app, /Loading existing local branches… New branch creation is available\./, "branch picker loading copy should make clear that creation is still available");
|
|
627
|
+
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");
|
|
628
|
+
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");
|
|
629
|
+
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
630
|
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
631
|
assert.match(app, /if \(footerBranchPickerOpen\) elements\.statusBar\.append\(renderFooterBranchPicker\(\)\)/, "footer should append the branch picker above the status bar when open");
|
|
606
632
|
assert.match(server, /url\.pathname === "\/api\/git-branches"[\s\S]*readGitBranches\(tab\.cwd\)/, "server should expose local git branch listing for the footer picker");
|
|
@@ -765,26 +791,32 @@ assert.match(app, /if \(isThinkingPart\) \{[\s\S]*?visibleThinkingText\(assistan
|
|
|
765
791
|
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
792
|
assert.match(app, /function showStreamingThinking\(initialText = ""\)[\s\S]*?if \(initialText && !streamThinking\.textContent\) streamThinking\.textContent = initialText;/, "live thinking should not create a visible placeholder card before content arrives");
|
|
767
793
|
assert.match(app, /function setStreamingThinkingText\(text\)[\s\S]*?const thinking = visibleThinkingText\(text\);[\s\S]*?if \(!thinkingOutputVisible \|\| !thinking\) return false;[\s\S]*?return true;/, "live thinking text setters should ignore empty text instead of clearing or flashing the card");
|
|
768
|
-
assert.match(app, /function
|
|
794
|
+
assert.match(app, /function syncStreamingThinkingFromUpdate\(event, update, \{ placeholder = "" \} = \{\}\)[\s\S]*?return setStreamingThinkingText\(streamThinkingRawText \|\| placeholder\);/, "incremental thinking sync should only report success after setting visible thinking text");
|
|
769
795
|
assert.doesNotMatch(app, /text \|\| placeholder \|\| streamThinkingBubble/, "partial-message thinking sync should not clear an existing thinking card when a partial carries no visible thinking text");
|
|
770
796
|
assert.match(app, /if \(thinkingOutputVisible && delta && \(!synced \|\| !streamThinking\?\.textContent\)\) \{/, "live thinking delta fallback should require visible delta text before creating a card");
|
|
771
797
|
assert.match(app, /function thinkingDeltaText\(update\) \{[\s\S]*?return visibleThinkingText\(update\.delta \|\| update\.thinking \|\| update\.content \|\| ""\);/, "live thinking deltas should suppress provider no-thinking placeholders too");
|
|
772
798
|
assert.match(app, /const THINKING_VISIBILITY_STORAGE_KEY = "pi-webui-thinking-visible"/, "thinking visibility should persist in browser storage");
|
|
773
799
|
assert.match(app, /function setThinkingOutputVisible\(visible[\s\S]*renderAllMessages\(\{ preserveScroll: true \}\)/, "thinking visibility changes should immediately re-render the transcript");
|
|
774
800
|
assert.match(app, /function assistantStreamingMessage\(event\)/, "live streaming should read the authoritative partial assistant message from RPC events like the TUI");
|
|
775
|
-
assert.match(app, /
|
|
776
|
-
assert.match(app, /
|
|
801
|
+
assert.match(app, /function syncStreamingThinkingFromUpdate\(event, update[\s\S]*?const fallback = streamingThinkingTextFallback\(event\);[\s\S]*?setStreamThinkingRawText\(fallback\);[\s\S]*?return setStreamingThinkingText\(streamThinkingRawText \|\| placeholder\);/, "live thinking end should replace deltas with the final partial-message thinking content");
|
|
802
|
+
assert.match(app, /function setStreamRawText\(text\)[\s\S]*?streamRawText = nextText;[\s\S]*?resetStreamDerivedTextCache\(\);/, "live assistant text should synchronize from partial messages through a cache-aware setter");
|
|
777
803
|
assert.match(app, /const TODO_PROGRESS_LINE_REGEX = /, "frontend should recognize live todo progress lines that will be moved into the todo widget");
|
|
778
804
|
assert.match(app, /function stripTodoProgressLines\(text, \{ streaming = false \} = \{\}\)/, "live Assistant output should strip todo-progress lines before rendering final-output text");
|
|
779
|
-
assert.match(app, /function
|
|
780
|
-
assert.match(app, /
|
|
781
|
-
assert.match(app, /const finalText = thinkingFormat\?\.hasThinkingFormat \?
|
|
805
|
+
assert.match(app, /function syncLiveTodoProgressWidgetFromText\(text, tabId = activeTabId\)/, "live Assistant checklist text should update the todo-progress widget before tool execution events");
|
|
806
|
+
assert.match(app, /scheduleLiveTodoProgressWidgetSync\(streamRawText, event\.tabId \|\| activeTabId\)/, "streaming assistant text should feed the live todo-progress widget through the coalesced sync scheduler");
|
|
807
|
+
assert.match(app, /function renderStreamingAssistantText\(\)[\s\S]*?const thinkingFormat = syncStreamingThinkingFormat\(\);[\s\S]*?const finalText = thinkingFormat\?\.hasThinkingFormat \? streamDerivedText\(\)\.finalText : streamRenderableAssistantText\(\);/, "streamed Assistant text should render cached derived output without directly rescanning raw stream text");
|
|
808
|
+
assert.match(app, /function syncStreamingThinkingFormat\(\)[\s\S]*?const parsed = streamDerivedText\(\)\.thinkingFormat;[\s\S]*?setStreamingThinkingText\(thinking\)/, "tagged <think> streaming output should update the live thinking card from cached parse state instead of flashing raw tags");
|
|
809
|
+
assert.match(app, /const finalText = thinkingFormat\?\.hasThinkingFormat \? streamDerivedText\(\)\.finalText : streamRenderableAssistantText\(\);/, "tagged <think> streaming output should render only final response text in the Assistant card");
|
|
782
810
|
assert.match(app, /const STREAM_OUTPUT_HIDE_DELAY_MS = 300/, "stream output hiding should be debounced to prevent rapid flicker");
|
|
783
811
|
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");
|
|
784
812
|
assert.match(app, /function scheduleStreamBubbleHide\([\s\S]*?STREAM_OUTPUT_MIN_VISIBLE_MS/, "stream output cards should observe a minimum visible duration before hiding");
|
|
785
813
|
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");
|
|
786
|
-
assert.match(app, /
|
|
814
|
+
assert.match(app, /scheduleStreamingAssistantTextRender\(\{ immediate: !!\(streamToolCallSeen \|\| streamBubble\) \}\);/, "live assistant text should wait briefly before showing unless it is already visible or follows a tool call");
|
|
787
815
|
assert.match(app, /streamToolCallSeen = true;\n\s+suppressStreamingAssistantTextBeforeToolCall\(\);/, "tool-call starts should remove pending assistant text from the live transcript");
|
|
816
|
+
assert.match(app, /function renderStreamingToolCallCard\(\{ scroll = false \} = \{\}\)[\s\S]*?appendMessage\(message, \{ streaming: true \}\)[\s\S]*?streamToolCallText\.textContent !== displayText/, "live tool-call cards should render and update the arguments stream in place");
|
|
817
|
+
assert.match(app, /update\.type === "toolcall_delta"[\s\S]*?updateStreamingToolCallFromEvent\(event, \{ appendDelta: true \}\)[\s\S]*?Building tool call:/, "tool-call deltas should update visible streamed arguments instead of a static placeholder");
|
|
818
|
+
assert.match(app, /case "tool_execution_start":[\s\S]*?removeStreamingToolCallCard\(\)[\s\S]*?handleToolExecutionStart\(event\)/, "the streamed tool-call argument card should be removed when the real tool execution card starts");
|
|
819
|
+
assert.doesNotMatch(app, /Preparing tool call:/, "tool-call streaming should no longer show only the static preparing placeholder");
|
|
788
820
|
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");
|
|
789
821
|
assert.match(app, /function renderMarkdownInto\(parent, text\)/, "assistant output should have a browser-native Markdown renderer");
|
|
790
822
|
assert.match(app, /safeMarkdownLinkHref\(url\)/, "Markdown links should be sanitized before rendering");
|
|
@@ -1104,13 +1136,16 @@ assert.match(server, /const EXTENSION_UI_BLOCKING_METHODS = new Set\(\["select",
|
|
|
1104
1136
|
assert.match(server, /function trackPendingExtensionUiRequest\(tab, event\)/, "server should track blocking extension UI requests per tab");
|
|
1105
1137
|
assert.match(server, /pendingExtensionUiRequests: new Map\(\)/, "new tabs should initialize pending extension UI request storage");
|
|
1106
1138
|
assert.match(server, /extensionStatuses: new Map\(\)/, "new tabs should initialize replayable extension status storage");
|
|
1139
|
+
assert.match(server, /extensionWidgets: new Map\(\)/, "new tabs should initialize replayable extension widget storage");
|
|
1107
1140
|
assert.match(server, /function rememberExtensionStatusEvent\(tab, event\)[\s\S]*event\.method !== "setStatus"[\s\S]*statuses\.set\(String\(event\.statusKey\), String\(event\.statusText\)\)/, "server should retain extension status events for reconnects");
|
|
1108
|
-
assert.match(server, /
|
|
1141
|
+
assert.match(server, /function rememberExtensionWidgetEvent\(tab, event\)[\s\S]*event\.method !== "setWidget"[\s\S]*widgets\.set\(String\(widgetKey\)/, "server should retain extension widget events for reconnects");
|
|
1142
|
+
assert.match(server, /rememberExtensionStatusEvent\(tab, scopedEvent\)[\s\S]*rememberExtensionWidgetEvent\(tab, scopedEvent\)[\s\S]*trackPendingExtensionUiRequest\(tab, scopedEvent\)/, "RPC events should retain extension statuses and widgets before broadcasting");
|
|
1109
1143
|
assert.match(server, /trackPendingExtensionUiRequest\(tab, scopedEvent\)/, "RPC events should populate pending extension UI storage before broadcasting");
|
|
1110
1144
|
assert.match(server, /scopedEvent = \{ \.\.\.scopedEvent,[\s\S]*?pendingExtensionUiRequestCount: pendingExtensionUiRequests\(tab\)\.length \}/, "RPC events should broadcast pending blocker counts for tab indicators");
|
|
1111
1145
|
assert.match(server, /function replayExtensionStatuses\(tab, res\)[\s\S]*method: "setStatus"/, "server should replay latest extension statuses on SSE reconnect");
|
|
1146
|
+
assert.match(server, /function replayExtensionWidgets\(tab, res\)[\s\S]*method: "setWidget"/, "server should replay latest extension widgets on SSE reconnect");
|
|
1112
1147
|
assert.match(server, /function replayPendingExtensionUiRequests\(tab, res\)/, "server should be able to replay missed extension UI requests on SSE reconnect");
|
|
1113
|
-
assert.match(server, /replayExtensionStatuses\(tab, res\);\n\s+replayPendingExtensionUiRequests\(tab, res\)/, "SSE connections should replay extension statuses before pending blockers");
|
|
1148
|
+
assert.match(server, /replayExtensionStatuses\(tab, res\);\n\s+replayExtensionWidgets\(tab, res\);\n\s+replayPendingExtensionUiRequests\(tab, res\)/, "SSE connections should replay extension statuses and widgets before pending blockers");
|
|
1114
1149
|
assert.match(server, /pendingExtensionUiRequests: pendingExtensionUiRequestSummaries\(tab\)/, "detailed Web UI status should expose pending extension UI blockers");
|
|
1115
1150
|
assert.match(server, /resolvePendingExtensionUiRequest\(tab, payload\.id\)/, "extension UI responses should clear the pending blocker cache");
|
|
1116
1151
|
assert.match(server, /type: "webui_extension_ui_resolved"[\s\S]*?pendingExtensionUiRequestCount/, "extension UI responses should notify clients that a blocker resolved");
|
|
@@ -1313,7 +1348,7 @@ assert.match(app, /pruneDisconnectedLiveToolCards\(\);/, "reconciliation must pr
|
|
|
1313
1348
|
// --- Performance: incremental streaming markdown (P0-3) ---
|
|
1314
1349
|
assert.match(app, /function streamingMarkdownStableBoundary\(text\)[\s\S]*?for \(let index = 0; index < lines\.length - 1; index \+= 1\)/, "streaming markdown boundary must never treat the final partial line as stable");
|
|
1315
1350
|
assert.match(app, /function renderStreamingMarkdown\(block, text\)[\s\S]*?if \(!text\.startsWith\(state\.stableText\)\)/, "streaming markdown must fall back to a full re-render when earlier content changes");
|
|
1316
|
-
assert.match(app, /streamRawText = "";\n streamMarkdownState = null;/, "resetting the stream bubble must clear incremental markdown state");
|
|
1351
|
+
assert.match(app, /streamRawText = "";\n streamThinkingRawText = "";\n resetStreamDerivedTextCache\(\);\n streamMarkdownState = null;/, "resetting the stream bubble must clear incremental markdown state and derived caches");
|
|
1317
1352
|
|
|
1318
1353
|
// --- Performance: delta transcript fetch (P1-1) ---
|
|
1319
1354
|
assert.match(app, /function mergeMessagesDelta\(previous, data\)[\s\S]*?messagesLookEqual\(previous\[since\], data\.messages\[0\]\)/, "delta merges must verify the one-message overlap before applying");
|