@firstpick/pi-package-webui 0.4.5 → 0.4.7
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/bin/pi-webui.mjs +153 -23
- package/package.json +10 -3
- package/public/app.js +469 -45
- package/public/index.html +4 -1
- package/public/styles.css +66 -0
- package/tests/fixtures/fake-pi.mjs +0 -0
- package/tests/http-endpoints-harness.test.mjs +48 -0
- package/tests/mobile-static.test.mjs +14 -3
package/public/app.js
CHANGED
|
@@ -114,6 +114,7 @@ const elements = {
|
|
|
114
114
|
gitChangesStatus: $("#gitChangesStatus"),
|
|
115
115
|
gitChangesBody: $("#gitChangesBody"),
|
|
116
116
|
gitChangesRefreshButton: $("#gitChangesRefreshButton"),
|
|
117
|
+
gitChangesPullButton: $("#gitChangesPullButton"),
|
|
117
118
|
gitChangesCloseButton: $("#gitChangesCloseButton"),
|
|
118
119
|
modelSelect: $("#modelSelect"),
|
|
119
120
|
setModelButton: $("#setModelButton"),
|
|
@@ -265,7 +266,7 @@ let foregroundReconcileTimer = null;
|
|
|
265
266
|
let eventSource = null;
|
|
266
267
|
let activeDialog = null;
|
|
267
268
|
let activeGitPrDialogResolve = null;
|
|
268
|
-
let gitChangesState = { loading: false, error: "", data: null, tabId: null };
|
|
269
|
+
let gitChangesState = { loading: false, pulling: false, error: "", message: "", data: null, tabId: null };
|
|
269
270
|
let gitChangesRequestSerial = 0;
|
|
270
271
|
const gitChangesUntrackedContentRequests = new Set();
|
|
271
272
|
let nativeCommandTabId = null;
|
|
@@ -276,12 +277,16 @@ let pathFastPicksReady = false;
|
|
|
276
277
|
let pathFastPicksLoadPromise = null;
|
|
277
278
|
let mobileTabsExpanded = false;
|
|
278
279
|
let openTerminalTabGroupKey = null;
|
|
280
|
+
let terminalCustomGroups = new Map();
|
|
281
|
+
let terminalCustomGroupSerial = 1;
|
|
282
|
+
let terminalTabDragId = null;
|
|
279
283
|
let newTabMenuOpen = false;
|
|
280
284
|
let nativeCommandMenuOpen = false;
|
|
281
285
|
let appRunnerMenuOpen = false;
|
|
282
286
|
let busyPromptBehaviorMenuOpen = false;
|
|
283
287
|
const skillUsageByTab = new Map();
|
|
284
288
|
let appRunnerCustomDraft = { id: "", label: "", command: "./", path: "", args: "" };
|
|
289
|
+
let appRunnerCustomFeedback = { type: "", message: "" };
|
|
285
290
|
let appRunnerFileBrowserState = { open: false, loading: false, path: "", data: null, error: "" };
|
|
286
291
|
let optionsMenuOpen = false;
|
|
287
292
|
let availableCommands = [];
|
|
@@ -409,6 +414,8 @@ const THINKING_VISIBILITY_STORAGE_KEY = "pi-webui-thinking-visible";
|
|
|
409
414
|
const BUSY_PROMPT_BEHAVIOR_STORAGE_KEY = "pi-webui-busy-prompt-behavior";
|
|
410
415
|
const SKILL_USAGE_STORAGE_KEY = "pi-webui-skill-usage-v1";
|
|
411
416
|
const TERMINAL_TABS_LAYOUT_STORAGE_KEY = "pi-webui-terminal-tabs-layout";
|
|
417
|
+
const TERMINAL_CUSTOM_GROUPS_STORAGE_KEY = "pi-webui-terminal-custom-groups-v1";
|
|
418
|
+
const TERMINAL_TAB_DRAG_MIME = "application/x-pi-terminal-tab-id";
|
|
412
419
|
const TOOL_OUTPUT_EXPANDED_STORAGE_KEY = "pi-webui-tool-output-expanded";
|
|
413
420
|
const THEME_STORAGE_KEY = "pi-webui-theme";
|
|
414
421
|
const CUSTOM_BACKGROUND_STORAGE_KEY = "pi-webui-custom-background";
|
|
@@ -1245,6 +1252,252 @@ function restoreTerminalTabsLayoutSetting() {
|
|
|
1245
1252
|
setTerminalTabsLayout(readStoredTerminalTabsLayout(), { persist: false });
|
|
1246
1253
|
}
|
|
1247
1254
|
|
|
1255
|
+
function normalizeTerminalCustomGroupTitle(value, fallback = "Custom group") {
|
|
1256
|
+
const title = String(value || "").replace(/\s+/g, " ").trim();
|
|
1257
|
+
return (title || fallback).slice(0, 40);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function normalizeTerminalCustomGroupTabIds(value) {
|
|
1261
|
+
const ids = [];
|
|
1262
|
+
const seen = new Set();
|
|
1263
|
+
for (const item of Array.isArray(value) ? value : []) {
|
|
1264
|
+
const id = String(item || "").trim();
|
|
1265
|
+
if (!id || seen.has(id)) continue;
|
|
1266
|
+
seen.add(id);
|
|
1267
|
+
ids.push(id);
|
|
1268
|
+
}
|
|
1269
|
+
return ids;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function nextTerminalCustomGroupId() {
|
|
1273
|
+
const id = `group-${Date.now().toString(36)}-${terminalCustomGroupSerial.toString(36)}`;
|
|
1274
|
+
terminalCustomGroupSerial += 1;
|
|
1275
|
+
return id;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function restoreTerminalCustomGroups() {
|
|
1279
|
+
terminalCustomGroups = new Map();
|
|
1280
|
+
terminalCustomGroupSerial = 1;
|
|
1281
|
+
let parsed = null;
|
|
1282
|
+
try {
|
|
1283
|
+
parsed = JSON.parse(localStorage.getItem(TERMINAL_CUSTOM_GROUPS_STORAGE_KEY) || "null");
|
|
1284
|
+
} catch {
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
const records = Array.isArray(parsed?.groups) ? parsed.groups : Array.isArray(parsed) ? parsed : [];
|
|
1288
|
+
const usedTabIds = new Set();
|
|
1289
|
+
for (const record of records) {
|
|
1290
|
+
const rawId = String(record?.id || "").trim();
|
|
1291
|
+
const id = rawId && !terminalCustomGroups.has(rawId) ? rawId : nextTerminalCustomGroupId();
|
|
1292
|
+
const title = normalizeTerminalCustomGroupTitle(record?.title, `Group ${terminalCustomGroups.size + 1}`);
|
|
1293
|
+
const tabIds = normalizeTerminalCustomGroupTabIds(record?.tabIds).filter((tabId) => {
|
|
1294
|
+
if (usedTabIds.has(tabId)) return false;
|
|
1295
|
+
usedTabIds.add(tabId);
|
|
1296
|
+
return true;
|
|
1297
|
+
});
|
|
1298
|
+
if (tabIds.length < 2) continue;
|
|
1299
|
+
terminalCustomGroups.set(id, { id, title, tabIds });
|
|
1300
|
+
const serialMatch = /^Group\s+(\d+)$/i.exec(title);
|
|
1301
|
+
if (serialMatch) terminalCustomGroupSerial = Math.max(terminalCustomGroupSerial, Number(serialMatch[1]) + 1);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function persistTerminalCustomGroups() {
|
|
1306
|
+
try {
|
|
1307
|
+
const groups = [...terminalCustomGroups.values()].map((group) => ({
|
|
1308
|
+
id: group.id,
|
|
1309
|
+
title: normalizeTerminalCustomGroupTitle(group.title),
|
|
1310
|
+
tabIds: normalizeTerminalCustomGroupTabIds(group.tabIds),
|
|
1311
|
+
})).filter((group) => group.tabIds.length >= 2);
|
|
1312
|
+
localStorage.setItem(TERMINAL_CUSTOM_GROUPS_STORAGE_KEY, JSON.stringify({ version: 1, groups }));
|
|
1313
|
+
} catch {
|
|
1314
|
+
// Ignore storage failures; custom tab groups still work for this page load.
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function terminalCustomGroupByTabId() {
|
|
1319
|
+
const map = new Map();
|
|
1320
|
+
for (const [groupId, group] of terminalCustomGroups) {
|
|
1321
|
+
for (const tabId of group.tabIds || []) {
|
|
1322
|
+
if (!map.has(tabId)) map.set(tabId, groupId);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return map;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function terminalCustomGroupIdForTab(tabId) {
|
|
1329
|
+
const id = String(tabId || "").trim();
|
|
1330
|
+
if (!id) return null;
|
|
1331
|
+
for (const [groupId, group] of terminalCustomGroups) {
|
|
1332
|
+
if (group.tabIds?.includes(id)) return groupId;
|
|
1333
|
+
}
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function syncTerminalCustomGroupsWithTabs(currentTabs = tabs, { persist = true } = {}) {
|
|
1338
|
+
const validTabIds = new Set((currentTabs || []).map((tab) => tab.id).filter(Boolean));
|
|
1339
|
+
const claimedTabIds = new Set();
|
|
1340
|
+
let changed = false;
|
|
1341
|
+
for (const [groupId, group] of [...terminalCustomGroups]) {
|
|
1342
|
+
const filtered = [];
|
|
1343
|
+
for (const tabId of normalizeTerminalCustomGroupTabIds(group.tabIds)) {
|
|
1344
|
+
if (!validTabIds.has(tabId) || claimedTabIds.has(tabId)) {
|
|
1345
|
+
changed = true;
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
claimedTabIds.add(tabId);
|
|
1349
|
+
filtered.push(tabId);
|
|
1350
|
+
}
|
|
1351
|
+
if (filtered.length < 2) {
|
|
1352
|
+
terminalCustomGroups.delete(groupId);
|
|
1353
|
+
changed = true;
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
if (filtered.length !== group.tabIds.length || filtered.some((tabId, index) => tabId !== group.tabIds[index])) {
|
|
1357
|
+
group.tabIds = filtered;
|
|
1358
|
+
changed = true;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
if (changed && persist) persistTerminalCustomGroups();
|
|
1362
|
+
return changed;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function removeTabsFromOtherTerminalCustomGroups(tabIds, keepGroupId = null) {
|
|
1366
|
+
const moving = new Set(normalizeTerminalCustomGroupTabIds(tabIds));
|
|
1367
|
+
if (!moving.size) return false;
|
|
1368
|
+
let changed = false;
|
|
1369
|
+
for (const [groupId, group] of [...terminalCustomGroups]) {
|
|
1370
|
+
if (groupId === keepGroupId) continue;
|
|
1371
|
+
const filtered = normalizeTerminalCustomGroupTabIds(group.tabIds).filter((tabId) => !moving.has(tabId));
|
|
1372
|
+
if (filtered.length === group.tabIds.length) continue;
|
|
1373
|
+
changed = true;
|
|
1374
|
+
if (filtered.length < 2) terminalCustomGroups.delete(groupId);
|
|
1375
|
+
else group.tabIds = filtered;
|
|
1376
|
+
}
|
|
1377
|
+
return changed;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
function createTerminalCustomGroup(tabIds) {
|
|
1381
|
+
const validTabIds = new Set(tabs.map((tab) => tab.id).filter(Boolean));
|
|
1382
|
+
const unique = normalizeTerminalCustomGroupTabIds(tabIds).filter((tabId) => validTabIds.has(tabId));
|
|
1383
|
+
if (unique.length < 2) return null;
|
|
1384
|
+
removeTabsFromOtherTerminalCustomGroups(unique);
|
|
1385
|
+
const title = `Group ${terminalCustomGroupSerial}`;
|
|
1386
|
+
const id = nextTerminalCustomGroupId();
|
|
1387
|
+
const group = { id, title, tabIds: unique };
|
|
1388
|
+
terminalCustomGroups.set(id, group);
|
|
1389
|
+
syncTerminalCustomGroupsWithTabs(tabs, { persist: false });
|
|
1390
|
+
persistTerminalCustomGroups();
|
|
1391
|
+
return terminalCustomGroups.get(id) || group;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function addTabsToTerminalCustomGroup(groupId, tabIds) {
|
|
1395
|
+
const group = terminalCustomGroups.get(groupId);
|
|
1396
|
+
if (!group) return null;
|
|
1397
|
+
const validTabIds = new Set(tabs.map((tab) => tab.id).filter(Boolean));
|
|
1398
|
+
const unique = normalizeTerminalCustomGroupTabIds(tabIds).filter((tabId) => validTabIds.has(tabId) && !group.tabIds.includes(tabId));
|
|
1399
|
+
if (!unique.length) return group;
|
|
1400
|
+
removeTabsFromOtherTerminalCustomGroups(unique, groupId);
|
|
1401
|
+
group.tabIds = normalizeTerminalCustomGroupTabIds([...group.tabIds, ...unique]);
|
|
1402
|
+
syncTerminalCustomGroupsWithTabs(tabs, { persist: false });
|
|
1403
|
+
persistTerminalCustomGroups();
|
|
1404
|
+
return terminalCustomGroups.get(groupId) || null;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function terminalTabById(tabId) {
|
|
1408
|
+
return tabs.find((tab) => tab.id === tabId) || null;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function terminalTabDragIdFromEvent(event) {
|
|
1412
|
+
return event?.dataTransfer?.getData?.(TERMINAL_TAB_DRAG_MIME) || terminalTabDragId || "";
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function canDropTerminalTabOnTarget(sourceTabId, target) {
|
|
1416
|
+
if (!sourceTabId || !terminalTabById(sourceTabId) || !target) return false;
|
|
1417
|
+
if (target.type === "group") {
|
|
1418
|
+
const groupTabs = target.group?.tabs || [];
|
|
1419
|
+
return groupTabs.length > 0 && !groupTabs.some((tab) => tab.id === sourceTabId);
|
|
1420
|
+
}
|
|
1421
|
+
return Boolean(target.tabId && target.tabId !== sourceTabId && terminalTabById(target.tabId));
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function clearTerminalTabDragState() {
|
|
1425
|
+
terminalTabDragId = null;
|
|
1426
|
+
document.body.classList.remove("terminal-tab-dragging");
|
|
1427
|
+
elements.tabBar?.querySelectorAll(".terminal-tab-dragging, .terminal-tab-drag-over").forEach((item) => {
|
|
1428
|
+
item.classList.remove("terminal-tab-dragging", "terminal-tab-drag-over");
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function bindTerminalTabDragAndDrop(element, { sourceTabId = "", target = null } = {}) {
|
|
1433
|
+
if (!element) return;
|
|
1434
|
+
if (sourceTabId) {
|
|
1435
|
+
element.draggable = true;
|
|
1436
|
+
element.dataset.dragTabId = sourceTabId;
|
|
1437
|
+
element.addEventListener("dragstart", (event) => {
|
|
1438
|
+
if (event.target?.closest?.(".terminal-tab-close, .terminal-tab-group-add")) {
|
|
1439
|
+
event.preventDefault();
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
terminalTabDragId = sourceTabId;
|
|
1443
|
+
document.body.classList.add("terminal-tab-dragging");
|
|
1444
|
+
element.classList.add("terminal-tab-dragging");
|
|
1445
|
+
try {
|
|
1446
|
+
event.dataTransfer?.setData(TERMINAL_TAB_DRAG_MIME, sourceTabId);
|
|
1447
|
+
event.dataTransfer?.setData("text/plain", terminalTabById(sourceTabId)?.title || sourceTabId);
|
|
1448
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
|
|
1449
|
+
} catch {
|
|
1450
|
+
// Ignore browser drag payload restrictions; same-page state still carries the tab id.
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
element.addEventListener("dragend", clearTerminalTabDragState);
|
|
1454
|
+
}
|
|
1455
|
+
if (!target) return;
|
|
1456
|
+
element.addEventListener("dragover", (event) => {
|
|
1457
|
+
const sourceTabIdFromEvent = terminalTabDragIdFromEvent(event);
|
|
1458
|
+
if (!canDropTerminalTabOnTarget(sourceTabIdFromEvent, target)) return;
|
|
1459
|
+
event.preventDefault();
|
|
1460
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
|
|
1461
|
+
element.classList.add("terminal-tab-drag-over");
|
|
1462
|
+
if (target.type === "group") setOpenTerminalTabGroup(target.group?.key);
|
|
1463
|
+
});
|
|
1464
|
+
element.addEventListener("dragleave", (event) => {
|
|
1465
|
+
if (event.relatedTarget && element.contains(event.relatedTarget)) return;
|
|
1466
|
+
element.classList.remove("terminal-tab-drag-over");
|
|
1467
|
+
});
|
|
1468
|
+
element.addEventListener("drop", (event) => {
|
|
1469
|
+
const sourceTabIdFromEvent = terminalTabDragIdFromEvent(event);
|
|
1470
|
+
element.classList.remove("terminal-tab-drag-over");
|
|
1471
|
+
if (!canDropTerminalTabOnTarget(sourceTabIdFromEvent, target)) return;
|
|
1472
|
+
event.preventDefault();
|
|
1473
|
+
event.stopPropagation();
|
|
1474
|
+
handleTerminalTabDrop(sourceTabIdFromEvent, target);
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
function handleTerminalTabDrop(sourceTabId, target) {
|
|
1479
|
+
const sourceTab = terminalTabById(sourceTabId);
|
|
1480
|
+
if (!sourceTab) return;
|
|
1481
|
+
let group = null;
|
|
1482
|
+
if (target.type === "group" && target.group) {
|
|
1483
|
+
if (target.group.custom && target.group.customGroupId) {
|
|
1484
|
+
group = addTabsToTerminalCustomGroup(target.group.customGroupId, [sourceTabId]);
|
|
1485
|
+
} else {
|
|
1486
|
+
group = createTerminalCustomGroup([...target.group.tabs.map((tab) => tab.id), sourceTabId]);
|
|
1487
|
+
}
|
|
1488
|
+
} else if (target.tabId) {
|
|
1489
|
+
const targetTab = terminalTabById(target.tabId);
|
|
1490
|
+
if (!targetTab || targetTab.id === sourceTabId) return;
|
|
1491
|
+
const targetGroupId = terminalCustomGroupIdForTab(targetTab.id);
|
|
1492
|
+
group = targetGroupId ? addTabsToTerminalCustomGroup(targetGroupId, [sourceTabId]) : createTerminalCustomGroup([targetTab.id, sourceTabId]);
|
|
1493
|
+
}
|
|
1494
|
+
if (!group) return;
|
|
1495
|
+
clearOpenTerminalTabGroup(null, { force: true });
|
|
1496
|
+
clearTerminalTabDragState();
|
|
1497
|
+
renderTabs();
|
|
1498
|
+
addEvent(`added ${sourceTab.title || "tab"} to ${normalizeTerminalCustomGroupTitle(group.title).toLowerCase()}`, "info");
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1248
1501
|
function removeStreamingThinkingBubble() {
|
|
1249
1502
|
streamThinkingBubble?.remove();
|
|
1250
1503
|
streamThinkingBubble = null;
|
|
@@ -4261,13 +4514,39 @@ function tabCwdGroupKey(tab) {
|
|
|
4261
4514
|
}
|
|
4262
4515
|
|
|
4263
4516
|
function tabCwdGroups() {
|
|
4517
|
+
syncTerminalCustomGroupsWithTabs(tabs);
|
|
4264
4518
|
const groups = [];
|
|
4265
4519
|
const byKey = new Map();
|
|
4520
|
+
const customByTab = terminalCustomGroupByTabId();
|
|
4521
|
+
const customGroups = new Map([...terminalCustomGroups.values()].map((group) => [group.id, {
|
|
4522
|
+
key: `custom:${group.id}`,
|
|
4523
|
+
custom: true,
|
|
4524
|
+
customGroupId: group.id,
|
|
4525
|
+
title: normalizeTerminalCustomGroupTitle(group.title),
|
|
4526
|
+
tabs: [],
|
|
4527
|
+
cwd: "",
|
|
4528
|
+
}]));
|
|
4529
|
+
|
|
4266
4530
|
for (const tab of tabs) {
|
|
4531
|
+
const customGroupId = customByTab.get(tab.id);
|
|
4532
|
+
if (customGroupId) customGroups.get(customGroupId)?.tabs.push(tab);
|
|
4533
|
+
}
|
|
4534
|
+
|
|
4535
|
+
const emittedCustomGroups = new Set();
|
|
4536
|
+
for (const tab of tabs) {
|
|
4537
|
+
const customGroupId = customByTab.get(tab.id);
|
|
4538
|
+
if (customGroupId) {
|
|
4539
|
+
const customGroup = customGroups.get(customGroupId);
|
|
4540
|
+
if (customGroup && !emittedCustomGroups.has(customGroupId)) {
|
|
4541
|
+
groups.push(customGroup);
|
|
4542
|
+
emittedCustomGroups.add(customGroupId);
|
|
4543
|
+
}
|
|
4544
|
+
continue;
|
|
4545
|
+
}
|
|
4267
4546
|
const key = tabCwdGroupKey(tab);
|
|
4268
4547
|
let group = byKey.get(key);
|
|
4269
4548
|
if (!group) {
|
|
4270
|
-
group = { key, cwd: tab.cwd || "", tabs: [] };
|
|
4549
|
+
group = { key, custom: false, cwd: tab.cwd || "", tabs: [] };
|
|
4271
4550
|
byKey.set(key, group);
|
|
4272
4551
|
groups.push(group);
|
|
4273
4552
|
}
|
|
@@ -4282,6 +4561,18 @@ function tabGroupTitle(cwd, fallback = "cwd") {
|
|
|
4282
4561
|
return leaf.length > 26 ? `…${leaf.slice(-25)}` : leaf;
|
|
4283
4562
|
}
|
|
4284
4563
|
|
|
4564
|
+
function terminalDisplayGroupTitle(group, fallback = "group") {
|
|
4565
|
+
return group?.custom ? normalizeTerminalCustomGroupTitle(group.title, "Custom group") : tabGroupTitle(group?.cwd, fallback);
|
|
4566
|
+
}
|
|
4567
|
+
|
|
4568
|
+
function terminalDisplayGroupDetail(group, fallback = "group") {
|
|
4569
|
+
if (!group?.custom) return normalizeDisplayPath(group?.cwd || fallback);
|
|
4570
|
+
const cwdLabels = [...new Set((group.tabs || []).map((tab) => normalizeDisplayPath(tab.cwd || "")).filter(Boolean))];
|
|
4571
|
+
if (!cwdLabels.length) return "custom group";
|
|
4572
|
+
if (cwdLabels.length === 1) return cwdLabels[0];
|
|
4573
|
+
return `${cwdLabels[0]} + ${cwdLabels.length - 1} cwd${cwdLabels.length === 2 ? "" : "s"}`;
|
|
4574
|
+
}
|
|
4575
|
+
|
|
4285
4576
|
function terminalTabMeta(tab, indicator) {
|
|
4286
4577
|
return tab.running ? `${indicator.meta} · pid ${tab.pid || "…"}` : "stopped";
|
|
4287
4578
|
}
|
|
@@ -4299,12 +4590,15 @@ function renderTerminalTab(tab) {
|
|
|
4299
4590
|
const isActive = tab.id === activeTabId;
|
|
4300
4591
|
const indicator = tabIndicator(tab);
|
|
4301
4592
|
const wrapper = make("div", `terminal-tab activity-${indicator.state}${isActive ? " active" : ""}${tab.running ? "" : " stopped"}`);
|
|
4593
|
+
wrapper.dataset.tabId = tab.id;
|
|
4594
|
+
bindTerminalTabDragAndDrop(wrapper, { sourceTabId: tab.id, target: { type: "tab", tabId: tab.id } });
|
|
4302
4595
|
const button = make("button", "terminal-tab-button");
|
|
4303
4596
|
button.type = "button";
|
|
4597
|
+
button.draggable = false;
|
|
4304
4598
|
button.setAttribute("role", "tab");
|
|
4305
4599
|
button.setAttribute("aria-selected", isActive ? "true" : "false");
|
|
4306
4600
|
button.setAttribute("aria-label", `${tab.title}: ${indicator.label}`);
|
|
4307
|
-
button.title = `${tab.title} · ${indicator.label}${tab.running ? ` · pid ${tab.pid || "starting"}` : " · stopped"}`;
|
|
4601
|
+
button.title = `${tab.title} · ${indicator.label}${tab.running ? ` · pid ${tab.pid || "starting"}` : " · stopped"} · drag onto another tab or group to group`;
|
|
4308
4602
|
appendTerminalTabContent(button, { title: tab.title, indicator, meta: terminalTabMeta(tab, indicator) });
|
|
4309
4603
|
button.addEventListener("click", () => switchTab(tab.id));
|
|
4310
4604
|
wrapper.append(button);
|
|
@@ -4312,6 +4606,7 @@ function renderTerminalTab(tab) {
|
|
|
4312
4606
|
if (tabs.length > 1) {
|
|
4313
4607
|
const close = make("button", "terminal-tab-close", "×");
|
|
4314
4608
|
close.type = "button";
|
|
4609
|
+
close.draggable = false;
|
|
4315
4610
|
close.title = `Close ${tab.title}`;
|
|
4316
4611
|
close.setAttribute("aria-label", `Close ${tab.title}`);
|
|
4317
4612
|
close.addEventListener("click", (event) => {
|
|
@@ -4324,16 +4619,19 @@ function renderTerminalTab(tab) {
|
|
|
4324
4619
|
return wrapper;
|
|
4325
4620
|
}
|
|
4326
4621
|
|
|
4327
|
-
function renderTerminalTabGroupItem(tab) {
|
|
4622
|
+
function renderTerminalTabGroupItem(tab, group) {
|
|
4328
4623
|
const isActive = tab.id === activeTabId;
|
|
4329
4624
|
const indicator = tabIndicator(tab);
|
|
4330
4625
|
const item = make("div", `terminal-tab-group-item activity-${indicator.state}${isActive ? " active" : ""}${tab.running ? "" : " stopped"}`);
|
|
4626
|
+
item.dataset.tabId = tab.id;
|
|
4627
|
+
bindTerminalTabDragAndDrop(item, { sourceTabId: tab.id, target: group?.custom ? { type: "group", group } : { type: "tab", tabId: tab.id } });
|
|
4331
4628
|
const button = make("button", "terminal-tab-button terminal-tab-group-item-button");
|
|
4332
4629
|
button.type = "button";
|
|
4630
|
+
button.draggable = false;
|
|
4333
4631
|
button.setAttribute("role", "tab");
|
|
4334
4632
|
button.setAttribute("aria-selected", isActive ? "true" : "false");
|
|
4335
4633
|
button.setAttribute("aria-label", `${tab.title}: ${indicator.label}`);
|
|
4336
|
-
button.title = `${tab.title} · ${indicator.label}${tab.running ? ` · pid ${tab.pid || "starting"}` : " · stopped"}`;
|
|
4634
|
+
button.title = `${tab.title} · ${indicator.label}${tab.running ? ` · pid ${tab.pid || "starting"}` : " · stopped"} · drag onto another tab or group to group`;
|
|
4337
4635
|
appendTerminalTabContent(button, { title: tab.title, indicator, meta: terminalTabMeta(tab, indicator) });
|
|
4338
4636
|
button.addEventListener("click", (event) => {
|
|
4339
4637
|
event.stopPropagation();
|
|
@@ -4344,6 +4642,7 @@ function renderTerminalTabGroupItem(tab) {
|
|
|
4344
4642
|
if (tabs.length > 1) {
|
|
4345
4643
|
const close = make("button", "terminal-tab-close terminal-tab-group-item-close", "×");
|
|
4346
4644
|
close.type = "button";
|
|
4645
|
+
close.draggable = false;
|
|
4347
4646
|
close.title = `Close ${tab.title}`;
|
|
4348
4647
|
close.setAttribute("aria-label", `Close ${tab.title}`);
|
|
4349
4648
|
close.addEventListener("click", (event) => {
|
|
@@ -4357,6 +4656,7 @@ function renderTerminalTabGroupItem(tab) {
|
|
|
4357
4656
|
}
|
|
4358
4657
|
|
|
4359
4658
|
function shouldRenderTerminalTabGroup(group, groupCount) {
|
|
4659
|
+
if (group.custom) return group.tabs.length > 1;
|
|
4360
4660
|
return groupCount > 1 && group.tabs.length > 1 && Boolean(group.cwd);
|
|
4361
4661
|
}
|
|
4362
4662
|
|
|
@@ -4366,11 +4666,13 @@ function renderTerminalTabGroup(group, groupCount = 1) {
|
|
|
4366
4666
|
const isActive = groupTabs.some((tab) => tab.id === activeTabId);
|
|
4367
4667
|
const isStopped = groupTabs.every((tab) => !tab.running);
|
|
4368
4668
|
const indicator = tabGroupIndicator(groupTabs);
|
|
4369
|
-
const
|
|
4370
|
-
const activeTitle = activeGroupTab?.title ||
|
|
4371
|
-
const
|
|
4372
|
-
const wrapper = make("div", `terminal-tab terminal-tab-group activity-${indicator.state}${isActive ? " active" : ""}${isStopped ? " stopped" : ""}`);
|
|
4669
|
+
const groupTitle = terminalDisplayGroupTitle(group, activeGroupTab?.title || "group");
|
|
4670
|
+
const activeTitle = activeGroupTab?.title || groupTitle;
|
|
4671
|
+
const groupDetail = terminalDisplayGroupDetail(group, groupTitle);
|
|
4672
|
+
const wrapper = make("div", `terminal-tab terminal-tab-group${group.custom ? " terminal-tab-custom-group" : ""} activity-${indicator.state}${isActive ? " active" : ""}${isStopped ? " stopped" : ""}`);
|
|
4373
4673
|
wrapper.dataset.groupKey = group.key;
|
|
4674
|
+
if (group.customGroupId) wrapper.dataset.customGroupId = group.customGroupId;
|
|
4675
|
+
bindTerminalTabDragAndDrop(wrapper, { sourceTabId: activeGroupTab?.id || "", target: { type: "group", group } });
|
|
4374
4676
|
wrapper.addEventListener("pointerenter", () => setOpenTerminalTabGroup(group.key));
|
|
4375
4677
|
wrapper.addEventListener("pointerleave", () => clearOpenTerminalTabGroup(group.key));
|
|
4376
4678
|
wrapper.addEventListener("focusin", () => setOpenTerminalTabGroup(group.key));
|
|
@@ -4381,21 +4683,23 @@ function renderTerminalTabGroup(group, groupCount = 1) {
|
|
|
4381
4683
|
});
|
|
4382
4684
|
const button = make("button", "terminal-tab-button terminal-tab-group-button");
|
|
4383
4685
|
button.type = "button";
|
|
4686
|
+
button.draggable = false;
|
|
4384
4687
|
button.setAttribute("role", "tab");
|
|
4385
4688
|
button.setAttribute("aria-selected", isActive ? "true" : "false");
|
|
4386
4689
|
button.setAttribute("aria-haspopup", "true");
|
|
4387
4690
|
button.setAttribute("aria-expanded", group.key === openTerminalTabGroupKey ? "true" : "false");
|
|
4388
|
-
button.setAttribute("aria-label", `${
|
|
4389
|
-
button.title = `${activeTitle} · ${
|
|
4390
|
-
appendTerminalTabContent(button, { title: activeTitle, indicator, meta: `${
|
|
4691
|
+
button.setAttribute("aria-label", `${groupTitle} ${group.custom ? "custom" : "cwd"} group: ${groupTabs.length} tabs, ${indicator.label}. Active ${activeTitle}`);
|
|
4692
|
+
button.title = `${activeTitle} · ${groupTitle} · ${groupDetail} · ${groupTabs.length} tabs · ${indicator.label} · drop tabs here to add to group`;
|
|
4693
|
+
appendTerminalTabContent(button, { title: activeTitle, indicator, meta: `${groupTitle} · ${indicator.meta}`, count: groupTabs.length });
|
|
4391
4694
|
button.addEventListener("click", () => switchTab(activeGroupTab.id));
|
|
4392
4695
|
wrapper.append(button);
|
|
4393
4696
|
|
|
4394
|
-
if (groupCount > 1) {
|
|
4697
|
+
if (groupCount > 1 || group.custom) {
|
|
4395
4698
|
const close = make("button", "terminal-tab-close terminal-tab-group-close", "×");
|
|
4396
4699
|
close.type = "button";
|
|
4397
|
-
close.
|
|
4398
|
-
close.
|
|
4700
|
+
close.draggable = false;
|
|
4701
|
+
close.title = `Close ${groupTitle} group`;
|
|
4702
|
+
close.setAttribute("aria-label", `Close ${groupTitle} group`);
|
|
4399
4703
|
close.addEventListener("click", (event) => {
|
|
4400
4704
|
event.stopPropagation();
|
|
4401
4705
|
closeTerminalTabGroup(group);
|
|
@@ -4405,16 +4709,17 @@ function renderTerminalTabGroup(group, groupCount = 1) {
|
|
|
4405
4709
|
|
|
4406
4710
|
const menu = make("div", "terminal-tab-group-menu");
|
|
4407
4711
|
menu.setAttribute("role", "group");
|
|
4408
|
-
menu.setAttribute("aria-label", `${
|
|
4409
|
-
for (const tab of groupTabs) menu.append(renderTerminalTabGroupItem(tab));
|
|
4712
|
+
menu.setAttribute("aria-label", `${groupTitle} tabs`);
|
|
4713
|
+
for (const tab of groupTabs) menu.append(renderTerminalTabGroupItem(tab, group));
|
|
4410
4714
|
|
|
4411
4715
|
const add = make("button", "terminal-tab-group-add", "+ Tab");
|
|
4412
4716
|
add.type = "button";
|
|
4413
|
-
add.
|
|
4414
|
-
add.
|
|
4717
|
+
add.draggable = false;
|
|
4718
|
+
add.title = `Add tab in ${groupTitle}`;
|
|
4719
|
+
add.setAttribute("aria-label", `Add tab in ${groupTitle}`);
|
|
4415
4720
|
add.addEventListener("click", (event) => {
|
|
4416
4721
|
event.stopPropagation();
|
|
4417
|
-
createTerminalTab(group.cwd, { triggerButton: add });
|
|
4722
|
+
createTerminalTab(activeGroupTab?.cwd || group.cwd || currentDirectoryForNewTab(), { triggerButton: add, customGroupId: group.customGroupId || null });
|
|
4418
4723
|
});
|
|
4419
4724
|
menu.append(add);
|
|
4420
4725
|
wrapper.append(menu);
|
|
@@ -4543,7 +4848,7 @@ function currentDirectoryForNewTab() {
|
|
|
4543
4848
|
return latestWorkspace?.cwd || activeTab()?.cwd || "";
|
|
4544
4849
|
}
|
|
4545
4850
|
|
|
4546
|
-
async function createTerminalTab(cwd = currentDirectoryForNewTab(), { triggerButton = elements.newTabButton } = {}) {
|
|
4851
|
+
async function createTerminalTab(cwd = currentDirectoryForNewTab(), { triggerButton = elements.newTabButton, customGroupId = null } = {}) {
|
|
4547
4852
|
setMobileTabsExpanded(false);
|
|
4548
4853
|
setNewTabMenuOpen(false);
|
|
4549
4854
|
const resolvedCwd = cwd || currentDirectoryForNewTab();
|
|
@@ -4558,6 +4863,7 @@ async function createTerminalTab(cwd = currentDirectoryForNewTab(), { triggerBut
|
|
|
4558
4863
|
tabs = response.data?.tabs || tabs;
|
|
4559
4864
|
syncTabMetadata(tabs);
|
|
4560
4865
|
const tab = response.data?.tab;
|
|
4866
|
+
if (tab?.id && customGroupId && terminalCustomGroups.has(customGroupId)) addTabsToTerminalCustomGroup(customGroupId, [tab.id]);
|
|
4561
4867
|
renderTabs();
|
|
4562
4868
|
if (tab?.id) {
|
|
4563
4869
|
await switchTab(tab.id);
|
|
@@ -4657,6 +4963,7 @@ async function closeTerminalTabs(tabIds, { label = "selected terminal tabs" } =
|
|
|
4657
4963
|
appRunnerDataByTab.delete(id);
|
|
4658
4964
|
tabMessagesCache.delete(id);
|
|
4659
4965
|
}
|
|
4966
|
+
syncTerminalCustomGroupsWithTabs(tabs);
|
|
4660
4967
|
clearOpenTerminalTabGroup(null, { force: true });
|
|
4661
4968
|
|
|
4662
4969
|
const activeTabNeedsFallback = closedIds.includes(activeTabId) || !tabs.some((item) => item.id === activeTabId);
|
|
@@ -4689,7 +4996,7 @@ async function closeTerminalTab(tabId) {
|
|
|
4689
4996
|
}
|
|
4690
4997
|
|
|
4691
4998
|
async function closeTerminalTabGroup(group) {
|
|
4692
|
-
const title =
|
|
4999
|
+
const title = terminalDisplayGroupTitle(group, group.tabs[0]?.title || "group");
|
|
4693
5000
|
await closeTerminalTabs(group.tabs.map((tab) => tab.id), { label: `${title} group` });
|
|
4694
5001
|
}
|
|
4695
5002
|
|
|
@@ -6077,13 +6384,23 @@ function renderGitChangesOverview(data) {
|
|
|
6077
6384
|
return overview;
|
|
6078
6385
|
}
|
|
6079
6386
|
|
|
6387
|
+
function gitDiffDisplayLine(row, side) {
|
|
6388
|
+
const type = row.type || "context";
|
|
6389
|
+
if (side === "old") {
|
|
6390
|
+
const text = row.left ?? "";
|
|
6391
|
+
return row.oldNumber !== "" && (type === "removed" || type === "changed") ? `-${text}` : text;
|
|
6392
|
+
}
|
|
6393
|
+
const text = row.right ?? "";
|
|
6394
|
+
return row.newNumber !== "" && (type === "added" || type === "changed") ? `+${text}` : text;
|
|
6395
|
+
}
|
|
6396
|
+
|
|
6080
6397
|
function renderGitDiffRow(row) {
|
|
6081
6398
|
const node = make("div", `git-diff-row ${row.type || "context"}`.trim());
|
|
6082
6399
|
node.append(
|
|
6083
6400
|
make("span", "git-diff-line-number old", row.oldNumber === "" ? "" : String(row.oldNumber)),
|
|
6084
|
-
make("code", "git-diff-line old", row
|
|
6401
|
+
make("code", "git-diff-line old", gitDiffDisplayLine(row, "old")),
|
|
6085
6402
|
make("span", "git-diff-line-number new", row.newNumber === "" ? "" : String(row.newNumber)),
|
|
6086
|
-
make("code", "git-diff-line new", row
|
|
6403
|
+
make("code", "git-diff-line new", gitDiffDisplayLine(row, "new")),
|
|
6087
6404
|
);
|
|
6088
6405
|
return node;
|
|
6089
6406
|
}
|
|
@@ -6300,16 +6617,28 @@ function gitChangesGeneratedLabel(data) {
|
|
|
6300
6617
|
|
|
6301
6618
|
function renderGitChangesDialog() {
|
|
6302
6619
|
if (!elements.gitChangesDialog || !elements.gitChangesBody) return;
|
|
6303
|
-
const { loading, error, data } = gitChangesState;
|
|
6304
|
-
|
|
6305
|
-
|
|
6620
|
+
const { loading, pulling, error, message, data } = gitChangesState;
|
|
6621
|
+
const behind = Number(data?.remote?.behind ?? data?.summary?.behind ?? 0) || 0;
|
|
6622
|
+
const canPull = behind > 0 && data?.remote?.canPull !== false;
|
|
6623
|
+
const remoteNotice = !error && data?.remote?.error ? `Incoming diff unavailable: ${data.remote.error}` : "";
|
|
6624
|
+
if (elements.gitChangesTitle) elements.gitChangesTitle.textContent = "Git Changes";
|
|
6625
|
+
if (elements.gitChangesSubtitle) {
|
|
6626
|
+
const base = data?.root ? `${data.branch || "detached"} · ${data.root}` : "Current tab git diff";
|
|
6627
|
+
elements.gitChangesSubtitle.textContent = data?.remote?.upstream ? `${base} · upstream ${data.remote.upstream}` : base;
|
|
6628
|
+
}
|
|
6306
6629
|
if (elements.gitChangesRefreshButton) {
|
|
6307
|
-
elements.gitChangesRefreshButton.disabled = loading;
|
|
6630
|
+
elements.gitChangesRefreshButton.disabled = loading || pulling;
|
|
6308
6631
|
elements.gitChangesRefreshButton.textContent = loading ? "Refreshing…" : "Refresh";
|
|
6309
6632
|
}
|
|
6633
|
+
if (elements.gitChangesPullButton) {
|
|
6634
|
+
elements.gitChangesPullButton.disabled = loading || pulling || !canPull;
|
|
6635
|
+
elements.gitChangesPullButton.textContent = pulling ? "Pulling…" : behind > 0 ? `Pull ↓${behind}` : "Pull";
|
|
6636
|
+
elements.gitChangesPullButton.title = canPull ? "Run git pull --ff-only for the current repository" : "No remote commits to pull";
|
|
6637
|
+
}
|
|
6310
6638
|
if (elements.gitChangesStatus) {
|
|
6311
|
-
|
|
6312
|
-
elements.gitChangesStatus.
|
|
6639
|
+
const statusText = error || (pulling ? "Pulling changes…" : loading ? "Loading git diff…" : message || remoteNotice || (data ? gitChangesGeneratedLabel(data) : ""));
|
|
6640
|
+
elements.gitChangesStatus.className = `git-changes-status ${error || remoteNotice ? "error" : message ? "success" : "muted"}`;
|
|
6641
|
+
elements.gitChangesStatus.textContent = statusText;
|
|
6313
6642
|
elements.gitChangesStatus.hidden = !elements.gitChangesStatus.textContent;
|
|
6314
6643
|
}
|
|
6315
6644
|
|
|
@@ -6319,7 +6648,7 @@ function renderGitChangesDialog() {
|
|
|
6319
6648
|
body.append(make("div", "git-changes-empty", "Loading git diff…"));
|
|
6320
6649
|
return;
|
|
6321
6650
|
}
|
|
6322
|
-
if (error) {
|
|
6651
|
+
if (error && !data) {
|
|
6323
6652
|
body.append(make("div", "git-changes-empty error", error));
|
|
6324
6653
|
return;
|
|
6325
6654
|
}
|
|
@@ -6337,20 +6666,23 @@ function renderGitChangesDialog() {
|
|
|
6337
6666
|
if (hasVisibleFiles) body.append(renderGitCurrentFileHeader());
|
|
6338
6667
|
for (const entry of parsedSections) body.append(renderGitDiffSection(entry.section, entry.files));
|
|
6339
6668
|
if (untracked.length) body.append(renderGitUntrackedSection(untracked));
|
|
6340
|
-
if (!hasVisibleFiles)
|
|
6669
|
+
if (!hasVisibleFiles) {
|
|
6670
|
+
const emptyMessage = behind > 0 ? "No textual incoming diff was available for the remote commits." : "Working tree is clean. No staged, unstaged, untracked, or incoming diff.";
|
|
6671
|
+
body.append(make("div", "git-changes-empty success", emptyMessage));
|
|
6672
|
+
}
|
|
6341
6673
|
if (hasVisibleFiles) requestAnimationFrame(updateGitChangesCurrentFileHeader);
|
|
6342
6674
|
}
|
|
6343
6675
|
|
|
6344
6676
|
async function loadGitChangesDialog(tabContext = activeTabContext()) {
|
|
6345
6677
|
const requestSerial = ++gitChangesRequestSerial;
|
|
6346
6678
|
gitChangesUntrackedContentRequests.clear();
|
|
6347
|
-
gitChangesState = { ...gitChangesState, loading: true, error: "", tabId: tabContext.tabId || activeTabId };
|
|
6679
|
+
gitChangesState = { ...gitChangesState, loading: true, error: "", message: "", tabId: tabContext.tabId || activeTabId };
|
|
6348
6680
|
renderGitChangesDialog();
|
|
6349
6681
|
try {
|
|
6350
6682
|
const response = await api("/api/git-changes", { tabId: tabContext.tabId });
|
|
6351
6683
|
if (requestSerial !== gitChangesRequestSerial) return;
|
|
6352
6684
|
if (!response.ok) throw new Error(response.error || "Failed to load git changes");
|
|
6353
|
-
gitChangesState = { loading: false, error: "", data: response.data || null, tabId: tabContext.tabId || activeTabId };
|
|
6685
|
+
gitChangesState = { loading: false, pulling: false, error: "", message: "", data: response.data || null, tabId: tabContext.tabId || activeTabId };
|
|
6354
6686
|
} catch (error) {
|
|
6355
6687
|
if (requestSerial !== gitChangesRequestSerial) return;
|
|
6356
6688
|
gitChangesState = { ...gitChangesState, loading: false, error: error.message || String(error) };
|
|
@@ -6363,7 +6695,7 @@ function openGitChangesDialog() {
|
|
|
6363
6695
|
hideFooterTooltip();
|
|
6364
6696
|
const tabContext = activeTabContext();
|
|
6365
6697
|
const tabId = tabContext.tabId || activeTabId;
|
|
6366
|
-
gitChangesState = { loading: true, error: "", data: gitChangesState.tabId === tabId ? gitChangesState.data : null, tabId };
|
|
6698
|
+
gitChangesState = { loading: true, pulling: false, error: "", message: "", data: gitChangesState.tabId === tabId ? gitChangesState.data : null, tabId };
|
|
6367
6699
|
renderGitChangesDialog();
|
|
6368
6700
|
if (!elements.gitChangesDialog.open) elements.gitChangesDialog.showModal();
|
|
6369
6701
|
loadGitChangesDialog(tabContext).catch((error) => addEvent(error.message || String(error), "error"));
|
|
@@ -6374,10 +6706,46 @@ function refreshGitChangesDialog() {
|
|
|
6374
6706
|
loadGitChangesDialog(tabContext).catch((error) => addEvent(error.message || String(error), "error"));
|
|
6375
6707
|
}
|
|
6376
6708
|
|
|
6709
|
+
async function pullGitChangesDialog() {
|
|
6710
|
+
const tabContext = { tabId: gitChangesState.tabId || activeTabId };
|
|
6711
|
+
const behind = Number(gitChangesState.data?.remote?.behind ?? gitChangesState.data?.summary?.behind ?? 0) || 0;
|
|
6712
|
+
if (behind <= 0 || gitChangesState.pulling || gitChangesState.loading) return;
|
|
6713
|
+
const root = gitChangesState.data?.root || "the current repository";
|
|
6714
|
+
if (!window.confirm(`Run git pull --ff-only in ${root}?`)) return;
|
|
6715
|
+
|
|
6716
|
+
const requestSerial = ++gitChangesRequestSerial;
|
|
6717
|
+
gitChangesState = { ...gitChangesState, pulling: true, loading: false, error: "", message: "", tabId: tabContext.tabId };
|
|
6718
|
+
renderGitChangesDialog();
|
|
6719
|
+
try {
|
|
6720
|
+
const response = await api("/api/git-changes/pull", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
6721
|
+
if (requestSerial !== gitChangesRequestSerial) return;
|
|
6722
|
+
if (!response.ok) {
|
|
6723
|
+
const detail = [response.error, response.data?.stderr || response.data?.stdout].filter(Boolean).join("\n").trim();
|
|
6724
|
+
throw new Error(detail || "Failed to pull git changes");
|
|
6725
|
+
}
|
|
6726
|
+
const output = String(response.data?.stdout || response.data?.stderr || "").trim();
|
|
6727
|
+
gitChangesState = {
|
|
6728
|
+
loading: false,
|
|
6729
|
+
pulling: false,
|
|
6730
|
+
error: "",
|
|
6731
|
+
message: output || "Pulled remote changes successfully.",
|
|
6732
|
+
data: response.data?.changes || gitChangesState.data,
|
|
6733
|
+
tabId: tabContext.tabId,
|
|
6734
|
+
};
|
|
6735
|
+
addEvent("Pulled remote git changes.", "success");
|
|
6736
|
+
requestGitFooterWebuiPayload(tabContext, { force: true });
|
|
6737
|
+
} catch (error) {
|
|
6738
|
+
if (requestSerial !== gitChangesRequestSerial) return;
|
|
6739
|
+
gitChangesState = { ...gitChangesState, pulling: false, error: error.message || String(error) };
|
|
6740
|
+
addEvent(error.message || String(error), "error");
|
|
6741
|
+
}
|
|
6742
|
+
renderGitChangesDialog();
|
|
6743
|
+
}
|
|
6744
|
+
|
|
6377
6745
|
function closeGitChangesDialog() {
|
|
6378
6746
|
gitChangesRequestSerial += 1;
|
|
6379
6747
|
gitChangesUntrackedContentRequests.clear();
|
|
6380
|
-
gitChangesState = { ...gitChangesState, loading: false };
|
|
6748
|
+
gitChangesState = { ...gitChangesState, loading: false, pulling: false };
|
|
6381
6749
|
if (elements.gitChangesDialog?.open) elements.gitChangesDialog.close();
|
|
6382
6750
|
}
|
|
6383
6751
|
|
|
@@ -8172,6 +8540,29 @@ async function refreshAppRunners(tabContext = activeTabContext()) {
|
|
|
8172
8540
|
renderWidgets();
|
|
8173
8541
|
}
|
|
8174
8542
|
|
|
8543
|
+
function appRunnerFailureState(runnerId, error, data = activeAppRunnerData()) {
|
|
8544
|
+
const runners = Array.isArray(data.runners) ? data.runners : [];
|
|
8545
|
+
const runner = runners.find((item) => item.id === runnerId) || {};
|
|
8546
|
+
const message = cleanStatusText(error?.message || String(error) || "Unknown app runner error");
|
|
8547
|
+
const command = runner.displayCommand || runner.shortDisplayCommand || runner.label || runnerId || "app runner";
|
|
8548
|
+
const timestamp = new Date().toISOString();
|
|
8549
|
+
return {
|
|
8550
|
+
id: `start-error:${Date.now()}`,
|
|
8551
|
+
runnerId,
|
|
8552
|
+
kind: runner.kind || "custom",
|
|
8553
|
+
label: runner.label || "App runner failed",
|
|
8554
|
+
command: runner.command || "",
|
|
8555
|
+
args: Array.isArray(runner.args) ? runner.args : [],
|
|
8556
|
+
displayCommand: command,
|
|
8557
|
+
cwd: data.cwd || "",
|
|
8558
|
+
status: "error",
|
|
8559
|
+
startedAt: timestamp,
|
|
8560
|
+
endedAt: timestamp,
|
|
8561
|
+
lineCount: 3,
|
|
8562
|
+
lines: [`$ ${command}`, "# failed to start app runner", `# ${message}`],
|
|
8563
|
+
};
|
|
8564
|
+
}
|
|
8565
|
+
|
|
8175
8566
|
async function runAppRunner(runnerId) {
|
|
8176
8567
|
const tabContext = activeTabContext();
|
|
8177
8568
|
if (!tabContext.tabId || !runnerId) return;
|
|
@@ -8186,7 +8577,12 @@ async function runAppRunner(runnerId) {
|
|
|
8186
8577
|
const command = response.data?.activeRun?.displayCommand || "app runner";
|
|
8187
8578
|
addEvent(`started ${command}`, "info");
|
|
8188
8579
|
} catch (error) {
|
|
8189
|
-
if (isCurrentTabContext(tabContext))
|
|
8580
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
8581
|
+
const message = cleanStatusText(error.message || String(error));
|
|
8582
|
+
setAppRunnerData(tabContext.tabId, { activeRun: appRunnerFailureState(runnerId, error, activeAppRunnerData()) });
|
|
8583
|
+
renderAppRunnerControls();
|
|
8584
|
+
renderWidgets();
|
|
8585
|
+
addEvent(`app runner failed: ${message}`, "error");
|
|
8190
8586
|
}
|
|
8191
8587
|
}
|
|
8192
8588
|
|
|
@@ -8267,9 +8663,14 @@ function activeAppRunnerCustomConfig() {
|
|
|
8267
8663
|
return activeAppRunnerData().customRunnerConfig || { runners: [], projectRoot: "", displayProjectRoot: "", displayConfigFile: "" };
|
|
8268
8664
|
}
|
|
8269
8665
|
|
|
8270
|
-
function resetAppRunnerCustomDraft() {
|
|
8666
|
+
function resetAppRunnerCustomDraft({ clearFeedback = true } = {}) {
|
|
8271
8667
|
appRunnerCustomDraft = { id: "", label: "", command: "./", path: "", args: "" };
|
|
8272
8668
|
appRunnerFileBrowserState = { open: false, loading: false, path: "", data: null, error: "" };
|
|
8669
|
+
if (clearFeedback) appRunnerCustomFeedback = { type: "", message: "" };
|
|
8670
|
+
}
|
|
8671
|
+
|
|
8672
|
+
function setAppRunnerCustomFeedback(type, message) {
|
|
8673
|
+
appRunnerCustomFeedback = { type, message: cleanStatusText(message || "") };
|
|
8273
8674
|
}
|
|
8274
8675
|
|
|
8275
8676
|
function appRunnerRelativeDir(filePath) {
|
|
@@ -8329,8 +8730,10 @@ async function saveAppRunnerCustomRunner(form) {
|
|
|
8329
8730
|
updateAppRunnerCustomDraftFrom(form);
|
|
8330
8731
|
const payload = appRunnerCustomDraftPayload();
|
|
8331
8732
|
if (!payload.path) {
|
|
8733
|
+
setAppRunnerCustomFeedback("warning", "Custom app runner path is required.");
|
|
8734
|
+
renderAppRunnerInfoDialog();
|
|
8735
|
+
requestAnimationFrame(() => document.querySelector("#appRunnerCustomPathInput")?.focus());
|
|
8332
8736
|
addEvent("custom app runner path is required", "warn");
|
|
8333
|
-
form?.querySelector("#appRunnerCustomPathInput")?.focus();
|
|
8334
8737
|
return;
|
|
8335
8738
|
}
|
|
8336
8739
|
const tabContext = activeTabContext();
|
|
@@ -8338,13 +8741,18 @@ async function saveAppRunnerCustomRunner(form) {
|
|
|
8338
8741
|
const response = await api("/api/app-runner-config", { method: "POST", body: { runner: payload }, tabId: tabContext.tabId });
|
|
8339
8742
|
if (!isCurrentTabContext(tabContext)) return;
|
|
8340
8743
|
setAppRunnerData(tabContext.tabId, response.data || {});
|
|
8341
|
-
resetAppRunnerCustomDraft();
|
|
8744
|
+
resetAppRunnerCustomDraft({ clearFeedback: false });
|
|
8745
|
+
setAppRunnerCustomFeedback("success", "Saved custom app runner. It should now appear in the Run menu when available.");
|
|
8342
8746
|
renderAppRunnerControls();
|
|
8343
8747
|
renderWidgets();
|
|
8344
8748
|
renderAppRunnerInfoDialog();
|
|
8345
8749
|
addEvent("saved custom app runner", "info");
|
|
8346
8750
|
} catch (error) {
|
|
8347
|
-
if (isCurrentTabContext(tabContext))
|
|
8751
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
8752
|
+
const message = error.message || String(error);
|
|
8753
|
+
setAppRunnerCustomFeedback("error", `Custom app runner was not saved: ${message}`);
|
|
8754
|
+
renderAppRunnerInfoDialog();
|
|
8755
|
+
addEvent(`custom app runner was not saved: ${message}`, "error");
|
|
8348
8756
|
}
|
|
8349
8757
|
}
|
|
8350
8758
|
|
|
@@ -8354,13 +8762,18 @@ async function deleteAppRunnerCustomRunner(id) {
|
|
|
8354
8762
|
const response = await api("/api/app-runner-config", { method: "DELETE", body: { id }, tabId: tabContext.tabId });
|
|
8355
8763
|
if (!isCurrentTabContext(tabContext)) return;
|
|
8356
8764
|
setAppRunnerData(tabContext.tabId, response.data || {});
|
|
8357
|
-
if (appRunnerCustomDraft.id === id) resetAppRunnerCustomDraft();
|
|
8765
|
+
if (appRunnerCustomDraft.id === id) resetAppRunnerCustomDraft({ clearFeedback: false });
|
|
8766
|
+
setAppRunnerCustomFeedback("success", "Deleted custom app runner.");
|
|
8358
8767
|
renderAppRunnerControls();
|
|
8359
8768
|
renderWidgets();
|
|
8360
8769
|
renderAppRunnerInfoDialog();
|
|
8361
8770
|
addEvent("deleted custom app runner", "warn");
|
|
8362
8771
|
} catch (error) {
|
|
8363
|
-
if (isCurrentTabContext(tabContext))
|
|
8772
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
8773
|
+
const message = error.message || String(error);
|
|
8774
|
+
setAppRunnerCustomFeedback("error", `Custom app runner was not deleted: ${message}`);
|
|
8775
|
+
renderAppRunnerInfoDialog();
|
|
8776
|
+
addEvent(`custom app runner was not deleted: ${message}`, "error");
|
|
8364
8777
|
}
|
|
8365
8778
|
}
|
|
8366
8779
|
|
|
@@ -8445,9 +8858,10 @@ function renderAppRunnerCustomSection() {
|
|
|
8445
8858
|
existing.append(make("div", "app-runner-custom-empty muted", "No custom runners saved for this project yet."));
|
|
8446
8859
|
} else {
|
|
8447
8860
|
for (const runner of customRunners) {
|
|
8448
|
-
const row = make("div",
|
|
8861
|
+
const row = make("div", `app-runner-custom-item${runner.available === false ? " unavailable" : ""}`);
|
|
8449
8862
|
const details = make("div", "app-runner-custom-item-details");
|
|
8450
8863
|
details.append(make("strong", "", runner.label || runner.path || "custom runner"), make("code", "", runner.displayCommand || runner.path || ""));
|
|
8864
|
+
if (runner.unavailableReason) details.append(make("span", "app-runner-custom-warning", `Not available: ${runner.unavailableReason}`));
|
|
8451
8865
|
const actions = make("div", "app-runner-custom-item-actions");
|
|
8452
8866
|
const edit = make("button", "", "Edit");
|
|
8453
8867
|
edit.type = "button";
|
|
@@ -8469,6 +8883,13 @@ function renderAppRunnerCustomSection() {
|
|
|
8469
8883
|
}
|
|
8470
8884
|
section.append(existing);
|
|
8471
8885
|
|
|
8886
|
+
const diagnostics = Array.isArray(config.diagnostics) ? config.diagnostics.filter((item) => item?.message) : [];
|
|
8887
|
+
if (diagnostics.length) {
|
|
8888
|
+
const diagnosticList = make("div", "app-runner-custom-diagnostics");
|
|
8889
|
+
for (const item of diagnostics) diagnosticList.append(make("div", `app-runner-custom-feedback ${item.severity || "warning"}`, item.message));
|
|
8890
|
+
section.append(diagnosticList);
|
|
8891
|
+
}
|
|
8892
|
+
|
|
8472
8893
|
const form = make("div", "app-runner-custom-form");
|
|
8473
8894
|
const labelField = appRunnerInputField({ id: "appRunnerCustomLabelInput", label: "Label", value: appRunnerCustomDraft.label, placeholder: "My app" });
|
|
8474
8895
|
const commandField = appRunnerInputField({ id: "appRunnerCustomCommandInput", label: "Command", value: appRunnerCustomDraft.command || "./", placeholder: "./", hint: "Use ./ to execute the selected file directly, or use bash, python3, node, bun, uv run, etc." });
|
|
@@ -8493,6 +8914,7 @@ function renderAppRunnerCustomSection() {
|
|
|
8493
8914
|
reset.addEventListener("click", () => { resetAppRunnerCustomDraft(); renderAppRunnerInfoDialog(); });
|
|
8494
8915
|
formActions.append(save, reset);
|
|
8495
8916
|
form.append(formActions);
|
|
8917
|
+
if (appRunnerCustomFeedback.message) form.append(make("div", `app-runner-custom-feedback ${appRunnerCustomFeedback.type || "info"}`, appRunnerCustomFeedback.message));
|
|
8496
8918
|
const browser = renderAppRunnerFileBrowser();
|
|
8497
8919
|
if (browser) form.append(browser);
|
|
8498
8920
|
section.append(form);
|
|
@@ -18408,6 +18830,7 @@ window.addEventListener("blur", () => {
|
|
|
18408
18830
|
});
|
|
18409
18831
|
|
|
18410
18832
|
elements.gitChangesRefreshButton?.addEventListener("click", refreshGitChangesDialog);
|
|
18833
|
+
elements.gitChangesPullButton?.addEventListener("click", () => pullGitChangesDialog().catch((error) => addEvent(error.message || String(error), "error")));
|
|
18411
18834
|
elements.gitChangesCloseButton?.addEventListener("click", closeGitChangesDialog);
|
|
18412
18835
|
elements.gitChangesBody?.addEventListener("scroll", updateGitChangesCurrentFileHeader, { passive: true });
|
|
18413
18836
|
elements.gitChangesDialog?.addEventListener("cancel", (event) => {
|
|
@@ -18416,7 +18839,7 @@ elements.gitChangesDialog?.addEventListener("cancel", (event) => {
|
|
|
18416
18839
|
});
|
|
18417
18840
|
elements.gitChangesDialog?.addEventListener("close", () => {
|
|
18418
18841
|
gitChangesRequestSerial += 1;
|
|
18419
|
-
gitChangesState = { ...gitChangesState, loading: false };
|
|
18842
|
+
gitChangesState = { ...gitChangesState, loading: false, pulling: false };
|
|
18420
18843
|
});
|
|
18421
18844
|
|
|
18422
18845
|
elements.refreshCodexUsageButton?.addEventListener("click", () => {
|
|
@@ -18549,6 +18972,7 @@ initializeFastPicks().catch((error) => addEvent(`failed to initialize path fast
|
|
|
18549
18972
|
restoreAgentDoneNotificationsSetting();
|
|
18550
18973
|
restoreThinkingVisibilitySetting();
|
|
18551
18974
|
restoreTerminalTabsLayoutSetting();
|
|
18975
|
+
restoreTerminalCustomGroups();
|
|
18552
18976
|
restoreToolOutputExpansionSetting();
|
|
18553
18977
|
restoreWorkspaceDashboardState();
|
|
18554
18978
|
restoreSidePanelSectionState();
|