@firstpick/pi-package-webui 0.3.3 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firstpick/pi-package-webui",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
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
  "homepage": "https://github.com/Firstp1ck/npm-packages/tree/main/pi-package-webui#readme",
package/public/app.js CHANGED
@@ -83,6 +83,8 @@ const elements = {
83
83
  setThinkingButton: $("#setThinkingButton"),
84
84
  thinkingVisibilityToggle: $("#thinkingVisibilityToggle"),
85
85
  thinkingVisibilityStatus: $("#thinkingVisibilityStatus"),
86
+ terminalTabsLayoutSelect: $("#terminalTabsLayoutSelect"),
87
+ terminalTabsLayoutStatus: $("#terminalTabsLayoutStatus"),
86
88
  themeSelect: $("#themeSelect"),
87
89
  backgroundInput: $("#backgroundInput"),
88
90
  backgroundChooseButton: $("#backgroundChooseButton"),
@@ -251,6 +253,7 @@ let blockedTabNotificationPermissionRequested = false;
251
253
  let blockedTabNotificationFallbackNoted = false;
252
254
  let agentDoneNotificationsEnabled = false;
253
255
  let thinkingOutputVisible = true;
256
+ let terminalTabsLayout = "top";
254
257
  let webuiSettings = {};
255
258
  let busyPromptBehavior = "followUp";
256
259
  let autocompleteMaxVisible = 12;
@@ -296,6 +299,7 @@ const TAB_STORAGE_KEY = "pi-webui-active-tab";
296
299
  const PATH_FAST_PICKS_STORAGE_KEY = "pi-webui-path-fast-picks";
297
300
  const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications";
298
301
  const THINKING_VISIBILITY_STORAGE_KEY = "pi-webui-thinking-visible";
302
+ const TERMINAL_TABS_LAYOUT_STORAGE_KEY = "pi-webui-terminal-tabs-layout";
299
303
  const TOOL_OUTPUT_EXPANDED_STORAGE_KEY = "pi-webui-tool-output-expanded";
300
304
  const THEME_STORAGE_KEY = "pi-webui-theme";
301
305
  const CUSTOM_BACKGROUND_STORAGE_KEY = "pi-webui-custom-background";
@@ -325,6 +329,8 @@ const LONG_INPUT_ATTACHMENT_MIME_TYPE = "text/plain";
325
329
  const INLINE_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
326
330
  const BACKGROUND_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
327
331
  const DEFAULT_THEME_NAME = "catppuccin-mocha";
332
+ const TERMINAL_TABS_LAYOUTS = new Set(["top", "left"]);
333
+ const TERMINAL_TABS_LAYOUT_LABELS = { top: "Top bar", left: "Left sidebar" };
328
334
  const MOBILE_VIEW_QUERY = "(max-width: 720px), (max-device-width: 720px), (pointer: coarse) and (hover: none)";
329
335
  const SIDE_PANEL_OVERLAY_QUERY = "(max-width: 1050px), (max-device-width: 720px), (pointer: coarse) and (hover: none)";
330
336
  const CHAT_BOTTOM_THRESHOLD_PX = 96;
@@ -789,6 +795,26 @@ function persistThinkingOutputVisible(visible) {
789
795
  }
790
796
  }
791
797
 
798
+ function normalizeTerminalTabsLayout(value) {
799
+ return TERMINAL_TABS_LAYOUTS.has(value) ? value : "top";
800
+ }
801
+
802
+ function readStoredTerminalTabsLayout() {
803
+ try {
804
+ return normalizeTerminalTabsLayout(localStorage.getItem(TERMINAL_TABS_LAYOUT_STORAGE_KEY));
805
+ } catch {
806
+ return "top";
807
+ }
808
+ }
809
+
810
+ function persistTerminalTabsLayout(layout) {
811
+ try {
812
+ localStorage.setItem(TERMINAL_TABS_LAYOUT_STORAGE_KEY, normalizeTerminalTabsLayout(layout));
813
+ } catch {
814
+ // Ignore storage failures; the layout control should still work for this page load.
815
+ }
816
+ }
817
+
792
818
  function readStoredToolOutputExpanded() {
793
819
  try {
794
820
  return localStorage.getItem(TOOL_OUTPUT_EXPANDED_STORAGE_KEY) === "1";
@@ -816,6 +842,30 @@ function renderThinkingVisibilityToggle() {
816
842
  if (elements.thinkingVisibilityStatus) elements.thinkingVisibilityStatus.textContent = thinkingVisibilityStatusText();
817
843
  }
818
844
 
845
+ function terminalTabsLayoutStatusText(layout = terminalTabsLayout) {
846
+ return TERMINAL_TABS_LAYOUT_LABELS[normalizeTerminalTabsLayout(layout)] || TERMINAL_TABS_LAYOUT_LABELS.top;
847
+ }
848
+
849
+ function renderTerminalTabsLayoutControl() {
850
+ const layout = normalizeTerminalTabsLayout(terminalTabsLayout);
851
+ if (elements.terminalTabsLayoutSelect) elements.terminalTabsLayoutSelect.value = layout;
852
+ if (elements.terminalTabsLayoutStatus) elements.terminalTabsLayoutStatus.textContent = terminalTabsLayoutStatusText(layout);
853
+ }
854
+
855
+ function setTerminalTabsLayout(layout, { persist = true, announce = false } = {}) {
856
+ const next = normalizeTerminalTabsLayout(layout);
857
+ terminalTabsLayout = next;
858
+ document.body.classList.toggle("terminal-tabs-left", next === "left");
859
+ if (next === "left" && mobileTabsExpanded) setMobileTabsExpanded(false);
860
+ if (persist) persistTerminalTabsLayout(next);
861
+ renderTerminalTabsLayoutControl();
862
+ if (announce) addEvent(`terminal tabs layout changed to ${terminalTabsLayoutStatusText(next).toLowerCase()}`);
863
+ }
864
+
865
+ function restoreTerminalTabsLayoutSetting() {
866
+ setTerminalTabsLayout(readStoredTerminalTabsLayout(), { persist: false });
867
+ }
868
+
819
869
  function removeStreamingThinkingBubble() {
820
870
  streamThinkingBubble?.remove();
821
871
  streamThinkingBubble = null;
@@ -12831,6 +12881,11 @@ if (elements.thinkingVisibilityToggle) {
12831
12881
  setThinkingOutputVisible(elements.thinkingVisibilityToggle.checked, { announce: true });
12832
12882
  });
12833
12883
  }
12884
+ if (elements.terminalTabsLayoutSelect) {
12885
+ elements.terminalTabsLayoutSelect.addEventListener("change", () => {
12886
+ setTerminalTabsLayout(elements.terminalTabsLayoutSelect.value, { announce: true });
12887
+ });
12888
+ }
12834
12889
  elements.toggleSidePanelButton.addEventListener("click", () => {
12835
12890
  setSidePanelCollapsed(true);
12836
12891
  });
@@ -13142,6 +13197,7 @@ initializeThemes().catch((error) => {
13142
13197
  initializeFastPicks().catch((error) => addEvent(`failed to initialize path fast picks: ${error.message}`, "error"));
13143
13198
  restoreAgentDoneNotificationsSetting();
13144
13199
  restoreThinkingVisibilitySetting();
13200
+ restoreTerminalTabsLayoutSetting();
13145
13201
  restoreToolOutputExpansionSetting();
13146
13202
  restoreSidePanelSectionState();
13147
13203
  bindSidePanelSectionToggles();
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=35" />
15
+ <link rel="stylesheet" href="/styles.css?v=36" />
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">
@@ -294,6 +294,14 @@
294
294
  </span>
295
295
  </label>
296
296
  </div>
297
+ <div class="control-field terminal-tabs-layout-control-field">
298
+ <label for="terminalTabsLayoutSelect">Tabs layout</label>
299
+ <select id="terminalTabsLayoutSelect" title="Terminal tabs layout" aria-describedby="terminalTabsLayoutStatus">
300
+ <option value="top">Top bar</option>
301
+ <option value="left">Left sidebar</option>
302
+ </select>
303
+ <div id="terminalTabsLayoutStatus" class="terminal-tabs-layout-status toggle-control-hint">Top bar</div>
304
+ </div>
297
305
  <div class="control-field">
298
306
  <label for="themeSelect">Theme</label>
299
307
  <select id="themeSelect" title="Theme"></select>
@@ -541,6 +549,6 @@
541
549
  </form>
542
550
  </dialog>
543
551
 
544
- <script type="module" src="/app.js?v=36"></script>
552
+ <script type="module" src="/app.js?v=37"></script>
545
553
  </body>
546
554
  </html>
package/public/styles.css CHANGED
@@ -1494,6 +1494,102 @@ body.side-panel-collapsed .terminal-tabs-shell {
1494
1494
  box-shadow: 0 0 1rem rgba(245, 194, 231, 0.20);
1495
1495
  }
1496
1496
 
1497
+ @media (min-width: 721px) {
1498
+ body.terminal-tabs-left .chat-panel {
1499
+ display: grid;
1500
+ grid-template-columns: clamp(13rem, 18vw, 19rem) minmax(0, 1fr);
1501
+ grid-template-rows: auto minmax(0, 1fr) auto auto auto auto auto;
1502
+ align-items: stretch;
1503
+ }
1504
+ body.terminal-tabs-left .terminal-tabs-shell {
1505
+ grid-column: 1;
1506
+ grid-row: 1 / -1;
1507
+ flex-direction: column;
1508
+ align-items: stretch;
1509
+ gap: 0.58rem;
1510
+ min-width: 0;
1511
+ min-height: 0;
1512
+ padding: 0.76rem;
1513
+ border-right: 1px solid rgba(180, 190, 254, 0.16);
1514
+ border-bottom: 0;
1515
+ background:
1516
+ linear-gradient(180deg, rgba(var(--ctp-crust-rgb), 0.96), rgba(var(--ctp-base-rgb), 0.82), rgba(var(--ctp-mantle-rgb), 0.92)),
1517
+ radial-gradient(circle at 0% 0%, rgba(245, 194, 231, 0.12), transparent 18rem);
1518
+ box-shadow: inset -1px 0 0 rgba(255,255,255,0.035), 0.45rem 0 1rem rgba(var(--ctp-crust-rgb), 0.18);
1519
+ }
1520
+ body.terminal-tabs-left.side-panel-collapsed .terminal-tabs-shell {
1521
+ padding-right: 0.76rem;
1522
+ }
1523
+ body.terminal-tabs-left .terminal-tabs {
1524
+ flex: 1 1 auto;
1525
+ flex-direction: column;
1526
+ align-items: stretch;
1527
+ min-height: 0;
1528
+ padding-right: 0.08rem;
1529
+ overflow-x: hidden;
1530
+ overflow-y: auto;
1531
+ }
1532
+ body.terminal-tabs-left .terminal-tabs.terminal-tabs-dense {
1533
+ flex-wrap: nowrap;
1534
+ max-height: none;
1535
+ overflow-x: hidden;
1536
+ overflow-y: auto;
1537
+ }
1538
+ body.terminal-tabs-left .terminal-tab,
1539
+ body.terminal-tabs-left .terminal-tabs.terminal-tabs-dense .terminal-tab {
1540
+ flex: 0 0 auto;
1541
+ width: 100%;
1542
+ min-width: 0;
1543
+ max-width: none;
1544
+ }
1545
+ body.terminal-tabs-left .terminal-tab-group-menu {
1546
+ --terminal-left-dropdown-bridge: 0.78rem;
1547
+ inset: 0 auto auto 100%;
1548
+ width: clamp(13rem, 18vw, 20rem);
1549
+ min-width: 13rem;
1550
+ max-width: min(22rem, calc(100vw - 2rem));
1551
+ padding-top: 0;
1552
+ padding-left: var(--terminal-left-dropdown-bridge);
1553
+ }
1554
+ body.terminal-tabs-left .terminal-new-tab-menu.composer-publish-menu {
1555
+ width: 100%;
1556
+ }
1557
+ body.terminal-tabs-left .terminal-new-tab-button,
1558
+ body.terminal-tabs-left .terminal-close-all-button {
1559
+ width: 100%;
1560
+ justify-content: flex-start;
1561
+ text-align: left;
1562
+ }
1563
+ body.terminal-tabs-left .terminal-new-tab-menu .composer-publish-menu-panel {
1564
+ --terminal-left-dropdown-bridge: 0.78rem;
1565
+ inset: 0 auto auto 100%;
1566
+ width: clamp(12rem, 16vw, 18rem);
1567
+ min-width: 12rem;
1568
+ padding-top: 0;
1569
+ padding-left: var(--terminal-left-dropdown-bridge);
1570
+ }
1571
+ body.terminal-tabs-left .terminal-close-all-button {
1572
+ margin-top: auto;
1573
+ }
1574
+ body.terminal-tabs-left .widget-area,
1575
+ body.terminal-tabs-left .chat,
1576
+ body.terminal-tabs-left .feedback-tray,
1577
+ body.terminal-tabs-left .jump-to-latest-button,
1578
+ body.terminal-tabs-left .statusbar,
1579
+ body.terminal-tabs-left .git-workflow-panel,
1580
+ body.terminal-tabs-left .composer {
1581
+ grid-column: 2;
1582
+ min-width: 0;
1583
+ }
1584
+ body.terminal-tabs-left .widget-area { grid-row: 1; }
1585
+ body.terminal-tabs-left .chat { grid-row: 2; }
1586
+ body.terminal-tabs-left .feedback-tray { grid-row: 3; }
1587
+ body.terminal-tabs-left .jump-to-latest-button { grid-row: 4; }
1588
+ body.terminal-tabs-left .statusbar { grid-row: 5; }
1589
+ body.terminal-tabs-left .git-workflow-panel { grid-row: 6; }
1590
+ body.terminal-tabs-left .composer { grid-row: 7; }
1591
+ }
1592
+
1497
1593
  .widget-area {
1498
1594
  flex: 0 0 auto;
1499
1595
  border-bottom: 1px solid rgba(180, 190, 254, 0.16);
@@ -61,6 +61,8 @@ assert.match(html, /id="agentDoneNotificationsToggle"/, "side panel should expos
61
61
  assert.match(html, /id="agentDoneNotificationsStatus"/, "agent-done notifications toggle should expose status text");
62
62
  assert.match(html, /id="thinkingVisibilityToggle"/, "side panel should expose a thinking-output visibility toggle");
63
63
  assert.match(html, /id="thinkingVisibilityStatus"/, "thinking-output visibility toggle should expose status text");
64
+ assert.match(html, /id="terminalTabsLayoutSelect"[\s\S]*<option value="left">Left sidebar<\/option>/, "side panel controls should expose a terminal-tabs layout selector");
65
+ assert.match(html, /id="terminalTabsLayoutStatus"/, "terminal-tabs layout selector should expose status text");
64
66
  assert.match(html, /id="nativeCommandDialog"/, "native slash selector UI should have a dedicated dialog");
65
67
  assert.match(html, /id="nativeCommandSearch"[^>]*type="search"/, "native slash selector dialog should expose a filter box");
66
68
  assert.match(html, /id="pathPickerCreateNameInput"[^>]*placeholder="New directory name"/, "cwd picker should expose a new-directory name input");
@@ -253,6 +255,11 @@ assert.match(css, /\.composer-actions-panel > \.composer-publish-menu[\s\S]*?gri
253
255
  assert.match(css, /\.composer-actions-panel[\s\S]*?bottom:\s*calc\(100% \+ 0\.42rem\)/, "mobile composer actions should open as an above-composer sheet");
254
256
  assert.match(css, /body\.composer-actions-open \.composer-actions-panel \{ display: grid; \}/, "composer actions panel should only open when toggled");
255
257
  assert.match(css, /\.terminal-tabs-toggle-button \{ display: none; \}/, "terminal tab toggle should be hidden outside mobile CSS");
258
+ assert.match(css, /body\.terminal-tabs-left \.chat-panel \{[\s\S]*?grid-template-columns:\s*clamp\(13rem, 18vw, 19rem\) minmax\(0, 1fr\)/, "terminal tabs left layout should split the chat panel into a sidebar and transcript area");
259
+ assert.match(css, /body\.terminal-tabs-left \.terminal-tabs-shell \{[\s\S]*?grid-column:\s*1;[\s\S]*?grid-row:\s*1 \/ -1;[\s\S]*?flex-direction:\s*column/, "terminal tabs left layout should turn the top tab strip into a vertical sidebar");
260
+ assert.match(css, /body\.terminal-tabs-left \.terminal-tabs \{[\s\S]*?flex-direction:\s*column/, "terminal tabs left layout should stack tabs vertically");
261
+ assert.match(css, /body\.terminal-tabs-left \.terminal-tab-group-menu \{[\s\S]*?inset:\s*0 auto auto 100%;[\s\S]*?padding-left:\s*var\(--terminal-left-dropdown-bridge\)/, "left-sidebar grouped tab menus should include a hover bridge so they do not vanish between button and dropdown");
262
+ assert.match(css, /body\.terminal-tabs-left \.terminal-new-tab-menu \.composer-publish-menu-panel \{[\s\S]*?inset:\s*0 auto auto 100%;[\s\S]*?padding-left:\s*var\(--terminal-left-dropdown-bridge\)/, "left-sidebar new-tab dropdown should include a hover bridge so it does not vanish between button and dropdown");
256
263
  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");
257
264
  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");
258
265
  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");
@@ -334,6 +341,9 @@ assert.match(app, /const CUSTOM_BACKGROUNDS_STORAGE_KEY = "pi-webui-custom-backg
334
341
  assert.match(app, /const CUSTOM_BACKGROUND_IDB_NAME = "pi-webui-custom-background"/, "custom backgrounds should prefer IndexedDB persistence for large images");
335
342
  assert.match(app, /const SIDE_PANEL_SECTION_STORAGE_KEY = "pi-webui-side-panel-sections-collapsed"/, "side-panel section collapse state should be persisted in browser storage");
336
343
  assert.match(app, /const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications"/, "agent-done notification preference should be persisted in browser storage");
344
+ assert.match(app, /const TERMINAL_TABS_LAYOUT_STORAGE_KEY = "pi-webui-terminal-tabs-layout"/, "terminal-tabs layout preference should be persisted in browser storage");
345
+ assert.match(app, /document\.body\.classList\.toggle\("terminal-tabs-left", next === "left"\)/, "terminal-tabs layout should toggle a body class for CSS layout");
346
+ assert.match(app, /terminalTabsLayoutSelect\.addEventListener\("change"/, "terminal-tabs layout selector should update the browser layout immediately");
337
347
  assert.match(app, /async function initializeThemes\(\)/, "frontend should initialize bundled themes");
338
348
  assert.match(app, /api\("\/api\/themes", \{ scoped: false \}\)/, "theme loading should use the unscoped themes endpoint");
339
349
  assert.match(app, /function applyTheme\(theme/, "frontend should apply a selected theme to CSS variables");