@firstpick/pi-package-webui 0.3.0 → 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 +1 -1
- package/public/app.js +106 -8
- package/public/index.html +9 -3
- package/public/service-worker.js +1 -1
- package/public/styles.css +63 -4
- package/tests/mobile-static.test.mjs +8 -1
package/package.json
CHANGED
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.
|
|
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
|
-
|
|
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 ||
|
|
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);
|
|
@@ -3748,7 +3796,7 @@ const GIT_FOOTER_TOOLTIP_COPY = {
|
|
|
3748
3796
|
git: "Current Git branch. detached means HEAD is not on a branch; no repo means the cwd is outside a Git work tree.",
|
|
3749
3797
|
"git-state": "Active Git operation or detached state. Finish or abort rebase/merge/cherry-pick/revert/bisect before normal commits.",
|
|
3750
3798
|
sync: "Remote tracking divergence. ↑ means local commits ahead; ↓ means remote commits to pull.",
|
|
3751
|
-
changes: "Working tree summary.
|
|
3799
|
+
changes: "Working tree summary. 🟢 staged, ✏️ modified unstaged, ➕ untracked, ⚠️ conflicted; ✅ means no changes.",
|
|
3752
3800
|
"git-extra": "Extra Git signals. 📦 stash, 🧩 dirty submodules, 🌳 worktrees, 🏷️ tag at HEAD, 🕒 last commit age, 🔓 signing mismatch.",
|
|
3753
3801
|
model: "Scoped model for this tab.",
|
|
3754
3802
|
thinking: "Reasoning/thinking effort for this 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
|
-
|
|
4477
|
-
|
|
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", () =>
|
|
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=
|
|
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
|
-
<
|
|
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=
|
|
476
|
+
<script type="module" src="/app.js?v=25"></script>
|
|
471
477
|
</body>
|
|
472
478
|
</html>
|
package/public/service-worker.js
CHANGED
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-
|
|
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");
|