@firstpick/pi-package-webui 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firstpick/pi-package-webui",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Pi Web UI companion package with a local browser UI CLI plus /webui-start and /webui-status commands.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/public/app.js CHANGED
@@ -5,7 +5,11 @@ const elements = {
5
5
  webuiDevBadge: $("#webuiDevBadge"),
6
6
  tabBar: $("#tabBar"),
7
7
  terminalTabsToggleButton: $("#terminalTabsToggleButton"),
8
+ newTabMenu: $("#newTabMenu"),
8
9
  newTabButton: $("#newTabButton"),
10
+ newTabMenuPanel: $("#newTabMenuPanel"),
11
+ newTabCurrentDirectoryButton: $("#newTabCurrentDirectoryButton"),
12
+ newTabChooseDirectoryButton: $("#newTabChooseDirectoryButton"),
9
13
  closeAllTabsButton: $("#closeAllTabsButton"),
10
14
  statusBar: $("#statusBar"),
11
15
  serverOfflinePanel: $("#serverOfflinePanel"),
@@ -181,6 +185,7 @@ let pathFastPicksReady = false;
181
185
  let pathFastPicksLoadPromise = null;
182
186
  let mobileTabsExpanded = false;
183
187
  let openTerminalTabGroupKey = null;
188
+ let newTabMenuOpen = false;
184
189
  let nativeCommandMenuOpen = false;
185
190
  let optionsMenuOpen = false;
186
191
  let availableCommands = [];
@@ -2873,6 +2878,34 @@ function clearOpenTerminalTabGroup(groupKey, { force = false } = {}) {
2873
2878
  syncTabPolling();
2874
2879
  }
2875
2880
 
2881
+ function setNewTabMenuOpen(open) {
2882
+ newTabMenuOpen = !!open;
2883
+ elements.newTabButton?.setAttribute("aria-expanded", newTabMenuOpen ? "true" : "false");
2884
+ elements.newTabButton?.classList.toggle("menu-open", newTabMenuOpen);
2885
+ elements.newTabMenu?.classList.toggle("open", newTabMenuOpen);
2886
+ }
2887
+
2888
+ function openNewTabMenu() {
2889
+ setPublishMenuOpen(false);
2890
+ setNativeCommandMenuOpen(false);
2891
+ setOptionsMenuOpen(false);
2892
+ setNewTabMenuOpen(true);
2893
+ }
2894
+
2895
+ function focusNewTabMenuItem(direction = "first") {
2896
+ const items = [elements.newTabCurrentDirectoryButton, elements.newTabChooseDirectoryButton].filter(Boolean);
2897
+ const item = direction === "last" ? items.at(-1) : items[0];
2898
+ item?.focus({ preventScroll: true });
2899
+ }
2900
+
2901
+ function moveNewTabMenuFocus(delta) {
2902
+ const items = [elements.newTabCurrentDirectoryButton, elements.newTabChooseDirectoryButton].filter(Boolean);
2903
+ if (!items.length) return;
2904
+ const currentIndex = Math.max(0, items.indexOf(document.activeElement));
2905
+ const nextIndex = (currentIndex + delta + items.length) % items.length;
2906
+ items[nextIndex].focus({ preventScroll: true });
2907
+ }
2908
+
2876
2909
  function renderTabs() {
2877
2910
  const active = activeTab();
2878
2911
  const activeIndicator = active ? tabIndicator(active) : null;
@@ -2891,7 +2924,7 @@ function renderTabs() {
2891
2924
  for (const tab of group.tabs) elements.tabBar.append(renderTerminalTab(tab));
2892
2925
  }
2893
2926
  }
2894
- elements.tabBar.append(elements.newTabButton);
2927
+ elements.tabBar.append(elements.newTabMenu);
2895
2928
  elements.closeAllTabsButton.disabled = tabs.length === 0;
2896
2929
  updateTerminalTabGroupOpenState();
2897
2930
  setMobileTabsExpanded(mobileTabsExpanded);
@@ -2933,12 +2966,17 @@ async function switchTab(tabId) {
2933
2966
  if (isCurrentTabContext(tabContext)) markTabOutputSeen();
2934
2967
  }
2935
2968
 
2936
- async function createTerminalTab(cwd = activeTab()?.cwd, { triggerButton = elements.newTabButton } = {}) {
2969
+ function currentDirectoryForNewTab() {
2970
+ return latestWorkspace?.cwd || activeTab()?.cwd || "";
2971
+ }
2972
+
2973
+ async function createTerminalTab(cwd = currentDirectoryForNewTab(), { triggerButton = elements.newTabButton } = {}) {
2937
2974
  setMobileTabsExpanded(false);
2975
+ setNewTabMenuOpen(false);
2938
2976
  const disabledButtons = new Set([elements.newTabButton, triggerButton].filter(Boolean));
2939
2977
  for (const button of disabledButtons) button.disabled = true;
2940
2978
  try {
2941
- const response = await api("/api/tabs", { method: "POST", body: { cwd: cwd || activeTab()?.cwd }, scoped: false });
2979
+ const response = await api("/api/tabs", { method: "POST", body: { cwd: cwd || currentDirectoryForNewTab() }, scoped: false });
2942
2980
  tabs = response.data?.tabs || tabs;
2943
2981
  syncTabMetadata(tabs);
2944
2982
  const tab = response.data?.tab;
@@ -2954,6 +2992,16 @@ async function createTerminalTab(cwd = activeTab()?.cwd, { triggerButton = eleme
2954
2992
  }
2955
2993
  }
2956
2994
 
2995
+ async function createTerminalTabFromChosenDirectory({ triggerButton = elements.newTabChooseDirectoryButton } = {}) {
2996
+ const sourceTab = activeTab();
2997
+ const initialCwd = currentDirectoryForNewTab();
2998
+ setMobileTabsExpanded(false);
2999
+ setNewTabMenuOpen(false);
3000
+ const cwd = await pickCwd(sourceTab || { id: "new-tab", title: "new tab" }, initialCwd, { title: "Choose CWD for new tab" });
3001
+ if (!cwd) return;
3002
+ await createTerminalTab(cwd, { triggerButton });
3003
+ }
3004
+
2957
3005
  function tabHasActiveAgent(tab) {
2958
3006
  const activity = activityForTab(tab);
2959
3007
  const indicator = tabIndicator(tab);
@@ -4469,12 +4517,13 @@ function closePathPicker(cwd) {
4469
4517
  state.resolve(cwd || null);
4470
4518
  }
4471
4519
 
4472
- function pickCwd(tab, initialCwd) {
4520
+ function pickCwd(tab, initialCwd, { title } = {}) {
4473
4521
  if (pathPickerState) return Promise.resolve(null);
4474
4522
 
4475
4523
  return new Promise((resolve) => {
4476
- pathPickerState = { tabId: tab.id, cwd: initialCwd, requestId: 0, loading: false, creatingDirectory: false, directories: [], filteredDirectories: [], resolve };
4477
- elements.pathPickerTitle.textContent = `Choose CWD for ${tab.title}`;
4524
+ const pickerTab = tab || { id: "path-picker", title: "tab" };
4525
+ pathPickerState = { tabId: pickerTab.id, cwd: initialCwd, requestId: 0, loading: false, creatingDirectory: false, directories: [], filteredDirectories: [], resolve };
4526
+ elements.pathPickerTitle.textContent = title || `Choose CWD for ${pickerTab.title}`;
4478
4527
  elements.pathPickerCurrent.textContent = "Loading…";
4479
4528
  elements.pathPickerCreateNameInput.value = "";
4480
4529
  elements.pathPickerSearchInput.value = "";
@@ -11051,7 +11100,47 @@ elements.followUpButton.addEventListener("click", () => sendPromptFromModeButton
11051
11100
  elements.terminalTabsToggleButton.addEventListener("click", () => {
11052
11101
  setMobileTabsExpanded(!document.body.classList.contains("mobile-tabs-expanded"));
11053
11102
  });
11054
- elements.newTabButton.addEventListener("click", () => createTerminalTab());
11103
+ elements.newTabButton.addEventListener("click", (event) => {
11104
+ event.stopPropagation();
11105
+ openNewTabMenu();
11106
+ });
11107
+ elements.newTabButton.addEventListener("keydown", (event) => {
11108
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp" && event.key !== "Enter" && event.key !== " ") return;
11109
+ event.preventDefault();
11110
+ openNewTabMenu();
11111
+ focusNewTabMenuItem(event.key === "ArrowUp" ? "last" : "first");
11112
+ });
11113
+ elements.newTabMenuPanel?.addEventListener("keydown", (event) => {
11114
+ if (event.key === "Escape") {
11115
+ event.preventDefault();
11116
+ setNewTabMenuOpen(false);
11117
+ elements.newTabButton.focus({ preventScroll: true });
11118
+ } else if (event.key === "ArrowDown") {
11119
+ event.preventDefault();
11120
+ moveNewTabMenuFocus(1);
11121
+ } else if (event.key === "ArrowUp") {
11122
+ event.preventDefault();
11123
+ moveNewTabMenuFocus(-1);
11124
+ } else if (event.key === "Home") {
11125
+ event.preventDefault();
11126
+ focusNewTabMenuItem("first");
11127
+ } else if (event.key === "End") {
11128
+ event.preventDefault();
11129
+ focusNewTabMenuItem("last");
11130
+ }
11131
+ });
11132
+ elements.newTabMenu?.addEventListener("pointerenter", () => openNewTabMenu());
11133
+ elements.newTabMenu?.addEventListener("pointerleave", () => {
11134
+ if (!elements.newTabMenu?.contains(document.activeElement)) setNewTabMenuOpen(false);
11135
+ });
11136
+ elements.newTabMenu?.addEventListener("focusin", () => openNewTabMenu());
11137
+ elements.newTabMenu?.addEventListener("focusout", () => {
11138
+ setTimeout(() => {
11139
+ if (!elements.newTabMenu?.contains(document.activeElement)) setNewTabMenuOpen(false);
11140
+ }, 0);
11141
+ });
11142
+ elements.newTabCurrentDirectoryButton?.addEventListener("click", () => createTerminalTab(currentDirectoryForNewTab(), { triggerButton: elements.newTabCurrentDirectoryButton }));
11143
+ elements.newTabChooseDirectoryButton?.addEventListener("click", () => createTerminalTabFromChosenDirectory({ triggerButton: elements.newTabChooseDirectoryButton }));
11055
11144
  elements.closeAllTabsButton.addEventListener("click", () => closeAllTerminalTabs());
11056
11145
  elements.gitWorkflowButton.addEventListener("click", () => {
11057
11146
  setComposerActionsOpen(false);
@@ -11339,6 +11428,9 @@ document.addEventListener("pointerdown", (event) => {
11339
11428
  if (openTerminalTabGroupKey && !event.target?.closest?.(".terminal-tab-group")) {
11340
11429
  clearOpenTerminalTabGroup(openTerminalTabGroupKey);
11341
11430
  }
11431
+ if (newTabMenuOpen && !event.target?.closest?.(".terminal-new-tab-menu")) {
11432
+ setNewTabMenuOpen(false);
11433
+ }
11342
11434
  if (document.body.classList.contains("composer-actions-open") && !elements.composer.contains(event.target)) {
11343
11435
  setComposerActionsOpen(false);
11344
11436
  }
@@ -11352,6 +11444,7 @@ document.addEventListener("pointerdown", (event) => {
11352
11444
  setOptionsMenuOpen(false);
11353
11445
  }
11354
11446
  if (document.body.classList.contains("mobile-tabs-expanded") && !elements.tabBar.contains(event.target) && !elements.terminalTabsToggleButton.contains(event.target)) {
11447
+ setNewTabMenuOpen(false);
11355
11448
  setMobileTabsExpanded(false);
11356
11449
  }
11357
11450
  if (isFooterPickerOpen() && !elements.statusBar.contains(event.target)) {
@@ -11446,11 +11539,16 @@ window.addEventListener("keydown", (event) => {
11446
11539
  setOptionsMenuOpen(false);
11447
11540
  return;
11448
11541
  }
11542
+ if (newTabMenuOpen) {
11543
+ setNewTabMenuOpen(false);
11544
+ return;
11545
+ }
11449
11546
  if (document.body.classList.contains("composer-actions-open")) {
11450
11547
  setComposerActionsOpen(false);
11451
11548
  return;
11452
11549
  }
11453
11550
  if (document.body.classList.contains("mobile-tabs-expanded")) {
11551
+ setNewTabMenuOpen(false);
11454
11552
  setMobileTabsExpanded(false);
11455
11553
  return;
11456
11554
  }
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=24" />
15
+ <link rel="stylesheet" href="/styles.css?v=25" />
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">
@@ -50,7 +50,13 @@
50
50
  <header class="terminal-tabs-shell">
51
51
  <button id="terminalTabsToggleButton" class="terminal-tabs-toggle-button" type="button" aria-controls="tabBar" aria-expanded="false">Tabs</button>
52
52
  <div id="tabBar" class="terminal-tabs" role="tablist" aria-label="Pi terminal tabs">
53
- <button id="newTabButton" class="terminal-new-tab-button" type="button" title="Start a separate isolated Pi terminal">+ Tab</button>
53
+ <div id="newTabMenu" class="terminal-new-tab-menu composer-publish-menu">
54
+ <button id="newTabButton" class="terminal-new-tab-button" type="button" title="Start a separate isolated Pi terminal" aria-haspopup="menu" aria-expanded="false" aria-controls="newTabMenuPanel">+ Tab ▾</button>
55
+ <div id="newTabMenuPanel" class="terminal-new-tab-menu-panel composer-publish-menu-panel" role="menu" aria-labelledby="newTabButton">
56
+ <button id="newTabCurrentDirectoryButton" class="terminal-new-tab-menu-item composer-publish-menu-item" type="button" role="menuitem"><span>Current Directory</span></button>
57
+ <button id="newTabChooseDirectoryButton" class="terminal-new-tab-menu-item composer-publish-menu-item" type="button" role="menuitem"><span>Choose Directory</span></button>
58
+ </div>
59
+ </div>
54
60
  </div>
55
61
  <button id="closeAllTabsButton" class="terminal-close-all-button" type="button" title="Close all terminal tabs">Close all Tabs</button>
56
62
  </header>
@@ -467,6 +473,6 @@
467
473
  </form>
468
474
  </dialog>
469
475
 
470
- <script type="module" src="/app.js?v=24"></script>
476
+ <script type="module" src="/app.js?v=25"></script>
471
477
  </body>
472
478
  </html>
@@ -1,4 +1,4 @@
1
- const CACHE_NAME = "pi-webui-pwa-v23";
1
+ const CACHE_NAME = "pi-webui-pwa-v24";
2
2
  const APP_SHELL = [
3
3
  "/",
4
4
  "/index.html",
package/public/styles.css CHANGED
@@ -1173,12 +1173,18 @@ body.side-panel-collapsed .terminal-tabs-shell {
1173
1173
  }
1174
1174
  .terminal-tabs-shell:has(.terminal-tab-group:hover),
1175
1175
  .terminal-tabs-shell:has(.terminal-tab-group:focus-within),
1176
- .terminal-tabs-shell:has(.terminal-tab-group.menu-open) {
1176
+ .terminal-tabs-shell:has(.terminal-tab-group.menu-open),
1177
+ .terminal-tabs-shell:has(.terminal-new-tab-menu:hover),
1178
+ .terminal-tabs-shell:has(.terminal-new-tab-menu:focus-within),
1179
+ .terminal-tabs-shell:has(.terminal-new-tab-menu.open) {
1177
1180
  z-index: 90;
1178
1181
  }
1179
1182
  .terminal-tabs:has(.terminal-tab-group:hover),
1180
1183
  .terminal-tabs:has(.terminal-tab-group:focus-within),
1181
- .terminal-tabs:has(.terminal-tab-group.menu-open) {
1184
+ .terminal-tabs:has(.terminal-tab-group.menu-open),
1185
+ .terminal-tabs:has(.terminal-new-tab-menu:hover),
1186
+ .terminal-tabs:has(.terminal-new-tab-menu:focus-within),
1187
+ .terminal-tabs:has(.terminal-new-tab-menu.open) {
1182
1188
  overflow: visible;
1183
1189
  }
1184
1190
  .terminal-tab {
@@ -1445,6 +1451,10 @@ body.side-panel-collapsed .terminal-tabs-shell {
1445
1451
  .terminal-tab-group-close {
1446
1452
  border-left-color: rgba(243, 139, 168, 0.18);
1447
1453
  }
1454
+ .terminal-new-tab-menu.composer-publish-menu {
1455
+ flex: 0 0 auto;
1456
+ margin-right: 0;
1457
+ }
1448
1458
  .terminal-new-tab-button {
1449
1459
  flex: 0 0 auto;
1450
1460
  padding: 0.45rem 0.72rem;
@@ -1455,10 +1465,33 @@ body.side-panel-collapsed .terminal-tabs-shell {
1455
1465
  linear-gradient(120deg, rgba(245, 194, 231, 0.12), rgba(137, 180, 250, 0.08)),
1456
1466
  rgba(var(--ctp-crust-rgb), 0.58);
1457
1467
  }
1458
- .terminal-new-tab-button:hover {
1468
+ .terminal-new-tab-button:hover,
1469
+ .terminal-new-tab-button.menu-open {
1470
+ color: #11111b;
1471
+ border-color: transparent;
1472
+ background: linear-gradient(120deg, var(--ctp-pink), var(--ctp-mauve), var(--ctp-blue));
1473
+ }
1474
+ .terminal-new-tab-menu.open .terminal-new-tab-button {
1475
+ border-color: rgba(245, 194, 231, 0.58);
1476
+ }
1477
+ .terminal-new-tab-menu .composer-publish-menu-panel {
1478
+ inset: 100% 0 auto auto;
1479
+ padding-top: 0.38rem;
1480
+ padding-bottom: 0;
1481
+ }
1482
+ .terminal-new-tab-menu .composer-publish-menu-item {
1483
+ color: var(--ctp-pink);
1484
+ border-color: rgba(245, 194, 231, 0.32);
1485
+ background:
1486
+ linear-gradient(120deg, rgba(245, 194, 231, 0.12), rgba(137, 180, 250, 0.08)),
1487
+ var(--ctp-crust);
1488
+ }
1489
+ .terminal-new-tab-menu .composer-publish-menu-item:hover,
1490
+ .terminal-new-tab-menu .composer-publish-menu-item:focus-visible {
1459
1491
  color: #11111b;
1460
1492
  border-color: transparent;
1461
1493
  background: linear-gradient(120deg, var(--ctp-pink), var(--ctp-mauve), var(--ctp-blue));
1494
+ box-shadow: 0 0 1rem rgba(245, 194, 231, 0.20);
1462
1495
  }
1463
1496
 
1464
1497
  .widget-area {
@@ -4589,7 +4622,10 @@ summary { cursor: pointer; color: var(--warning); }
4589
4622
  }
4590
4623
  .terminal-tabs:has(.terminal-tab-group:hover),
4591
4624
  .terminal-tabs:has(.terminal-tab-group:focus-within),
4592
- .terminal-tabs:has(.terminal-tab-group.menu-open) {
4625
+ .terminal-tabs:has(.terminal-tab-group.menu-open),
4626
+ .terminal-tabs:has(.terminal-new-tab-menu:hover),
4627
+ .terminal-tabs:has(.terminal-new-tab-menu:focus-within),
4628
+ .terminal-tabs:has(.terminal-new-tab-menu.open) {
4593
4629
  overflow: auto;
4594
4630
  }
4595
4631
  .terminal-tab { min-width: min(11rem, 100%); max-width: 100%; flex: 1 1 9rem; }
@@ -4620,9 +4656,27 @@ summary { cursor: pointer; color: var(--warning); }
4620
4656
  max-width: 100%;
4621
4657
  flex: 0 0 auto;
4622
4658
  }
4659
+ .terminal-new-tab-menu {
4660
+ width: auto;
4661
+ flex-wrap: wrap;
4662
+ }
4663
+ .terminal-new-tab-menu.open,
4664
+ .terminal-new-tab-menu:focus-within {
4665
+ flex-basis: 100%;
4666
+ }
4667
+ .terminal-new-tab-menu .composer-publish-menu-panel {
4668
+ position: static;
4669
+ width: 100%;
4670
+ min-width: min(11rem, 100%);
4671
+ max-width: 100%;
4672
+ margin: 0.34rem 0 0;
4673
+ padding-top: 0;
4674
+ overflow: visible;
4675
+ }
4623
4676
  .terminal-tab-button,
4624
4677
  .terminal-tab-close,
4625
4678
  .terminal-new-tab-button,
4679
+ .terminal-new-tab-menu-item,
4626
4680
  .terminal-tab-group-add { min-height: 44px; }
4627
4681
  .terminal-tab-button { padding: 0.32rem 0.52rem; }
4628
4682
  .terminal-tab-title { font-size: 0.78rem; }
@@ -4637,6 +4691,11 @@ summary { cursor: pointer; color: var(--warning); }
4637
4691
  padding: 0.36rem 0.58rem;
4638
4692
  white-space: nowrap;
4639
4693
  }
4694
+ .terminal-new-tab-menu-item {
4695
+ width: 100%;
4696
+ max-width: 100%;
4697
+ padding: 0.42rem 0.58rem;
4698
+ }
4640
4699
  .widget-area {
4641
4700
  display: block;
4642
4701
  max-height: 34dvh;
@@ -41,6 +41,11 @@ assert.match(html, /<meta name="theme-color" content="#11111b" \/>/, "PWA should
41
41
  assert.match(html, /<link rel="manifest" href="\/manifest\.webmanifest" \/>/, "PWA should expose a web app manifest");
42
42
  assert.match(html, /<link rel="apple-touch-icon" href="\/apple-touch-icon\.png" \/>/, "PWA should expose the conventional iOS home-screen icon path");
43
43
  assert.match(html, /id="terminalTabsToggleButton"/, "mobile should expose a compact terminal-tabs toggle");
44
+ assert.match(html, /id="newTabMenu" class="terminal-new-tab-menu composer-publish-menu"/, "new-tab control should reuse the shared composer dropdown container");
45
+ assert.match(html, /id="newTabButton"[\s\S]*aria-haspopup="menu"[\s\S]*aria-controls="newTabMenuPanel"/, "new-tab control should open a dropdown menu");
46
+ assert.match(html, /id="newTabMenuPanel" class="terminal-new-tab-menu-panel composer-publish-menu-panel"/, "new-tab menu should reuse the shared composer dropdown panel");
47
+ assert.match(html, /id="newTabCurrentDirectoryButton" class="terminal-new-tab-menu-item composer-publish-menu-item"[\s\S]*<span>Current Directory<\/span>/, "new-tab menu should offer the active cwd");
48
+ assert.match(html, /id="newTabChooseDirectoryButton" class="terminal-new-tab-menu-item composer-publish-menu-item"[\s\S]*<span>Choose Directory<\/span>/, "new-tab menu should offer the cwd picker");
44
49
  assert.match(html, /id="closeAllTabsButton"[\s\S]*?>Close all Tabs<\/button>/, "tab header should expose a top-right close-all tabs action");
45
50
  assert.match(html, /id="sidePanelBackdrop"/, "mobile side panel needs an overlay/backdrop close target");
46
51
  assert.match(html, /<strong class="side-panel-title">[\s\S]*Control Deck[\s\S]*id="webuiVersionBadge"[\s\S]*id="webuiDevBadge"/, "Control Deck title should expose Web UI version and dev badges");
@@ -216,6 +221,8 @@ assert.match(css, /\.composer-actions-panel > \.composer-publish-menu[\s\S]*?gri
216
221
  assert.match(css, /\.composer-actions-panel[\s\S]*?bottom:\s*calc\(100% \+ 0\.42rem\)/, "mobile composer actions should open as an above-composer sheet");
217
222
  assert.match(css, /body\.composer-actions-open \.composer-actions-panel \{ display: grid; \}/, "composer actions panel should only open when toggled");
218
223
  assert.match(css, /\.terminal-tabs-toggle-button \{ display: none; \}/, "terminal tab toggle should be hidden outside mobile CSS");
224
+ assert.match(css, /\.terminal-new-tab-menu \.composer-publish-menu-panel \{[\s\S]*?inset:\s*100% 0 auto auto;[\s\S]*?padding-top:\s*0\.38rem/, "new-tab dropdown should reuse the shared composer panel and open below the tab bar");
225
+ assert.match(css, /\.terminal-new-tab-menu \.composer-publish-menu-item \{[\s\S]*?color:\s*var\(--ctp-pink\)/, "new-tab dropdown items should reuse shared composer menu items with a tab-specific color");
219
226
  assert.match(css, /\.terminal-close-all-button \{[\s\S]*?color:\s*var\(--ctp-red\)/, "close-all tabs action should render as a top-right destructive tab action");
220
227
  assert.match(css, /\.terminal-tabs\.terminal-tabs-dense \{[\s\S]*?flex-wrap:\s*wrap/, "large terminal tab sets should wrap into a readable dense tab strip");
221
228
  assert.match(css, /\.terminal-tab-group-close \{[\s\S]*?border-left-color/, "terminal tab groups should style their close button distinctly");
@@ -737,7 +744,7 @@ assert.equal(manifest.start_url, "/", "PWA manifest should start at the web UI r
737
744
  assert.ok(manifest.icons?.some((icon) => icon.src === "/apple-touch-icon.png" && icon.sizes === "180x180"), "PWA manifest should include a conventional 180px apple touch icon");
738
745
  assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-192.png" && icon.sizes === "192x192"), "PWA manifest should include a 192px icon");
739
746
  assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-512.png" && icon.sizes === "512x512"), "PWA manifest should include a 512px icon");
740
- assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v23"/, "PWA service worker should define an app-shell cache");
747
+ assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v24"/, "PWA service worker should define an app-shell cache");
741
748
  assert.match(serviceWorker, /self\.addEventListener\("notificationclick"/, "PWA service worker should focus Web UI when blocked-tab notifications are clicked");
742
749
  assert.match(serviceWorker, /event\.notification\.data\?\.url/, "blocked-tab notifications should carry a URL for service-worker click handling");
743
750
  assert.match(serviceWorker, /"\/apple-touch-icon\.png"/, "PWA service worker should cache the apple touch icon");