@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/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=56" />
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=55"></script>
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 { overflow-x: hidden; }
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]*?widgets\.delete\(widgetKey\);[\s\S]*?showRemoteWebuiQrPopup\(widgetKey, request\.widgetLines, request\)/, "remote WebUI QR widget events should not render in the generic widget area");
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, /Create new branch[\s\S]*createFooterGitBranch\(\)/, "git branch picker should offer branch creation when no other branches are available");
603
- assert.match(app, /function promptFooterGitBranchName\(\)[\s\S]*window\.prompt\("New git branch name:"[\s\S]*function createFooterGitBranch\(\)[\s\S]*confirmFooterGitBranchAction\(branchName, \{ create: true, requireConfirm: true/, "new branch creation should prompt for a branch name and require confirmation before creating it");
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 \(assistantText\) \{[\s\S]*?renderStreamingMarkdown\(streamText, assistantText\);[\s\S]*?\} else \{\n\s+scheduleStreamBubbleHide\(\);/, "empty filtered stream output should schedule hide while visible stream output renders as Markdown");
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");