@firstpick/pi-package-webui 0.1.4 → 0.1.5
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/README.md +25 -6
- package/bin/pi-webui.mjs +183 -13
- package/package.json +34 -4
- package/public/app.js +567 -42
- package/public/index.html +3 -0
- package/public/service-worker.js +1 -1
- package/public/styles.css +145 -2
- package/tests/mobile-static.test.mjs +89 -8
package/public/app.js
CHANGED
|
@@ -5,6 +5,7 @@ const elements = {
|
|
|
5
5
|
tabBar: $("#tabBar"),
|
|
6
6
|
terminalTabsToggleButton: $("#terminalTabsToggleButton"),
|
|
7
7
|
newTabButton: $("#newTabButton"),
|
|
8
|
+
closeAllTabsButton: $("#closeAllTabsButton"),
|
|
8
9
|
statusBar: $("#statusBar"),
|
|
9
10
|
widgetArea: $("#widgetArea"),
|
|
10
11
|
stickyUserPromptButton: $("#stickyUserPromptButton"),
|
|
@@ -47,6 +48,7 @@ const elements = {
|
|
|
47
48
|
openNetworkButton: $("#openNetworkButton"),
|
|
48
49
|
agentDoneNotificationsToggle: $("#agentDoneNotificationsToggle"),
|
|
49
50
|
agentDoneNotificationsStatus: $("#agentDoneNotificationsStatus"),
|
|
51
|
+
optionalFeaturesBox: $("#optionalFeaturesBox"),
|
|
50
52
|
toggleSidePanelButton: $("#toggleSidePanelButton"),
|
|
51
53
|
sidePanelExpandButton: $("#sidePanelExpandButton"),
|
|
52
54
|
sidePanelBackdrop: $("#sidePanelBackdrop"),
|
|
@@ -83,6 +85,8 @@ let streamText = null;
|
|
|
83
85
|
let streamRawText = "";
|
|
84
86
|
let streamBubbleVisibleSince = 0;
|
|
85
87
|
let streamBubbleHideTimer = null;
|
|
88
|
+
let streamTextRenderTimer = null;
|
|
89
|
+
let streamToolCallSeen = false;
|
|
86
90
|
let streamThinkingBubble = null;
|
|
87
91
|
let streamThinking = null;
|
|
88
92
|
let runIndicatorBubble = null;
|
|
@@ -120,6 +124,8 @@ let latestWorkspace = null;
|
|
|
120
124
|
let latestNetwork = null;
|
|
121
125
|
let latestMessages = [];
|
|
122
126
|
let transientMessages = [];
|
|
127
|
+
let actionEntrySeenKeysByTab = new Map();
|
|
128
|
+
let actionEntryAnimationPrimedTabs = new Set();
|
|
123
129
|
let lastUserPromptByTab = new Map();
|
|
124
130
|
let actionFeedbackByTab = new Map();
|
|
125
131
|
let actionFeedbackSendBusy = false;
|
|
@@ -154,6 +160,7 @@ const TAB_STORAGE_KEY = "pi-webui-active-tab";
|
|
|
154
160
|
const PATH_FAST_PICKS_STORAGE_KEY = "pi-webui-path-fast-picks";
|
|
155
161
|
const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications";
|
|
156
162
|
const THEME_STORAGE_KEY = "pi-webui-theme";
|
|
163
|
+
const OPTIONAL_FEATURES_STORAGE_KEY = "pi-webui-optional-features-disabled";
|
|
157
164
|
const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
|
|
158
165
|
const DEFAULT_THEME_NAME = "catppuccin-mocha";
|
|
159
166
|
const MOBILE_VIEW_QUERY = "(max-width: 720px), (max-device-width: 720px), (pointer: coarse) and (hover: none)";
|
|
@@ -167,6 +174,7 @@ const RUN_INDICATOR_TICK_MS = 1000;
|
|
|
167
174
|
const RUN_INDICATOR_START_GRACE_MS = 2500;
|
|
168
175
|
const RUN_INDICATOR_STATE_RECHECK_MS = 5000;
|
|
169
176
|
const STREAM_OUTPUT_HIDE_DELAY_MS = 300;
|
|
177
|
+
const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220;
|
|
170
178
|
const STREAM_OUTPUT_MIN_VISIBLE_MS = 900;
|
|
171
179
|
const TODO_PROGRESS_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)\]\s+.+$/;
|
|
172
180
|
const TODO_PROGRESS_PARTIAL_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)?\]?\s*.*$/;
|
|
@@ -180,6 +188,79 @@ const BLOCKED_TAB_NOTIFICATION_ICON = "/icon-192.png";
|
|
|
180
188
|
const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
|
|
181
189
|
const statusEntries = new Map();
|
|
182
190
|
const widgets = new Map();
|
|
191
|
+
// Optional feature detection intentionally checks loaded Pi capabilities (RPC-visible
|
|
192
|
+
// commands and live widget events), not npm package folders. This keeps local dev
|
|
193
|
+
// symlinks and independently installed packages working.
|
|
194
|
+
const optionalFeatureAvailability = {
|
|
195
|
+
gitWorkflow: false,
|
|
196
|
+
releaseNpm: false,
|
|
197
|
+
releaseAur: false,
|
|
198
|
+
statsCommand: false,
|
|
199
|
+
gitFooterStatus: false,
|
|
200
|
+
todoProgressWidget: false,
|
|
201
|
+
themeBundle: false,
|
|
202
|
+
};
|
|
203
|
+
const OPTIONAL_FEATURES = [
|
|
204
|
+
{
|
|
205
|
+
id: "gitWorkflow",
|
|
206
|
+
label: "Guided Git workflow",
|
|
207
|
+
packageName: "@firstpick/pi-prompts-git-pr",
|
|
208
|
+
capabilityLabel: "/git-staged-msg",
|
|
209
|
+
description: "Generate staged commit messages for the guided Git workflow.",
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: "releaseNpm",
|
|
213
|
+
label: "NPM Release",
|
|
214
|
+
packageName: "@firstpick/pi-extension-release-npm",
|
|
215
|
+
capabilityLabel: "/release-npm",
|
|
216
|
+
description: "Publish menu action and live npm release widgets.",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "releaseAur",
|
|
220
|
+
label: "AUR Release",
|
|
221
|
+
packageName: "@firstpick/pi-extension-release-aur",
|
|
222
|
+
capabilityLabel: "/release-aur",
|
|
223
|
+
description: "Publish menu action, setup helpers, skills, and AUR release widgets.",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: "todoProgressWidget",
|
|
227
|
+
label: "Todo progress widget",
|
|
228
|
+
packageName: "@firstpick/pi-extension-todo-progress",
|
|
229
|
+
capabilityLabel: "/todo-progress-status or todo-progress widget event",
|
|
230
|
+
description: "Styled live checklist rendering for assistant todo updates.",
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: "gitFooterStatus",
|
|
234
|
+
label: "Git footer status",
|
|
235
|
+
packageName: "@firstpick/pi-extension-git-footer-status",
|
|
236
|
+
capabilityLabel: "/git-footer-refresh or git-footer status event",
|
|
237
|
+
description: "Enhanced Pi footer/status telemetry when loaded by Pi.",
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
id: "statsCommand",
|
|
241
|
+
label: "Stats command",
|
|
242
|
+
packageName: "@firstpick/pi-extension-stats",
|
|
243
|
+
capabilityLabel: "/stats",
|
|
244
|
+
description: "Token and cost usage analytics commands.",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: "themeBundle",
|
|
248
|
+
label: "Theme bundle",
|
|
249
|
+
packageName: "@firstpick/pi-themes-bundle",
|
|
250
|
+
capabilityLabel: "/api/themes returned themes",
|
|
251
|
+
description: "Additional browser theme-picker and Pi theme resources.",
|
|
252
|
+
},
|
|
253
|
+
];
|
|
254
|
+
const OPTIONAL_FEATURE_BY_ID = new Map(OPTIONAL_FEATURES.map((feature) => [feature.id, feature]));
|
|
255
|
+
const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
256
|
+
["git-staged-msg", "gitWorkflow"],
|
|
257
|
+
["release-npm", "releaseNpm"],
|
|
258
|
+
["release-aur", "releaseAur"],
|
|
259
|
+
["stats", "statsCommand"],
|
|
260
|
+
["git-footer-refresh", "gitFooterStatus"],
|
|
261
|
+
["todo-progress-status", "todoProgressWidget"],
|
|
262
|
+
]);
|
|
263
|
+
const optionalFeatureInstallInProgress = new Set();
|
|
183
264
|
const gitWorkflow = {
|
|
184
265
|
active: false,
|
|
185
266
|
step: "idle",
|
|
@@ -497,6 +578,49 @@ function storeThemeName(name) {
|
|
|
497
578
|
}
|
|
498
579
|
}
|
|
499
580
|
|
|
581
|
+
function loadDisabledOptionalFeatures() {
|
|
582
|
+
try {
|
|
583
|
+
const parsed = JSON.parse(localStorage.getItem(OPTIONAL_FEATURES_STORAGE_KEY) || "[]");
|
|
584
|
+
return Array.isArray(parsed) ? parsed.filter((id) => OPTIONAL_FEATURE_BY_ID.has(id)) : [];
|
|
585
|
+
} catch {
|
|
586
|
+
return [];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
let disabledOptionalFeatures = new Set(loadDisabledOptionalFeatures());
|
|
591
|
+
|
|
592
|
+
function storeDisabledOptionalFeatures() {
|
|
593
|
+
try {
|
|
594
|
+
localStorage.setItem(OPTIONAL_FEATURES_STORAGE_KEY, JSON.stringify([...disabledOptionalFeatures].sort()));
|
|
595
|
+
} catch {
|
|
596
|
+
// Optional feature toggles should still work for this page load.
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function isOptionalFeatureDetected(featureId) {
|
|
601
|
+
return optionalFeatureAvailability[featureId] === true;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function isOptionalFeatureDisabled(featureId) {
|
|
605
|
+
return disabledOptionalFeatures.has(featureId);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function isOptionalFeatureEnabled(featureId) {
|
|
609
|
+
return isOptionalFeatureDetected(featureId) && !isOptionalFeatureDisabled(featureId);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function setOptionalFeatureDisabled(featureId, disabled) {
|
|
613
|
+
if (!OPTIONAL_FEATURE_BY_ID.has(featureId)) return;
|
|
614
|
+
if (disabled) disabledOptionalFeatures.add(featureId);
|
|
615
|
+
else disabledOptionalFeatures.delete(featureId);
|
|
616
|
+
storeDisabledOptionalFeatures();
|
|
617
|
+
renderOptionalFeatureControls();
|
|
618
|
+
renderThemeSelect();
|
|
619
|
+
renderWidgets();
|
|
620
|
+
renderStatus();
|
|
621
|
+
refreshCommands().catch((error) => addEvent(error.message || String(error), "error"));
|
|
622
|
+
}
|
|
623
|
+
|
|
500
624
|
function displayThemeName(name) {
|
|
501
625
|
return String(name || "")
|
|
502
626
|
.split(/[-_]+/)
|
|
@@ -685,6 +809,13 @@ function applyTheme(theme, { persist = false, announce = false } = {}) {
|
|
|
685
809
|
function renderThemeSelect({ unavailableLabel = "Theme bundle unavailable" } = {}) {
|
|
686
810
|
if (!elements.themeSelect) return;
|
|
687
811
|
elements.themeSelect.replaceChildren();
|
|
812
|
+
if (isOptionalFeatureDisabled("themeBundle")) {
|
|
813
|
+
const option = make("option", undefined, "Theme feature disabled");
|
|
814
|
+
option.value = "";
|
|
815
|
+
elements.themeSelect.append(option);
|
|
816
|
+
elements.themeSelect.disabled = true;
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
688
819
|
if (!availableThemes.length) {
|
|
689
820
|
const option = make("option", undefined, unavailableLabel);
|
|
690
821
|
option.value = "";
|
|
@@ -702,6 +833,7 @@ function renderThemeSelect({ unavailableLabel = "Theme bundle unavailable" } = {
|
|
|
702
833
|
}
|
|
703
834
|
|
|
704
835
|
function setThemeByName(name, options = {}) {
|
|
836
|
+
if (!isOptionalFeatureEnabled("themeBundle")) return;
|
|
705
837
|
const theme = availableThemes.find((item) => item.name === name);
|
|
706
838
|
if (!theme) return;
|
|
707
839
|
applyTheme(theme, options);
|
|
@@ -713,16 +845,20 @@ async function initializeThemes() {
|
|
|
713
845
|
response = await api("/api/themes", { scoped: false });
|
|
714
846
|
} catch (error) {
|
|
715
847
|
availableThemes = [];
|
|
848
|
+
optionalFeatureAvailability.themeBundle = false;
|
|
849
|
+
renderOptionalFeatureControls();
|
|
716
850
|
const label = error.statusCode === 404 ? "Restart Web UI to load themes" : "Theme bundle unavailable";
|
|
717
851
|
renderThemeSelect({ unavailableLabel: label });
|
|
718
852
|
throw error;
|
|
719
853
|
}
|
|
720
854
|
availableThemes = Array.isArray(response.data?.themes) ? response.data.themes : [];
|
|
855
|
+
optionalFeatureAvailability.themeBundle = availableThemes.length > 0;
|
|
856
|
+
renderOptionalFeatureControls();
|
|
721
857
|
const stored = storedThemeName();
|
|
722
858
|
currentThemeName = availableThemes.some((theme) => theme.name === stored) ? stored : DEFAULT_THEME_NAME;
|
|
723
859
|
renderThemeSelect();
|
|
724
860
|
setThemeByName(currentThemeName, { persist: false });
|
|
725
|
-
if (!availableThemes.some((theme) => theme.name === currentThemeName) && availableThemes[0]) applyTheme(availableThemes[0], { persist: false });
|
|
861
|
+
if (isOptionalFeatureEnabled("themeBundle") && !availableThemes.some((theme) => theme.name === currentThemeName) && availableThemes[0]) applyTheme(availableThemes[0], { persist: false });
|
|
726
862
|
if (!availableThemes.length) addEvent("theme bundle unavailable; using built-in default theme", "warn");
|
|
727
863
|
}
|
|
728
864
|
|
|
@@ -1044,6 +1180,7 @@ function resetActiveTabUi() {
|
|
|
1044
1180
|
widgets.clear();
|
|
1045
1181
|
transientMessages = [];
|
|
1046
1182
|
availableCommands = [];
|
|
1183
|
+
resetOptionalFeatureAvailability();
|
|
1047
1184
|
commandSuggestions = [];
|
|
1048
1185
|
pathSuggestions = [];
|
|
1049
1186
|
suggestionMode = "none";
|
|
@@ -1199,7 +1336,7 @@ function shouldRenderTerminalTabGroup(group, groupCount) {
|
|
|
1199
1336
|
return groupCount > 1 && group.tabs.length > 1 && Boolean(group.cwd);
|
|
1200
1337
|
}
|
|
1201
1338
|
|
|
1202
|
-
function renderTerminalTabGroup(group) {
|
|
1339
|
+
function renderTerminalTabGroup(group, groupCount = 1) {
|
|
1203
1340
|
const groupTabs = group.tabs;
|
|
1204
1341
|
const activeGroupTab = groupTabs.find((tab) => tab.id === activeTabId) || groupTabs[0];
|
|
1205
1342
|
const isActive = groupTabs.some((tab) => tab.id === activeTabId);
|
|
@@ -1229,6 +1366,18 @@ function renderTerminalTabGroup(group) {
|
|
|
1229
1366
|
button.addEventListener("click", () => switchTab(activeGroupTab.id));
|
|
1230
1367
|
wrapper.append(button);
|
|
1231
1368
|
|
|
1369
|
+
if (groupCount > 1) {
|
|
1370
|
+
const close = make("button", "terminal-tab-close terminal-tab-group-close", "×");
|
|
1371
|
+
close.type = "button";
|
|
1372
|
+
close.title = `Close ${displayCwd} group`;
|
|
1373
|
+
close.setAttribute("aria-label", `Close ${displayCwd} group`);
|
|
1374
|
+
close.addEventListener("click", (event) => {
|
|
1375
|
+
event.stopPropagation();
|
|
1376
|
+
closeTerminalTabGroup(group);
|
|
1377
|
+
});
|
|
1378
|
+
wrapper.append(close);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1232
1381
|
const menu = make("div", "terminal-tab-group-menu");
|
|
1233
1382
|
menu.setAttribute("role", "group");
|
|
1234
1383
|
menu.setAttribute("aria-label", `${displayCwd} tabs`);
|
|
@@ -1279,12 +1428,13 @@ function renderTabs() {
|
|
|
1279
1428
|
if (openTerminalTabGroupKey && !renderedGroupKeys.has(openTerminalTabGroupKey)) openTerminalTabGroupKey = null;
|
|
1280
1429
|
for (const group of groups) {
|
|
1281
1430
|
if (shouldRenderTerminalTabGroup(group, groups.length)) {
|
|
1282
|
-
elements.tabBar.append(renderTerminalTabGroup(group));
|
|
1431
|
+
elements.tabBar.append(renderTerminalTabGroup(group, groups.length));
|
|
1283
1432
|
} else {
|
|
1284
1433
|
for (const tab of group.tabs) elements.tabBar.append(renderTerminalTab(tab));
|
|
1285
1434
|
}
|
|
1286
1435
|
}
|
|
1287
1436
|
elements.tabBar.append(elements.newTabButton);
|
|
1437
|
+
elements.closeAllTabsButton.disabled = tabs.length === 0;
|
|
1288
1438
|
updateTerminalTabGroupOpenState();
|
|
1289
1439
|
setMobileTabsExpanded(mobileTabsExpanded);
|
|
1290
1440
|
updateDocumentTitle();
|
|
@@ -1345,21 +1495,58 @@ async function createTerminalTab(cwd = activeTab()?.cwd, { triggerButton = eleme
|
|
|
1345
1495
|
}
|
|
1346
1496
|
}
|
|
1347
1497
|
|
|
1348
|
-
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1351
|
-
|
|
1498
|
+
function tabHasActiveAgent(tab) {
|
|
1499
|
+
const activity = activityForTab(tab);
|
|
1500
|
+
const indicator = tabIndicator(tab);
|
|
1501
|
+
return !!activity.isWorking || indicator.state === "working" || indicator.state === "blocked";
|
|
1502
|
+
}
|
|
1352
1503
|
|
|
1353
|
-
|
|
1354
|
-
const
|
|
1504
|
+
function confirmCloseTerminalTabs(targetTabs, label) {
|
|
1505
|
+
const count = targetTabs.length;
|
|
1506
|
+
const noun = count === 1 ? "tab" : "tabs";
|
|
1507
|
+
const activeAgentTabs = targetTabs.filter(tabHasActiveAgent);
|
|
1508
|
+
const tabList = targetTabs.map((tab) => `- ${tab.title}`).join("\n");
|
|
1509
|
+
const activeList = activeAgentTabs.map((tab) => `- ${tab.title} (${tabIndicator(tab).label})`).join("\n");
|
|
1510
|
+
const base = [
|
|
1511
|
+
`Close ${label || `${count} terminal ${noun}`}?`,
|
|
1512
|
+
"",
|
|
1513
|
+
`This terminates ${count === 1 ? "its isolated Pi process" : "their isolated Pi processes"}.`,
|
|
1514
|
+
count > 1 ? `\nTabs to close:\n${tabList}` : "",
|
|
1515
|
+
].filter(Boolean).join("\n");
|
|
1516
|
+
const warning = activeAgentTabs.length
|
|
1517
|
+
? [
|
|
1518
|
+
`WARNING: ${activeAgentTabs.length} ${activeAgentTabs.length === 1 ? "tab has an agent" : "tabs have agents"} still running or waiting for input:`,
|
|
1519
|
+
activeList,
|
|
1520
|
+
"",
|
|
1521
|
+
base,
|
|
1522
|
+
"",
|
|
1523
|
+
"Close anyway?",
|
|
1524
|
+
].join("\n")
|
|
1525
|
+
: base;
|
|
1526
|
+
return confirm(warning);
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
async function closeTerminalTabs(tabIds, { label = "selected terminal tabs" } = {}) {
|
|
1530
|
+
const targetIds = [...new Set(tabIds.filter(Boolean))];
|
|
1531
|
+
const targetTabs = targetIds.map((id) => tabs.find((item) => item.id === id)).filter(Boolean);
|
|
1532
|
+
if (!targetTabs.length) return;
|
|
1533
|
+
if (!confirmCloseTerminalTabs(targetTabs, label)) return;
|
|
1534
|
+
|
|
1535
|
+
const closedActiveTab = targetTabs.some((tab) => tab.id === activeTabId);
|
|
1536
|
+
const fallbackTabId = tabs.find((item) => !targetIds.includes(item.id))?.id || null;
|
|
1355
1537
|
try {
|
|
1356
|
-
if (
|
|
1357
|
-
const response = await api(
|
|
1358
|
-
|
|
1538
|
+
if (closedActiveTab) eventSource?.close();
|
|
1539
|
+
const response = await api("/api/tabs/close", { method: "POST", body: { ids: targetIds }, scoped: false });
|
|
1540
|
+
const closedIds = response.data?.closedIds || targetIds;
|
|
1541
|
+
tabs = response.data?.tabs || tabs.filter((item) => !closedIds.includes(item.id));
|
|
1359
1542
|
syncTabMetadata(tabs);
|
|
1360
|
-
tabDrafts.delete(
|
|
1361
|
-
|
|
1362
|
-
|
|
1543
|
+
for (const id of closedIds) tabDrafts.delete(id);
|
|
1544
|
+
clearOpenTerminalTabGroup(null, { force: true });
|
|
1545
|
+
|
|
1546
|
+
if (closedActiveTab || !tabs.some((item) => item.id === activeTabId)) {
|
|
1547
|
+
activeTabId = (response.data?.activeTabId && tabs.some((item) => item.id === response.data.activeTabId)
|
|
1548
|
+
? response.data.activeTabId
|
|
1549
|
+
: (fallbackTabId && tabs.some((item) => item.id === fallbackTabId) ? fallbackTabId : tabs[0]?.id)) || null;
|
|
1363
1550
|
rememberActiveTab();
|
|
1364
1551
|
resetActiveTabUi();
|
|
1365
1552
|
renderTabs();
|
|
@@ -1373,11 +1560,27 @@ async function closeTerminalTab(tabId) {
|
|
|
1373
1560
|
} else {
|
|
1374
1561
|
renderTabs();
|
|
1375
1562
|
}
|
|
1563
|
+
addEvent(`closed ${closedIds.length || targetTabs.length} terminal ${closedIds.length === 1 ? "tab" : "tabs"}`, "warn");
|
|
1376
1564
|
} catch (error) {
|
|
1377
1565
|
addEvent(error.message, "error");
|
|
1378
1566
|
}
|
|
1379
1567
|
}
|
|
1380
1568
|
|
|
1569
|
+
async function closeTerminalTab(tabId) {
|
|
1570
|
+
const tab = tabs.find((item) => item.id === tabId);
|
|
1571
|
+
if (!tab) return;
|
|
1572
|
+
await closeTerminalTabs([tabId], { label: tab.title });
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
async function closeTerminalTabGroup(group) {
|
|
1576
|
+
const title = tabGroupTitle(group.cwd, group.tabs[0]?.title || "cwd");
|
|
1577
|
+
await closeTerminalTabs(group.tabs.map((tab) => tab.id), { label: `${title} group` });
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
async function closeAllTerminalTabs() {
|
|
1581
|
+
await closeTerminalTabs(tabs.map((tab) => tab.id), { label: "all terminal tabs" });
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1381
1584
|
async function initializeTabs() {
|
|
1382
1585
|
await refreshTabs({ selectStored: true });
|
|
1383
1586
|
resetActiveTabUi();
|
|
@@ -1791,6 +1994,7 @@ function formatStatusEntry(key, value) {
|
|
|
1791
1994
|
const cleanKey = cleanStatusText(key);
|
|
1792
1995
|
const cleanValue = cleanStatusText(value);
|
|
1793
1996
|
if (!cleanValue) return "";
|
|
1997
|
+
if (cleanKey === "git-footer" && !isOptionalFeatureEnabled("gitFooterStatus")) return "";
|
|
1794
1998
|
if (cleanKey === "plan-mode") return `Plan: ${cleanValue}`;
|
|
1795
1999
|
if (cleanKey === "extension") return cleanValue;
|
|
1796
2000
|
return `${cleanKey}: ${cleanValue}`;
|
|
@@ -2435,6 +2639,7 @@ function renderReleaseDialogMessage(parent, text) {
|
|
|
2435
2639
|
}
|
|
2436
2640
|
|
|
2437
2641
|
function stripTodoProgressLines(text, { streaming = false } = {}) {
|
|
2642
|
+
if (!isOptionalFeatureEnabled("todoProgressWidget")) return String(text || "");
|
|
2438
2643
|
let inFence = false;
|
|
2439
2644
|
const kept = [];
|
|
2440
2645
|
const raw = String(text || "");
|
|
@@ -2576,6 +2781,7 @@ function releaseNpmActionButton(label, command, className = "") {
|
|
|
2576
2781
|
}
|
|
2577
2782
|
|
|
2578
2783
|
function renderReleaseNpmOutputWidget() {
|
|
2784
|
+
if (!isOptionalFeatureEnabled("releaseNpm")) return null;
|
|
2579
2785
|
const outputLines = getWidgetLines("release-npm:output");
|
|
2580
2786
|
const footerLines = getWidgetLines("release-npm:footer");
|
|
2581
2787
|
if (outputLines.length === 0 && footerLines.length === 0) return null;
|
|
@@ -2613,6 +2819,7 @@ function renderReleaseNpmOutputWidget() {
|
|
|
2613
2819
|
}
|
|
2614
2820
|
|
|
2615
2821
|
function renderReleaseNpmLogWidget() {
|
|
2822
|
+
if (!isOptionalFeatureEnabled("releaseNpm")) return null;
|
|
2616
2823
|
const lines = getWidgetLines("release-npm:logs");
|
|
2617
2824
|
if (lines.length === 0) return null;
|
|
2618
2825
|
|
|
@@ -2640,6 +2847,7 @@ function renderReleaseNpmLogWidget() {
|
|
|
2640
2847
|
}
|
|
2641
2848
|
|
|
2642
2849
|
function renderReleaseAurOutputWidget() {
|
|
2850
|
+
if (!isOptionalFeatureEnabled("releaseAur")) return null;
|
|
2643
2851
|
const outputLines = getWidgetLines("release-aur:output");
|
|
2644
2852
|
const footerLines = getWidgetLines("release-aur:footer");
|
|
2645
2853
|
if (outputLines.length === 0 && footerLines.length === 0) return null;
|
|
@@ -2677,6 +2885,7 @@ function renderReleaseAurOutputWidget() {
|
|
|
2677
2885
|
}
|
|
2678
2886
|
|
|
2679
2887
|
function renderReleaseAurLogWidget() {
|
|
2888
|
+
if (!isOptionalFeatureEnabled("releaseAur")) return null;
|
|
2680
2889
|
const lines = getWidgetLines("release-aur:logs");
|
|
2681
2890
|
if (lines.length === 0) return null;
|
|
2682
2891
|
|
|
@@ -2714,11 +2923,12 @@ function renderWidgets() {
|
|
|
2714
2923
|
const releaseAurLog = renderReleaseAurLogWidget();
|
|
2715
2924
|
if (releaseAurLog) elements.widgetArea.append(releaseAurLog);
|
|
2716
2925
|
|
|
2717
|
-
const releaseWidgetKeys = new Set(["release-npm:output", "release-npm:footer", "release-npm:logs", "release-aur:output", "release-aur:footer", "release-aur:logs"]);
|
|
2718
2926
|
for (const [key, value] of widgets) {
|
|
2719
|
-
|
|
2927
|
+
const widgetFeatureId = optionalFeatureWidgetFeatureId(key);
|
|
2928
|
+
if (widgetFeatureId && !isOptionalFeatureEnabled(widgetFeatureId)) continue;
|
|
2929
|
+
if (widgetFeatureId && key !== "todo-progress") continue;
|
|
2720
2930
|
const lines = Array.isArray(value.widgetLines) ? value.widgetLines : [];
|
|
2721
|
-
const specialized = key === "todo-progress" ? renderTodoProgressWidget(key, lines) : null;
|
|
2931
|
+
const specialized = key === "todo-progress" && isOptionalFeatureEnabled("todoProgressWidget") ? renderTodoProgressWidget(key, lines) : null;
|
|
2722
2932
|
if (specialized) {
|
|
2723
2933
|
elements.widgetArea.append(specialized);
|
|
2724
2934
|
continue;
|
|
@@ -2866,6 +3076,11 @@ function failGitWorkflow(error, step = gitWorkflow.step) {
|
|
|
2866
3076
|
}
|
|
2867
3077
|
|
|
2868
3078
|
function startGitWorkflow() {
|
|
3079
|
+
if (!isOptionalFeatureEnabled("gitWorkflow")) {
|
|
3080
|
+
addEvent(commandUnavailableMessage("git-staged-msg"), "warn");
|
|
3081
|
+
refreshCommands().catch((error) => addEvent(error.message || String(error), "error"));
|
|
3082
|
+
return;
|
|
3083
|
+
}
|
|
2869
3084
|
if (gitWorkflow.active && !["done", "cancelled", "error"].includes(gitWorkflow.step) && !confirm("Restart the active git workflow?")) return;
|
|
2870
3085
|
gitWorkflow.runId += 1;
|
|
2871
3086
|
setGitWorkflow({
|
|
@@ -3306,6 +3521,14 @@ function assistantThinkingText(part) {
|
|
|
3306
3521
|
return typeof part.content === "string" ? part.content : "";
|
|
3307
3522
|
}
|
|
3308
3523
|
|
|
3524
|
+
function isAssistantToolCallPart(part) {
|
|
3525
|
+
return !!(part && typeof part === "object" && (part.type === "toolCall" || part.toolCall));
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
function assistantHasToolCallAfter(content, index) {
|
|
3529
|
+
return Array.isArray(content) && content.slice(index + 1).some(isAssistantToolCallPart);
|
|
3530
|
+
}
|
|
3531
|
+
|
|
3309
3532
|
function assistantToolCallName(part) {
|
|
3310
3533
|
return String(part?.name || part?.toolName || part?.toolCall?.name || "unknown");
|
|
3311
3534
|
}
|
|
@@ -3342,14 +3565,15 @@ function assistantDisplayMessages(message) {
|
|
|
3342
3565
|
|
|
3343
3566
|
const displayMessages = [];
|
|
3344
3567
|
const finalParts = [];
|
|
3345
|
-
for (
|
|
3568
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
3569
|
+
const part = content[index];
|
|
3346
3570
|
const isThinkingPart = part && typeof part === "object" && (part.type === "thinking" || typeof part.thinking === "string");
|
|
3347
3571
|
if (isThinkingPart) {
|
|
3348
3572
|
const thinking = assistantThinkingText(part) || "No thinking content was exposed by the provider.";
|
|
3349
3573
|
displayMessages.push({ ...base, role: "thinking", title: "thinking", content: thinking, thinking });
|
|
3350
3574
|
continue;
|
|
3351
3575
|
}
|
|
3352
|
-
if (part
|
|
3576
|
+
if (isAssistantToolCallPart(part)) {
|
|
3353
3577
|
const toolName = assistantToolCallName(part);
|
|
3354
3578
|
const args = assistantToolCallArguments(part);
|
|
3355
3579
|
displayMessages.push({ ...base, role: "toolCall", title: `tool call: ${toolName}`, toolName, arguments: args, content: args });
|
|
@@ -3357,7 +3581,7 @@ function assistantDisplayMessages(message) {
|
|
|
3357
3581
|
}
|
|
3358
3582
|
const finalPart = assistantFinalOutputPart(part);
|
|
3359
3583
|
if (finalPart) {
|
|
3360
|
-
finalParts.push(finalPart);
|
|
3584
|
+
if (!assistantHasToolCallAfter(content, index)) finalParts.push(finalPart);
|
|
3361
3585
|
continue;
|
|
3362
3586
|
}
|
|
3363
3587
|
if (part !== undefined && part !== null) {
|
|
@@ -3520,6 +3744,15 @@ function updateStickyUserPromptButton() {
|
|
|
3520
3744
|
);
|
|
3521
3745
|
}
|
|
3522
3746
|
|
|
3747
|
+
function toolResultPreviewText(message, lineLimit = 10) {
|
|
3748
|
+
const text = textFromContent(message?.content).replace(/\s+$/g, "");
|
|
3749
|
+
if (!text) return "(empty tool result)";
|
|
3750
|
+
const lines = text.split(/\r?\n/);
|
|
3751
|
+
const preview = lines.slice(0, lineLimit).join("\n");
|
|
3752
|
+
const remaining = Math.max(0, lines.length - lineLimit);
|
|
3753
|
+
return remaining > 0 ? `${preview}\n… ${remaining} more line${remaining === 1 ? "" : "s"}; expand for full output` : preview;
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3523
3756
|
function jumpToStickyUserPrompt() {
|
|
3524
3757
|
const button = elements.stickyUserPromptButton;
|
|
3525
3758
|
const index = Number(button?.dataset.messageIndex);
|
|
@@ -3534,10 +3767,10 @@ function jumpToStickyUserPrompt() {
|
|
|
3534
3767
|
requestAnimationFrame(updateStickyUserPromptButton);
|
|
3535
3768
|
}
|
|
3536
3769
|
|
|
3537
|
-
function appendMessage(message, { streaming = false, messageIndex = -1, transient = false } = {}) {
|
|
3770
|
+
function appendMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false } = {}) {
|
|
3538
3771
|
const role = String(message.role || "message");
|
|
3539
3772
|
const safeRole = role.replace(/[^a-z0-9_-]/gi, "");
|
|
3540
|
-
const bubble = make("article", `message ${safeRole}${message.level ? ` ${message.level}` : ""}${streaming ? " streaming" : ""}`);
|
|
3773
|
+
const bubble = make("article", `message ${safeRole}${message.level ? ` ${message.level}` : ""}${streaming ? " streaming" : ""}${animateEntry ? " action-enter" : ""}`);
|
|
3541
3774
|
if (!transient && messageIndex >= 0) {
|
|
3542
3775
|
bubble.dataset.messageIndex = String(messageIndex);
|
|
3543
3776
|
if (role === "user") bubble.dataset.userPrompt = "true";
|
|
@@ -3557,7 +3790,8 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
3557
3790
|
renderContent(body, message.content);
|
|
3558
3791
|
if (message.isError) bubble.classList.add("error");
|
|
3559
3792
|
} else if (message.role === "thinking") {
|
|
3560
|
-
|
|
3793
|
+
const thinkingText = message.thinking || textFromContent(message.content);
|
|
3794
|
+
if (thinkingText || !streaming) appendText(body, thinkingText || "No thinking content was exposed by the provider.", "thinking-text");
|
|
3561
3795
|
} else if (message.role === "toolCall") {
|
|
3562
3796
|
appendText(body, JSON.stringify(message.arguments ?? message.content ?? {}, null, 2), "code-block");
|
|
3563
3797
|
} else if (message.role === "assistantEvent") {
|
|
@@ -3571,6 +3805,11 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
3571
3805
|
if (message.isError) details.open = true;
|
|
3572
3806
|
details.append(header, body);
|
|
3573
3807
|
bubble.append(details);
|
|
3808
|
+
if (message.role === "toolResult" && !message.isError) {
|
|
3809
|
+
const preview = make("div", "tool-result-preview");
|
|
3810
|
+
appendText(preview, toolResultPreviewText(message, 10), "code-block tool-result-preview-text");
|
|
3811
|
+
bubble.append(preview);
|
|
3812
|
+
}
|
|
3574
3813
|
} else {
|
|
3575
3814
|
bubble.append(header, body);
|
|
3576
3815
|
}
|
|
@@ -3579,9 +3818,9 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
3579
3818
|
return { bubble, body };
|
|
3580
3819
|
}
|
|
3581
3820
|
|
|
3582
|
-
function appendTranscriptMessage(message, { streaming = false, messageIndex = -1, transient = false } = {}) {
|
|
3821
|
+
function appendTranscriptMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false } = {}) {
|
|
3583
3822
|
if (streaming || transient || message?.role !== "assistant") {
|
|
3584
|
-
return appendMessage(message, { streaming, messageIndex, transient });
|
|
3823
|
+
return appendMessage(message, { streaming, messageIndex, transient, animateEntry });
|
|
3585
3824
|
}
|
|
3586
3825
|
|
|
3587
3826
|
let finalOutput = null;
|
|
@@ -3591,6 +3830,7 @@ function appendTranscriptMessage(message, { streaming = false, messageIndex = -1
|
|
|
3591
3830
|
streaming: false,
|
|
3592
3831
|
messageIndex: displayMessage.role === "assistant" ? messageIndex : -1,
|
|
3593
3832
|
transient: false,
|
|
3833
|
+
animateEntry: animateEntry && isActionTranscriptMessage(displayMessage),
|
|
3594
3834
|
});
|
|
3595
3835
|
if (displayMessage.role === "assistant") finalOutput = created;
|
|
3596
3836
|
});
|
|
@@ -3641,7 +3881,7 @@ function formatRunIndicatorElapsed() {
|
|
|
3641
3881
|
|
|
3642
3882
|
function runIndicatorHeadline() {
|
|
3643
3883
|
if (currentState?.isCompacting && !currentState?.isStreaming) return "Pi is compacting context:";
|
|
3644
|
-
return "Agent is
|
|
3884
|
+
return "Agent is running: ";
|
|
3645
3885
|
}
|
|
3646
3886
|
|
|
3647
3887
|
function runIndicatorShowsElapsed() {
|
|
@@ -3675,7 +3915,7 @@ function ensureRunIndicatorBubble() {
|
|
|
3675
3915
|
if (runIndicatorBubble?.parentElement !== elements.chat) {
|
|
3676
3916
|
runIndicatorBubble = make("article", "message runIndicator run-indicator-message streaming");
|
|
3677
3917
|
runIndicatorBubble.setAttribute("aria-live", "polite");
|
|
3678
|
-
runIndicatorBubble.setAttribute("aria-label", "Agent is
|
|
3918
|
+
runIndicatorBubble.setAttribute("aria-label", "Agent is running:");
|
|
3679
3919
|
|
|
3680
3920
|
const body = make("div", "message-body");
|
|
3681
3921
|
const row = make("div", "run-indicator-row");
|
|
@@ -3773,6 +4013,56 @@ function messageTimestampMs(message) {
|
|
|
3773
4013
|
return Number.isFinite(time) ? time : 0;
|
|
3774
4014
|
}
|
|
3775
4015
|
|
|
4016
|
+
function isActionTranscriptMessage(message) {
|
|
4017
|
+
return ["assistantEvent", "bashExecution", "toolCall", "toolResult"].includes(message?.role);
|
|
4018
|
+
}
|
|
4019
|
+
|
|
4020
|
+
function assistantMessageHasActionContent(message) {
|
|
4021
|
+
return message?.role === "assistant" && Array.isArray(message.content) && message.content.some(isAssistantToolCallPart);
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
function isActionEntryItem(item) {
|
|
4025
|
+
return isActionTranscriptMessage(item?.message) || assistantMessageHasActionContent(item?.message);
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
function actionEntrySeenKeys(tabId = activeTabId) {
|
|
4029
|
+
if (!tabId) return new Set();
|
|
4030
|
+
let keys = actionEntrySeenKeysByTab.get(tabId);
|
|
4031
|
+
if (!keys) {
|
|
4032
|
+
keys = new Set();
|
|
4033
|
+
actionEntrySeenKeysByTab.set(tabId, keys);
|
|
4034
|
+
}
|
|
4035
|
+
return keys;
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
function actionEntryKey(item) {
|
|
4039
|
+
const message = item?.message || {};
|
|
4040
|
+
return [
|
|
4041
|
+
item?.transient ? "transient" : "message",
|
|
4042
|
+
item?.messageIndex ?? -1,
|
|
4043
|
+
message.role || "message",
|
|
4044
|
+
message.toolName || "",
|
|
4045
|
+
message.command || "",
|
|
4046
|
+
message.title || "",
|
|
4047
|
+
message.timestamp || "",
|
|
4048
|
+
textFromContent(message.content).slice(0, 240),
|
|
4049
|
+
].join("|");
|
|
4050
|
+
}
|
|
4051
|
+
|
|
4052
|
+
function shouldAnimateActionEntry(item) {
|
|
4053
|
+
if (!activeTabId || !actionEntryAnimationPrimedTabs.has(activeTabId) || !isActionEntryItem(item)) return false;
|
|
4054
|
+
return !actionEntrySeenKeys(activeTabId).has(actionEntryKey(item));
|
|
4055
|
+
}
|
|
4056
|
+
|
|
4057
|
+
function rememberActionEntries(items) {
|
|
4058
|
+
if (!activeTabId) return;
|
|
4059
|
+
const keys = actionEntrySeenKeys(activeTabId);
|
|
4060
|
+
for (const item of items) {
|
|
4061
|
+
if (isActionEntryItem(item)) keys.add(actionEntryKey(item));
|
|
4062
|
+
}
|
|
4063
|
+
actionEntryAnimationPrimedTabs.add(activeTabId);
|
|
4064
|
+
}
|
|
4065
|
+
|
|
3776
4066
|
function orderedTranscriptItems() {
|
|
3777
4067
|
const items = [];
|
|
3778
4068
|
latestMessages.forEach((message, index) => {
|
|
@@ -3788,9 +4078,15 @@ function renderAllMessages({ preserveScroll = false } = {}) {
|
|
|
3788
4078
|
const shouldFollow = !preserveScroll && (autoFollowChat || isChatNearBottom());
|
|
3789
4079
|
const previousScrollTop = elements.chat.scrollTop;
|
|
3790
4080
|
resetChatOutput();
|
|
3791
|
-
|
|
3792
|
-
|
|
4081
|
+
const transcriptItems = orderedTranscriptItems();
|
|
4082
|
+
for (const item of transcriptItems) {
|
|
4083
|
+
appendTranscriptMessage(item.message, {
|
|
4084
|
+
messageIndex: item.messageIndex,
|
|
4085
|
+
transient: item.transient,
|
|
4086
|
+
animateEntry: shouldAnimateActionEntry(item),
|
|
4087
|
+
});
|
|
3793
4088
|
}
|
|
4089
|
+
rememberActionEntries(transcriptItems);
|
|
3794
4090
|
renderRunIndicator({ scroll: false });
|
|
3795
4091
|
updateStickyUserPromptButton();
|
|
3796
4092
|
if (shouldFollow) scrollChatToBottom({ force: true });
|
|
@@ -3940,9 +4236,189 @@ function setPublishMenuOpen(open) {
|
|
|
3940
4236
|
elements.publishButton.parentElement?.classList.toggle("open", publishMenuOpen);
|
|
3941
4237
|
}
|
|
3942
4238
|
|
|
4239
|
+
function optionalFeatureIdForCommand(name) {
|
|
4240
|
+
if (OPTIONAL_COMMAND_FEATURES.has(name)) return OPTIONAL_COMMAND_FEATURES.get(name);
|
|
4241
|
+
if (name === "release-toggle" || name === "release-abort" || name === "release-npm-logs") return "releaseNpm";
|
|
4242
|
+
if (name === "release-aur" || name.startsWith("release-aur-")) return "releaseAur";
|
|
4243
|
+
if (name === "stats" || name.startsWith("stats-") || name === "calibrate") return "statsCommand";
|
|
4244
|
+
return null;
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
function isCommandVisible(command) {
|
|
4248
|
+
const featureId = optionalFeatureIdForCommand(command.name);
|
|
4249
|
+
return !featureId || isOptionalFeatureEnabled(featureId);
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
function visibleCommands() {
|
|
4253
|
+
return availableCommands.filter(isCommandVisible);
|
|
4254
|
+
}
|
|
4255
|
+
|
|
4256
|
+
function hasAvailableCommand(name) {
|
|
4257
|
+
return availableCommands.some((command) => command.name === name);
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
function optionalFeatureUnavailableMessage(featureId) {
|
|
4261
|
+
const feature = OPTIONAL_FEATURE_BY_ID.get(featureId);
|
|
4262
|
+
if (!feature) return "Optional feature unavailable.";
|
|
4263
|
+
if (isOptionalFeatureDisabled(featureId)) return `${feature.label} is disabled in the Web UI optional-features panel.`;
|
|
4264
|
+
return `${feature.label} unavailable: ${feature.capabilityLabel} is not loaded. Install or enable ${feature.packageName}.`;
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
function setOptionalControlState(button, available, unavailableTitle) {
|
|
4268
|
+
if (!button) return;
|
|
4269
|
+
if (!button.dataset.defaultTitle) button.dataset.defaultTitle = button.getAttribute("title") || "";
|
|
4270
|
+
button.disabled = !available;
|
|
4271
|
+
button.setAttribute("aria-disabled", available ? "false" : "true");
|
|
4272
|
+
button.classList.toggle("feature-unavailable", !available);
|
|
4273
|
+
button.setAttribute("title", available ? button.dataset.defaultTitle : unavailableTitle);
|
|
4274
|
+
}
|
|
4275
|
+
|
|
4276
|
+
function resetOptionalFeatureAvailability() {
|
|
4277
|
+
for (const key of Object.keys(optionalFeatureAvailability)) optionalFeatureAvailability[key] = false;
|
|
4278
|
+
optionalFeatureAvailability.themeBundle = availableThemes.length > 0;
|
|
4279
|
+
renderOptionalFeatureControls();
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
function updateOptionalFeatureAvailability() {
|
|
4283
|
+
optionalFeatureAvailability.gitWorkflow = hasAvailableCommand("git-staged-msg");
|
|
4284
|
+
optionalFeatureAvailability.releaseNpm = hasAvailableCommand("release-npm");
|
|
4285
|
+
optionalFeatureAvailability.releaseAur = hasAvailableCommand("release-aur");
|
|
4286
|
+
optionalFeatureAvailability.statsCommand = hasAvailableCommand("stats");
|
|
4287
|
+
optionalFeatureAvailability.gitFooterStatus = hasAvailableCommand("git-footer-refresh") || optionalFeatureAvailability.gitFooterStatus || statusEntries.has("git-footer");
|
|
4288
|
+
optionalFeatureAvailability.todoProgressWidget = hasAvailableCommand("todo-progress-status") || optionalFeatureAvailability.todoProgressWidget || widgets.has("todo-progress");
|
|
4289
|
+
optionalFeatureAvailability.themeBundle = availableThemes.length > 0;
|
|
4290
|
+
renderOptionalFeatureControls();
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
function optionalFeatureStatus(featureId) {
|
|
4294
|
+
const detected = isOptionalFeatureDetected(featureId);
|
|
4295
|
+
const disabled = isOptionalFeatureDisabled(featureId);
|
|
4296
|
+
if (detected && !disabled) return { label: "Enabled", className: "enabled", detail: "Detected and enabled in Web UI" };
|
|
4297
|
+
if (detected && disabled) return { label: "Disabled", className: "disabled", detail: "Detected, but disabled in Web UI" };
|
|
4298
|
+
return { label: "Install needed", className: "missing", detail: "Not detected in the active Pi tab" };
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
function optionalFeatureWidgetFeatureId(key) {
|
|
4302
|
+
if (key.startsWith("release-npm:")) return "releaseNpm";
|
|
4303
|
+
if (key.startsWith("release-aur:")) return "releaseAur";
|
|
4304
|
+
if (key === "todo-progress") return "todoProgressWidget";
|
|
4305
|
+
return null;
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
function renderOptionalFeaturePanel() {
|
|
4309
|
+
if (!elements.optionalFeaturesBox) return;
|
|
4310
|
+
elements.optionalFeaturesBox.replaceChildren();
|
|
4311
|
+
elements.optionalFeaturesBox.classList.remove("muted");
|
|
4312
|
+
|
|
4313
|
+
for (const feature of OPTIONAL_FEATURES) {
|
|
4314
|
+
const detected = isOptionalFeatureDetected(feature.id);
|
|
4315
|
+
const enabled = isOptionalFeatureEnabled(feature.id);
|
|
4316
|
+
const installing = optionalFeatureInstallInProgress.has(feature.id);
|
|
4317
|
+
const status = optionalFeatureStatus(feature.id);
|
|
4318
|
+
const row = make("div", `optional-feature-row ${status.className}`);
|
|
4319
|
+
|
|
4320
|
+
const main = make("div", "optional-feature-main");
|
|
4321
|
+
const title = make("div", "optional-feature-title");
|
|
4322
|
+
title.append(make("strong", undefined, feature.label), make("span", `optional-feature-pill ${status.className}`, status.label));
|
|
4323
|
+
const detail = make("div", "optional-feature-detail", `${status.detail} · checks ${feature.capabilityLabel}`);
|
|
4324
|
+
const description = make("div", "optional-feature-description", feature.description);
|
|
4325
|
+
const packageLine = make("code", "optional-feature-package", feature.packageName);
|
|
4326
|
+
main.append(title, detail, description, packageLine);
|
|
4327
|
+
|
|
4328
|
+
const action = make("button", "optional-feature-action");
|
|
4329
|
+
action.type = "button";
|
|
4330
|
+
action.disabled = installing;
|
|
4331
|
+
if (installing) {
|
|
4332
|
+
action.textContent = "Installing…";
|
|
4333
|
+
} else if (detected) {
|
|
4334
|
+
action.textContent = enabled ? "Disable" : "Enable";
|
|
4335
|
+
action.addEventListener("click", () => setOptionalFeatureDisabled(feature.id, enabled));
|
|
4336
|
+
} else {
|
|
4337
|
+
action.textContent = "Install…";
|
|
4338
|
+
action.classList.add("install");
|
|
4339
|
+
action.addEventListener("click", () => installOptionalFeature(feature.id));
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
row.append(main, action);
|
|
4343
|
+
elements.optionalFeaturesBox.append(row);
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
function renderOptionalFeatureControls() {
|
|
4348
|
+
setOptionalControlState(
|
|
4349
|
+
elements.gitWorkflowButton,
|
|
4350
|
+
isOptionalFeatureEnabled("gitWorkflow"),
|
|
4351
|
+
optionalFeatureUnavailableMessage("gitWorkflow"),
|
|
4352
|
+
);
|
|
4353
|
+
|
|
4354
|
+
elements.releaseNpmButton.hidden = !isOptionalFeatureEnabled("releaseNpm");
|
|
4355
|
+
elements.releaseAurButton.hidden = !isOptionalFeatureEnabled("releaseAur");
|
|
4356
|
+
const hasPublishWorkflow = isOptionalFeatureEnabled("releaseNpm") || isOptionalFeatureEnabled("releaseAur");
|
|
4357
|
+
const publishContainer = elements.publishButton.parentElement;
|
|
4358
|
+
if (publishContainer) publishContainer.hidden = !hasPublishWorkflow;
|
|
4359
|
+
setOptionalControlState(
|
|
4360
|
+
elements.publishButton,
|
|
4361
|
+
hasPublishWorkflow,
|
|
4362
|
+
"Publish workflows unavailable: enable/install NPM Release and/or AUR Release in Optional features.",
|
|
4363
|
+
);
|
|
4364
|
+
if (!hasPublishWorkflow && publishMenuOpen) setPublishMenuOpen(false);
|
|
4365
|
+
|
|
4366
|
+
renderOptionalFeaturePanel();
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4369
|
+
function commandUnavailableMessage(commandName) {
|
|
4370
|
+
const featureId = optionalFeatureIdForCommand(commandName);
|
|
4371
|
+
if (featureId) return optionalFeatureUnavailableMessage(featureId);
|
|
4372
|
+
return `Command unavailable: /${commandName} is not loaded in the active Pi tab.`;
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
async function installOptionalFeature(featureId) {
|
|
4376
|
+
const feature = OPTIONAL_FEATURE_BY_ID.get(featureId);
|
|
4377
|
+
if (!feature || optionalFeatureInstallInProgress.has(featureId)) return;
|
|
4378
|
+
|
|
4379
|
+
const warning = [
|
|
4380
|
+
`Install optional feature: ${feature.label}?`,
|
|
4381
|
+
"",
|
|
4382
|
+
`This will run npm install for ${feature.packageName} in the Web UI package install root.`,
|
|
4383
|
+
"It can download code from npm and modify the local Pi/Web UI npm installation.",
|
|
4384
|
+
"If this feature is already installed but disabled in Pi settings, cancel and enable it there instead.",
|
|
4385
|
+
"",
|
|
4386
|
+
"Continue?",
|
|
4387
|
+
].join("\n");
|
|
4388
|
+
if (!confirm(warning)) return;
|
|
4389
|
+
|
|
4390
|
+
optionalFeatureInstallInProgress.add(featureId);
|
|
4391
|
+
renderOptionalFeatureControls();
|
|
4392
|
+
addEvent(`installing optional feature ${feature.label} (${feature.packageName})…`, "warn");
|
|
4393
|
+
try {
|
|
4394
|
+
const response = await api("/api/optional-feature-install", { method: "POST", body: { featureId }, scoped: false });
|
|
4395
|
+
disabledOptionalFeatures.delete(featureId);
|
|
4396
|
+
storeDisabledOptionalFeatures();
|
|
4397
|
+
addEvent(response.data?.message || `installed ${feature.packageName}`, "info");
|
|
4398
|
+
if (confirm(`${feature.label} install finished. Reload the active Pi tab now to enable newly loaded resources?`)) {
|
|
4399
|
+
sendPrompt("prompt", "/reload");
|
|
4400
|
+
} else {
|
|
4401
|
+
await Promise.allSettled([refreshCommands(), initializeThemes()]);
|
|
4402
|
+
renderOptionalFeatureControls();
|
|
4403
|
+
}
|
|
4404
|
+
} catch (error) {
|
|
4405
|
+
addEvent(error.message || String(error), "error");
|
|
4406
|
+
} finally {
|
|
4407
|
+
optionalFeatureInstallInProgress.delete(featureId);
|
|
4408
|
+
renderOptionalFeatureControls();
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
|
|
3943
4412
|
function runPublishWorkflow(command) {
|
|
3944
4413
|
setComposerActionsOpen(false);
|
|
3945
4414
|
setPublishMenuOpen(false);
|
|
4415
|
+
const commandName = String(command || "").replace(/^\//, "").split(/\s+/)[0];
|
|
4416
|
+
const featureId = OPTIONAL_COMMAND_FEATURES.get(commandName);
|
|
4417
|
+
if ((featureId && !isOptionalFeatureEnabled(featureId)) || !hasAvailableCommand(commandName)) {
|
|
4418
|
+
addEvent(commandUnavailableMessage(commandName), "warn");
|
|
4419
|
+
refreshCommands().catch((error) => addEvent(error.message || String(error), "error"));
|
|
4420
|
+
return;
|
|
4421
|
+
}
|
|
3946
4422
|
sendPrompt("prompt", command);
|
|
3947
4423
|
}
|
|
3948
4424
|
|
|
@@ -3965,7 +4441,13 @@ function cancelStreamBubbleHide() {
|
|
|
3965
4441
|
streamBubbleHideTimer = null;
|
|
3966
4442
|
}
|
|
3967
4443
|
|
|
4444
|
+
function cancelStreamingAssistantTextRender() {
|
|
4445
|
+
clearTimeout(streamTextRenderTimer);
|
|
4446
|
+
streamTextRenderTimer = null;
|
|
4447
|
+
}
|
|
4448
|
+
|
|
3968
4449
|
function removeStreamBubble() {
|
|
4450
|
+
cancelStreamingAssistantTextRender();
|
|
3969
4451
|
cancelStreamBubbleHide();
|
|
3970
4452
|
streamBubble?.remove();
|
|
3971
4453
|
streamBubble = null;
|
|
@@ -3986,6 +4468,29 @@ function scheduleStreamBubbleHide() {
|
|
|
3986
4468
|
}, delayMs);
|
|
3987
4469
|
}
|
|
3988
4470
|
|
|
4471
|
+
function renderStreamingAssistantText() {
|
|
4472
|
+
const assistantText = stripTodoProgressLines(streamRawText, { streaming: true });
|
|
4473
|
+
if (assistantText) {
|
|
4474
|
+
ensureStreamBubble();
|
|
4475
|
+
streamText.textContent = assistantText;
|
|
4476
|
+
} else {
|
|
4477
|
+
scheduleStreamBubbleHide();
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
|
|
4481
|
+
function scheduleStreamingAssistantTextRender() {
|
|
4482
|
+
if (streamTextRenderTimer) return;
|
|
4483
|
+
streamTextRenderTimer = setTimeout(() => {
|
|
4484
|
+
streamTextRenderTimer = null;
|
|
4485
|
+
renderStreamingAssistantText();
|
|
4486
|
+
}, STREAM_OUTPUT_TOOLCALL_GUARD_MS);
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
function suppressStreamingAssistantTextBeforeToolCall() {
|
|
4490
|
+
streamRawText = "";
|
|
4491
|
+
removeStreamBubble();
|
|
4492
|
+
}
|
|
4493
|
+
|
|
3989
4494
|
function ensureStreamBubble() {
|
|
3990
4495
|
cancelStreamBubbleHide();
|
|
3991
4496
|
if (streamBubble) return;
|
|
@@ -4012,11 +4517,13 @@ function showStreamingThinking(placeholder = "Thinking…") {
|
|
|
4012
4517
|
}
|
|
4013
4518
|
|
|
4014
4519
|
function resetStreamBubble() {
|
|
4520
|
+
cancelStreamingAssistantTextRender();
|
|
4015
4521
|
cancelStreamBubbleHide();
|
|
4016
4522
|
streamBubble = null;
|
|
4017
4523
|
streamText = null;
|
|
4018
4524
|
streamRawText = "";
|
|
4019
4525
|
streamBubbleVisibleSince = 0;
|
|
4526
|
+
streamToolCallSeen = false;
|
|
4020
4527
|
streamThinkingBubble = null;
|
|
4021
4528
|
streamThinking = null;
|
|
4022
4529
|
}
|
|
@@ -4035,9 +4542,13 @@ function assistantTextFromMessage(message) {
|
|
|
4035
4542
|
const content = message?.content;
|
|
4036
4543
|
if (typeof content === "string") return content;
|
|
4037
4544
|
if (!Array.isArray(content)) return null;
|
|
4038
|
-
const parts =
|
|
4039
|
-
|
|
4040
|
-
|
|
4545
|
+
const parts = [];
|
|
4546
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
4547
|
+
const part = content[index];
|
|
4548
|
+
if (part && typeof part === "object" && part.type === "text" && typeof part.text === "string" && !assistantHasToolCallAfter(content, index)) {
|
|
4549
|
+
parts.push(part.text);
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4041
4552
|
return parts.length ? parts.join("\n\n") : "";
|
|
4042
4553
|
}
|
|
4043
4554
|
|
|
@@ -4093,17 +4604,14 @@ function handleMessageUpdate(event) {
|
|
|
4093
4604
|
if (typeof partialText === "string") streamRawText = partialText;
|
|
4094
4605
|
else if (update.type === "text_end" && typeof update.content === "string") streamRawText = update.content;
|
|
4095
4606
|
else streamRawText += delta;
|
|
4096
|
-
const assistantText = stripTodoProgressLines(streamRawText, { streaming: true });
|
|
4097
4607
|
setRunIndicatorActivity("Writing response…", { scroll: false });
|
|
4098
|
-
if (
|
|
4099
|
-
|
|
4100
|
-
streamText.textContent = assistantText;
|
|
4101
|
-
} else {
|
|
4102
|
-
scheduleStreamBubbleHide();
|
|
4103
|
-
}
|
|
4608
|
+
if (streamToolCallSeen || streamBubble) renderStreamingAssistantText();
|
|
4609
|
+
else scheduleStreamingAssistantTextRender();
|
|
4104
4610
|
renderFooter();
|
|
4105
4611
|
scrollChatToBottom();
|
|
4106
4612
|
} else if (update.type === "toolcall_start") {
|
|
4613
|
+
streamToolCallSeen = true;
|
|
4614
|
+
suppressStreamingAssistantTextBeforeToolCall();
|
|
4107
4615
|
const name = runIndicatorToolName(update.name || update.toolName || update.toolCall?.name);
|
|
4108
4616
|
setRunIndicatorActivity(`Preparing tool call: ${name}…`);
|
|
4109
4617
|
addEvent(`tool call started in assistant message`, "info");
|
|
@@ -4355,7 +4863,7 @@ function scoreCommandSuggestion(command, query) {
|
|
|
4355
4863
|
}
|
|
4356
4864
|
|
|
4357
4865
|
function getCommandMatches(query) {
|
|
4358
|
-
return
|
|
4866
|
+
return visibleCommands()
|
|
4359
4867
|
.map((command) => ({ command, score: scoreCommandSuggestion(command, query) }))
|
|
4360
4868
|
.filter((item) => Number.isFinite(item.score))
|
|
4361
4869
|
.sort((a, b) => a.score - b.score || a.command.name.localeCompare(b.command.name))
|
|
@@ -4622,6 +5130,7 @@ function insertPathSuggestion(index = commandSuggestIndex) {
|
|
|
4622
5130
|
async function refreshCommands() {
|
|
4623
5131
|
const response = await api("/api/commands");
|
|
4624
5132
|
availableCommands = normalizeCommands(response.data?.commands || []);
|
|
5133
|
+
updateOptionalFeatureAvailability();
|
|
4625
5134
|
elements.commandsBox.replaceChildren();
|
|
4626
5135
|
if (!availableCommands.length) {
|
|
4627
5136
|
elements.commandsBox.textContent = "No RPC-visible commands.";
|
|
@@ -4629,8 +5138,15 @@ async function refreshCommands() {
|
|
|
4629
5138
|
hideCommandSuggestions();
|
|
4630
5139
|
return;
|
|
4631
5140
|
}
|
|
5141
|
+
const commandsToShow = visibleCommands();
|
|
5142
|
+
if (!commandsToShow.length) {
|
|
5143
|
+
elements.commandsBox.textContent = "No enabled commands visible. Re-enable optional features to show their commands.";
|
|
5144
|
+
elements.commandsBox.classList.add("muted");
|
|
5145
|
+
hideCommandSuggestions();
|
|
5146
|
+
return;
|
|
5147
|
+
}
|
|
4632
5148
|
elements.commandsBox.classList.remove("muted");
|
|
4633
|
-
for (const command of
|
|
5149
|
+
for (const command of commandsToShow.slice(0, 80)) {
|
|
4634
5150
|
const item = make("button", "command-item");
|
|
4635
5151
|
item.type = "button";
|
|
4636
5152
|
item.title = `Send /${command.name}`;
|
|
@@ -4823,11 +5339,13 @@ function handleExtensionUiRequest(request) {
|
|
|
4823
5339
|
case "setStatus":
|
|
4824
5340
|
if (request.statusText) statusEntries.set(request.statusKey || "extension", request.statusText);
|
|
4825
5341
|
else statusEntries.delete(request.statusKey || "extension");
|
|
5342
|
+
updateOptionalFeatureAvailability();
|
|
4826
5343
|
renderStatus();
|
|
4827
5344
|
return;
|
|
4828
5345
|
case "setWidget":
|
|
4829
5346
|
if (Array.isArray(request.widgetLines)) widgets.set(request.widgetKey || request.id, request);
|
|
4830
5347
|
else widgets.delete(request.widgetKey || request.id);
|
|
5348
|
+
updateOptionalFeatureAvailability();
|
|
4831
5349
|
renderWidgets();
|
|
4832
5350
|
return;
|
|
4833
5351
|
case "setTitle":
|
|
@@ -4967,6 +5485,11 @@ function handleEvent(event) {
|
|
|
4967
5485
|
case "webui_tab_reloaded":
|
|
4968
5486
|
addEvent(`${event.tabTitle || "terminal"} reloaded`);
|
|
4969
5487
|
addTransientMessage({ role: "native", title: "/reload", content: `${event.tabTitle || "terminal"} reloaded. Keybindings, extensions, skills, prompts, and themes were refreshed by restarting the RPC tab${event.sessionFile ? ` and resuming ${event.sessionFile}` : ""}.`, level: "info" });
|
|
5488
|
+
statusEntries.clear();
|
|
5489
|
+
widgets.clear();
|
|
5490
|
+
resetOptionalFeatureAvailability();
|
|
5491
|
+
renderStatus();
|
|
5492
|
+
renderWidgets();
|
|
4970
5493
|
refreshTabs().catch((error) => addEvent(error.message, "error"));
|
|
4971
5494
|
setTimeout(() => refreshAll().catch((error) => addEvent(error.message, "error")), 500);
|
|
4972
5495
|
break;
|
|
@@ -5129,6 +5652,7 @@ elements.terminalTabsToggleButton.addEventListener("click", () => {
|
|
|
5129
5652
|
setMobileTabsExpanded(!document.body.classList.contains("mobile-tabs-expanded"));
|
|
5130
5653
|
});
|
|
5131
5654
|
elements.newTabButton.addEventListener("click", () => createTerminalTab());
|
|
5655
|
+
elements.closeAllTabsButton.addEventListener("click", () => closeAllTerminalTabs());
|
|
5132
5656
|
elements.gitWorkflowButton.addEventListener("click", () => {
|
|
5133
5657
|
setComposerActionsOpen(false);
|
|
5134
5658
|
startGitWorkflow();
|
|
@@ -5358,6 +5882,7 @@ elements.promptInput.addEventListener("blur", () => {
|
|
|
5358
5882
|
resizePromptInput();
|
|
5359
5883
|
focusPromptInput({ defer: true });
|
|
5360
5884
|
updateComposerModeButtons();
|
|
5885
|
+
updateOptionalFeatureAvailability();
|
|
5361
5886
|
loadLastUserPromptCache();
|
|
5362
5887
|
installViewportHandlers();
|
|
5363
5888
|
initializeThemes().catch((error) => addEvent(`failed to load themes: ${error.message}`, "warn"));
|