@firstpick/pi-package-webui 0.4.3 → 0.4.4

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=53" />
15
+ <link rel="stylesheet" href="/styles.css?v=54" />
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">
@@ -289,6 +289,15 @@
289
289
  data-tooltip="Follow-up usage:&#10;1. Type your message in the textarea.&#10;2. Tap Follow-up to send it as a follow-up.&#10;Use for extra context or the next request."
290
290
  >Follow-up</button>
291
291
  <button id="abortButton" class="composer-abort-button danger" type="button" title="Hold Esc or the Abort button for 3 seconds to abort the active Pi run" aria-label="Hold Esc or the Abort button for 3 seconds to abort the active Pi run" hidden disabled>Abort</button>
292
+ <button
293
+ id="btwButton"
294
+ class="composer-icon-button composer-btw-button"
295
+ type="button"
296
+ title="Ask as a /btw side question"
297
+ aria-label="Ask the current composer text as a /btw side question"
298
+ data-tooltip="/btw side question:&#10;Type a question, then tap /btw to ask without adding it to the main conversation."
299
+ hidden
300
+ ><svg class="composer-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5.5 6.5h10.75a2.25 2.25 0 0 1 2.25 2.25v4.5a2.25 2.25 0 0 1-2.25 2.25h-4.4L8 18.25V15.5H5.5a2.25 2.25 0 0 1-2.25-2.25v-4.5A2.25 2.25 0 0 1 5.5 6.5Z" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/><path d="m15.75 5.25 1.1-2.1 1.1 2.1 2.3 1.1-2.3 1.1-1.1 2.1-1.1-2.1-2.3-1.1 2.3-1.1Z" fill="currentColor"/><path d="M7.25 10.25h5.8M7.25 12.75h4.1" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg></button>
292
301
  <button id="sendButton" type="submit" class="primary" title="Send prompt">Send</button>
293
302
  </div>
294
303
  </form>
@@ -703,6 +712,6 @@
703
712
  </form>
704
713
  </dialog>
705
714
 
706
- <script type="module" src="/app.js?v=50"></script>
715
+ <script type="module" src="/app.js?v=52"></script>
707
716
  </body>
708
717
  </html>
package/public/styles.css CHANGED
@@ -1701,6 +1701,7 @@ body.side-panel-collapsed .terminal-tabs-shell {
1701
1701
  }
1702
1702
  .widget-area:has(.release-npm-live-widget .release-npm-output-details[open]),
1703
1703
  .widget-area:has(.release-aur-live-widget .release-npm-output-details[open]),
1704
+ .widget-area:has(.workflow-live-widget .release-npm-output-details[open]),
1704
1705
  .widget-area:has(.app-runner-live-widget .release-npm-output-details[open]) {
1705
1706
  flex: 0 0 min(44rem, 68dvh);
1706
1707
  min-height: 0;
@@ -2745,6 +2746,120 @@ button.git-workflow-step:hover:not(:disabled) {
2745
2746
  linear-gradient(145deg, rgba(var(--ctp-crust-rgb), 0.98), rgba(var(--ctp-mantle-rgb), 0.92));
2746
2747
  }
2747
2748
  .app-runner-widget .release-npm-kicker { color: var(--ctp-green); }
2749
+ .workflow-widget {
2750
+ border-color: rgba(137, 180, 250, 0.62);
2751
+ border-left-color: rgba(137, 180, 250, 0.88);
2752
+ background:
2753
+ radial-gradient(circle at 4% 0%, rgba(137, 180, 250, 0.20), transparent 16rem),
2754
+ radial-gradient(circle at 96% 12%, rgba(203, 166, 247, 0.14), transparent 18rem),
2755
+ linear-gradient(145deg, rgba(var(--ctp-crust-rgb), 0.98), rgba(var(--ctp-mantle-rgb), 0.92));
2756
+ }
2757
+ .workflow-widget .release-npm-kicker { color: var(--ctp-blue); }
2758
+ .workflow-widget .release-npm-pill.workflow-status.completed { color: var(--ctp-green); }
2759
+ .workflow-widget .release-npm-pill.workflow-status.failed { color: var(--ctp-red); }
2760
+ .workflow-widget .release-npm-pill.workflow-status.cancelled { color: var(--ctp-yellow); }
2761
+ .workflow-widget .release-npm-pill.workflow-status.running,
2762
+ .workflow-widget .release-npm-pill.workflow-status.queued { color: var(--ctp-blue); }
2763
+ .workflow-widget .release-npm-pill.workflow-truncated { color: var(--ctp-peach); }
2764
+ .btw-widget {
2765
+ border-color: rgba(148, 226, 213, 0.62);
2766
+ border-left-color: rgba(148, 226, 213, 0.88);
2767
+ background:
2768
+ radial-gradient(circle at 4% 0%, rgba(148, 226, 213, 0.20), transparent 16rem),
2769
+ radial-gradient(circle at 96% 12%, rgba(137, 180, 250, 0.14), transparent 18rem),
2770
+ linear-gradient(145deg, rgba(var(--ctp-crust-rgb), 0.98), rgba(var(--ctp-mantle-rgb), 0.92));
2771
+ }
2772
+ .btw-widget .release-npm-kicker { color: var(--ctp-teal); }
2773
+ .btw-widget-question {
2774
+ display: grid;
2775
+ gap: 0.28rem;
2776
+ min-width: 0;
2777
+ padding: 0.56rem 0.64rem;
2778
+ border: 1px solid rgba(148, 226, 213, 0.20);
2779
+ border-radius: 0.68rem;
2780
+ background: rgba(var(--ctp-crust-rgb), 0.54);
2781
+ }
2782
+ .btw-widget-question-label {
2783
+ color: var(--ctp-teal);
2784
+ font-size: 0.66rem;
2785
+ font-weight: 950;
2786
+ letter-spacing: 0.12em;
2787
+ text-transform: uppercase;
2788
+ }
2789
+ .btw-widget-question-text {
2790
+ min-width: 0;
2791
+ color: rgba(var(--ctp-text-rgb), 0.94);
2792
+ line-height: 1.4;
2793
+ overflow-wrap: anywhere;
2794
+ }
2795
+ .btw-widget .release-npm-line {
2796
+ width: auto;
2797
+ white-space: pre-wrap;
2798
+ overflow-wrap: anywhere;
2799
+ }
2800
+ .btw-widget-composer {
2801
+ display: grid;
2802
+ grid-template-columns: minmax(0, 1fr) auto;
2803
+ gap: 0.5rem;
2804
+ align-items: stretch;
2805
+ min-width: 0;
2806
+ }
2807
+ .btw-widget-input {
2808
+ min-height: 2.5rem;
2809
+ max-height: min(16dvh, 7rem);
2810
+ padding: 0.64rem 0.72rem;
2811
+ resize: vertical;
2812
+ border: 1px solid rgba(148, 226, 213, 0.28);
2813
+ border-radius: 0.72rem;
2814
+ color: rgba(var(--ctp-text-rgb), 0.96);
2815
+ background: rgba(var(--ctp-crust-rgb), 0.72);
2816
+ box-shadow: inset 0 1px 0 rgba(var(--ctp-text-rgb), 0.04);
2817
+ font: inherit;
2818
+ line-height: 1.35;
2819
+ }
2820
+ .btw-widget-input:focus {
2821
+ outline: 2px solid rgba(148, 226, 213, 0.62);
2822
+ outline-offset: 0.12rem;
2823
+ border-color: rgba(148, 226, 213, 0.54);
2824
+ }
2825
+ .btw-widget-send {
2826
+ min-width: 5.8rem;
2827
+ color: var(--ctp-teal);
2828
+ border-color: rgba(148, 226, 213, 0.34);
2829
+ }
2830
+ .btw-widget-send:not(:disabled):hover,
2831
+ .btw-widget-send:not(:disabled):focus-visible {
2832
+ color: #11111b;
2833
+ border-color: transparent;
2834
+ background: linear-gradient(120deg, var(--ctp-teal), var(--ctp-blue));
2835
+ }
2836
+ .btw-transfer-action {
2837
+ display: inline-flex;
2838
+ align-items: center;
2839
+ gap: 0.38rem;
2840
+ color: var(--ctp-teal);
2841
+ border-color: rgba(148, 226, 213, 0.34);
2842
+ }
2843
+ .btw-transfer-action:not(:disabled):hover,
2844
+ .btw-transfer-action:not(:disabled):focus-visible {
2845
+ color: #11111b;
2846
+ border-color: transparent;
2847
+ background: linear-gradient(120deg, var(--ctp-teal), var(--ctp-blue));
2848
+ }
2849
+ .btw-transfer-icon {
2850
+ display: block;
2851
+ width: 1.05rem;
2852
+ height: 1.05rem;
2853
+ flex: 0 0 auto;
2854
+ }
2855
+ .btw-widget .release-npm-pill.btw-status.ready { color: var(--ctp-teal); }
2856
+ .btw-widget .release-npm-pill.btw-status.done { color: var(--ctp-green); }
2857
+ .btw-widget .release-npm-pill.btw-status.error { color: var(--ctp-red); }
2858
+ .btw-widget .release-npm-pill.btw-status.aborted { color: var(--ctp-yellow); }
2859
+ .btw-live-widget .release-npm-output-details[open] .release-npm-terminal {
2860
+ height: clamp(9rem, 28dvh, 22rem);
2861
+ min-height: 0;
2862
+ }
2748
2863
  .app-runner-status.done { color: var(--ctp-green); }
2749
2864
  .app-runner-status.failed,
2750
2865
  .app-runner-status.error { color: var(--ctp-red); }
@@ -2951,6 +3066,7 @@ button.git-workflow-step:hover:not(:disabled) {
2951
3066
  }
2952
3067
  .release-npm-live-widget .release-npm-output-details[open] .release-npm-terminal,
2953
3068
  .release-aur-live-widget .release-npm-output-details[open] .release-npm-terminal,
3069
+ .workflow-live-widget .release-npm-output-details[open] .release-npm-terminal,
2954
3070
  .app-runner-live-widget .release-npm-output-details[open] .release-npm-terminal {
2955
3071
  height: clamp(15rem, 42dvh, 31rem);
2956
3072
  min-height: 0;
@@ -4223,6 +4339,23 @@ button.composer-skill-tag:focus-visible {
4223
4339
  }
4224
4340
  .composer-actions-button { display: none; }
4225
4341
  .composer-actions-panel { display: contents; }
4342
+ .composer-btw-button {
4343
+ color: var(--ctp-teal);
4344
+ border-color: rgba(148, 226, 213, 0.34);
4345
+ background:
4346
+ linear-gradient(120deg, rgba(148, 226, 213, 0.14), rgba(137, 180, 250, 0.10)),
4347
+ linear-gradient(180deg, rgba(var(--ctp-surface-rgb), 0.82), rgba(var(--ctp-crust-rgb), 0.86));
4348
+ }
4349
+ .composer-btw-button .composer-icon {
4350
+ width: 1.34rem;
4351
+ height: 1.34rem;
4352
+ }
4353
+ .composer-btw-button:not(:disabled):hover,
4354
+ .composer-btw-button:not(:disabled):focus-visible {
4355
+ color: #11111b;
4356
+ border-color: transparent;
4357
+ background: linear-gradient(120deg, var(--ctp-teal), var(--ctp-blue));
4358
+ }
4226
4359
  .composer-abort-button,
4227
4360
  .composer-row button.primary {
4228
4361
  min-width: 5.8rem;
@@ -6434,6 +6567,8 @@ button.composer-skill-tag:focus-visible {
6434
6567
  }
6435
6568
  .release-npm-meta,
6436
6569
  .release-npm-actions { justify-content: flex-start; }
6570
+ .btw-widget-composer { grid-template-columns: minmax(0, 1fr); }
6571
+ .btw-widget-send { width: 100%; }
6437
6572
  .release-npm-stream-header {
6438
6573
  padding: 0.3rem 0.42rem;
6439
6574
  font-size: 0.66rem;
@@ -6770,7 +6905,8 @@ button.composer-skill-tag:focus-visible {
6770
6905
  min-height: 40px;
6771
6906
  font-size: 0.86rem;
6772
6907
  }
6773
- body:not(.pi-run-active):not(.mobile-keyboard-open) .composer-row button.primary { grid-column: span 4; }
6908
+ body:not(.pi-run-active):not(.mobile-keyboard-open) .composer-row button.primary { grid-column: span 2; }
6909
+ body:not(.pi-run-active):not(.mobile-keyboard-open) .composer-btw-button[hidden] + button.primary { grid-column: span 4; }
6774
6910
  body.pi-run-active:not(.mobile-keyboard-open) .composer-abort-button:not([hidden]) {
6775
6911
  order: 1;
6776
6912
  grid-column: span 2;
@@ -6778,12 +6914,21 @@ button.composer-skill-tag:focus-visible {
6778
6914
  body.pi-run-active:not(.mobile-keyboard-open) .composer-row > #steerButton { order: 2; }
6779
6915
  body.pi-run-active:not(.mobile-keyboard-open) .composer-row > #followUpButton { order: 3; }
6780
6916
  body.pi-run-active:not(.mobile-keyboard-open) .composer-actions-button { order: 4; }
6917
+ body.pi-run-active:not(.mobile-keyboard-open) .composer-btw-button:not([hidden]) {
6918
+ order: 5;
6919
+ grid-column: span 2;
6920
+ }
6781
6921
  body.pi-run-active:not(.mobile-keyboard-open) .composer-row button.primary {
6922
+ order: 6;
6923
+ grid-column: span 2;
6924
+ }
6925
+ body.pi-run-active:not(.mobile-keyboard-open) .composer-btw-button[hidden] + button.primary {
6782
6926
  order: 5;
6783
6927
  grid-column: span 4;
6784
6928
  }
6785
6929
  .composer-row > #followUpButton,
6786
6930
  .composer-row > #steerButton,
6931
+ .composer-row > #btwButton,
6787
6932
  .composer-actions-button { grid-column: span 2; }
6788
6933
  .composer-actions-panel > #followUpButton,
6789
6934
  .composer-actions-panel > #steerButton,
@@ -12,7 +12,7 @@ const [pkgRaw, html, css, app, server, extension, readme, startScript, manifestR
12
12
  readFile(join(root, "bin", "pi-webui.mjs"), "utf8"),
13
13
  readFile(join(root, "index.ts"), "utf8"),
14
14
  readFile(join(root, "README.md"), "utf8"),
15
- readFile(join(root, "start-webui.sh"), "utf8"),
15
+ readFile(join(root, "dev", "scripts", "start-webui.sh"), "utf8"),
16
16
  readFile(join(root, "public", "manifest.webmanifest"), "utf8"),
17
17
  readFile(join(root, "public", "service-worker.js"), "utf8"),
18
18
  readFile(join(root, "public", "apple-touch-icon.png")),
@@ -24,6 +24,7 @@ const [pkgRaw, html, css, app, server, extension, readme, startScript, manifestR
24
24
  const pkg = JSON.parse(pkgRaw);
25
25
  const manifest = JSON.parse(manifestRaw);
26
26
  const companionDependencies = {
27
+ "@firstpick/pi-extension-btw": "^0.1.0",
27
28
  "@firstpick/pi-extension-git-footer-status": "^0.3.3",
28
29
  "@firstpick/pi-extension-release-aur": "^0.1.6",
29
30
  "@firstpick/pi-extension-release-npm": "^0.4.0",
@@ -72,6 +73,7 @@ assert.match(html, /id="pathPickerCreateButton"[^>]*>Create directory<\/button>/
72
73
  assert.match(html, /id="pathPickerSearchInput"[^>]*type="search"[^>]*placeholder="Search current directory…"/, "cwd picker should expose a current-directory search box");
73
74
  assert.match(html, /id="pathPickerClearSearchButton"[^>]*hidden[^>]*>Clear<\/button>/, "cwd picker should expose a clear-search action");
74
75
  assert.match(html, /id="optionalFeaturesBox"/, "side panel should expose optional feature status and controls");
76
+ assert.doesNotMatch(html, /id="btwOverlayDialog"/, "/btw should not use a blocking modal overlay");
75
77
  assert.match(html, /id="codexUsageBox"/, "side panel should expose Codex subscription usage status");
76
78
  assert.match(html, /data-side-panel-section="codex-usage"/, "Codex usage should live in a collapsible side-panel section");
77
79
  assert.match(html, /data-side-panel-section="queue"[\s\S]*id="createPromptListButton"[\s\S]*>Create prompt list<\/button>/, "Queue section should expose prompt-list creation");
@@ -125,7 +127,9 @@ assert.match(app, /attachmentTextDialog\?\.addEventListener\("keydown"[\s\S]*eve
125
127
  assert.match(css, /\.attachment-text-dialog[\s\S]*\.attachment-text-editor/, "text attachment editor should have dedicated dialog styling");
126
128
  assert.match(html, /id="composerActionsButton"/, "mobile composer should expose a compact actions trigger");
127
129
  assert.match(html, /id="composerActionsPanel"/, "secondary composer controls should live in a mobile actions panel");
128
- assert.match(html, /<div class="composer-row">[\s\S]*id="abortButton"[\s\S]*id="sendButton"/, "Abort should live in the bottom composer row beside Send");
130
+ assert.match(html, /<div class="composer-row">[\s\S]*id="abortButton"[\s\S]*id="btwButton"[\s\S]*id="sendButton"/, "Abort and /btw should live in the bottom composer row beside Send");
131
+ assert.match(html, /id="btwButton"[\s\S]*class="composer-icon-button composer-btw-button"[\s\S]*?<svg class="composer-icon"/, "composer should expose an icon-only /btw side-question button");
132
+ assert.doesNotMatch(html, /id="btwButton"[\s\S]*?<span>\/btw<\/span>[\s\S]*?id="sendButton"/, "/btw composer button should not show a text label");
129
133
  assert.match(html, /id="abortButton"[^>]*Hold Esc or the Abort button for 3 seconds/, "Abort should advertise guarded Esc and long-press affordances");
130
134
  assert.doesNotMatch(html, /class="side-panel-controls"[\s\S]*id="abortButton"/, "Abort should not be buried in the side-panel controls");
131
135
  assert.match(html, /id="publishButton"[\s\S]*?aria-controls="publishMenu"/, "composer should expose a Publish workflow menu button");
@@ -188,7 +192,7 @@ assert.match(css, /\.composer-row button \{\n\s+width:\s*100%;\n\s+min-height:\s
188
192
  assert.match(css, /\.composer-abort-button,\n\.composer-row button\.primary \{[\s\S]*?min-width:/, "Abort and Send should share stable bottom-row sizing");
189
193
  assert.match(css, /\.composer-abort-button\.long-pressing::after[\s\S]*?animation:\s*abort-long-press-fill var\(--abort-long-press-duration, 3000ms\) linear forwards/, "Abort should expose a visible 3-second long-press progress affordance");
190
194
  assert.match(css, /body\.pi-run-active:not\(\.mobile-keyboard-open\) \.composer-abort-button:not\(\[hidden\]\) \{\n\s+order:\s*1;\n\s+grid-column:\s*span 2;/, "active mobile runs should move Abort to the top row");
191
- assert.match(css, /body\.pi-run-active:not\(\.mobile-keyboard-open\) \.composer-actions-button \{ order:\s*4; \}[\s\S]*?body\.pi-run-active:not\(\.mobile-keyboard-open\) \.composer-row button\.primary \{\n\s+order:\s*5;\n\s+grid-column:\s*span 4;/, "active mobile runs should keep Actions beside Send on the bottom row");
195
+ assert.match(css, /body\.pi-run-active:not\(\.mobile-keyboard-open\) \.composer-actions-button \{ order:\s*4; \}[\s\S]*?body\.pi-run-active:not\(\.mobile-keyboard-open\) \.composer-btw-button:not\(\[hidden\]\) \{\n\s+order:\s*5;\n\s+grid-column:\s*span 2;[\s\S]*?body\.pi-run-active:not\(\.mobile-keyboard-open\) \.composer-row button\.primary \{\n\s+order:\s*6;\n\s+grid-column:\s*span 2;/, "active mobile runs should keep Actions, /btw, and Send on the bottom row");
192
196
  assert.match(css, /#promptInput \{[\s\S]*?min-height:\s*calc\(1\.5em \+ 1\.8rem\)/, "prompt input should default to a compact single-line height");
193
197
  assert.match(css, /#promptInput \{[\s\S]*?overflow-y:\s*hidden/, "prompt input should be JS-resized instead of showing a scrollbar by default");
194
198
  assert.match(css, /\.composer-context-tags \{[\s\S]*?top:\s*-0\.48rem;[\s\S]*?left:\s*0\.75rem;/, "busy prompt behavior and skill tags should sit at the top-left of the input frame");
@@ -224,6 +228,7 @@ assert.match(css, /\.message\.toolResult \.message-collapse\[open\] > \.message-
224
228
  assert.match(css, /\.tool-output-details\[open\] > \.tool-output-code \{[\s\S]*?max-height:\s*min\(34rem, 52dvh\);[\s\S]*?overflow:\s*auto/, "expanded live tool output should get an internal scrollbar");
225
229
  assert.match(css, /\.run-indicator-pulse \{[\s\S]*?animation:\s*run-indicator-pulse/, "active agent run indicator should have an animated pulse");
226
230
  assert.match(css, /\.optional-features-box \{[\s\S]*?display:\s*grid/, "optional features should render as a side-panel feature list");
231
+ assert.match(css, /\.btw-widget \{[\s\S]*?\.btw-widget-composer \{[\s\S]*?\.btw-transfer-action \{[\s\S]*?\.btw-live-widget \.release-npm-output-details\[open\] \.release-npm-terminal \{[\s\S]*?height:\s*clamp/, "/btw should render as a non-blocking release-style output widget with its own input and transfer action");
227
232
  assert.match(css, /\.prompt-list-controls \{[\s\S]*?display:\s*grid/, "Queue prompt-list controls should render as a side-panel control group");
228
233
  assert.match(css, /\.prompt-list-dialog \{[\s\S]*?width:\s*min\(58rem/, "prompt-list editor dialog should have a wider prompt-friendly layout");
229
234
  assert.match(css, /\.prompt-list-editor-rows \{[\s\S]*?max-height:/, "prompt-list dialog should scroll long follow-up lists inside the editor");
@@ -322,7 +327,7 @@ assert.match(css, /\.terminal-tabs[\s\S]*?position:\s*absolute/, "expanded mobil
322
327
  assert.match(css, /body\.mobile-keyboard-open \.terminal-tabs-shell,[\s\S]*?body\.mobile-keyboard-open \.widget-area,[\s\S]*?body\.mobile-keyboard-open \.statusbar/, "mobile keyboard mode should hide header, widgets, and footer");
323
328
  assert.match(css, /body\.mobile-keyboard-open \.composer-actions-button,[\s\S]*?body\.mobile-keyboard-open \.composer-actions-panel/, "mobile keyboard mode should hide the secondary actions sheet while keeping active-run controls available");
324
329
  assert.match(css, /\.server-offline-panel/, "PWA/offline shell should style a backend-offline recovery panel");
325
- assert.match(css, /body:not\(\.pi-run-active\):not\(\.mobile-keyboard-open\) \.composer-row button\.primary \{ grid-column: span 4; \}/, "idle mobile composer should keep Actions and Send on one compact row");
330
+ assert.match(css, /body:not\(\.pi-run-active\):not\(\.mobile-keyboard-open\) \.composer-row button\.primary \{ grid-column: span 2; \}[\s\S]*?body:not\(\.pi-run-active\):not\(\.mobile-keyboard-open\) \.composer-btw-button\[hidden\] \+ button\.primary \{ grid-column: span 4; \}/, "idle mobile composer should keep Actions, /btw, and Send on one compact row with a hidden-button fallback");
326
331
  assert.match(css, /button\[hidden\] \{ display: none !important; \}/, "hidden bottom-row controls should not occupy layout space");
327
332
  assert.match(css, /\.statusbar-tui-footer \{[\s\S]*?gap:\s*0/, "default TUI-like footer should reduce statusbar chrome around the compact line");
328
333
  assert.match(css, /\.statusbar-git-footer \{[\s\S]*?--footer-chip-min-width:\s*7\.6rem;[\s\S]*?gap:\s*0\.58rem/, "enabled git-footer extension should keep styled spacing and one shared minimal chip width token");
@@ -363,6 +368,8 @@ assert.match(css, /\.footer-model-option\.active/, "footer model picker should s
363
368
  assert.match(app, /async function createPathPickerDirectory\(\)/, "cwd picker should implement create-directory behavior in the browser");
364
369
  assert.match(app, /function renderPathPickerDirectoryList\(\)[\s\S]*pathPickerDirectoryMatchesSearch/, "cwd picker should filter current-directory entries in the browser");
365
370
  assert.match(app, /elements\.pathPickerSearchInput\.addEventListener\("input", renderPathPickerDirectoryList\)/, "cwd picker should update directory matches as the user types");
371
+ assert.match(app, /function shouldOpenCwdChangeInNewTab\(tab\) \{[\s\S]*!!tab\?\.conversationStarted[\s\S]*activeTabHasConversationMessages\(tab\)[\s\S]*stateHasVisibleWork\(currentState\)[\s\S]*tabHasActiveAgent\(tab\)/, "cwd changes for started conversations should be routed to a new tab");
372
+ assert.match(app, /if \(shouldOpenCwdChangeInNewTab\(tab\)\) \{[\s\S]*await createTerminalTab\(cwd, \{ triggerButton: null \}\);[\s\S]*return;[\s\S]*window\.confirm\(`Restart/, "footer cwd changes should open a new tab before destructive cwd restarts once a session is active");
366
373
  assert.match(server, /async function createDirectoryPickerDirectory\(parentPath, nameValue, activeCwd\)/, "server should implement cwd picker directory creation");
367
374
  assert.match(server, /function directoryPickerActiveCwd\(req, url, body = \{\}\)/, "server should let the cwd picker run before any Pi tabs exist");
368
375
  assert.match(server, /url\.pathname === "\/api\/directories" && req\.method === "POST"/, "server should expose POST /api/directories for cwd picker directory creation");
@@ -538,6 +545,8 @@ assert.match(app, /GIT_FOOTER_WEBUI_STATUS_KEY = "git-footer-webui"/, "git foote
538
545
  assert.match(app, /function parseGitFooterWebuiPayloadRaw\(raw\)[\s\S]*GIT_FOOTER_WEBUI_PAYLOAD_TYPE[\s\S]*GIT_FOOTER_WEBUI_PAYLOAD_VERSION/, "Web UI footer should parse the structured payload emitted by git-footer-status");
539
546
  assert.match(app, /function normalizeFooterPayloadChangedFile\(value\)[\s\S]*FOOTER_CHANGED_FILE_KINDS\.has\(value\.kind\)[\s\S]*oldPath/, "git footer payload parsing should preserve changed-file details for changes popovers");
540
547
  assert.match(app, /const files = value\.files\.map\(normalizeFooterPayloadChangedFile\)\.filter\(Boolean\)\.slice\(0, 80\);[\s\S]*chip\.files = files;/, "git footer payload chips should retain bounded changed-file lists");
548
+ assert.match(app, /FOOTER_PAYLOAD_ACTIONS = new Set\(\["calibrate-current", "calibrate-probe"\]\)[\s\S]*chip\.action = value\.action;/, "git footer payload chips should preserve allowlisted actions such as PI calibration");
549
+ assert.match(app, /async function runGitFooterPiCalibration\(mode = "current", tabContext = activeTabContext\(\)\)[\s\S]*resolveAvailableCommandName\("calibrate", \{ rpcOnly: true \}\)[\s\S]*mode === "probe" \? `\/\$\{commandName\}` : `\/\$\{commandName\} current`[\s\S]*scheduleGitFooterPiCalibrationRefresh\(tabContext, mode === "probe" \? \[5000, 14000\] : \[600, 1600\]\)/, "clicking an uncalibrated PI footer chip should run current/probe calibration and refresh the git footer value");
541
550
  assert.match(app, /title: cleanFooterPayloadText\(value\.title, "", 4000\)/, "git footer tooltip titles should preserve long cwd paths instead of truncating at chip display length");
542
551
  assert.match(app, /const sourceTitle = cleanFooterPayloadText\(chip\?\.title, "", 4000\)/, "git footer tooltip rendering should keep full source titles for long cwd paths");
543
552
  assert.match(app, /function renderFooter\(\)[\s\S]*parseGitFooterWebuiPayload\(\)[\s\S]*renderGitFooterPayload\(footerPayloadWithLiveModel\(gitFooterPayload\)\)/, "detailed footer rendering should prefer the git-footer-status extension payload");
@@ -602,6 +611,9 @@ assert.match(app, /api\("\/api\/optional-features"/, "optional feature panel sho
602
611
  assert.match(app, /packageStatus\?\.updateAvailable[\s\S]*action\.textContent = "Update…"/, "optional feature package drift should turn the install action into an update action");
603
612
  assert.match(app, /optionalFeatureInstallMessages\.set\(featureId[\s\S]*waiting for package-manager output/, "optional feature installs should show running feedback while npm is active");
604
613
  assert.match(app, /api\("\/api\/optional-feature-install"/, "optional feature install action should call the backend installer endpoint");
614
+ assert.match(app, /id: "btwCommand"[\s\S]*?@firstpick\/pi-extension-btw/, "optional features should include the /btw companion");
615
+ assert.match(app, /BTW_OUTPUT_WIDGET_KEY = "btw:output"[\s\S]*function renderBtwOutputWidget/, "Web UI should render structured /btw output widgets");
616
+ assert.match(app, /if \(key\.startsWith\("btw:"\)\) return "btwCommand"/, "extension widget routing should associate /btw widgets with the optional feature");
605
617
  assert.match(app, /id: "safetyGuard"[\s\S]*?@firstpick\/pi-extension-safety-guard/, "optional features should include the safety guard companion");
606
618
  assert.match(app, /id: "tuiSkillsCommand"[\s\S]*?@firstpick\/pi-extension-setup-skills/, "optional features should include the TUI skills command companion");
607
619
  assert.match(app, /id: "tuiToolsCommand"[\s\S]*?@firstpick\/pi-extension-tools/, "optional features should include the TUI tools command companion");
@@ -827,6 +839,11 @@ assert.match(app, /busyPromptBehaviorMenu\?\.addEventListener\("click"[\s\S]*cho
827
839
  assert.match(app, /setBusyPromptBehavior\(controls\.busyBehavior\.select\.value\)/, "native settings should update the busy prompt behavior tag immediately");
828
840
  assert.match(app, /sendPromptFromModeButton\("steer", elements\.steerButton\)/, "Steer should show tooltip instead of silently doing nothing when input is empty");
829
841
  assert.match(app, /sendPromptFromModeButton\("follow-up", elements\.followUpButton\)/, "Follow-up should show tooltip instead of silently doing nothing when input is empty");
842
+ assert.match(app, /async function sendBtwQuestion\(question,[\s\S]*?`\/btw \$\{cleanQuestion\}`[\s\S]*?await sendPrompt\("prompt", message, \{ targetTabId, throwOnError: true \}\)/, "/btw helper should send text as an ephemeral slash command");
843
+ assert.match(app, /async function sendBtwPromptFromButton\(\)[\s\S]*?if \(!question\) \{\n\s+openBtwComposerWidget\(\);/, "empty /btw button should open the side-question widget input");
844
+ assert.match(app, /function renderBtwComposerForm\(\)[\s\S]*?form\.requestSubmit\(\)[\s\S]*?sendBtwQuestion\(question\)/, "/btw widget input should submit each message as a /btw trigger");
845
+ assert.match(app, /function makeBtwTransferIcon\(\)[\s\S]*?class", "btw-transfer-icon"[\s\S]*?function transferBtwContextToMain\(button\)[\s\S]*?`\/btw-transfer \$\{encoded\}`[\s\S]*?streamingBehavior: liveSteer \? "steer" : undefined/, "/btw widget should expose an iconified transfer-context action that steers during active runs");
846
+ assert.match(app, /async function sendPrompt\(kind = "prompt", explicitMessage, \{ targetTabId = activeTabId, throwOnError = false, streamingBehavior \} = \{\}\)[\s\S]*?if \(targetWasStreaming\) body\.streamingBehavior = streamingBehavior \|\| busyBehavior/, "prompt sending should support a per-call streaming behavior override");
830
847
  assert.match(app, /function runPublishWorkflow\(command\)[\s\S]*?sendPrompt\("prompt", `\/\$\{resolvedCommandName\}\$\{commandRest\}`\)/, "Publish workflows should send resolved slash commands directly without replacing the draft");
831
848
  assert.match(app, /async function runNativeCommandMenu\(command\)[\s\S]*?await handleNativeSlashSelectorCommand\(command\)/, "skills/tools command menu should open native selector dialogs directly");
832
849
  assert.match(app, /async function runNativeCommandMenu\(command\)[\s\S]*?sendPrompt\("prompt", command\)/, "generic native command menu should fall back to slash-command prompt execution");
@@ -848,7 +865,7 @@ for (const command of ["resume", "reload", "remote", "name", "clone", "settings"
848
865
  const id = command.replace(/^./, (letter) => letter.toUpperCase());
849
866
  assert.match(app, new RegExp(`options${id}Button\\.addEventListener\\("click", \\(\\) => runNativeCommandMenu\\("\\/${command}"\\)\\)`), `Options menu should launch /${command}`);
850
867
  }
851
- assert.match(app, /async function sendPrompt\(kind = "prompt", explicitMessage, \{ targetTabId = activeTabId, throwOnError = false \} = \{\}\)/, "prompt sending should accept direct messages that bypass the input field and optional target tab");
868
+ assert.match(app, /async function sendPrompt\(kind = "prompt", explicitMessage, \{ targetTabId = activeTabId, throwOnError = false, streamingBehavior \} = \{\}\)/, "prompt sending should accept direct messages that bypass the input field and optional target tab");
852
869
  assert.match(app, /const rawMessage = usesPromptInput \? elements\.promptInput\.value : explicitMessage/, "direct prompt sends should not read the input textarea");
853
870
  assert.match(app, /if \(usesPromptInput\) \{[\s\S]*?if \(targetStillActive\) \{[\s\S]*?elements\.promptInput\.value = "";/, "direct prompt sends should preserve the input textarea draft");
854
871
  assert.match(app, /make\("button", "command-item"\)[\s\S]*?sendPrompt\("prompt", `\/\$\{command\.name\}`\)/, "side-panel command clicks should send the slash command directly");
@@ -866,6 +883,7 @@ assert.match(app, /function openNativeTreeSelector\(\)[\s\S]*?\/api\/session-tre
866
883
  assert.match(app, /async function openNativeAuthSelector\(mode\)[\s\S]*?\/api\/auth-providers[\s\S]*?Browser login is not implemented yet/, "native /login should list provider status without browser credential entry");
867
884
  assert.match(app, /\/api\/auth-logout[\s\S]*?confirmed: true/, "native /logout should remove stored credentials through a confirmed localhost-only endpoint");
868
885
  assert.match(app, /const HIDDEN_COMMAND_NAMES = new Set\(\["webui-tree-navigate", "webui-helper"\]\)/, "internal Web UI helper commands should stay out of command pickers");
886
+ assert.match(app, /HIDDEN_COMMAND_NAMES\.add\("btw-transfer"\)/, "/btw transfer helper command should stay out of command pickers");
869
887
  assert.match(app, /function shouldSendPromptFromEnter\(event\)/, "prompt keyboard handling should be centralized");
870
888
  assert.match(app, /const PROMPT_HISTORY_STORAGE_KEY = "pi-webui-prompt-history"/, "prompt history should be persisted per browser for keyboard recall");
871
889
  assert.match(app, /function recallPreviousPromptFromHistory\(\)/, "prompt history should support recalling older prompts from the textarea");
@@ -1019,6 +1037,7 @@ assert.match(server, /WEBUI_TUI_NATIVE_PARITY\.json/, "native command descriptio
1019
1037
  assert.match(server, /function parseSlashCommand\(message\)/, "server should parse native slash commands before prompt forwarding");
1020
1038
  assert.match(server, /function generatedTabTitleFromPrompt\(message\)/, "server should derive concise automatic tab titles from first prompts");
1021
1039
  assert.match(server, /function maybeNameTabForConversation\(tab, command\)/, "server should auto-name default tabs when a conversation starts");
1040
+ assert.match(server, /function maybeNameTabForConversation\(tab, command\) \{[\s\S]*const shouldRename = !tab\.conversationStarted && tab\.titleSource !== "explicit";[\s\S]*tab\.conversationStarted = true;[\s\S]*if \(!shouldRename\) return false;/, "server should mark conversations as started even when an explicit title prevents auto-renaming");
1022
1041
  assert.match(server, /function createTabActivity\(/, "server should track per-tab activity for idle, working, and completed work");
1023
1042
  assert.match(server, /function reconcileTabActivityFromState\(tab, state/, "server should recover stale working tab activity from get_state snapshots");
1024
1043
  assert.match(server, /pendingExtensionUiRequests\(tab\)\.length > 0[\s\S]*?markTabWorking\(tab, timestamp\)/, "server should keep tabs with pending blockers in working activity until the blocker resolves");
@@ -1123,6 +1142,7 @@ assert.match(server, /type: "set_follow_up_mode"/, "server should expose follow-
1123
1142
  assert.match(server, /type: "set_auto_compaction"/, "server should expose auto-compaction changes for native /settings");
1124
1143
  assert.match(server, /@firstpick\/pi-themes-bundle/, "server should discover themes from the optional theme package");
1125
1144
  assert.match(server, /const OPTIONAL_FEATURE_PACKAGES = new Map/, "server should whitelist optional feature packages for install actions");
1145
+ assert.match(server, /\["btwCommand", "@firstpick\/pi-extension-btw"\]/, "server should allow installing the /btw optional feature");
1126
1146
  assert.match(server, /\["safetyGuard", "@firstpick\/pi-extension-safety-guard"\]/, "server should allow installing the safety guard optional feature");
1127
1147
  assert.match(server, /\["tuiSkillsCommand", "@firstpick\/pi-extension-setup-skills"\]/, "server should allow installing the TUI skills optional feature");
1128
1148
  assert.match(server, /\["tuiToolsCommand", "@firstpick\/pi-extension-tools"\]/, "server should allow installing the TUI tools optional feature");
@@ -1163,7 +1183,7 @@ assert.match(readme, /GET \/api\/path-suggestions\?tab=<tabId>&query=<path>/, "R
1163
1183
  assert.match(readme, /GET \/api\/optional-features/, "README should document optional feature status endpoint");
1164
1184
  assert.match(readme, /POST \/api\/optional-feature-install/, "README should document optional feature install endpoint");
1165
1185
  assert.match(readme, /server-persisted fast picks/, "README should describe server-persisted fast picks");
1166
- assert.match(readme, /browser notifications when a tab needs an extension UI response and an optional side-panel toggle for agent-done notifications/, "README should describe blocked-tab and agent-done notifications");
1186
+ assert.match(readme, /`\/btw` side-question output widgets with optional context transfer\/live steering, browser notifications when a tab needs an extension UI response, and an optional side-panel toggle for agent-done notifications/, "README should describe /btw, blocked-tab, and agent-done notifications");
1167
1187
  assert.match(readme, /blocked-tab browser notifications, and optional agent-done notifications require browser service-worker\/notification support/, "README should document notification requirements");
1168
1188
  assert.match(readme, /Side-panel theme picker backed by optional `@firstpick\/pi-themes-bundle` themes when loaded/, "README should describe optional theme selection");
1169
1189
  assert.match(readme, /## Optional companion packages/, "README should document optional Web UI companion packages");
@@ -1173,17 +1193,19 @@ assert.match(readme, /avoiding duplicate loads while keeping global `pi-webui` l
1173
1193
  assert.match(readme, /checks loaded Pi capabilities directly through RPC-visible commands and live widget events/, "README should document capability-based startup checks");
1174
1194
  assert.match(readme, /side panel shows each optional feature as enabled, disabled, installed-but-not-loaded, update-available, or install-needed/, "README should document optional feature side-panel controls");
1175
1195
  assert.match(readme, /Installing or updating a feature is an explicit, warned action with running\/failure feedback/, "README should document optional feature install and update warning behavior");
1176
- assert.match(readme, /\.\/start-webui\.sh --dev --cwd \/path\/to\/project/, "README should document the dev helper launcher");
1196
+ assert.match(readme, /\.\/dev\/scripts\/start-webui\.sh --dev --cwd \/path\/to\/project/, "README should document the dev helper launcher");
1177
1197
  assert.match(readme, /sync-pi-package-symlinks\.sh[\s\S]*only one copy is loaded/, "README should document dev companion symlink setup");
1178
1198
  assert.match(startScript, /--dev\)/, "start-webui.sh should accept a --dev flag");
1179
1199
  assert.match(startScript, /local_pi_webui_bin\(\)/, "start-webui.sh should resolve this checkout's local server entrypoint");
1200
+ assert.match(startScript, /candidate="\$\(package_root\)\/bin\/pi-webui\.mjs"/, "start-webui.sh should resolve the package-root bin from dev/scripts");
1180
1201
  assert.match(startScript, /webui_cmd=\(node "\$local_webui_bin"\)/, "start-webui.sh --dev should run the local bin with node");
1181
1202
  assert.match(startScript, /export PI_WEBUI_DEV=1/, "start-webui.sh --dev should mark the Web UI server as dev mode");
1182
1203
  assert.match(startScript, /"\$\{webui_cmd\[@\]\}" --cwd "\$cwd" --host "\$host" --port "\$port" "\$\{pass_args\[@\]\}"/, "start-webui.sh should launch through the selected server command without forwarding --dev");
1183
1204
 
1184
1205
  assert.match(pkg.scripts?.test || "", /node tests\/run-all\.mjs/, "package test script should run every tests/*.test.mjs through the shared runner");
1185
- assert.ok(pkg.files?.includes("start-webui.sh"), "npm package should include the Bash helper launcher");
1186
- assert.ok(pkg.files?.includes("start-webui.ps1"), "npm package should include the PowerShell helper launcher");
1206
+ assert.ok(!pkg.files?.includes("start-webui.sh"), "npm package should not list the moved Bash dev helper at the package root");
1207
+ assert.ok(!pkg.files?.includes("start-webui.ps1"), "npm package should not list the moved PowerShell dev helper at the package root");
1208
+ assert.ok(!pkg.files?.some((entry) => entry === "dev/scripts" || entry.startsWith("dev/scripts/")), "npm package should not publish development helper scripts");
1187
1209
  for (const [name, range] of Object.entries(companionDependencies)) {
1188
1210
  assert.equal(pkg.optionalDependencies?.[name], range, `webui package should optionally depend on ${name}`);
1189
1211
  assert.equal(pkg.dependencies?.[name], undefined, `webui package should not require optional companion ${name}`);
@@ -28,7 +28,7 @@ import {
28
28
  } from "../lib/native-command-adapter.mjs";
29
29
 
30
30
  const root = join(dirname(fileURLToPath(import.meta.url)), "..");
31
- const parity = JSON.parse(await readFile(join(root, "WEBUI_TUI_NATIVE_PARITY.json"), "utf8"));
31
+ const parity = JSON.parse(await readFile(join(root, "dev", "docs", "WEBUI_TUI_NATIVE_PARITY.json"), "utf8"));
32
32
 
33
33
  const localReq = { socket: { remoteAddress: "127.0.0.1" } };
34
34
  const remoteReq = { socket: { remoteAddress: "192.168.1.50" } };
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
 
6
6
  const root = join(dirname(fileURLToPath(import.meta.url)), "..");
7
7
  const [parityRaw, server, app, pkgRaw] = await Promise.all([
8
- readFile(join(root, "WEBUI_TUI_NATIVE_PARITY.json"), "utf8"),
8
+ readFile(join(root, "dev", "docs", "WEBUI_TUI_NATIVE_PARITY.json"), "utf8"),
9
9
  readFile(join(root, "bin", "pi-webui.mjs"), "utf8"),
10
10
  readFile(join(root, "public", "app.js"), "utf8"),
11
11
  readFile(join(root, "package.json"), "utf8"),
@@ -181,7 +181,7 @@ assert.match(app, /let userBashQueuesByTab = new Map\(\)/, "frontend should trac
181
181
  assert.match(app, /enqueueUserBashCommand\(parsed, \{ usesPromptInput, targetTabId \}\)/, "user bash should enqueue while an active or queued bash command exists");
182
182
  assert.match(server, /function sendQueuedBashCommand\(tab, command\)/, "server should serialize user bash commands per tab");
183
183
  assert.match(server, /command\.type === "bash"[\s\S]*?await sendQueuedBashCommand\(tab, command\)[\s\S]*?: await tab\.rpc\.send\(command\)/, "generic POST handling should route bash through the FIFO queue");
184
- assert.ok(pkg.files.includes("WEBUI_TUI_NATIVE_PARITY.json"), "published package should include the native parity matrix");
184
+ assert.ok(pkg.files.includes("dev/docs/WEBUI_TUI_NATIVE_PARITY.json"), "published package should include the native parity matrix");
185
185
  assert.ok(pkg.files.includes("lib"), "published package should include shared Web UI foundation modules");
186
186
  assert.ok(pkg.files.includes("webui-rpc-helper.mjs"), "published package should include the Web UI RPC helper extension");
187
187