@firstpick/pi-package-webui 0.1.4 → 0.1.6
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 +34 -11
- package/bin/pi-webui.mjs +608 -26
- package/index.ts +82 -10
- package/package.json +34 -4
- package/public/app.js +3118 -211
- package/public/catppuccin-mocha-background.png +0 -0
- package/public/index.html +152 -52
- package/public/matrix-background.webp +0 -0
- package/public/service-worker.js +3 -1
- package/public/styles.css +772 -17
- package/tests/mobile-static.test.mjs +231 -36
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"),
|
|
@@ -20,6 +21,9 @@ const elements = {
|
|
|
20
21
|
promptInput: $("#promptInput"),
|
|
21
22
|
sendButton: $("#sendButton"),
|
|
22
23
|
commandSuggest: $("#commandSuggest"),
|
|
24
|
+
attachmentTray: $("#attachmentTray"),
|
|
25
|
+
attachButton: $("#attachButton"),
|
|
26
|
+
attachmentInput: $("#attachmentInput"),
|
|
23
27
|
busyBehavior: $("#busyBehavior"),
|
|
24
28
|
steerButton: $("#steerButton"),
|
|
25
29
|
followUpButton: $("#followUpButton"),
|
|
@@ -42,11 +46,18 @@ const elements = {
|
|
|
42
46
|
setModelButton: $("#setModelButton"),
|
|
43
47
|
thinkingSelect: $("#thinkingSelect"),
|
|
44
48
|
setThinkingButton: $("#setThinkingButton"),
|
|
49
|
+
thinkingVisibilityToggle: $("#thinkingVisibilityToggle"),
|
|
50
|
+
thinkingVisibilityStatus: $("#thinkingVisibilityStatus"),
|
|
45
51
|
themeSelect: $("#themeSelect"),
|
|
52
|
+
backgroundInput: $("#backgroundInput"),
|
|
53
|
+
backgroundChooseButton: $("#backgroundChooseButton"),
|
|
54
|
+
backgroundClearButton: $("#backgroundClearButton"),
|
|
55
|
+
backgroundStatus: $("#backgroundStatus"),
|
|
46
56
|
networkStatus: $("#networkStatus"),
|
|
47
57
|
openNetworkButton: $("#openNetworkButton"),
|
|
48
58
|
agentDoneNotificationsToggle: $("#agentDoneNotificationsToggle"),
|
|
49
59
|
agentDoneNotificationsStatus: $("#agentDoneNotificationsStatus"),
|
|
60
|
+
optionalFeaturesBox: $("#optionalFeaturesBox"),
|
|
50
61
|
toggleSidePanelButton: $("#toggleSidePanelButton"),
|
|
51
62
|
sidePanelExpandButton: $("#sidePanelExpandButton"),
|
|
52
63
|
sidePanelBackdrop: $("#sidePanelBackdrop"),
|
|
@@ -70,12 +81,21 @@ const elements = {
|
|
|
70
81
|
pathPickerError: $("#pathPickerError"),
|
|
71
82
|
pathPickerCancelButton: $("#pathPickerCancelButton"),
|
|
72
83
|
pathPickerChooseButton: $("#pathPickerChooseButton"),
|
|
84
|
+
nativeCommandDialog: $("#nativeCommandDialog"),
|
|
85
|
+
nativeCommandTitle: $("#nativeCommandTitle"),
|
|
86
|
+
nativeCommandMessage: $("#nativeCommandMessage"),
|
|
87
|
+
nativeCommandSearch: $("#nativeCommandSearch"),
|
|
88
|
+
nativeCommandBody: $("#nativeCommandBody"),
|
|
89
|
+
nativeCommandError: $("#nativeCommandError"),
|
|
90
|
+
nativeCommandActions: $("#nativeCommandActions"),
|
|
73
91
|
};
|
|
74
92
|
|
|
75
93
|
let currentState = null;
|
|
76
94
|
let tabs = [];
|
|
77
95
|
let activeTabId = null;
|
|
96
|
+
let activeTabGeneration = 0;
|
|
78
97
|
let tabDrafts = new Map();
|
|
98
|
+
let tabAttachments = new Map();
|
|
79
99
|
let tabActivities = new Map();
|
|
80
100
|
let tabSeenCompletionSerials = new Map();
|
|
81
101
|
let streamBubble = null;
|
|
@@ -83,6 +103,8 @@ let streamText = null;
|
|
|
83
103
|
let streamRawText = "";
|
|
84
104
|
let streamBubbleVisibleSince = 0;
|
|
85
105
|
let streamBubbleHideTimer = null;
|
|
106
|
+
let streamTextRenderTimer = null;
|
|
107
|
+
let streamToolCallSeen = false;
|
|
86
108
|
let streamThinkingBubble = null;
|
|
87
109
|
let streamThinking = null;
|
|
88
110
|
let runIndicatorBubble = null;
|
|
@@ -100,6 +122,7 @@ let refreshFooterTimer = null;
|
|
|
100
122
|
let refreshTabsTimer = null;
|
|
101
123
|
let eventSource = null;
|
|
102
124
|
let activeDialog = null;
|
|
125
|
+
let nativeCommandTabId = null;
|
|
103
126
|
let pathPickerState = null;
|
|
104
127
|
let pathFastPicks = [];
|
|
105
128
|
let pathFastPicksReady = false;
|
|
@@ -120,6 +143,8 @@ let latestWorkspace = null;
|
|
|
120
143
|
let latestNetwork = null;
|
|
121
144
|
let latestMessages = [];
|
|
122
145
|
let transientMessages = [];
|
|
146
|
+
let actionEntrySeenKeysByTab = new Map();
|
|
147
|
+
let actionEntryAnimationPrimedTabs = new Set();
|
|
123
148
|
let lastUserPromptByTab = new Map();
|
|
124
149
|
let actionFeedbackByTab = new Map();
|
|
125
150
|
let actionFeedbackSendBusy = false;
|
|
@@ -127,12 +152,16 @@ let blockedTabNotificationKeys = new Set();
|
|
|
127
152
|
let blockedTabNotificationPermissionRequested = false;
|
|
128
153
|
let blockedTabNotificationFallbackNoted = false;
|
|
129
154
|
let agentDoneNotificationsEnabled = false;
|
|
155
|
+
let thinkingOutputVisible = true;
|
|
130
156
|
let agentDoneNotificationPermissionRequested = false;
|
|
131
157
|
let agentDoneNotificationFallbackNoted = false;
|
|
132
158
|
let agentDoneNotificationKeys = new Set();
|
|
133
159
|
let availableModels = [];
|
|
134
160
|
let availableThemes = [];
|
|
135
161
|
let currentThemeName = "catppuccin-mocha";
|
|
162
|
+
let customBackground = null;
|
|
163
|
+
let customBackgroundObjectUrl = null;
|
|
164
|
+
let customBackgroundLoading = false;
|
|
136
165
|
let footerScopedModels = [];
|
|
137
166
|
let footerScopedModelPatterns = [];
|
|
138
167
|
let footerScopedModelSource = "none";
|
|
@@ -148,13 +177,32 @@ let maxVisualViewportHeight = 0;
|
|
|
148
177
|
let currentRunStartedAt = null;
|
|
149
178
|
let currentRunStreamChars = 0;
|
|
150
179
|
let latestTokPerSecond = null;
|
|
180
|
+
let abortRequestInFlight = false;
|
|
181
|
+
let abortLongPressTimer = null;
|
|
182
|
+
let abortLongPressHandled = false;
|
|
151
183
|
const dialogQueue = [];
|
|
152
184
|
const SIDE_PANEL_STORAGE_KEY = "pi-webui-side-panel-collapsed";
|
|
185
|
+
const SIDE_PANEL_SECTION_STORAGE_KEY = "pi-webui-side-panel-sections-collapsed";
|
|
153
186
|
const TAB_STORAGE_KEY = "pi-webui-active-tab";
|
|
154
187
|
const PATH_FAST_PICKS_STORAGE_KEY = "pi-webui-path-fast-picks";
|
|
155
188
|
const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications";
|
|
189
|
+
const THINKING_VISIBILITY_STORAGE_KEY = "pi-webui-thinking-visible";
|
|
156
190
|
const THEME_STORAGE_KEY = "pi-webui-theme";
|
|
191
|
+
const CUSTOM_BACKGROUND_STORAGE_KEY = "pi-webui-custom-background";
|
|
192
|
+
const CUSTOM_BACKGROUNDS_STORAGE_KEY = "pi-webui-custom-backgrounds";
|
|
193
|
+
const CUSTOM_BACKGROUND_IDB_NAME = "pi-webui-custom-background";
|
|
194
|
+
const CUSTOM_BACKGROUND_IDB_STORE = "backgrounds";
|
|
195
|
+
const CUSTOM_BACKGROUND_LEGACY_ID = "active";
|
|
196
|
+
const CUSTOM_BACKGROUND_MAX_FILE_BYTES = 24 * 1024 * 1024;
|
|
197
|
+
const OPTIONAL_FEATURES_STORAGE_KEY = "pi-webui-optional-features-disabled";
|
|
157
198
|
const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
|
|
199
|
+
const ATTACHMENT_MAX_FILES = 12;
|
|
200
|
+
const ATTACHMENT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
201
|
+
const ATTACHMENT_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
202
|
+
const ATTACHMENT_INLINE_IMAGE_MAX_BYTES = 8 * 1024 * 1024;
|
|
203
|
+
const ATTACHMENT_INLINE_IMAGE_TOTAL_MAX_BYTES = 16 * 1024 * 1024;
|
|
204
|
+
const INLINE_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
205
|
+
const BACKGROUND_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
158
206
|
const DEFAULT_THEME_NAME = "catppuccin-mocha";
|
|
159
207
|
const MOBILE_VIEW_QUERY = "(max-width: 720px), (max-device-width: 720px), (pointer: coarse) and (hover: none)";
|
|
160
208
|
const CHAT_BOTTOM_THRESHOLD_PX = 96;
|
|
@@ -166,7 +214,9 @@ const CHAT_USER_SCROLL_INTENT_MS = 700;
|
|
|
166
214
|
const RUN_INDICATOR_TICK_MS = 1000;
|
|
167
215
|
const RUN_INDICATOR_START_GRACE_MS = 2500;
|
|
168
216
|
const RUN_INDICATOR_STATE_RECHECK_MS = 5000;
|
|
217
|
+
const ABORT_LONG_PRESS_MS = 700;
|
|
169
218
|
const STREAM_OUTPUT_HIDE_DELAY_MS = 300;
|
|
219
|
+
const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220;
|
|
170
220
|
const STREAM_OUTPUT_MIN_VISIBLE_MS = 900;
|
|
171
221
|
const TODO_PROGRESS_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)\]\s+.+$/;
|
|
172
222
|
const TODO_PROGRESS_PARTIAL_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)?\]?\s*.*$/;
|
|
@@ -180,6 +230,83 @@ const BLOCKED_TAB_NOTIFICATION_ICON = "/icon-192.png";
|
|
|
180
230
|
const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
|
|
181
231
|
const statusEntries = new Map();
|
|
182
232
|
const widgets = new Map();
|
|
233
|
+
const liveToolRuns = new Map();
|
|
234
|
+
const liveToolCards = new Map();
|
|
235
|
+
// Optional feature detection intentionally checks loaded Pi capabilities (RPC-visible
|
|
236
|
+
// commands and live widget events), not npm package folders. This keeps local dev
|
|
237
|
+
// symlinks and independently installed packages working.
|
|
238
|
+
const optionalFeatureAvailability = {
|
|
239
|
+
gitWorkflow: false,
|
|
240
|
+
releaseNpm: false,
|
|
241
|
+
releaseAur: false,
|
|
242
|
+
statsCommand: false,
|
|
243
|
+
gitFooterStatus: false,
|
|
244
|
+
todoProgressWidget: false,
|
|
245
|
+
themeBundle: false,
|
|
246
|
+
};
|
|
247
|
+
const OPTIONAL_FEATURES = [
|
|
248
|
+
{
|
|
249
|
+
id: "gitWorkflow",
|
|
250
|
+
label: "Guided Git workflow",
|
|
251
|
+
packageName: "@firstpick/pi-prompts-git-pr",
|
|
252
|
+
capabilityLabel: "/git-staged-msg",
|
|
253
|
+
description: "Generate staged commit messages for the guided Git workflow.",
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: "releaseNpm",
|
|
257
|
+
label: "NPM Release",
|
|
258
|
+
packageName: "@firstpick/pi-extension-release-npm",
|
|
259
|
+
capabilityLabel: "/release-npm",
|
|
260
|
+
description: "Publish menu action and live npm release widgets.",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
id: "releaseAur",
|
|
264
|
+
label: "AUR Release",
|
|
265
|
+
packageName: "@firstpick/pi-extension-release-aur",
|
|
266
|
+
capabilityLabel: "/release-aur",
|
|
267
|
+
description: "Publish menu action, setup helpers, skills, and AUR release widgets.",
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: "todoProgressWidget",
|
|
271
|
+
label: "Todo progress widget",
|
|
272
|
+
packageName: "@firstpick/pi-extension-todo-progress",
|
|
273
|
+
capabilityLabel: "/todo-progress-status or todo-progress widget event",
|
|
274
|
+
description: "Styled live checklist rendering for assistant todo updates.",
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
id: "gitFooterStatus",
|
|
278
|
+
label: "Git footer status",
|
|
279
|
+
packageName: "@firstpick/pi-extension-git-footer-status",
|
|
280
|
+
capabilityLabel: "/git-footer-refresh or git-footer status event",
|
|
281
|
+
description: "Enhanced Pi footer/status telemetry when loaded by Pi.",
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: "statsCommand",
|
|
285
|
+
label: "Stats command",
|
|
286
|
+
packageName: "@firstpick/pi-extension-stats",
|
|
287
|
+
capabilityLabel: "/stats",
|
|
288
|
+
description: "Token and cost usage analytics commands.",
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
id: "themeBundle",
|
|
292
|
+
label: "Theme bundle",
|
|
293
|
+
packageName: "@firstpick/pi-themes-bundle",
|
|
294
|
+
capabilityLabel: "/api/themes returned themes",
|
|
295
|
+
description: "Additional browser theme-picker and Pi theme resources.",
|
|
296
|
+
},
|
|
297
|
+
];
|
|
298
|
+
const OPTIONAL_FEATURE_BY_ID = new Map(OPTIONAL_FEATURES.map((feature) => [feature.id, feature]));
|
|
299
|
+
const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
300
|
+
["git-staged-msg", "gitWorkflow"],
|
|
301
|
+
["release-npm", "releaseNpm"],
|
|
302
|
+
["release-aur", "releaseAur"],
|
|
303
|
+
["stats", "statsCommand"],
|
|
304
|
+
["git-footer-refresh", "gitFooterStatus"],
|
|
305
|
+
["todo-progress-status", "todoProgressWidget"],
|
|
306
|
+
]);
|
|
307
|
+
const HIDDEN_COMMAND_NAMES = new Set(["webui-tree-navigate"]);
|
|
308
|
+
const NATIVE_SELECTOR_COMMANDS = new Set(["model", "settings", "theme", "fork", "clone", "resume", "tree", "login", "logout", "scoped-models"]);
|
|
309
|
+
const optionalFeatureInstallInProgress = new Set();
|
|
183
310
|
const gitWorkflow = {
|
|
184
311
|
active: false,
|
|
185
312
|
step: "idle",
|
|
@@ -232,6 +359,63 @@ function readStoredSidePanelCollapsed() {
|
|
|
232
359
|
}
|
|
233
360
|
}
|
|
234
361
|
|
|
362
|
+
function sidePanelSectionRecords() {
|
|
363
|
+
return Array.from(elements.sidePanel.querySelectorAll("[data-side-panel-section]"))
|
|
364
|
+
.map((section) => {
|
|
365
|
+
const id = section.dataset.sidePanelSection || "";
|
|
366
|
+
const button = section.querySelector("[data-side-panel-section-toggle]");
|
|
367
|
+
const contentId = button?.getAttribute("aria-controls") || "";
|
|
368
|
+
const content = contentId ? document.getElementById(contentId) : null;
|
|
369
|
+
return { id, section, button, content };
|
|
370
|
+
})
|
|
371
|
+
.filter((record) => record.id && record.button && record.content);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function readStoredSidePanelSectionCollapsedIds() {
|
|
375
|
+
try {
|
|
376
|
+
const parsed = JSON.parse(localStorage.getItem(SIDE_PANEL_SECTION_STORAGE_KEY) || "[]");
|
|
377
|
+
return new Set(Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : []);
|
|
378
|
+
} catch {
|
|
379
|
+
return new Set();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function persistSidePanelSectionState() {
|
|
384
|
+
try {
|
|
385
|
+
const collapsed = sidePanelSectionRecords()
|
|
386
|
+
.filter(({ section }) => section.classList.contains("collapsed"))
|
|
387
|
+
.map(({ id }) => id);
|
|
388
|
+
localStorage.setItem(SIDE_PANEL_SECTION_STORAGE_KEY, JSON.stringify(collapsed));
|
|
389
|
+
} catch {
|
|
390
|
+
// Ignore storage failures; section toggles should still work for this page load.
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function setSidePanelSectionCollapsed(record, collapsed, { persist = true } = {}) {
|
|
395
|
+
const label = record.button.querySelector(".side-panel-section-label")?.textContent?.trim() || "side panel";
|
|
396
|
+
record.section.classList.toggle("collapsed", collapsed);
|
|
397
|
+
record.content.hidden = collapsed;
|
|
398
|
+
record.button.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
399
|
+
record.button.setAttribute("aria-label", `${collapsed ? "Expand" : "Collapse"} ${label} section`);
|
|
400
|
+
record.button.setAttribute("title", `${collapsed ? "Expand" : "Collapse"} ${label} section`);
|
|
401
|
+
if (persist) persistSidePanelSectionState();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function restoreSidePanelSectionState() {
|
|
405
|
+
const collapsedIds = readStoredSidePanelSectionCollapsedIds();
|
|
406
|
+
for (const record of sidePanelSectionRecords()) {
|
|
407
|
+
setSidePanelSectionCollapsed(record, collapsedIds.has(record.id), { persist: false });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function bindSidePanelSectionToggles() {
|
|
412
|
+
for (const record of sidePanelSectionRecords()) {
|
|
413
|
+
record.button.addEventListener("click", () => {
|
|
414
|
+
setSidePanelSectionCollapsed(record, !record.section.classList.contains("collapsed"));
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
235
419
|
function readStoredAgentDoneNotificationsEnabled() {
|
|
236
420
|
try {
|
|
237
421
|
return localStorage.getItem(AGENT_DONE_NOTIFICATIONS_STORAGE_KEY) === "1";
|
|
@@ -298,6 +482,55 @@ function restoreAgentDoneNotificationsSetting() {
|
|
|
298
482
|
renderAgentDoneNotificationsToggle();
|
|
299
483
|
}
|
|
300
484
|
|
|
485
|
+
function readStoredThinkingOutputVisible() {
|
|
486
|
+
try {
|
|
487
|
+
const stored = localStorage.getItem(THINKING_VISIBILITY_STORAGE_KEY);
|
|
488
|
+
return stored === null ? true : stored === "1";
|
|
489
|
+
} catch {
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function persistThinkingOutputVisible(visible) {
|
|
495
|
+
try {
|
|
496
|
+
localStorage.setItem(THINKING_VISIBILITY_STORAGE_KEY, visible ? "1" : "0");
|
|
497
|
+
} catch {
|
|
498
|
+
// Ignore storage failures; the toggle should still work for this page load.
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function thinkingVisibilityStatusText() {
|
|
503
|
+
return thinkingOutputVisible ? "Visible" : "Hidden from transcript";
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function renderThinkingVisibilityToggle() {
|
|
507
|
+
if (!elements.thinkingVisibilityToggle) return;
|
|
508
|
+
elements.thinkingVisibilityToggle.checked = thinkingOutputVisible;
|
|
509
|
+
elements.thinkingVisibilityToggle.setAttribute("aria-describedby", "thinkingVisibilityStatus");
|
|
510
|
+
if (elements.thinkingVisibilityStatus) elements.thinkingVisibilityStatus.textContent = thinkingVisibilityStatusText();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function removeStreamingThinkingBubble() {
|
|
514
|
+
streamThinkingBubble?.remove();
|
|
515
|
+
streamThinkingBubble = null;
|
|
516
|
+
streamThinking = null;
|
|
517
|
+
renderRunIndicator({ scroll: false });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function setThinkingOutputVisible(visible, { announce = false } = {}) {
|
|
521
|
+
thinkingOutputVisible = !!visible;
|
|
522
|
+
persistThinkingOutputVisible(thinkingOutputVisible);
|
|
523
|
+
renderThinkingVisibilityToggle();
|
|
524
|
+
if (!thinkingOutputVisible) removeStreamingThinkingBubble();
|
|
525
|
+
renderAllMessages({ preserveScroll: true });
|
|
526
|
+
if (announce) addEvent(thinkingOutputVisible ? "thinking output shown" : "thinking output hidden", thinkingOutputVisible ? "info" : "warn");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function restoreThinkingVisibilitySetting() {
|
|
530
|
+
thinkingOutputVisible = readStoredThinkingOutputVisible();
|
|
531
|
+
renderThinkingVisibilityToggle();
|
|
532
|
+
}
|
|
533
|
+
|
|
301
534
|
function setComposerActionsOpen(open) {
|
|
302
535
|
const shouldOpen = open && isMobileView();
|
|
303
536
|
document.body.classList.toggle("composer-actions-open", shouldOpen);
|
|
@@ -306,7 +539,11 @@ function setComposerActionsOpen(open) {
|
|
|
306
539
|
}
|
|
307
540
|
|
|
308
541
|
function isRunActive() {
|
|
309
|
-
return !!currentState?.isStreaming;
|
|
542
|
+
return !!currentState?.isStreaming || (runIndicatorLocallyActive && !currentState?.isCompacting);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function isAbortAvailable() {
|
|
546
|
+
return runIndicatorIsActive();
|
|
310
547
|
}
|
|
311
548
|
|
|
312
549
|
function resizePromptInput() {
|
|
@@ -320,12 +557,20 @@ function resizePromptInput() {
|
|
|
320
557
|
|
|
321
558
|
function updateComposerModeButtons() {
|
|
322
559
|
const runActive = isRunActive();
|
|
560
|
+
const abortAvailable = isAbortAvailable();
|
|
323
561
|
const target = runActive ? elements.composerRow : elements.composerActionsPanel;
|
|
324
|
-
const before = runActive ? elements.
|
|
562
|
+
const before = runActive ? elements.abortButton : null;
|
|
325
563
|
for (const button of [elements.steerButton, elements.followUpButton]) {
|
|
326
564
|
if (button.parentElement !== target) target.insertBefore(button, before);
|
|
565
|
+
button.hidden = !runActive;
|
|
566
|
+
button.disabled = !runActive;
|
|
327
567
|
}
|
|
328
|
-
|
|
568
|
+
elements.abortButton.hidden = !abortAvailable;
|
|
569
|
+
elements.abortButton.disabled = !abortAvailable || abortRequestInFlight;
|
|
570
|
+
elements.abortButton.textContent = abortRequestInFlight ? "Aborting…" : "Abort";
|
|
571
|
+
elements.abortButton.title = abortAvailable ? "Abort the active Pi run (Esc or hold)" : "Abort is available while Pi is running";
|
|
572
|
+
elements.abortButton.setAttribute("aria-label", elements.abortButton.title);
|
|
573
|
+
document.body.classList.toggle("pi-run-active", runActive || abortAvailable);
|
|
329
574
|
}
|
|
330
575
|
|
|
331
576
|
function updateFooterModelPickerPosition() {
|
|
@@ -481,6 +726,633 @@ async function api(path, { method = "GET", body, tabId = activeTabId, scoped = t
|
|
|
481
726
|
return data;
|
|
482
727
|
}
|
|
483
728
|
|
|
729
|
+
function formatBytes(bytes) {
|
|
730
|
+
const value = Number(bytes) || 0;
|
|
731
|
+
if (value < 1024) return `${value} B`;
|
|
732
|
+
const units = ["KB", "MB", "GB"];
|
|
733
|
+
let scaled = value / 1024;
|
|
734
|
+
for (const unit of units) {
|
|
735
|
+
if (scaled < 1024 || unit === units[units.length - 1]) return `${scaled.toFixed(scaled >= 10 ? 1 : 2)} ${unit}`;
|
|
736
|
+
scaled /= 1024;
|
|
737
|
+
}
|
|
738
|
+
return `${value} B`;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function inferMimeTypeFromName(name = "") {
|
|
742
|
+
const ext = String(name).split(".").pop()?.toLowerCase() || "";
|
|
743
|
+
const map = {
|
|
744
|
+
md: "text/markdown",
|
|
745
|
+
markdown: "text/markdown",
|
|
746
|
+
txt: "text/plain",
|
|
747
|
+
log: "text/plain",
|
|
748
|
+
csv: "text/csv",
|
|
749
|
+
json: "application/json",
|
|
750
|
+
xml: "application/xml",
|
|
751
|
+
yaml: "application/x-yaml",
|
|
752
|
+
yml: "application/x-yaml",
|
|
753
|
+
toml: "application/toml",
|
|
754
|
+
pdf: "application/pdf",
|
|
755
|
+
doc: "application/msword",
|
|
756
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
757
|
+
mp3: "audio/mpeg",
|
|
758
|
+
wav: "audio/wav",
|
|
759
|
+
m4a: "audio/mp4",
|
|
760
|
+
mp4: "video/mp4",
|
|
761
|
+
mov: "video/quicktime",
|
|
762
|
+
webm: "video/webm",
|
|
763
|
+
};
|
|
764
|
+
return map[ext] || "application/octet-stream";
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function attachmentKind(mimeType = "", name = "") {
|
|
768
|
+
const type = String(mimeType || inferMimeTypeFromName(name));
|
|
769
|
+
if (type.startsWith("image/")) return "image";
|
|
770
|
+
if (type.startsWith("video/")) return "video";
|
|
771
|
+
if (type.startsWith("audio/")) return "audio";
|
|
772
|
+
if (type.startsWith("text/") || /(?:json|xml|pdf|word|excel|powerpoint|document|spreadsheet|presentation|markdown|csv)/i.test(type)) return "doc";
|
|
773
|
+
return "file";
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function attachmentIcon(kind) {
|
|
777
|
+
return kind === "image" ? "🖼️" : kind === "video" ? "🎞️" : kind === "audio" ? "🎵" : kind === "doc" ? "📄" : "📎";
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function attachmentsForTab(tabId = activeTabId) {
|
|
781
|
+
return tabId ? tabAttachments.get(tabId) || [] : [];
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function ensureAttachmentsForTab(tabId = activeTabId) {
|
|
785
|
+
if (!tabId) return [];
|
|
786
|
+
if (!tabAttachments.has(tabId)) tabAttachments.set(tabId, []);
|
|
787
|
+
return tabAttachments.get(tabId);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function hasComposerPayload() {
|
|
791
|
+
return !!elements.promptInput.value.trim() || attachmentsForTab().length > 0;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function renderAttachmentTray() {
|
|
795
|
+
const tray = elements.attachmentTray;
|
|
796
|
+
if (!tray) return;
|
|
797
|
+
const attachments = attachmentsForTab();
|
|
798
|
+
tray.innerHTML = "";
|
|
799
|
+
tray.hidden = attachments.length === 0;
|
|
800
|
+
if (attachments.length === 0) return;
|
|
801
|
+
|
|
802
|
+
for (const attachment of attachments) {
|
|
803
|
+
const pill = make("span", "attachment-pill");
|
|
804
|
+
pill.title = `${attachment.name}\n${attachment.mimeType}\n${formatBytes(attachment.size)}`;
|
|
805
|
+
const icon = make("span", "attachment-pill-icon", attachmentIcon(attachment.kind));
|
|
806
|
+
const name = make("span", "attachment-pill-name", attachment.name);
|
|
807
|
+
const meta = make("span", "attachment-pill-meta", `${attachment.kind} · ${formatBytes(attachment.size)}`);
|
|
808
|
+
const remove = make("button", "attachment-remove-button", "×");
|
|
809
|
+
remove.type = "button";
|
|
810
|
+
remove.setAttribute("aria-label", `Remove ${attachment.name}`);
|
|
811
|
+
remove.addEventListener("click", () => removeAttachment(attachment.id));
|
|
812
|
+
pill.append(icon, name, meta, remove);
|
|
813
|
+
tray.append(pill);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function removeAttachment(id, tabId = activeTabId) {
|
|
818
|
+
const attachments = attachmentsForTab(tabId);
|
|
819
|
+
const index = attachments.findIndex((attachment) => attachment.id === id);
|
|
820
|
+
if (index === -1) return;
|
|
821
|
+
const [removed] = attachments.splice(index, 1);
|
|
822
|
+
if (removed?.previewUrl) URL.revokeObjectURL(removed.previewUrl);
|
|
823
|
+
if (attachments.length === 0) tabAttachments.delete(tabId);
|
|
824
|
+
if (tabId === activeTabId) renderAttachmentTray();
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function clearAttachments(tabId = activeTabId) {
|
|
828
|
+
const attachments = attachmentsForTab(tabId);
|
|
829
|
+
for (const attachment of attachments) {
|
|
830
|
+
if (attachment.previewUrl) URL.revokeObjectURL(attachment.previewUrl);
|
|
831
|
+
}
|
|
832
|
+
if (tabId) tabAttachments.delete(tabId);
|
|
833
|
+
if (tabId === activeTabId) renderAttachmentTray();
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function addAttachmentFiles(fileList, source = "picker") {
|
|
837
|
+
const files = Array.from(fileList || []).filter(Boolean);
|
|
838
|
+
if (!files.length) return;
|
|
839
|
+
const attachments = ensureAttachmentsForTab();
|
|
840
|
+
if (!attachments.length && !activeTabId) return;
|
|
841
|
+
let totalBytes = attachments.reduce((sum, attachment) => sum + attachment.size, 0);
|
|
842
|
+
let added = 0;
|
|
843
|
+
const skipped = [];
|
|
844
|
+
|
|
845
|
+
for (const file of files) {
|
|
846
|
+
const name = file.name || `${source}-attachment`;
|
|
847
|
+
if (attachments.length >= ATTACHMENT_MAX_FILES) {
|
|
848
|
+
skipped.push(`${name}: attachment limit is ${ATTACHMENT_MAX_FILES}`);
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (file.size > ATTACHMENT_MAX_FILE_BYTES) {
|
|
852
|
+
skipped.push(`${name}: larger than ${formatBytes(ATTACHMENT_MAX_FILE_BYTES)}`);
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
if (totalBytes + file.size > ATTACHMENT_MAX_TOTAL_BYTES) {
|
|
856
|
+
skipped.push(`${name}: total attachment limit is ${formatBytes(ATTACHMENT_MAX_TOTAL_BYTES)}`);
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const mimeType = file.type || inferMimeTypeFromName(name);
|
|
860
|
+
const kind = attachmentKind(mimeType, name);
|
|
861
|
+
attachments.push({
|
|
862
|
+
id: `att-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
863
|
+
file,
|
|
864
|
+
name,
|
|
865
|
+
mimeType,
|
|
866
|
+
size: file.size || 0,
|
|
867
|
+
source,
|
|
868
|
+
kind,
|
|
869
|
+
previewUrl: kind === "image" ? URL.createObjectURL(file) : undefined,
|
|
870
|
+
});
|
|
871
|
+
totalBytes += file.size || 0;
|
|
872
|
+
added++;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
renderAttachmentTray();
|
|
876
|
+
if (added) addEvent(`attached ${added} ${added === 1 ? "file" : "files"} from ${source}`, "info");
|
|
877
|
+
if (skipped.length) addEvent(`skipped attachments: ${skipped.join("; ")}`, "warn");
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function clipboardFiles(dataTransfer) {
|
|
881
|
+
const files = [];
|
|
882
|
+
const seen = new Set();
|
|
883
|
+
for (const file of Array.from(dataTransfer?.files || [])) {
|
|
884
|
+
const key = `${file.name}:${file.size}:${file.type}`;
|
|
885
|
+
if (!seen.has(key)) {
|
|
886
|
+
seen.add(key);
|
|
887
|
+
files.push(file);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
for (const item of Array.from(dataTransfer?.items || [])) {
|
|
891
|
+
if (item.kind !== "file") continue;
|
|
892
|
+
const file = item.getAsFile?.();
|
|
893
|
+
if (!file) continue;
|
|
894
|
+
const key = `${file.name}:${file.size}:${file.type}`;
|
|
895
|
+
if (!seen.has(key)) {
|
|
896
|
+
seen.add(key);
|
|
897
|
+
files.push(file);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return files;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function handleAttachmentPaste(event) {
|
|
904
|
+
const files = clipboardFiles(event.clipboardData);
|
|
905
|
+
if (!files.length) return;
|
|
906
|
+
event.preventDefault();
|
|
907
|
+
addAttachmentFiles(files, "clipboard");
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function isFileDrag(event) {
|
|
911
|
+
return Array.from(event.dataTransfer?.types || []).includes("Files");
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function handleComposerDragOver(event) {
|
|
915
|
+
if (!isFileDrag(event)) return;
|
|
916
|
+
event.preventDefault();
|
|
917
|
+
elements.composer.classList.add("drag-over");
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function handleComposerDragLeave(event) {
|
|
921
|
+
if (!elements.composer.contains(event.relatedTarget)) elements.composer.classList.remove("drag-over");
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function handleComposerDrop(event) {
|
|
925
|
+
if (!isFileDrag(event)) return;
|
|
926
|
+
event.preventDefault();
|
|
927
|
+
elements.composer.classList.remove("drag-over");
|
|
928
|
+
addAttachmentFiles(event.dataTransfer?.files, "drop");
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function readFileAsBase64(file) {
|
|
932
|
+
return new Promise((resolve, reject) => {
|
|
933
|
+
const reader = new FileReader();
|
|
934
|
+
reader.onerror = () => reject(reader.error || new Error("Failed to read attachment"));
|
|
935
|
+
reader.onload = () => {
|
|
936
|
+
const result = String(reader.result || "");
|
|
937
|
+
const comma = result.indexOf(",");
|
|
938
|
+
resolve(comma === -1 ? result : result.slice(comma + 1));
|
|
939
|
+
};
|
|
940
|
+
reader.readAsDataURL(file);
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function readFileAsDataUrl(file) {
|
|
945
|
+
return new Promise((resolve, reject) => {
|
|
946
|
+
const reader = new FileReader();
|
|
947
|
+
reader.onerror = () => reject(reader.error || new Error("Failed to read background image"));
|
|
948
|
+
reader.onload = () => resolve(String(reader.result || ""));
|
|
949
|
+
reader.readAsDataURL(file);
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function sanitizeBackgroundName(name) {
|
|
954
|
+
const safe = String(name || "custom background").replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
955
|
+
return safe || "custom background";
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function backgroundMimeType(file) {
|
|
959
|
+
const declared = String(file?.type || "").split(";", 1)[0].trim().toLowerCase();
|
|
960
|
+
if (BACKGROUND_IMAGE_MIME_TYPES.has(declared)) return declared;
|
|
961
|
+
const ext = String(file?.name || "").split(".").pop()?.toLowerCase() || "";
|
|
962
|
+
const byExt = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp", gif: "image/gif" };
|
|
963
|
+
return byExt[ext] || declared || "application/octet-stream";
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function normalizeCustomBackgroundRecord(value) {
|
|
967
|
+
if (!value || typeof value !== "object") return null;
|
|
968
|
+
const dataUrl = String(value.dataUrl || "");
|
|
969
|
+
const match = dataUrl.match(/^data:(image\/(?:png|jpeg|webp|gif));base64,[A-Za-z0-9+/]+={0,2}$/i);
|
|
970
|
+
if (!match) return null;
|
|
971
|
+
return {
|
|
972
|
+
name: sanitizeBackgroundName(value.name),
|
|
973
|
+
mimeType: match[1].toLowerCase(),
|
|
974
|
+
size: Math.max(0, Number(value.size) || 0),
|
|
975
|
+
dataUrl,
|
|
976
|
+
updatedAt: Number(value.updatedAt) || Date.now(),
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function dataUrlToBlob(dataUrl) {
|
|
981
|
+
const match = String(dataUrl || "").match(/^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/]+={0,2})$/i);
|
|
982
|
+
if (!match) throw new Error("Invalid background data URL");
|
|
983
|
+
const binary = atob(match[2]);
|
|
984
|
+
const bytes = new Uint8Array(binary.length);
|
|
985
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
986
|
+
return new Blob([bytes], { type: match[1].toLowerCase() });
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function revokeCustomBackgroundObjectUrl() {
|
|
990
|
+
if (!customBackgroundObjectUrl) return;
|
|
991
|
+
URL.revokeObjectURL(customBackgroundObjectUrl);
|
|
992
|
+
customBackgroundObjectUrl = null;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function setCustomBackgroundRecord(background, { objectUrl = null } = {}) {
|
|
996
|
+
const record = normalizeCustomBackgroundRecord(background);
|
|
997
|
+
revokeCustomBackgroundObjectUrl();
|
|
998
|
+
customBackground = record;
|
|
999
|
+
if (!record) return null;
|
|
1000
|
+
if (objectUrl) customBackgroundObjectUrl = objectUrl;
|
|
1001
|
+
else {
|
|
1002
|
+
try {
|
|
1003
|
+
customBackgroundObjectUrl = URL.createObjectURL(dataUrlToBlob(record.dataUrl));
|
|
1004
|
+
} catch {
|
|
1005
|
+
customBackgroundObjectUrl = null;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return record;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function idbRequest(request) {
|
|
1012
|
+
return new Promise((resolve, reject) => {
|
|
1013
|
+
request.onsuccess = () => resolve(request.result);
|
|
1014
|
+
request.onerror = () => reject(request.error || new Error("IndexedDB request failed"));
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function idbTransactionDone(transaction) {
|
|
1019
|
+
return new Promise((resolve, reject) => {
|
|
1020
|
+
transaction.oncomplete = () => resolve();
|
|
1021
|
+
transaction.onerror = () => reject(transaction.error || new Error("IndexedDB transaction failed"));
|
|
1022
|
+
transaction.onabort = () => reject(transaction.error || new Error("IndexedDB transaction aborted"));
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function openCustomBackgroundDb() {
|
|
1027
|
+
return new Promise((resolve, reject) => {
|
|
1028
|
+
const indexedDb = window.indexedDB;
|
|
1029
|
+
if (!indexedDb) {
|
|
1030
|
+
reject(new Error("IndexedDB unavailable"));
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const request = indexedDb.open(CUSTOM_BACKGROUND_IDB_NAME, 1);
|
|
1034
|
+
request.onupgradeneeded = () => {
|
|
1035
|
+
const db = request.result;
|
|
1036
|
+
if (!db.objectStoreNames.contains(CUSTOM_BACKGROUND_IDB_STORE)) db.createObjectStore(CUSTOM_BACKGROUND_IDB_STORE);
|
|
1037
|
+
};
|
|
1038
|
+
request.onsuccess = () => resolve(request.result);
|
|
1039
|
+
request.onerror = () => reject(request.error || new Error("Failed to open background storage"));
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function customBackgroundThemeKey(themeName = currentThemeName) {
|
|
1044
|
+
return String(themeName || DEFAULT_THEME_NAME).trim() || DEFAULT_THEME_NAME;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async function readCustomBackgroundFromIndexedDb(themeName = currentThemeName) {
|
|
1048
|
+
const db = await openCustomBackgroundDb();
|
|
1049
|
+
try {
|
|
1050
|
+
return await idbRequest(db.transaction(CUSTOM_BACKGROUND_IDB_STORE, "readonly").objectStore(CUSTOM_BACKGROUND_IDB_STORE).get(customBackgroundThemeKey(themeName)));
|
|
1051
|
+
} finally {
|
|
1052
|
+
db.close();
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
async function readLegacyCustomBackgroundFromIndexedDb() {
|
|
1057
|
+
const db = await openCustomBackgroundDb();
|
|
1058
|
+
try {
|
|
1059
|
+
return await idbRequest(db.transaction(CUSTOM_BACKGROUND_IDB_STORE, "readonly").objectStore(CUSTOM_BACKGROUND_IDB_STORE).get(CUSTOM_BACKGROUND_LEGACY_ID));
|
|
1060
|
+
} finally {
|
|
1061
|
+
db.close();
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async function writeCustomBackgroundToIndexedDb(background, themeName = currentThemeName) {
|
|
1066
|
+
const db = await openCustomBackgroundDb();
|
|
1067
|
+
try {
|
|
1068
|
+
const transaction = db.transaction(CUSTOM_BACKGROUND_IDB_STORE, "readwrite");
|
|
1069
|
+
transaction.objectStore(CUSTOM_BACKGROUND_IDB_STORE).put(background, customBackgroundThemeKey(themeName));
|
|
1070
|
+
await idbTransactionDone(transaction);
|
|
1071
|
+
} finally {
|
|
1072
|
+
db.close();
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
async function deleteCustomBackgroundFromIndexedDb(themeName = currentThemeName) {
|
|
1077
|
+
const db = await openCustomBackgroundDb();
|
|
1078
|
+
try {
|
|
1079
|
+
const transaction = db.transaction(CUSTOM_BACKGROUND_IDB_STORE, "readwrite");
|
|
1080
|
+
transaction.objectStore(CUSTOM_BACKGROUND_IDB_STORE).delete(customBackgroundThemeKey(themeName));
|
|
1081
|
+
await idbTransactionDone(transaction);
|
|
1082
|
+
} finally {
|
|
1083
|
+
db.close();
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async function deleteLegacyCustomBackgroundFromIndexedDb() {
|
|
1088
|
+
const db = await openCustomBackgroundDb();
|
|
1089
|
+
try {
|
|
1090
|
+
const transaction = db.transaction(CUSTOM_BACKGROUND_IDB_STORE, "readwrite");
|
|
1091
|
+
transaction.objectStore(CUSTOM_BACKGROUND_IDB_STORE).delete(CUSTOM_BACKGROUND_LEGACY_ID);
|
|
1092
|
+
await idbTransactionDone(transaction);
|
|
1093
|
+
} finally {
|
|
1094
|
+
db.close();
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function readCustomBackgroundFromLocalStorage(themeName = currentThemeName, { includeLegacy = false } = {}) {
|
|
1099
|
+
try {
|
|
1100
|
+
const parsed = JSON.parse(localStorage.getItem(CUSTOM_BACKGROUNDS_STORAGE_KEY) || "{}");
|
|
1101
|
+
const record = parsed && typeof parsed === "object" ? normalizeCustomBackgroundRecord(parsed[customBackgroundThemeKey(themeName)]) : null;
|
|
1102
|
+
if (record) return record;
|
|
1103
|
+
} catch {
|
|
1104
|
+
// Fall through to legacy storage below.
|
|
1105
|
+
}
|
|
1106
|
+
if (!includeLegacy) return null;
|
|
1107
|
+
try {
|
|
1108
|
+
return normalizeCustomBackgroundRecord(JSON.parse(localStorage.getItem(CUSTOM_BACKGROUND_STORAGE_KEY) || "null"));
|
|
1109
|
+
} catch {
|
|
1110
|
+
return null;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function writeCustomBackgroundToLocalStorage(background, themeName = currentThemeName) {
|
|
1115
|
+
const record = normalizeCustomBackgroundRecord(background);
|
|
1116
|
+
if (!record) throw new Error("Invalid background image data");
|
|
1117
|
+
const key = customBackgroundThemeKey(themeName);
|
|
1118
|
+
const parsed = JSON.parse(localStorage.getItem(CUSTOM_BACKGROUNDS_STORAGE_KEY) || "{}");
|
|
1119
|
+
const backgrounds = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1120
|
+
backgrounds[key] = record;
|
|
1121
|
+
localStorage.setItem(CUSTOM_BACKGROUNDS_STORAGE_KEY, JSON.stringify(backgrounds));
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function removeCustomBackgroundFromLocalStorage(themeName = currentThemeName, { includeLegacy = false } = {}) {
|
|
1125
|
+
const key = customBackgroundThemeKey(themeName);
|
|
1126
|
+
try {
|
|
1127
|
+
const parsed = JSON.parse(localStorage.getItem(CUSTOM_BACKGROUNDS_STORAGE_KEY) || "{}");
|
|
1128
|
+
const backgrounds = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1129
|
+
delete backgrounds[key];
|
|
1130
|
+
localStorage.setItem(CUSTOM_BACKGROUNDS_STORAGE_KEY, JSON.stringify(backgrounds));
|
|
1131
|
+
} catch {
|
|
1132
|
+
// Ignore fallback cleanup failures.
|
|
1133
|
+
}
|
|
1134
|
+
if (includeLegacy) {
|
|
1135
|
+
try {
|
|
1136
|
+
localStorage.removeItem(CUSTOM_BACKGROUND_STORAGE_KEY);
|
|
1137
|
+
} catch {
|
|
1138
|
+
// Ignore legacy cleanup failures.
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
async function readStoredCustomBackground(themeName = currentThemeName, { includeLegacy = false } = {}) {
|
|
1144
|
+
try {
|
|
1145
|
+
const stored = normalizeCustomBackgroundRecord(await readCustomBackgroundFromIndexedDb(themeName));
|
|
1146
|
+
if (stored) return stored;
|
|
1147
|
+
if (includeLegacy) {
|
|
1148
|
+
const legacy = normalizeCustomBackgroundRecord(await readLegacyCustomBackgroundFromIndexedDb());
|
|
1149
|
+
if (legacy) return legacy;
|
|
1150
|
+
}
|
|
1151
|
+
} catch {
|
|
1152
|
+
// Fall back to localStorage for older browsers or private browsing modes.
|
|
1153
|
+
}
|
|
1154
|
+
return readCustomBackgroundFromLocalStorage(themeName, { includeLegacy });
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
async function persistCustomBackground(background, themeName = currentThemeName) {
|
|
1158
|
+
const record = normalizeCustomBackgroundRecord(background);
|
|
1159
|
+
if (!record) throw new Error("Invalid background image data");
|
|
1160
|
+
try {
|
|
1161
|
+
await writeCustomBackgroundToIndexedDb(record, themeName);
|
|
1162
|
+
removeCustomBackgroundFromLocalStorage(themeName);
|
|
1163
|
+
return;
|
|
1164
|
+
} catch {
|
|
1165
|
+
// Fall back to localStorage when IndexedDB is unavailable.
|
|
1166
|
+
}
|
|
1167
|
+
writeCustomBackgroundToLocalStorage(record, themeName);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
async function clearStoredCustomBackground(themeName = currentThemeName, { includeLegacy = false } = {}) {
|
|
1171
|
+
await Promise.allSettled([
|
|
1172
|
+
deleteCustomBackgroundFromIndexedDb(themeName),
|
|
1173
|
+
includeLegacy ? deleteLegacyCustomBackgroundFromIndexedDb() : Promise.resolve(),
|
|
1174
|
+
Promise.resolve().then(() => removeCustomBackgroundFromLocalStorage(themeName, { includeLegacy })),
|
|
1175
|
+
]);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function customBackgroundCssImage(background = customBackground) {
|
|
1179
|
+
if (!background?.dataUrl) return null;
|
|
1180
|
+
return `url("${customBackgroundObjectUrl || background.dataUrl}")`;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function renderBackgroundControl() {
|
|
1184
|
+
if (!elements.backgroundStatus) return;
|
|
1185
|
+
const active = !!customBackground?.dataUrl;
|
|
1186
|
+
const themeLabel = displayThemeName(currentThemeName) || currentThemeName || "theme";
|
|
1187
|
+
elements.backgroundStatus.textContent = customBackgroundLoading
|
|
1188
|
+
? `Loading ${themeLabel} background…`
|
|
1189
|
+
: active
|
|
1190
|
+
? `${themeLabel}: ${customBackground.name || "background"}`
|
|
1191
|
+
: `${themeLabel}: theme default`;
|
|
1192
|
+
if (elements.backgroundChooseButton) {
|
|
1193
|
+
elements.backgroundChooseButton.disabled = customBackgroundLoading;
|
|
1194
|
+
elements.backgroundChooseButton.textContent = active ? "Change background" : "Add background";
|
|
1195
|
+
}
|
|
1196
|
+
if (elements.backgroundInput) elements.backgroundInput.disabled = customBackgroundLoading;
|
|
1197
|
+
if (elements.backgroundClearButton) {
|
|
1198
|
+
elements.backgroundClearButton.hidden = !active;
|
|
1199
|
+
elements.backgroundClearButton.disabled = customBackgroundLoading;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function applyCustomBackgroundOverride({ render = true } = {}) {
|
|
1204
|
+
const activeImage = customBackgroundCssImage();
|
|
1205
|
+
document.body.classList.toggle("custom-background-active", !!activeImage);
|
|
1206
|
+
if (activeImage) document.documentElement.style.setProperty("--theme-background-image", activeImage);
|
|
1207
|
+
if (render) renderBackgroundControl();
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function reapplyCurrentThemeBackground() {
|
|
1211
|
+
const theme = availableThemes.find((item) => item.name === currentThemeName);
|
|
1212
|
+
if (theme && isOptionalFeatureEnabled("themeBundle")) applyTheme(theme, { persist: false });
|
|
1213
|
+
else {
|
|
1214
|
+
document.documentElement.style.setProperty("--theme-background-image", "none");
|
|
1215
|
+
applyCustomBackgroundOverride();
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
async function loadCustomBackgroundForTheme(themeName = currentThemeName, { includeLegacy = false } = {}) {
|
|
1220
|
+
const themeKey = customBackgroundThemeKey(themeName);
|
|
1221
|
+
customBackgroundLoading = true;
|
|
1222
|
+
renderBackgroundControl();
|
|
1223
|
+
try {
|
|
1224
|
+
const background = await readStoredCustomBackground(themeKey, { includeLegacy });
|
|
1225
|
+
if (customBackgroundThemeKey(currentThemeName) !== themeKey) return;
|
|
1226
|
+
setCustomBackgroundRecord(background);
|
|
1227
|
+
if (background && includeLegacy) {
|
|
1228
|
+
persistCustomBackground(background, themeKey).catch(() => {});
|
|
1229
|
+
}
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
if (customBackgroundThemeKey(currentThemeName) === themeKey) {
|
|
1232
|
+
addEvent(`failed to load ${displayThemeName(themeKey) || themeKey} background: ${error.message || String(error)}`, "warn");
|
|
1233
|
+
setCustomBackgroundRecord(null);
|
|
1234
|
+
}
|
|
1235
|
+
} finally {
|
|
1236
|
+
if (customBackgroundThemeKey(currentThemeName) === themeKey) {
|
|
1237
|
+
customBackgroundLoading = false;
|
|
1238
|
+
applyCustomBackgroundOverride();
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
async function setCustomBackgroundFromFile(file) {
|
|
1244
|
+
if (!file) return;
|
|
1245
|
+
const mimeType = backgroundMimeType(file);
|
|
1246
|
+
if (!BACKGROUND_IMAGE_MIME_TYPES.has(mimeType)) {
|
|
1247
|
+
addEvent("background must be a PNG, JPEG, WebP, or GIF image", "error");
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if ((file.size || 0) > CUSTOM_BACKGROUND_MAX_FILE_BYTES) {
|
|
1251
|
+
addEvent(`background image is larger than ${formatBytes(CUSTOM_BACKGROUND_MAX_FILE_BYTES)}`, "error");
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const themeName = customBackgroundThemeKey(currentThemeName);
|
|
1256
|
+
customBackgroundLoading = true;
|
|
1257
|
+
renderBackgroundControl();
|
|
1258
|
+
try {
|
|
1259
|
+
const rawDataUrl = await readFileAsDataUrl(file);
|
|
1260
|
+
const dataUrl = rawDataUrl.replace(/^data:;base64,/i, `data:${mimeType};base64,`);
|
|
1261
|
+
const background = normalizeCustomBackgroundRecord({
|
|
1262
|
+
name: file.name,
|
|
1263
|
+
mimeType,
|
|
1264
|
+
size: file.size || 0,
|
|
1265
|
+
dataUrl,
|
|
1266
|
+
updatedAt: Date.now(),
|
|
1267
|
+
});
|
|
1268
|
+
if (!background) throw new Error("Unsupported or invalid background image data");
|
|
1269
|
+
let objectUrl = null;
|
|
1270
|
+
try {
|
|
1271
|
+
objectUrl = URL.createObjectURL(file);
|
|
1272
|
+
} catch {
|
|
1273
|
+
objectUrl = null;
|
|
1274
|
+
}
|
|
1275
|
+
const targetStillActive = customBackgroundThemeKey(currentThemeName) === themeName;
|
|
1276
|
+
if (targetStillActive) {
|
|
1277
|
+
setCustomBackgroundRecord(background, { objectUrl });
|
|
1278
|
+
applyCustomBackgroundOverride({ render: false });
|
|
1279
|
+
} else if (objectUrl) {
|
|
1280
|
+
URL.revokeObjectURL(objectUrl);
|
|
1281
|
+
}
|
|
1282
|
+
try {
|
|
1283
|
+
await persistCustomBackground(background, themeName);
|
|
1284
|
+
addEvent(`custom background saved for ${displayThemeName(themeName) || themeName}: ${background.name}`);
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
addEvent(`background changed for this page, but persistent save failed: ${error.message || String(error)}`, "warn");
|
|
1287
|
+
}
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
addEvent(`failed to set background: ${error.message || String(error)}`, "error");
|
|
1290
|
+
} finally {
|
|
1291
|
+
if (customBackgroundThemeKey(currentThemeName) === themeName) {
|
|
1292
|
+
customBackgroundLoading = false;
|
|
1293
|
+
renderBackgroundControl();
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
async function clearCustomBackground() {
|
|
1299
|
+
const themeName = customBackgroundThemeKey(currentThemeName);
|
|
1300
|
+
const hadBackground = !!customBackground?.dataUrl;
|
|
1301
|
+
setCustomBackgroundRecord(null);
|
|
1302
|
+
customBackgroundLoading = true;
|
|
1303
|
+
renderBackgroundControl();
|
|
1304
|
+
await clearStoredCustomBackground(themeName, { includeLegacy: true });
|
|
1305
|
+
customBackgroundLoading = false;
|
|
1306
|
+
reapplyCurrentThemeBackground();
|
|
1307
|
+
renderBackgroundControl();
|
|
1308
|
+
if (hadBackground) addEvent(`custom background removed for ${displayThemeName(themeName) || themeName}`);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
async function initializeCustomBackground() {
|
|
1312
|
+
await loadCustomBackgroundForTheme(currentThemeName, { includeLegacy: true });
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
async function prepareAttachmentsForPrompt(attachments, tabId) {
|
|
1316
|
+
if (!attachments.length) return { images: [], uploadedFiles: [], inlineImageIds: new Set() };
|
|
1317
|
+
const files = [];
|
|
1318
|
+
const images = [];
|
|
1319
|
+
const inlineImageIds = new Set();
|
|
1320
|
+
let inlineImageBytes = 0;
|
|
1321
|
+
|
|
1322
|
+
for (const attachment of attachments) {
|
|
1323
|
+
const data = await readFileAsBase64(attachment.file);
|
|
1324
|
+
files.push({
|
|
1325
|
+
id: attachment.id,
|
|
1326
|
+
name: attachment.name,
|
|
1327
|
+
mimeType: attachment.mimeType,
|
|
1328
|
+
size: attachment.size,
|
|
1329
|
+
data,
|
|
1330
|
+
});
|
|
1331
|
+
if (
|
|
1332
|
+
INLINE_IMAGE_MIME_TYPES.has(attachment.mimeType) &&
|
|
1333
|
+
attachment.size <= ATTACHMENT_INLINE_IMAGE_MAX_BYTES &&
|
|
1334
|
+
inlineImageBytes + attachment.size <= ATTACHMENT_INLINE_IMAGE_TOTAL_MAX_BYTES
|
|
1335
|
+
) {
|
|
1336
|
+
images.push({ type: "image", data, mimeType: attachment.mimeType });
|
|
1337
|
+
inlineImageIds.add(attachment.id);
|
|
1338
|
+
inlineImageBytes += attachment.size;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const response = await api("/api/attachments", { method: "POST", body: { files }, tabId });
|
|
1343
|
+
return { images, uploadedFiles: response.data?.files || [], inlineImageIds };
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function composeMessageWithAttachments(message, uploadedFiles, inlineImageIds) {
|
|
1347
|
+
if (!uploadedFiles.length) return message;
|
|
1348
|
+
const baseMessage = message || "Please inspect the attached file(s).";
|
|
1349
|
+
const lines = uploadedFiles.map((file, index) => {
|
|
1350
|
+
const inlineNote = inlineImageIds.has(file.id) ? "sent inline and saved at" : "saved at";
|
|
1351
|
+
return `- ${index + 1}. ${file.name || "attachment"} (${file.mimeType || "application/octet-stream"}, ${formatBytes(file.size)}): ${inlineNote} ${file.path}`;
|
|
1352
|
+
});
|
|
1353
|
+
return `${baseMessage}\n\nAttached files:\n${lines.join("\n")}`;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
484
1356
|
function storedThemeName() {
|
|
485
1357
|
try {
|
|
486
1358
|
return localStorage.getItem(THEME_STORAGE_KEY) || DEFAULT_THEME_NAME;
|
|
@@ -497,6 +1369,65 @@ function storeThemeName(name) {
|
|
|
497
1369
|
}
|
|
498
1370
|
}
|
|
499
1371
|
|
|
1372
|
+
function loadDisabledOptionalFeatures() {
|
|
1373
|
+
try {
|
|
1374
|
+
const parsed = JSON.parse(localStorage.getItem(OPTIONAL_FEATURES_STORAGE_KEY) || "[]");
|
|
1375
|
+
return Array.isArray(parsed) ? parsed.filter((id) => OPTIONAL_FEATURE_BY_ID.has(id)) : [];
|
|
1376
|
+
} catch {
|
|
1377
|
+
return [];
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
let disabledOptionalFeatures = new Set(loadDisabledOptionalFeatures());
|
|
1382
|
+
|
|
1383
|
+
function storeDisabledOptionalFeatures() {
|
|
1384
|
+
try {
|
|
1385
|
+
localStorage.setItem(OPTIONAL_FEATURES_STORAGE_KEY, JSON.stringify([...disabledOptionalFeatures].sort()));
|
|
1386
|
+
} catch {
|
|
1387
|
+
// Optional feature toggles should still work for this page load.
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function isOptionalFeatureDetected(featureId) {
|
|
1392
|
+
return optionalFeatureAvailability[featureId] === true;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function isOptionalFeatureDisabled(featureId) {
|
|
1396
|
+
return disabledOptionalFeatures.has(featureId);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function isOptionalFeatureEnabled(featureId) {
|
|
1400
|
+
return isOptionalFeatureDetected(featureId) && !isOptionalFeatureDisabled(featureId);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function renderOptionalFeatureDependentDisplays() {
|
|
1404
|
+
renderOptionalFeatureControls();
|
|
1405
|
+
renderThemeSelect();
|
|
1406
|
+
renderWidgets();
|
|
1407
|
+
renderStatus();
|
|
1408
|
+
renderCommands();
|
|
1409
|
+
cancelStreamingAssistantTextRender();
|
|
1410
|
+
cancelStreamBubbleHide();
|
|
1411
|
+
streamBubble?.remove();
|
|
1412
|
+
streamBubble = null;
|
|
1413
|
+
streamText = null;
|
|
1414
|
+
streamBubbleVisibleSince = 0;
|
|
1415
|
+
renderAllMessages({ preserveScroll: true });
|
|
1416
|
+
if (streamRawText) renderStreamingAssistantText();
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function setOptionalFeatureDisabled(featureId, disabled) {
|
|
1420
|
+
if (!OPTIONAL_FEATURE_BY_ID.has(featureId)) return;
|
|
1421
|
+
if (disabled) disabledOptionalFeatures.add(featureId);
|
|
1422
|
+
else disabledOptionalFeatures.delete(featureId);
|
|
1423
|
+
storeDisabledOptionalFeatures();
|
|
1424
|
+
renderOptionalFeatureDependentDisplays();
|
|
1425
|
+
const tabContext = activeTabContext();
|
|
1426
|
+
refreshCommands(tabContext).catch((error) => {
|
|
1427
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
|
|
500
1431
|
function displayThemeName(name) {
|
|
501
1432
|
return String(name || "")
|
|
502
1433
|
.split(/[-_]+/)
|
|
@@ -522,6 +1453,16 @@ function themeExportColor(theme, key, fallback) {
|
|
|
522
1453
|
return resolveThemeValue(theme, theme?.export?.[key], fallback);
|
|
523
1454
|
}
|
|
524
1455
|
|
|
1456
|
+
const LOCAL_BACKGROUND_IMAGE_PATTERN = /^(?:none|url\(["']?\/(?!\/)[A-Za-z0-9._~!$&'()*+,=:@%\/-]+["']?\))$/i;
|
|
1457
|
+
const BACKGROUND_OVERLAY_PATTERN = /^(?:none|linear-gradient\([^;\r\n{}<>]+\))$/i;
|
|
1458
|
+
const SAFE_BACKGROUND_TOKEN_PATTERN = /^[A-Za-z0-9%._ -]+$/;
|
|
1459
|
+
|
|
1460
|
+
function themeExportCssValue(theme, key, fallback, pattern = /^[^;\r\n{}<>]+$/) {
|
|
1461
|
+
const raw = String(theme?.export?.[key] ?? "").trim();
|
|
1462
|
+
if (!raw) return fallback;
|
|
1463
|
+
return pattern.test(raw) ? raw : fallback;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
525
1466
|
function hexToRgb(color) {
|
|
526
1467
|
const raw = String(color || "").trim();
|
|
527
1468
|
const match = raw.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
|
@@ -669,9 +1610,15 @@ function applyTheme(theme, { persist = false, announce = false } = {}) {
|
|
|
669
1610
|
"--background-glow-pink": colorWithAlpha(pink, isLight ? 0.16 : 0.34, pink),
|
|
670
1611
|
"--background-glow-blue": colorWithAlpha(accent, isLight ? 0.15 : 0.32, accent),
|
|
671
1612
|
"--background-glow-teal": colorWithAlpha(accent2, isLight ? 0.12 : 0.20, accent2),
|
|
1613
|
+
"--theme-background-image": themeExportCssValue(theme, "backgroundImage", "none", LOCAL_BACKGROUND_IMAGE_PATTERN),
|
|
1614
|
+
"--theme-background-overlay": themeExportCssValue(theme, "backgroundOverlay", "linear-gradient(180deg, rgba(17, 17, 27, 0), rgba(17, 17, 27, 0))", BACKGROUND_OVERLAY_PATTERN),
|
|
1615
|
+
"--theme-background-size": themeExportCssValue(theme, "backgroundSize", "cover", SAFE_BACKGROUND_TOKEN_PATTERN),
|
|
1616
|
+
"--theme-background-position": themeExportCssValue(theme, "backgroundPosition", "center", SAFE_BACKGROUND_TOKEN_PATTERN),
|
|
1617
|
+
"--theme-background-repeat": themeExportCssValue(theme, "backgroundRepeat", "no-repeat", SAFE_BACKGROUND_TOKEN_PATTERN),
|
|
672
1618
|
};
|
|
673
1619
|
|
|
674
1620
|
for (const [name, value] of Object.entries(vars)) root.style.setProperty(name, value);
|
|
1621
|
+
applyCustomBackgroundOverride({ render: false });
|
|
675
1622
|
root.style.colorScheme = isLight ? "light" : "dark";
|
|
676
1623
|
document.body.classList.toggle("theme-light", isLight);
|
|
677
1624
|
document.body.classList.toggle("theme-dark", !isLight);
|
|
@@ -685,6 +1632,13 @@ function applyTheme(theme, { persist = false, announce = false } = {}) {
|
|
|
685
1632
|
function renderThemeSelect({ unavailableLabel = "Theme bundle unavailable" } = {}) {
|
|
686
1633
|
if (!elements.themeSelect) return;
|
|
687
1634
|
elements.themeSelect.replaceChildren();
|
|
1635
|
+
if (isOptionalFeatureDisabled("themeBundle")) {
|
|
1636
|
+
const option = make("option", undefined, "Theme feature disabled");
|
|
1637
|
+
option.value = "";
|
|
1638
|
+
elements.themeSelect.append(option);
|
|
1639
|
+
elements.themeSelect.disabled = true;
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
688
1642
|
if (!availableThemes.length) {
|
|
689
1643
|
const option = make("option", undefined, unavailableLabel);
|
|
690
1644
|
option.value = "";
|
|
@@ -701,10 +1655,17 @@ function renderThemeSelect({ unavailableLabel = "Theme bundle unavailable" } = {
|
|
|
701
1655
|
elements.themeSelect.value = currentThemeName;
|
|
702
1656
|
}
|
|
703
1657
|
|
|
704
|
-
function setThemeByName(name, options = {}) {
|
|
1658
|
+
async function setThemeByName(name, options = {}) {
|
|
1659
|
+
if (!isOptionalFeatureEnabled("themeBundle")) return;
|
|
705
1660
|
const theme = availableThemes.find((item) => item.name === name);
|
|
706
1661
|
if (!theme) return;
|
|
1662
|
+
currentThemeName = theme.name;
|
|
1663
|
+
if (elements.themeSelect && elements.themeSelect.value !== theme.name) elements.themeSelect.value = theme.name;
|
|
1664
|
+
setCustomBackgroundRecord(null);
|
|
1665
|
+
customBackgroundLoading = true;
|
|
707
1666
|
applyTheme(theme, options);
|
|
1667
|
+
renderBackgroundControl();
|
|
1668
|
+
await loadCustomBackgroundForTheme(theme.name, { includeLegacy: !!options.includeLegacy });
|
|
708
1669
|
}
|
|
709
1670
|
|
|
710
1671
|
async function initializeThemes() {
|
|
@@ -713,16 +1674,20 @@ async function initializeThemes() {
|
|
|
713
1674
|
response = await api("/api/themes", { scoped: false });
|
|
714
1675
|
} catch (error) {
|
|
715
1676
|
availableThemes = [];
|
|
1677
|
+
optionalFeatureAvailability.themeBundle = false;
|
|
1678
|
+
renderOptionalFeatureControls();
|
|
716
1679
|
const label = error.statusCode === 404 ? "Restart Web UI to load themes" : "Theme bundle unavailable";
|
|
717
1680
|
renderThemeSelect({ unavailableLabel: label });
|
|
718
1681
|
throw error;
|
|
719
1682
|
}
|
|
720
1683
|
availableThemes = Array.isArray(response.data?.themes) ? response.data.themes : [];
|
|
1684
|
+
optionalFeatureAvailability.themeBundle = availableThemes.length > 0;
|
|
1685
|
+
renderOptionalFeatureControls();
|
|
721
1686
|
const stored = storedThemeName();
|
|
722
1687
|
currentThemeName = availableThemes.some((theme) => theme.name === stored) ? stored : DEFAULT_THEME_NAME;
|
|
723
1688
|
renderThemeSelect();
|
|
724
|
-
setThemeByName(currentThemeName, { persist: false });
|
|
725
|
-
if (!availableThemes.some((theme) => theme.name === currentThemeName) && availableThemes[0])
|
|
1689
|
+
await setThemeByName(currentThemeName, { persist: false, includeLegacy: true });
|
|
1690
|
+
if (isOptionalFeatureEnabled("themeBundle") && !availableThemes.some((theme) => theme.name === currentThemeName) && availableThemes[0]) await setThemeByName(availableThemes[0].name, { persist: false });
|
|
726
1691
|
if (!availableThemes.length) addEvent("theme bundle unavailable; using built-in default theme", "warn");
|
|
727
1692
|
}
|
|
728
1693
|
|
|
@@ -730,6 +1695,26 @@ function activeTab() {
|
|
|
730
1695
|
return tabs.find((tab) => tab.id === activeTabId) || null;
|
|
731
1696
|
}
|
|
732
1697
|
|
|
1698
|
+
function activeTabContext(tabId = activeTabId) {
|
|
1699
|
+
return { tabId: tabId || null, generation: activeTabGeneration };
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function setActiveTabId(tabId, { remember = false } = {}) {
|
|
1703
|
+
const nextTabId = tabId || null;
|
|
1704
|
+
if (nextTabId !== activeTabId) activeTabGeneration += 1;
|
|
1705
|
+
activeTabId = nextTabId;
|
|
1706
|
+
if (remember) rememberActiveTab();
|
|
1707
|
+
return activeTabContext(nextTabId);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function isCurrentTabContext(context) {
|
|
1711
|
+
return !!context && context.tabId === activeTabId && context.generation === activeTabGeneration;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
function eventTargetsActiveTab(event) {
|
|
1715
|
+
return !event?.tabId || event.tabId === activeTabId;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
733
1718
|
function normalizeTabActivity(activity = {}) {
|
|
734
1719
|
const status = activity.status === "working" || activity.isWorking ? "working" : activity.status === "done" ? "done" : "idle";
|
|
735
1720
|
const completionSerial = Number(activity.completionSerial);
|
|
@@ -987,11 +1972,12 @@ function restoreActiveDraft() {
|
|
|
987
1972
|
elements.promptInput.value = activeTabId ? tabDrafts.get(activeTabId) || "" : "";
|
|
988
1973
|
resizePromptInput();
|
|
989
1974
|
renderCommandSuggestions();
|
|
1975
|
+
renderAttachmentTray();
|
|
990
1976
|
}
|
|
991
1977
|
|
|
992
1978
|
function focusPromptInput({ defer = false } = {}) {
|
|
993
1979
|
const focus = () => {
|
|
994
|
-
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || document.visibilityState === "hidden") return;
|
|
1980
|
+
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || elements.nativeCommandDialog.open || document.visibilityState === "hidden") return;
|
|
995
1981
|
try {
|
|
996
1982
|
elements.promptInput.focus({ preventScroll: true });
|
|
997
1983
|
} catch {
|
|
@@ -1043,7 +2029,10 @@ function resetActiveTabUi() {
|
|
|
1043
2029
|
statusEntries.clear();
|
|
1044
2030
|
widgets.clear();
|
|
1045
2031
|
transientMessages = [];
|
|
2032
|
+
liveToolRuns.clear();
|
|
2033
|
+
liveToolCards.clear();
|
|
1046
2034
|
availableCommands = [];
|
|
2035
|
+
resetOptionalFeatureAvailability();
|
|
1047
2036
|
commandSuggestions = [];
|
|
1048
2037
|
pathSuggestions = [];
|
|
1049
2038
|
suggestionMode = "none";
|
|
@@ -1052,6 +2041,8 @@ function resetActiveTabUi() {
|
|
|
1052
2041
|
removeRunIndicatorBubble();
|
|
1053
2042
|
hideCommandSuggestions();
|
|
1054
2043
|
cancelPendingDialogs();
|
|
2044
|
+
if (elements.nativeCommandDialog.open) closeNativeCommandDialog();
|
|
2045
|
+
if (pathPickerState) closePathPicker(null);
|
|
1055
2046
|
Object.assign(gitWorkflow, {
|
|
1056
2047
|
active: false,
|
|
1057
2048
|
step: "idle",
|
|
@@ -1199,7 +2190,7 @@ function shouldRenderTerminalTabGroup(group, groupCount) {
|
|
|
1199
2190
|
return groupCount > 1 && group.tabs.length > 1 && Boolean(group.cwd);
|
|
1200
2191
|
}
|
|
1201
2192
|
|
|
1202
|
-
function renderTerminalTabGroup(group) {
|
|
2193
|
+
function renderTerminalTabGroup(group, groupCount = 1) {
|
|
1203
2194
|
const groupTabs = group.tabs;
|
|
1204
2195
|
const activeGroupTab = groupTabs.find((tab) => tab.id === activeTabId) || groupTabs[0];
|
|
1205
2196
|
const isActive = groupTabs.some((tab) => tab.id === activeTabId);
|
|
@@ -1229,6 +2220,18 @@ function renderTerminalTabGroup(group) {
|
|
|
1229
2220
|
button.addEventListener("click", () => switchTab(activeGroupTab.id));
|
|
1230
2221
|
wrapper.append(button);
|
|
1231
2222
|
|
|
2223
|
+
if (groupCount > 1) {
|
|
2224
|
+
const close = make("button", "terminal-tab-close terminal-tab-group-close", "×");
|
|
2225
|
+
close.type = "button";
|
|
2226
|
+
close.title = `Close ${displayCwd} group`;
|
|
2227
|
+
close.setAttribute("aria-label", `Close ${displayCwd} group`);
|
|
2228
|
+
close.addEventListener("click", (event) => {
|
|
2229
|
+
event.stopPropagation();
|
|
2230
|
+
closeTerminalTabGroup(group);
|
|
2231
|
+
});
|
|
2232
|
+
wrapper.append(close);
|
|
2233
|
+
}
|
|
2234
|
+
|
|
1232
2235
|
const menu = make("div", "terminal-tab-group-menu");
|
|
1233
2236
|
menu.setAttribute("role", "group");
|
|
1234
2237
|
menu.setAttribute("aria-label", `${displayCwd} tabs`);
|
|
@@ -1279,12 +2282,13 @@ function renderTabs() {
|
|
|
1279
2282
|
if (openTerminalTabGroupKey && !renderedGroupKeys.has(openTerminalTabGroupKey)) openTerminalTabGroupKey = null;
|
|
1280
2283
|
for (const group of groups) {
|
|
1281
2284
|
if (shouldRenderTerminalTabGroup(group, groups.length)) {
|
|
1282
|
-
elements.tabBar.append(renderTerminalTabGroup(group));
|
|
2285
|
+
elements.tabBar.append(renderTerminalTabGroup(group, groups.length));
|
|
1283
2286
|
} else {
|
|
1284
2287
|
for (const tab of group.tabs) elements.tabBar.append(renderTerminalTab(tab));
|
|
1285
2288
|
}
|
|
1286
2289
|
}
|
|
1287
2290
|
elements.tabBar.append(elements.newTabButton);
|
|
2291
|
+
elements.closeAllTabsButton.disabled = tabs.length === 0;
|
|
1288
2292
|
updateTerminalTabGroupOpenState();
|
|
1289
2293
|
setMobileTabsExpanded(mobileTabsExpanded);
|
|
1290
2294
|
updateDocumentTitle();
|
|
@@ -1300,8 +2304,7 @@ async function refreshTabs({ selectStored = false } = {}) {
|
|
|
1300
2304
|
syncAgentDoneNotificationsFromTabs(tabs, previousTabs);
|
|
1301
2305
|
const stored = selectStored ? restoreStoredTabId() : null;
|
|
1302
2306
|
if (!activeTabId || !tabs.some((tab) => tab.id === activeTabId)) {
|
|
1303
|
-
|
|
1304
|
-
rememberActiveTab();
|
|
2307
|
+
setActiveTabId((stored && tabs.some((tab) => tab.id === stored) ? stored : tabs[0]?.id) || null, { remember: true });
|
|
1305
2308
|
}
|
|
1306
2309
|
renderTabs();
|
|
1307
2310
|
return tabs;
|
|
@@ -1313,15 +2316,14 @@ async function switchTab(tabId) {
|
|
|
1313
2316
|
setMobileTabsExpanded(false);
|
|
1314
2317
|
footerModelPickerOpen = false;
|
|
1315
2318
|
saveActiveDraft();
|
|
1316
|
-
|
|
1317
|
-
rememberActiveTab();
|
|
2319
|
+
const tabContext = setActiveTabId(tabId, { remember: true });
|
|
1318
2320
|
resetActiveTabUi();
|
|
1319
2321
|
renderTabs();
|
|
1320
2322
|
restoreActiveDraft();
|
|
1321
2323
|
focusPromptInput({ defer: true });
|
|
1322
|
-
connectEvents();
|
|
1323
|
-
await refreshAll();
|
|
1324
|
-
markTabOutputSeen();
|
|
2324
|
+
connectEvents(tabContext);
|
|
2325
|
+
await refreshAll(tabContext);
|
|
2326
|
+
if (isCurrentTabContext(tabContext)) markTabOutputSeen();
|
|
1325
2327
|
}
|
|
1326
2328
|
|
|
1327
2329
|
async function createTerminalTab(cwd = activeTab()?.cwd, { triggerButton = elements.newTabButton } = {}) {
|
|
@@ -1345,49 +2347,106 @@ async function createTerminalTab(cwd = activeTab()?.cwd, { triggerButton = eleme
|
|
|
1345
2347
|
}
|
|
1346
2348
|
}
|
|
1347
2349
|
|
|
1348
|
-
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1351
|
-
|
|
2350
|
+
function tabHasActiveAgent(tab) {
|
|
2351
|
+
const activity = activityForTab(tab);
|
|
2352
|
+
const indicator = tabIndicator(tab);
|
|
2353
|
+
return !!activity.isWorking || indicator.state === "working" || indicator.state === "blocked";
|
|
2354
|
+
}
|
|
1352
2355
|
|
|
1353
|
-
|
|
1354
|
-
const
|
|
2356
|
+
function confirmCloseTerminalTabs(targetTabs, label) {
|
|
2357
|
+
const count = targetTabs.length;
|
|
2358
|
+
const noun = count === 1 ? "tab" : "tabs";
|
|
2359
|
+
const activeAgentTabs = targetTabs.filter(tabHasActiveAgent);
|
|
2360
|
+
const tabList = targetTabs.map((tab) => `- ${tab.title}`).join("\n");
|
|
2361
|
+
const activeList = activeAgentTabs.map((tab) => `- ${tab.title} (${tabIndicator(tab).label})`).join("\n");
|
|
2362
|
+
const base = [
|
|
2363
|
+
`Close ${label || `${count} terminal ${noun}`}?`,
|
|
2364
|
+
"",
|
|
2365
|
+
`This terminates ${count === 1 ? "its isolated Pi process" : "their isolated Pi processes"}.`,
|
|
2366
|
+
count > 1 ? `\nTabs to close:\n${tabList}` : "",
|
|
2367
|
+
].filter(Boolean).join("\n");
|
|
2368
|
+
const warning = activeAgentTabs.length
|
|
2369
|
+
? [
|
|
2370
|
+
`WARNING: ${activeAgentTabs.length} ${activeAgentTabs.length === 1 ? "tab has an agent" : "tabs have agents"} still running or waiting for input:`,
|
|
2371
|
+
activeList,
|
|
2372
|
+
"",
|
|
2373
|
+
base,
|
|
2374
|
+
"",
|
|
2375
|
+
"Close anyway?",
|
|
2376
|
+
].join("\n")
|
|
2377
|
+
: base;
|
|
2378
|
+
return confirm(warning);
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
async function closeTerminalTabs(tabIds, { label = "selected terminal tabs" } = {}) {
|
|
2382
|
+
const targetIds = [...new Set(tabIds.filter(Boolean))];
|
|
2383
|
+
const targetTabs = targetIds.map((id) => tabs.find((item) => item.id === id)).filter(Boolean);
|
|
2384
|
+
if (!targetTabs.length) return;
|
|
2385
|
+
if (!confirmCloseTerminalTabs(targetTabs, label)) return;
|
|
2386
|
+
|
|
2387
|
+
const closedActiveTab = targetTabs.some((tab) => tab.id === activeTabId);
|
|
2388
|
+
const fallbackTabId = tabs.find((item) => !targetIds.includes(item.id))?.id || null;
|
|
1355
2389
|
try {
|
|
1356
|
-
if (
|
|
1357
|
-
const response = await api(
|
|
1358
|
-
|
|
2390
|
+
if (closedActiveTab) eventSource?.close();
|
|
2391
|
+
const response = await api("/api/tabs/close", { method: "POST", body: { ids: targetIds }, scoped: false });
|
|
2392
|
+
const closedIds = response.data?.closedIds || targetIds;
|
|
2393
|
+
tabs = response.data?.tabs || tabs.filter((item) => !closedIds.includes(item.id));
|
|
1359
2394
|
syncTabMetadata(tabs);
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
2395
|
+
for (const id of closedIds) {
|
|
2396
|
+
tabDrafts.delete(id);
|
|
2397
|
+
clearAttachments(id);
|
|
2398
|
+
}
|
|
2399
|
+
clearOpenTerminalTabGroup(null, { force: true });
|
|
2400
|
+
|
|
2401
|
+
const activeTabNeedsFallback = closedIds.includes(activeTabId) || !tabs.some((item) => item.id === activeTabId);
|
|
2402
|
+
if (activeTabNeedsFallback) {
|
|
2403
|
+
const tabContext = setActiveTabId((response.data?.activeTabId && tabs.some((item) => item.id === response.data.activeTabId)
|
|
2404
|
+
? response.data.activeTabId
|
|
2405
|
+
: (fallbackTabId && tabs.some((item) => item.id === fallbackTabId) ? fallbackTabId : tabs[0]?.id)) || null, { remember: true });
|
|
1364
2406
|
resetActiveTabUi();
|
|
1365
2407
|
renderTabs();
|
|
1366
2408
|
restoreActiveDraft();
|
|
1367
2409
|
focusPromptInput({ defer: true });
|
|
1368
|
-
connectEvents();
|
|
2410
|
+
connectEvents(tabContext);
|
|
1369
2411
|
if (activeTabId) {
|
|
1370
|
-
await refreshAll();
|
|
1371
|
-
markTabOutputSeen();
|
|
2412
|
+
await refreshAll(tabContext);
|
|
2413
|
+
if (isCurrentTabContext(tabContext)) markTabOutputSeen();
|
|
1372
2414
|
}
|
|
1373
2415
|
} else {
|
|
1374
2416
|
renderTabs();
|
|
1375
2417
|
}
|
|
2418
|
+
addEvent(`closed ${closedIds.length || targetTabs.length} terminal ${closedIds.length === 1 ? "tab" : "tabs"}`, "warn");
|
|
1376
2419
|
} catch (error) {
|
|
1377
2420
|
addEvent(error.message, "error");
|
|
1378
2421
|
}
|
|
1379
2422
|
}
|
|
1380
2423
|
|
|
2424
|
+
async function closeTerminalTab(tabId) {
|
|
2425
|
+
const tab = tabs.find((item) => item.id === tabId);
|
|
2426
|
+
if (!tab) return;
|
|
2427
|
+
await closeTerminalTabs([tabId], { label: tab.title });
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
async function closeTerminalTabGroup(group) {
|
|
2431
|
+
const title = tabGroupTitle(group.cwd, group.tabs[0]?.title || "cwd");
|
|
2432
|
+
await closeTerminalTabs(group.tabs.map((tab) => tab.id), { label: `${title} group` });
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
async function closeAllTerminalTabs() {
|
|
2436
|
+
await closeTerminalTabs(tabs.map((tab) => tab.id), { label: "all terminal tabs" });
|
|
2437
|
+
}
|
|
2438
|
+
|
|
1381
2439
|
async function initializeTabs() {
|
|
1382
2440
|
await refreshTabs({ selectStored: true });
|
|
1383
2441
|
resetActiveTabUi();
|
|
1384
2442
|
renderTabs();
|
|
1385
2443
|
restoreActiveDraft();
|
|
1386
2444
|
focusPromptInput({ defer: true });
|
|
1387
|
-
|
|
2445
|
+
const tabContext = activeTabContext();
|
|
2446
|
+
connectEvents(tabContext);
|
|
1388
2447
|
if (activeTabId) {
|
|
1389
|
-
await refreshAll();
|
|
1390
|
-
markTabOutputSeen();
|
|
2448
|
+
await refreshAll(tabContext);
|
|
2449
|
+
if (isCurrentTabContext(tabContext)) markTabOutputSeen();
|
|
1391
2450
|
}
|
|
1392
2451
|
}
|
|
1393
2452
|
|
|
@@ -1791,6 +2850,7 @@ function formatStatusEntry(key, value) {
|
|
|
1791
2850
|
const cleanKey = cleanStatusText(key);
|
|
1792
2851
|
const cleanValue = cleanStatusText(value);
|
|
1793
2852
|
if (!cleanValue) return "";
|
|
2853
|
+
if (cleanKey === "git-footer" && !isOptionalFeatureEnabled("gitFooterStatus")) return "";
|
|
1794
2854
|
if (cleanKey === "plan-mode") return `Plan: ${cleanValue}`;
|
|
1795
2855
|
if (cleanKey === "extension") return cleanValue;
|
|
1796
2856
|
return `${cleanKey}: ${cleanValue}`;
|
|
@@ -1939,16 +2999,20 @@ function setFooterModelPickerOpen(open) {
|
|
|
1939
2999
|
|
|
1940
3000
|
async function applyFooterModel(model) {
|
|
1941
3001
|
if (!model?.provider || !model?.id) return;
|
|
3002
|
+
const tabContext = activeTabContext();
|
|
1942
3003
|
try {
|
|
1943
3004
|
footerModelPickerOpen = false;
|
|
1944
|
-
await api("/api/model", { method: "POST", body: { provider: model.provider, modelId: model.id } });
|
|
1945
|
-
|
|
1946
|
-
await
|
|
3005
|
+
await api("/api/model", { method: "POST", body: { provider: model.provider, modelId: model.id }, tabId: tabContext.tabId });
|
|
3006
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
3007
|
+
await refreshState(tabContext);
|
|
3008
|
+
await refreshModels(tabContext);
|
|
1947
3009
|
} catch (error) {
|
|
1948
|
-
addEvent(error.message, "error");
|
|
3010
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
1949
3011
|
} finally {
|
|
1950
|
-
|
|
1951
|
-
|
|
3012
|
+
if (isCurrentTabContext(tabContext)) {
|
|
3013
|
+
document.body.classList.toggle("footer-model-picker-open", footerModelPickerOpen);
|
|
3014
|
+
renderFooter();
|
|
3015
|
+
}
|
|
1952
3016
|
}
|
|
1953
3017
|
}
|
|
1954
3018
|
|
|
@@ -2218,10 +3282,11 @@ function pickCwd(tab, initialCwd) {
|
|
|
2218
3282
|
async function changeActiveTabCwd() {
|
|
2219
3283
|
const tab = activeTab();
|
|
2220
3284
|
if (!tab) return;
|
|
3285
|
+
const tabContext = activeTabContext(tab.id);
|
|
2221
3286
|
|
|
2222
3287
|
const currentCwd = latestWorkspace?.cwd || tab.cwd || "";
|
|
2223
3288
|
const cwd = await pickCwd(tab, currentCwd);
|
|
2224
|
-
if (!cwd || cwd === currentCwd) return;
|
|
3289
|
+
if (!isCurrentTabContext(tabContext) || !cwd || cwd === currentCwd) return;
|
|
2225
3290
|
if (!window.confirm(`Restart ${tab.title} in:\n${cwd}\n\nCurrent in-flight work in this tab will be stopped.`)) return;
|
|
2226
3291
|
|
|
2227
3292
|
saveActiveDraft();
|
|
@@ -2229,16 +3294,21 @@ async function changeActiveTabCwd() {
|
|
|
2229
3294
|
const response = await api(`/api/tabs/${encodeURIComponent(tab.id)}`, { method: "PATCH", body: { cwd }, scoped: false });
|
|
2230
3295
|
tabs = response.data?.tabs || tabs;
|
|
2231
3296
|
syncTabMetadata(tabs);
|
|
2232
|
-
|
|
3297
|
+
if (!isCurrentTabContext(tabContext)) {
|
|
3298
|
+
renderTabs();
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
const nextContext = setActiveTabId(response.data?.tab?.id || activeTabId);
|
|
2233
3302
|
resetActiveTabUi();
|
|
2234
3303
|
renderTabs();
|
|
2235
3304
|
restoreActiveDraft();
|
|
2236
|
-
connectEvents();
|
|
2237
|
-
await refreshAll();
|
|
3305
|
+
connectEvents(nextContext);
|
|
3306
|
+
await refreshAll(nextContext);
|
|
3307
|
+
if (!isCurrentTabContext(nextContext)) return;
|
|
2238
3308
|
const changedCwd = response.data?.tab?.cwd || cwd;
|
|
2239
3309
|
addEvent(response.data?.changed === false ? `cwd unchanged: ${changedCwd}` : `changed ${tab.title} cwd to ${changedCwd}`, "info");
|
|
2240
3310
|
} catch (error) {
|
|
2241
|
-
addEvent(error.message, "error");
|
|
3311
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
2242
3312
|
}
|
|
2243
3313
|
}
|
|
2244
3314
|
|
|
@@ -2301,19 +3371,34 @@ function renderFooter() {
|
|
|
2301
3371
|
updateFooterModelPickerPosition();
|
|
2302
3372
|
}
|
|
2303
3373
|
|
|
2304
|
-
function scheduleRefreshMessages(delay = 120) {
|
|
3374
|
+
function scheduleRefreshMessages(delay = 120, tabContext = activeTabContext()) {
|
|
2305
3375
|
clearTimeout(refreshMessagesTimer);
|
|
2306
|
-
refreshMessagesTimer = setTimeout(() =>
|
|
3376
|
+
refreshMessagesTimer = setTimeout(() => {
|
|
3377
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
3378
|
+
refreshMessages(tabContext).catch((error) => {
|
|
3379
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
3380
|
+
});
|
|
3381
|
+
}, delay);
|
|
2307
3382
|
}
|
|
2308
3383
|
|
|
2309
|
-
function scheduleRefreshState(delay = 120) {
|
|
3384
|
+
function scheduleRefreshState(delay = 120, tabContext = activeTabContext()) {
|
|
2310
3385
|
clearTimeout(refreshStateTimer);
|
|
2311
|
-
refreshStateTimer = setTimeout(() =>
|
|
3386
|
+
refreshStateTimer = setTimeout(() => {
|
|
3387
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
3388
|
+
refreshState(tabContext).catch((error) => {
|
|
3389
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
3390
|
+
});
|
|
3391
|
+
}, delay);
|
|
2312
3392
|
}
|
|
2313
3393
|
|
|
2314
|
-
function scheduleRefreshFooter(delay = 300) {
|
|
3394
|
+
function scheduleRefreshFooter(delay = 300, tabContext = activeTabContext()) {
|
|
2315
3395
|
clearTimeout(refreshFooterTimer);
|
|
2316
|
-
refreshFooterTimer = setTimeout(() =>
|
|
3396
|
+
refreshFooterTimer = setTimeout(() => {
|
|
3397
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
3398
|
+
refreshFooterData(tabContext).catch((error) => {
|
|
3399
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
3400
|
+
});
|
|
3401
|
+
}, delay);
|
|
2317
3402
|
}
|
|
2318
3403
|
|
|
2319
3404
|
function renderStatus() {
|
|
@@ -2404,6 +3489,7 @@ function releaseDialogPromptParts(prompt) {
|
|
|
2404
3489
|
title: question,
|
|
2405
3490
|
message,
|
|
2406
3491
|
plainMessage: stripAnsi(message),
|
|
3492
|
+
featureId: isAurReleasePrompt ? "releaseAur" : "releaseNpm",
|
|
2407
3493
|
};
|
|
2408
3494
|
}
|
|
2409
3495
|
|
|
@@ -2435,6 +3521,7 @@ function renderReleaseDialogMessage(parent, text) {
|
|
|
2435
3521
|
}
|
|
2436
3522
|
|
|
2437
3523
|
function stripTodoProgressLines(text, { streaming = false } = {}) {
|
|
3524
|
+
if (!isOptionalFeatureEnabled("todoProgressWidget")) return String(text || "");
|
|
2438
3525
|
let inFence = false;
|
|
2439
3526
|
const kept = [];
|
|
2440
3527
|
const raw = String(text || "");
|
|
@@ -2558,13 +3645,17 @@ function appendReleaseNpmTerminalLine(parent, line) {
|
|
|
2558
3645
|
}
|
|
2559
3646
|
|
|
2560
3647
|
async function sendReleaseNpmCommand(command) {
|
|
3648
|
+
const tabContext = activeTabContext();
|
|
2561
3649
|
try {
|
|
2562
|
-
await api("/api/prompt", { method: "POST", body: { message: command }, tabId:
|
|
3650
|
+
await api("/api/prompt", { method: "POST", body: { message: command }, tabId: tabContext.tabId });
|
|
3651
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
2563
3652
|
addEvent(`${command} sent`, "info");
|
|
2564
|
-
scheduleRefreshState();
|
|
3653
|
+
scheduleRefreshState(120, tabContext);
|
|
2565
3654
|
} catch (error) {
|
|
2566
|
-
|
|
2567
|
-
|
|
3655
|
+
if (isCurrentTabContext(tabContext)) {
|
|
3656
|
+
addEvent(error.message, "error");
|
|
3657
|
+
addTransientMessage({ role: "error", title: command, content: error.message, level: "error" });
|
|
3658
|
+
}
|
|
2568
3659
|
}
|
|
2569
3660
|
}
|
|
2570
3661
|
|
|
@@ -2576,6 +3667,7 @@ function releaseNpmActionButton(label, command, className = "") {
|
|
|
2576
3667
|
}
|
|
2577
3668
|
|
|
2578
3669
|
function renderReleaseNpmOutputWidget() {
|
|
3670
|
+
if (!isOptionalFeatureEnabled("releaseNpm")) return null;
|
|
2579
3671
|
const outputLines = getWidgetLines("release-npm:output");
|
|
2580
3672
|
const footerLines = getWidgetLines("release-npm:footer");
|
|
2581
3673
|
if (outputLines.length === 0 && footerLines.length === 0) return null;
|
|
@@ -2613,6 +3705,7 @@ function renderReleaseNpmOutputWidget() {
|
|
|
2613
3705
|
}
|
|
2614
3706
|
|
|
2615
3707
|
function renderReleaseNpmLogWidget() {
|
|
3708
|
+
if (!isOptionalFeatureEnabled("releaseNpm")) return null;
|
|
2616
3709
|
const lines = getWidgetLines("release-npm:logs");
|
|
2617
3710
|
if (lines.length === 0) return null;
|
|
2618
3711
|
|
|
@@ -2640,6 +3733,7 @@ function renderReleaseNpmLogWidget() {
|
|
|
2640
3733
|
}
|
|
2641
3734
|
|
|
2642
3735
|
function renderReleaseAurOutputWidget() {
|
|
3736
|
+
if (!isOptionalFeatureEnabled("releaseAur")) return null;
|
|
2643
3737
|
const outputLines = getWidgetLines("release-aur:output");
|
|
2644
3738
|
const footerLines = getWidgetLines("release-aur:footer");
|
|
2645
3739
|
if (outputLines.length === 0 && footerLines.length === 0) return null;
|
|
@@ -2677,6 +3771,7 @@ function renderReleaseAurOutputWidget() {
|
|
|
2677
3771
|
}
|
|
2678
3772
|
|
|
2679
3773
|
function renderReleaseAurLogWidget() {
|
|
3774
|
+
if (!isOptionalFeatureEnabled("releaseAur")) return null;
|
|
2680
3775
|
const lines = getWidgetLines("release-aur:logs");
|
|
2681
3776
|
if (lines.length === 0) return null;
|
|
2682
3777
|
|
|
@@ -2714,11 +3809,12 @@ function renderWidgets() {
|
|
|
2714
3809
|
const releaseAurLog = renderReleaseAurLogWidget();
|
|
2715
3810
|
if (releaseAurLog) elements.widgetArea.append(releaseAurLog);
|
|
2716
3811
|
|
|
2717
|
-
const releaseWidgetKeys = new Set(["release-npm:output", "release-npm:footer", "release-npm:logs", "release-aur:output", "release-aur:footer", "release-aur:logs"]);
|
|
2718
3812
|
for (const [key, value] of widgets) {
|
|
2719
|
-
|
|
3813
|
+
const widgetFeatureId = optionalFeatureWidgetFeatureId(key);
|
|
3814
|
+
if (widgetFeatureId && !isOptionalFeatureEnabled(widgetFeatureId)) continue;
|
|
3815
|
+
if (widgetFeatureId && key !== "todo-progress") continue;
|
|
2720
3816
|
const lines = Array.isArray(value.widgetLines) ? value.widgetLines : [];
|
|
2721
|
-
const specialized = key === "todo-progress" ? renderTodoProgressWidget(key, lines) : null;
|
|
3817
|
+
const specialized = key === "todo-progress" && isOptionalFeatureEnabled("todoProgressWidget") ? renderTodoProgressWidget(key, lines) : null;
|
|
2722
3818
|
if (specialized) {
|
|
2723
3819
|
elements.widgetArea.append(specialized);
|
|
2724
3820
|
continue;
|
|
@@ -2866,6 +3962,14 @@ function failGitWorkflow(error, step = gitWorkflow.step) {
|
|
|
2866
3962
|
}
|
|
2867
3963
|
|
|
2868
3964
|
function startGitWorkflow() {
|
|
3965
|
+
if (!isOptionalFeatureEnabled("gitWorkflow")) {
|
|
3966
|
+
const tabContext = activeTabContext();
|
|
3967
|
+
addEvent(commandUnavailableMessage("git-staged-msg"), "warn");
|
|
3968
|
+
refreshCommands(tabContext).catch((error) => {
|
|
3969
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
3970
|
+
});
|
|
3971
|
+
return;
|
|
3972
|
+
}
|
|
2869
3973
|
if (gitWorkflow.active && !["done", "cancelled", "error"].includes(gitWorkflow.step) && !confirm("Restart the active git workflow?")) return;
|
|
2870
3974
|
gitWorkflow.runId += 1;
|
|
2871
3975
|
setGitWorkflow({
|
|
@@ -3010,6 +4114,287 @@ function appendText(parent, text, className = "text-block") {
|
|
|
3010
4114
|
return block;
|
|
3011
4115
|
}
|
|
3012
4116
|
|
|
4117
|
+
function safeMarkdownLinkHref(url) {
|
|
4118
|
+
const href = String(url || "").trim();
|
|
4119
|
+
if (!href || /[\u0000-\u001f\u007f]/.test(href)) return "";
|
|
4120
|
+
if (/^(?:https?:|mailto:)/i.test(href)) return href;
|
|
4121
|
+
if (/^(?:#|\/(?!\/)|\.\/|\.\.\/)/.test(href)) return href;
|
|
4122
|
+
return "";
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
function appendInlineMarkdown(parent, text, depth = 0) {
|
|
4126
|
+
const value = String(text || "");
|
|
4127
|
+
if (!value) return;
|
|
4128
|
+
if (depth > 6) {
|
|
4129
|
+
parent.append(document.createTextNode(value));
|
|
4130
|
+
return;
|
|
4131
|
+
}
|
|
4132
|
+
let index = 0;
|
|
4133
|
+
const appendPlain = (end) => {
|
|
4134
|
+
if (end > index) parent.append(document.createTextNode(value.slice(index, end)));
|
|
4135
|
+
index = end;
|
|
4136
|
+
};
|
|
4137
|
+
while (index < value.length) {
|
|
4138
|
+
if (value[index] === "`") {
|
|
4139
|
+
const end = value.indexOf("`", index + 1);
|
|
4140
|
+
if (end > index + 1) {
|
|
4141
|
+
const code = make("code", "markdown-inline-code", value.slice(index + 1, end));
|
|
4142
|
+
parent.append(code);
|
|
4143
|
+
index = end + 1;
|
|
4144
|
+
continue;
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
if (value[index] === "[") {
|
|
4148
|
+
const labelEnd = value.indexOf("](", index + 1);
|
|
4149
|
+
const linkEnd = labelEnd === -1 ? -1 : value.indexOf(")", labelEnd + 2);
|
|
4150
|
+
if (labelEnd !== -1 && linkEnd !== -1) {
|
|
4151
|
+
const label = value.slice(index + 1, labelEnd);
|
|
4152
|
+
const href = safeMarkdownLinkHref(value.slice(labelEnd + 2, linkEnd));
|
|
4153
|
+
if (href) {
|
|
4154
|
+
const link = make("a");
|
|
4155
|
+
link.href = href;
|
|
4156
|
+
if (/^https?:/i.test(href)) {
|
|
4157
|
+
link.target = "_blank";
|
|
4158
|
+
link.rel = "noopener noreferrer";
|
|
4159
|
+
}
|
|
4160
|
+
appendInlineMarkdown(link, label, depth + 1);
|
|
4161
|
+
parent.append(link);
|
|
4162
|
+
} else {
|
|
4163
|
+
parent.append(document.createTextNode(value.slice(index, linkEnd + 1)));
|
|
4164
|
+
}
|
|
4165
|
+
index = linkEnd + 1;
|
|
4166
|
+
continue;
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
const strongMarker = value.startsWith("**", index) ? "**" : value.startsWith("__", index) ? "__" : "";
|
|
4170
|
+
if (strongMarker) {
|
|
4171
|
+
const end = value.indexOf(strongMarker, index + 2);
|
|
4172
|
+
if (end > index + 2) {
|
|
4173
|
+
const strong = make("strong");
|
|
4174
|
+
appendInlineMarkdown(strong, value.slice(index + 2, end), depth + 1);
|
|
4175
|
+
parent.append(strong);
|
|
4176
|
+
index = end + 2;
|
|
4177
|
+
continue;
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
if (value.startsWith("~~", index)) {
|
|
4181
|
+
const end = value.indexOf("~~", index + 2);
|
|
4182
|
+
if (end > index + 2) {
|
|
4183
|
+
const del = make("del");
|
|
4184
|
+
appendInlineMarkdown(del, value.slice(index + 2, end), depth + 1);
|
|
4185
|
+
parent.append(del);
|
|
4186
|
+
index = end + 2;
|
|
4187
|
+
continue;
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
const emphasisMarker = value[index] === "*" || value[index] === "_" ? value[index] : "";
|
|
4191
|
+
if (emphasisMarker && value[index + 1] !== emphasisMarker) {
|
|
4192
|
+
const end = value.indexOf(emphasisMarker, index + 1);
|
|
4193
|
+
if (end > index + 1) {
|
|
4194
|
+
const em = make("em");
|
|
4195
|
+
appendInlineMarkdown(em, value.slice(index + 1, end), depth + 1);
|
|
4196
|
+
parent.append(em);
|
|
4197
|
+
index = end + 1;
|
|
4198
|
+
continue;
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
const nextSpecials = ["`", "[", "**", "__", "~~", "*", "_"]
|
|
4202
|
+
.map((marker) => value.indexOf(marker, index + 1))
|
|
4203
|
+
.filter((pos) => pos !== -1);
|
|
4204
|
+
appendPlain(nextSpecials.length ? Math.min(...nextSpecials) : value.length);
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
|
|
4208
|
+
function appendMarkdownParagraph(parent, lines) {
|
|
4209
|
+
const paragraph = make("p");
|
|
4210
|
+
lines.forEach((line, index) => {
|
|
4211
|
+
if (index > 0) paragraph.append(make("br"));
|
|
4212
|
+
appendInlineMarkdown(paragraph, line);
|
|
4213
|
+
});
|
|
4214
|
+
parent.append(paragraph);
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
function appendMarkdownCodeBlock(parent, code, language = "") {
|
|
4218
|
+
const wrapper = make("div", "markdown-code-block");
|
|
4219
|
+
if (language) wrapper.append(make("div", "markdown-code-language", language));
|
|
4220
|
+
const pre = make("pre", "code-block markdown-code");
|
|
4221
|
+
const codeNode = make("code", language ? `language-${language.replace(/[^a-z0-9_-]/gi, "")}` : "");
|
|
4222
|
+
codeNode.textContent = code.replace(/\n+$/g, "");
|
|
4223
|
+
pre.append(codeNode);
|
|
4224
|
+
wrapper.append(pre);
|
|
4225
|
+
parent.append(wrapper);
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
function markdownTableSeparator(line) {
|
|
4229
|
+
return /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line || "");
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
function splitMarkdownTableRow(line) {
|
|
4233
|
+
let row = String(line || "").trim();
|
|
4234
|
+
if (row.startsWith("|")) row = row.slice(1);
|
|
4235
|
+
if (row.endsWith("|")) row = row.slice(0, -1);
|
|
4236
|
+
return row.split(/(?<!\\)\|/).map((cell) => cell.replace(/\\\|/g, "|").trim());
|
|
4237
|
+
}
|
|
4238
|
+
|
|
4239
|
+
function appendMarkdownTable(parent, rows) {
|
|
4240
|
+
const wrapper = make("div", "markdown-table-wrapper");
|
|
4241
|
+
const table = make("table", "markdown-table");
|
|
4242
|
+
const thead = make("thead");
|
|
4243
|
+
const tbody = make("tbody");
|
|
4244
|
+
const headerRow = make("tr");
|
|
4245
|
+
for (const cell of rows[0] || []) {
|
|
4246
|
+
const th = make("th");
|
|
4247
|
+
appendInlineMarkdown(th, cell);
|
|
4248
|
+
headerRow.append(th);
|
|
4249
|
+
}
|
|
4250
|
+
thead.append(headerRow);
|
|
4251
|
+
for (const row of rows.slice(1)) {
|
|
4252
|
+
const tr = make("tr");
|
|
4253
|
+
for (const cell of row) {
|
|
4254
|
+
const td = make("td");
|
|
4255
|
+
appendInlineMarkdown(td, cell);
|
|
4256
|
+
tr.append(td);
|
|
4257
|
+
}
|
|
4258
|
+
tbody.append(tr);
|
|
4259
|
+
}
|
|
4260
|
+
table.append(thead, tbody);
|
|
4261
|
+
wrapper.append(table);
|
|
4262
|
+
parent.append(wrapper);
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
function markdownListMatch(line) {
|
|
4266
|
+
const unordered = line.match(/^\s{0,3}[-*+]\s+(.+)$/);
|
|
4267
|
+
if (unordered) return { ordered: false, text: unordered[1] };
|
|
4268
|
+
const ordered = line.match(/^\s{0,3}(\d+)[.)]\s+(.+)$/);
|
|
4269
|
+
if (ordered) return { ordered: true, start: Number(ordered[1]), text: ordered[2] };
|
|
4270
|
+
return null;
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
function appendMarkdownList(parent, items, ordered = false, start = null) {
|
|
4274
|
+
const list = make(ordered ? "ol" : "ul", "markdown-list");
|
|
4275
|
+
if (ordered && Number.isFinite(start) && start > 1) list.start = start;
|
|
4276
|
+
for (const itemText of items) {
|
|
4277
|
+
const li = make("li");
|
|
4278
|
+
const task = String(itemText).match(/^\[( |x|X|-)\]\s+(.+)$/);
|
|
4279
|
+
if (task) {
|
|
4280
|
+
li.classList.add("markdown-task-item");
|
|
4281
|
+
const checkbox = make("input", "markdown-task-checkbox");
|
|
4282
|
+
checkbox.type = "checkbox";
|
|
4283
|
+
checkbox.disabled = true;
|
|
4284
|
+
checkbox.checked = task[1].toLowerCase() === "x";
|
|
4285
|
+
li.append(checkbox);
|
|
4286
|
+
appendInlineMarkdown(li, task[2]);
|
|
4287
|
+
} else {
|
|
4288
|
+
appendInlineMarkdown(li, itemText);
|
|
4289
|
+
}
|
|
4290
|
+
list.append(li);
|
|
4291
|
+
}
|
|
4292
|
+
parent.append(list);
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4295
|
+
function renderMarkdownInto(parent, text) {
|
|
4296
|
+
const raw = String(text || "").replace(/\r\n?/g, "\n");
|
|
4297
|
+
const lines = raw.split("\n");
|
|
4298
|
+
let index = 0;
|
|
4299
|
+
let paragraph = [];
|
|
4300
|
+
const flushParagraph = () => {
|
|
4301
|
+
if (paragraph.length) appendMarkdownParagraph(parent, paragraph);
|
|
4302
|
+
paragraph = [];
|
|
4303
|
+
};
|
|
4304
|
+
|
|
4305
|
+
while (index < lines.length) {
|
|
4306
|
+
const line = lines[index];
|
|
4307
|
+
if (!line.trim()) {
|
|
4308
|
+
flushParagraph();
|
|
4309
|
+
index += 1;
|
|
4310
|
+
continue;
|
|
4311
|
+
}
|
|
4312
|
+
const fence = line.match(/^\s*```\s*([\w.+-]*)\s*$/);
|
|
4313
|
+
if (fence) {
|
|
4314
|
+
flushParagraph();
|
|
4315
|
+
const language = fence[1] || "";
|
|
4316
|
+
const codeLines = [];
|
|
4317
|
+
index += 1;
|
|
4318
|
+
while (index < lines.length && !/^\s*```\s*$/.test(lines[index])) {
|
|
4319
|
+
codeLines.push(lines[index]);
|
|
4320
|
+
index += 1;
|
|
4321
|
+
}
|
|
4322
|
+
if (index < lines.length) index += 1;
|
|
4323
|
+
appendMarkdownCodeBlock(parent, codeLines.join("\n"), language);
|
|
4324
|
+
continue;
|
|
4325
|
+
}
|
|
4326
|
+
if (markdownTableSeparator(lines[index + 1]) && line.includes("|")) {
|
|
4327
|
+
flushParagraph();
|
|
4328
|
+
const rows = [splitMarkdownTableRow(line)];
|
|
4329
|
+
index += 2;
|
|
4330
|
+
while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
|
|
4331
|
+
rows.push(splitMarkdownTableRow(lines[index]));
|
|
4332
|
+
index += 1;
|
|
4333
|
+
}
|
|
4334
|
+
appendMarkdownTable(parent, rows);
|
|
4335
|
+
continue;
|
|
4336
|
+
}
|
|
4337
|
+
const heading = line.match(/^\s{0,3}(#{1,6})\s+(.+?)\s*#*\s*$/);
|
|
4338
|
+
if (heading) {
|
|
4339
|
+
flushParagraph();
|
|
4340
|
+
const level = Math.min(6, heading[1].length);
|
|
4341
|
+
const node = make(`h${level}`, `markdown-heading markdown-heading-${level}`);
|
|
4342
|
+
appendInlineMarkdown(node, heading[2]);
|
|
4343
|
+
parent.append(node);
|
|
4344
|
+
index += 1;
|
|
4345
|
+
continue;
|
|
4346
|
+
}
|
|
4347
|
+
if (/^\s{0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
|
|
4348
|
+
flushParagraph();
|
|
4349
|
+
parent.append(make("hr", "markdown-rule"));
|
|
4350
|
+
index += 1;
|
|
4351
|
+
continue;
|
|
4352
|
+
}
|
|
4353
|
+
if (/^\s{0,3}>\s?/.test(line)) {
|
|
4354
|
+
flushParagraph();
|
|
4355
|
+
const quoteLines = [];
|
|
4356
|
+
while (index < lines.length && /^\s{0,3}>\s?/.test(lines[index])) {
|
|
4357
|
+
quoteLines.push(lines[index].replace(/^\s{0,3}>\s?/, ""));
|
|
4358
|
+
index += 1;
|
|
4359
|
+
}
|
|
4360
|
+
const quote = make("blockquote", "markdown-blockquote");
|
|
4361
|
+
renderMarkdownInto(quote, quoteLines.join("\n"));
|
|
4362
|
+
parent.append(quote);
|
|
4363
|
+
continue;
|
|
4364
|
+
}
|
|
4365
|
+
const listMatch = markdownListMatch(line);
|
|
4366
|
+
if (listMatch) {
|
|
4367
|
+
flushParagraph();
|
|
4368
|
+
const ordered = listMatch.ordered;
|
|
4369
|
+
const start = listMatch.start || null;
|
|
4370
|
+
const items = [];
|
|
4371
|
+
while (index < lines.length) {
|
|
4372
|
+
const item = markdownListMatch(lines[index]);
|
|
4373
|
+
if (!item || item.ordered !== ordered) break;
|
|
4374
|
+
items.push(item.text);
|
|
4375
|
+
index += 1;
|
|
4376
|
+
}
|
|
4377
|
+
appendMarkdownList(parent, items, ordered, start);
|
|
4378
|
+
continue;
|
|
4379
|
+
}
|
|
4380
|
+
paragraph.push(line);
|
|
4381
|
+
index += 1;
|
|
4382
|
+
}
|
|
4383
|
+
flushParagraph();
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
function appendMarkdown(parent, text) {
|
|
4387
|
+
const block = make("div", "markdown-body");
|
|
4388
|
+
renderMarkdownInto(block, text);
|
|
4389
|
+
parent.append(block);
|
|
4390
|
+
return block;
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
function renderMarkdown(block, text) {
|
|
4394
|
+
block.replaceChildren();
|
|
4395
|
+
renderMarkdownInto(block, text);
|
|
4396
|
+
}
|
|
4397
|
+
|
|
3013
4398
|
function appendImage(parent, part) {
|
|
3014
4399
|
const wrapper = make("div", "image-block");
|
|
3015
4400
|
const img = document.createElement("img");
|
|
@@ -3023,7 +4408,7 @@ function appendImage(parent, part) {
|
|
|
3023
4408
|
}
|
|
3024
4409
|
|
|
3025
4410
|
function isActionFeedbackMessage(message) {
|
|
3026
|
-
return message?.role === "assistant" || message?.role === "toolResult" || message?.role === "bashExecution";
|
|
4411
|
+
return message?.role === "assistant" || message?.role === "toolExecution" || message?.role === "toolResult" || message?.role === "bashExecution";
|
|
3027
4412
|
}
|
|
3028
4413
|
|
|
3029
4414
|
function truncateActionFeedbackText(text, limit = ACTION_FEEDBACK_SNIPPET_LIMIT) {
|
|
@@ -3038,6 +4423,7 @@ function actionFeedbackKey(message, messageIndex) {
|
|
|
3038
4423
|
messageIndex,
|
|
3039
4424
|
message?.role || "message",
|
|
3040
4425
|
message?.toolName || "",
|
|
4426
|
+
message?.toolCallId || "",
|
|
3041
4427
|
message?.command || "",
|
|
3042
4428
|
message?.timestamp || "",
|
|
3043
4429
|
].join("|");
|
|
@@ -3055,6 +4441,12 @@ function actionFeedbackSummary(message) {
|
|
|
3055
4441
|
snippet: truncateActionFeedbackText(`$ ${message.command || ""}\n\n${message.output || ""}`),
|
|
3056
4442
|
};
|
|
3057
4443
|
}
|
|
4444
|
+
if (message?.role === "toolExecution") {
|
|
4445
|
+
const result = toolExecutionResult(message);
|
|
4446
|
+
const args = message.arguments === undefined ? "" : JSON.stringify(message.arguments, null, 2);
|
|
4447
|
+
const output = toolResultText(result);
|
|
4448
|
+
return { kind: "action", title, snippet: truncateActionFeedbackText([args, output].filter(Boolean).join("\n\n")) };
|
|
4449
|
+
}
|
|
3058
4450
|
return { kind: "action", title, snippet: truncateActionFeedbackText(textFromContent(message?.content)) };
|
|
3059
4451
|
}
|
|
3060
4452
|
|
|
@@ -3083,9 +4475,10 @@ function actionFeedbackSteerMessage(item) {
|
|
|
3083
4475
|
}
|
|
3084
4476
|
|
|
3085
4477
|
async function sendLiveActionFeedback(item) {
|
|
4478
|
+
const tabContext = activeTabContext(item.tabId);
|
|
3086
4479
|
if (!isRunActive()) return;
|
|
3087
4480
|
await api("/api/steer", { method: "POST", body: { message: actionFeedbackSteerMessage(item) }, tabId: item.tabId });
|
|
3088
|
-
addEvent(`sent ${ACTION_FEEDBACK_REACTIONS[item.reaction]?.icon || "feedback"} action feedback as live steering`);
|
|
4481
|
+
if (isCurrentTabContext(tabContext)) addEvent(`sent ${ACTION_FEEDBACK_REACTIONS[item.reaction]?.icon || "feedback"} action feedback as live steering`);
|
|
3089
4482
|
}
|
|
3090
4483
|
|
|
3091
4484
|
function setActionFeedback(message, messageIndex, reaction) {
|
|
@@ -3167,13 +4560,13 @@ function isMissingActionFeedbackEndpoint(error) {
|
|
|
3167
4560
|
return error?.statusCode === 404 || /not found/i.test(error?.message || "");
|
|
3168
4561
|
}
|
|
3169
4562
|
|
|
3170
|
-
async function postQueuedFeedback(tabId, items) {
|
|
4563
|
+
async function postQueuedFeedback(tabId, items, tabContext = activeTabContext(tabId)) {
|
|
3171
4564
|
const feedback = items.map(serializeActionFeedback);
|
|
3172
4565
|
try {
|
|
3173
4566
|
await api("/api/action-feedback", { method: "POST", body: { feedback }, tabId });
|
|
3174
4567
|
} catch (error) {
|
|
3175
4568
|
if (!isMissingActionFeedbackEndpoint(error)) throw error;
|
|
3176
|
-
addEvent("/api/action-feedback not found; falling back to a normal prompt. Restart Web UI to use the dedicated endpoint.", "warn");
|
|
4569
|
+
if (isCurrentTabContext(tabContext)) addEvent("/api/action-feedback not found; falling back to a normal prompt. Restart Web UI to use the dedicated endpoint.", "warn");
|
|
3177
4570
|
await api("/api/prompt", { method: "POST", body: { message: formatActionFeedbackLearningPrompt(feedback) }, tabId });
|
|
3178
4571
|
}
|
|
3179
4572
|
}
|
|
@@ -3218,6 +4611,7 @@ function renderFeedbackTray() {
|
|
|
3218
4611
|
|
|
3219
4612
|
async function submitQueuedActionFeedback() {
|
|
3220
4613
|
const tabId = activeTabId;
|
|
4614
|
+
const tabContext = activeTabContext(tabId);
|
|
3221
4615
|
const items = queuedActionFeedback(tabId);
|
|
3222
4616
|
if (!tabId || items.length === 0 || actionFeedbackSendBusy) return;
|
|
3223
4617
|
if (isRunActive()) {
|
|
@@ -3231,28 +4625,32 @@ async function submitQueuedActionFeedback() {
|
|
|
3231
4625
|
setRunIndicatorActivity("Sending action feedback to Pi…");
|
|
3232
4626
|
renderFeedbackTray();
|
|
3233
4627
|
try {
|
|
3234
|
-
await postQueuedFeedback(tabId, items);
|
|
4628
|
+
await postQueuedFeedback(tabId, items, tabContext);
|
|
3235
4629
|
actionFeedbackByTab.get(tabId)?.clear();
|
|
4630
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
3236
4631
|
renderAllMessages({ preserveScroll: true });
|
|
3237
4632
|
addEvent("feedback sent; Pi will create a LEARNING");
|
|
3238
|
-
scheduleRefreshState();
|
|
3239
|
-
scheduleRefreshMessages();
|
|
3240
|
-
scheduleRefreshFooter();
|
|
4633
|
+
scheduleRefreshState(120, tabContext);
|
|
4634
|
+
scheduleRefreshMessages(120, tabContext);
|
|
4635
|
+
scheduleRefreshFooter(300, tabContext);
|
|
3241
4636
|
} catch (error) {
|
|
3242
4637
|
markTabIdleLocally(tabId);
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
4638
|
+
if (isCurrentTabContext(tabContext)) {
|
|
4639
|
+
clearRunIndicatorActivity();
|
|
4640
|
+
addEvent(error.message, "error");
|
|
4641
|
+
addTransientMessage({ role: "error", title: "feedback", content: error.message, level: "error" });
|
|
4642
|
+
}
|
|
3246
4643
|
} finally {
|
|
3247
4644
|
actionFeedbackSendBusy = false;
|
|
3248
4645
|
renderFeedbackTray();
|
|
3249
4646
|
}
|
|
3250
4647
|
}
|
|
3251
4648
|
|
|
3252
|
-
function renderContent(parent, content) {
|
|
4649
|
+
function renderContent(parent, content, { markdown = false } = {}) {
|
|
3253
4650
|
if (content === undefined || content === null) return;
|
|
3254
4651
|
if (typeof content === "string") {
|
|
3255
|
-
|
|
4652
|
+
if (markdown) appendMarkdown(parent, stripTodoProgressLines(content));
|
|
4653
|
+
else appendText(parent, content);
|
|
3256
4654
|
return;
|
|
3257
4655
|
}
|
|
3258
4656
|
if (!Array.isArray(content)) {
|
|
@@ -3266,8 +4664,11 @@ function renderContent(parent, content) {
|
|
|
3266
4664
|
continue;
|
|
3267
4665
|
}
|
|
3268
4666
|
if (part.type === "text") {
|
|
3269
|
-
|
|
4667
|
+
const text = assistantTextPartText(part);
|
|
4668
|
+
if (markdown) appendMarkdown(parent, stripTodoProgressLines(text));
|
|
4669
|
+
else appendText(parent, text);
|
|
3270
4670
|
} else if (part.type === "thinking") {
|
|
4671
|
+
if (!thinkingOutputVisible) continue;
|
|
3271
4672
|
const details = make("details", "thinking-block");
|
|
3272
4673
|
details.open = true;
|
|
3273
4674
|
details.append(make("summary", undefined, "thinking"));
|
|
@@ -3288,10 +4689,11 @@ function renderContent(parent, content) {
|
|
|
3288
4689
|
}
|
|
3289
4690
|
|
|
3290
4691
|
function messageTitle(message) {
|
|
3291
|
-
if (message.role === "assistant") return "
|
|
4692
|
+
if (message.role === "assistant") return message.title || "final output";
|
|
3292
4693
|
if (message.title) return message.title;
|
|
3293
4694
|
if (message.role === "thinking") return "thinking";
|
|
3294
4695
|
if (message.role === "toolCall") return `tool call: ${message.toolName || "unknown"}`;
|
|
4696
|
+
if (message.role === "toolExecution") return toolExecutionTitle(message);
|
|
3295
4697
|
if (message.role === "assistantEvent") return "assistant event";
|
|
3296
4698
|
if (message.role === "toolResult") return `tool result: ${message.toolName || "unknown"}`;
|
|
3297
4699
|
if (message.role === "bashExecution") return `bash: ${message.command || ""}`;
|
|
@@ -3306,6 +4708,14 @@ function assistantThinkingText(part) {
|
|
|
3306
4708
|
return typeof part.content === "string" ? part.content : "";
|
|
3307
4709
|
}
|
|
3308
4710
|
|
|
4711
|
+
function isAssistantToolCallPart(part) {
|
|
4712
|
+
return !!(part && typeof part === "object" && (part.type === "toolCall" || part.toolCall));
|
|
4713
|
+
}
|
|
4714
|
+
|
|
4715
|
+
function assistantHasToolCallAfter(content, index) {
|
|
4716
|
+
return Array.isArray(content) && content.slice(index + 1).some(isAssistantToolCallPart);
|
|
4717
|
+
}
|
|
4718
|
+
|
|
3309
4719
|
function assistantToolCallName(part) {
|
|
3310
4720
|
return String(part?.name || part?.toolName || part?.toolCall?.name || "unknown");
|
|
3311
4721
|
}
|
|
@@ -3314,13 +4724,26 @@ function assistantToolCallArguments(part) {
|
|
|
3314
4724
|
return part?.arguments || part?.args || part?.input || part?.toolCall?.arguments || {};
|
|
3315
4725
|
}
|
|
3316
4726
|
|
|
4727
|
+
function assistantTextPartText(part) {
|
|
4728
|
+
if (!part || typeof part !== "object" || part.type !== "text") return "";
|
|
4729
|
+
if (typeof part.text === "string") return part.text;
|
|
4730
|
+
return typeof part.content === "string" ? part.content : "";
|
|
4731
|
+
}
|
|
4732
|
+
|
|
4733
|
+
function isEmptyAssistantTextPart(part) {
|
|
4734
|
+
return !!(part && typeof part === "object" && part.type === "text" && !assistantTextPartText(part).trim());
|
|
4735
|
+
}
|
|
4736
|
+
|
|
3317
4737
|
function assistantFinalOutputPart(part) {
|
|
3318
4738
|
if (part === undefined || part === null) return null;
|
|
3319
4739
|
if (typeof part !== "object") {
|
|
3320
4740
|
const text = String(part);
|
|
3321
4741
|
return text.trim() ? { type: "text", text } : null;
|
|
3322
4742
|
}
|
|
3323
|
-
if (part.type === "text")
|
|
4743
|
+
if (part.type === "text") {
|
|
4744
|
+
const text = assistantTextPartText(part);
|
|
4745
|
+
return text.trim() ? { ...part, type: "text", text } : null;
|
|
4746
|
+
}
|
|
3324
4747
|
if (typeof part.text === "string") return part.text.trim() ? { ...part, type: "text", text: part.text } : null;
|
|
3325
4748
|
if (part.type === "image") return part;
|
|
3326
4749
|
if (typeof part.content === "string" && part.type !== "thinking" && part.type !== "toolCall" && typeof part.thinking !== "string") {
|
|
@@ -3334,39 +4757,42 @@ function assistantDisplayMessages(message) {
|
|
|
3334
4757
|
const base = { timestamp: message.timestamp };
|
|
3335
4758
|
const content = message.content;
|
|
3336
4759
|
if (typeof content === "string") {
|
|
3337
|
-
return content.trim() ? [{ ...message, title: "
|
|
4760
|
+
return content.trim() ? [{ ...message, title: "final output" }] : [];
|
|
3338
4761
|
}
|
|
3339
4762
|
if (!Array.isArray(content)) {
|
|
3340
|
-
return content === undefined || content === null ? [] : [{ ...message, title: "
|
|
4763
|
+
return content === undefined || content === null ? [] : [{ ...message, title: "final output" }];
|
|
3341
4764
|
}
|
|
3342
4765
|
|
|
3343
4766
|
const displayMessages = [];
|
|
3344
4767
|
const finalParts = [];
|
|
3345
|
-
for (
|
|
4768
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
4769
|
+
const part = content[index];
|
|
3346
4770
|
const isThinkingPart = part && typeof part === "object" && (part.type === "thinking" || typeof part.thinking === "string");
|
|
3347
4771
|
if (isThinkingPart) {
|
|
3348
4772
|
const thinking = assistantThinkingText(part) || "No thinking content was exposed by the provider.";
|
|
3349
4773
|
displayMessages.push({ ...base, role: "thinking", title: "thinking", content: thinking, thinking });
|
|
3350
4774
|
continue;
|
|
3351
4775
|
}
|
|
3352
|
-
if (part
|
|
4776
|
+
if (isAssistantToolCallPart(part)) {
|
|
3353
4777
|
const toolName = assistantToolCallName(part);
|
|
3354
4778
|
const args = assistantToolCallArguments(part);
|
|
3355
|
-
|
|
4779
|
+
const toolCallId = assistantToolCallId(part);
|
|
4780
|
+
displayMessages.push({ ...base, role: "toolCall", title: `tool call: ${toolName}`, toolName, toolCallId, arguments: args, content: args });
|
|
3356
4781
|
continue;
|
|
3357
4782
|
}
|
|
3358
4783
|
const finalPart = assistantFinalOutputPart(part);
|
|
3359
4784
|
if (finalPart) {
|
|
3360
|
-
finalParts.push(finalPart);
|
|
4785
|
+
if (!assistantHasToolCallAfter(content, index)) finalParts.push(finalPart);
|
|
3361
4786
|
continue;
|
|
3362
4787
|
}
|
|
4788
|
+
if (isEmptyAssistantTextPart(part)) continue;
|
|
3363
4789
|
if (part !== undefined && part !== null) {
|
|
3364
4790
|
displayMessages.push({ ...base, role: "assistantEvent", title: part?.type ? `assistant ${part.type}` : "assistant event", content: part });
|
|
3365
4791
|
}
|
|
3366
4792
|
}
|
|
3367
4793
|
|
|
3368
4794
|
if (finalParts.length > 0) {
|
|
3369
|
-
displayMessages.push({ ...message, title: "
|
|
4795
|
+
displayMessages.push({ ...message, title: "final output", content: finalParts });
|
|
3370
4796
|
}
|
|
3371
4797
|
return displayMessages;
|
|
3372
4798
|
}
|
|
@@ -3460,6 +4886,7 @@ function stickyUserPromptViewportGap() {
|
|
|
3460
4886
|
}
|
|
3461
4887
|
|
|
3462
4888
|
function resetChatOutput() {
|
|
4889
|
+
liveToolCards.clear();
|
|
3463
4890
|
elements.chat.replaceChildren();
|
|
3464
4891
|
if (elements.stickyUserPromptButton) elements.chat.append(elements.stickyUserPromptButton);
|
|
3465
4892
|
}
|
|
@@ -3520,6 +4947,422 @@ function updateStickyUserPromptButton() {
|
|
|
3520
4947
|
);
|
|
3521
4948
|
}
|
|
3522
4949
|
|
|
4950
|
+
function assistantToolCallId(part) {
|
|
4951
|
+
const id = part?.id || part?.toolCallId || part?.tool_call_id || part?.toolCall?.id || part?.toolCall?.toolCallId || part?.toolCall?.tool_call_id;
|
|
4952
|
+
return id === undefined || id === null ? "" : String(id);
|
|
4953
|
+
}
|
|
4954
|
+
|
|
4955
|
+
function toolResultCallId(message) {
|
|
4956
|
+
const id = message?.toolCallId || message?.tool_call_id;
|
|
4957
|
+
return id === undefined || id === null ? "" : String(id);
|
|
4958
|
+
}
|
|
4959
|
+
|
|
4960
|
+
function buildToolResultMap(messages = latestMessages) {
|
|
4961
|
+
const results = new Map();
|
|
4962
|
+
for (const message of messages || []) {
|
|
4963
|
+
if (message?.role !== "toolResult") continue;
|
|
4964
|
+
const id = toolResultCallId(message);
|
|
4965
|
+
if (id && !results.has(id)) results.set(id, message);
|
|
4966
|
+
}
|
|
4967
|
+
return results;
|
|
4968
|
+
}
|
|
4969
|
+
|
|
4970
|
+
function buildAssistantToolCallIdSet(messages = latestMessages) {
|
|
4971
|
+
const ids = new Set();
|
|
4972
|
+
for (const message of messages || []) {
|
|
4973
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
4974
|
+
for (const part of message.content) {
|
|
4975
|
+
if (!isAssistantToolCallPart(part)) continue;
|
|
4976
|
+
const id = assistantToolCallId(part);
|
|
4977
|
+
if (id) ids.add(id);
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
return ids;
|
|
4981
|
+
}
|
|
4982
|
+
|
|
4983
|
+
function toolResultForCallId(toolCallId, messages = latestMessages) {
|
|
4984
|
+
const id = String(toolCallId || "");
|
|
4985
|
+
if (!id) return null;
|
|
4986
|
+
for (const message of messages || []) {
|
|
4987
|
+
if (message?.role === "toolResult" && toolResultCallId(message) === id) return message;
|
|
4988
|
+
}
|
|
4989
|
+
return null;
|
|
4990
|
+
}
|
|
4991
|
+
|
|
4992
|
+
function cleanupLiveToolRunsForMessages(messages = latestMessages) {
|
|
4993
|
+
const results = buildToolResultMap(messages);
|
|
4994
|
+
for (const id of liveToolRuns.keys()) {
|
|
4995
|
+
if (results.has(id)) liveToolRuns.delete(id);
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4998
|
+
|
|
4999
|
+
function shortenToolPath(value, fallback = ".") {
|
|
5000
|
+
const path = normalizeDisplayPath(value || fallback);
|
|
5001
|
+
if (path.length <= 96) return path;
|
|
5002
|
+
return `…${path.slice(-95)}`;
|
|
5003
|
+
}
|
|
5004
|
+
|
|
5005
|
+
function toolArgValue(args, keys) {
|
|
5006
|
+
const keyList = Array.isArray(keys) ? keys : [keys];
|
|
5007
|
+
for (const key of keyList) {
|
|
5008
|
+
if (args && Object.prototype.hasOwnProperty.call(args, key)) return args[key];
|
|
5009
|
+
}
|
|
5010
|
+
return undefined;
|
|
5011
|
+
}
|
|
5012
|
+
|
|
5013
|
+
function toolArgText(args, keys, fallback = "") {
|
|
5014
|
+
const value = toolArgValue(args, keys);
|
|
5015
|
+
if (value === undefined || value === null) return fallback;
|
|
5016
|
+
if (typeof value === "string") return value;
|
|
5017
|
+
return String(value);
|
|
5018
|
+
}
|
|
5019
|
+
|
|
5020
|
+
function toolExecutionResult(message) {
|
|
5021
|
+
if (message?.result) return message.result;
|
|
5022
|
+
if (message?.partialResult) return { ...message.partialResult, isError: false };
|
|
5023
|
+
if (message?.role === "toolResult") return message;
|
|
5024
|
+
return null;
|
|
5025
|
+
}
|
|
5026
|
+
|
|
5027
|
+
function toolResultText(result) {
|
|
5028
|
+
if (!result) return "";
|
|
5029
|
+
return stripAnsi(textFromContent(result.content)).replace(/\s+$/g, "");
|
|
5030
|
+
}
|
|
5031
|
+
|
|
5032
|
+
function toolExecutionStatus(message) {
|
|
5033
|
+
const result = toolExecutionResult(message);
|
|
5034
|
+
if (message?.isPartial) return "running";
|
|
5035
|
+
if (!result) return "pending";
|
|
5036
|
+
return message?.isError || result?.isError ? "error" : "success";
|
|
5037
|
+
}
|
|
5038
|
+
|
|
5039
|
+
function toolExecutionTitle(message) {
|
|
5040
|
+
const name = runIndicatorToolName(message?.toolName || message?.name || "tool");
|
|
5041
|
+
const status = toolExecutionStatus(message);
|
|
5042
|
+
if (status === "running") return `tool: ${name} (running)`;
|
|
5043
|
+
if (status === "pending") return `tool: ${name} (pending)`;
|
|
5044
|
+
if (status === "error") return `tool: ${name} (failed)`;
|
|
5045
|
+
return `tool: ${name}`;
|
|
5046
|
+
}
|
|
5047
|
+
|
|
5048
|
+
function toolLineRange(args) {
|
|
5049
|
+
const offset = toolArgValue(args, "offset");
|
|
5050
|
+
const limit = toolArgValue(args, "limit");
|
|
5051
|
+
const start = Number.isFinite(Number(offset)) ? Number(offset) : null;
|
|
5052
|
+
const count = Number.isFinite(Number(limit)) ? Number(limit) : null;
|
|
5053
|
+
if (start === null && count === null) return "";
|
|
5054
|
+
const first = start ?? 1;
|
|
5055
|
+
const last = count === null ? "" : first + count - 1;
|
|
5056
|
+
return `:${first}${last ? `-${last}` : ""}`;
|
|
5057
|
+
}
|
|
5058
|
+
|
|
5059
|
+
function appendToolTitle(parent, name, subject = "", meta = []) {
|
|
5060
|
+
const line = make("div", "tool-title-line");
|
|
5061
|
+
line.append(make("span", "tool-name", name));
|
|
5062
|
+
if (subject) line.append(make("span", "tool-subject", subject));
|
|
5063
|
+
parent.append(line);
|
|
5064
|
+
const items = meta.filter(Boolean);
|
|
5065
|
+
if (items.length > 0) {
|
|
5066
|
+
const metaLine = make("div", "tool-meta-line");
|
|
5067
|
+
for (const item of items) metaLine.append(make("span", "tool-meta-pill", item));
|
|
5068
|
+
parent.append(metaLine);
|
|
5069
|
+
}
|
|
5070
|
+
}
|
|
5071
|
+
|
|
5072
|
+
function appendToolCommand(parent, command, meta = []) {
|
|
5073
|
+
const line = make("pre", "tool-command-line");
|
|
5074
|
+
line.textContent = `$ ${command || "..."}`;
|
|
5075
|
+
parent.append(line);
|
|
5076
|
+
const items = meta.filter(Boolean);
|
|
5077
|
+
if (items.length > 0) {
|
|
5078
|
+
const metaLine = make("div", "tool-meta-line");
|
|
5079
|
+
for (const item of items) metaLine.append(make("span", "tool-meta-pill", item));
|
|
5080
|
+
parent.append(metaLine);
|
|
5081
|
+
}
|
|
5082
|
+
}
|
|
5083
|
+
|
|
5084
|
+
function appendToolImages(parent, result) {
|
|
5085
|
+
if (!Array.isArray(result?.content)) return;
|
|
5086
|
+
for (const part of result.content) {
|
|
5087
|
+
if (part?.type === "image") appendImage(parent, part);
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
|
|
5091
|
+
function appendToolOutput(parent, text, { label = "output", previewLines = 10, previewFromEnd = false, open = false, emptyText = "" } = {}) {
|
|
5092
|
+
const clean = stripAnsi(text).replace(/\s+$/g, "");
|
|
5093
|
+
if (!clean) {
|
|
5094
|
+
if (emptyText) appendText(parent, emptyText, "code-block tool-output-code muted-output");
|
|
5095
|
+
return;
|
|
5096
|
+
}
|
|
5097
|
+
const lines = clean.split(/\r?\n/);
|
|
5098
|
+
if (lines.length > previewLines) {
|
|
5099
|
+
const details = make("details", "tool-output-details");
|
|
5100
|
+
details.open = open;
|
|
5101
|
+
details.append(make("summary", "tool-output-summary", `${label} (${lines.length} lines; expand)`));
|
|
5102
|
+
appendText(details, clean, "code-block tool-output-code");
|
|
5103
|
+
parent.append(details);
|
|
5104
|
+
|
|
5105
|
+
const preview = make("div", "tool-output-preview");
|
|
5106
|
+
const visibleLines = previewFromEnd ? lines.slice(-previewLines) : lines.slice(0, previewLines);
|
|
5107
|
+
const omitted = lines.length - visibleLines.length;
|
|
5108
|
+
const hint = previewFromEnd
|
|
5109
|
+
? `… ${omitted} earlier line${omitted === 1 ? "" : "s"}; expand for full output`
|
|
5110
|
+
: `… ${omitted} more line${omitted === 1 ? "" : "s"}; expand for full output`;
|
|
5111
|
+
appendText(preview, `${visibleLines.join("\n")}\n${hint}`, "code-block tool-output-code tool-output-preview-text");
|
|
5112
|
+
parent.append(preview);
|
|
5113
|
+
return;
|
|
5114
|
+
}
|
|
5115
|
+
appendText(parent, clean, "code-block tool-output-code");
|
|
5116
|
+
}
|
|
5117
|
+
|
|
5118
|
+
function appendToolWarnings(parent, details = {}) {
|
|
5119
|
+
const warnings = [];
|
|
5120
|
+
if (details.fullOutputPath) warnings.push(`Full output: ${details.fullOutputPath}`);
|
|
5121
|
+
const truncation = details.truncation;
|
|
5122
|
+
if (truncation?.truncated) {
|
|
5123
|
+
if (truncation.truncatedBy === "lines") warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
|
|
5124
|
+
else if (truncation.outputLines) warnings.push(`Truncated: ${truncation.outputLines} lines shown`);
|
|
5125
|
+
else warnings.push("Output truncated");
|
|
5126
|
+
}
|
|
5127
|
+
if (details.matchLimitReached) warnings.push(`Match limit reached: ${details.matchLimitReached}`);
|
|
5128
|
+
if (details.resultLimitReached) warnings.push(`Result limit reached: ${details.resultLimitReached}`);
|
|
5129
|
+
if (details.entryLimitReached) warnings.push(`Entry limit reached: ${details.entryLimitReached}`);
|
|
5130
|
+
if (warnings.length === 0) return;
|
|
5131
|
+
const box = make("div", "tool-warnings");
|
|
5132
|
+
for (const warning of warnings) box.append(make("div", "tool-warning", warning));
|
|
5133
|
+
parent.append(box);
|
|
5134
|
+
}
|
|
5135
|
+
|
|
5136
|
+
function appendToolDiff(parent, diff) {
|
|
5137
|
+
const value = String(diff || "").replace(/\s+$/g, "");
|
|
5138
|
+
if (!value) return false;
|
|
5139
|
+
const block = make("div", "tool-diff");
|
|
5140
|
+
for (const line of value.split(/\r?\n/)) {
|
|
5141
|
+
const cls = /^@@/.test(line)
|
|
5142
|
+
? "diff-hunk"
|
|
5143
|
+
: /^\+/.test(line) && !/^\+\+\+/.test(line)
|
|
5144
|
+
? "diff-added"
|
|
5145
|
+
: /^-/.test(line) && !/^---/.test(line)
|
|
5146
|
+
? "diff-removed"
|
|
5147
|
+
: /^(?:\+\+\+|---)/.test(line)
|
|
5148
|
+
? "diff-file"
|
|
5149
|
+
: "diff-context";
|
|
5150
|
+
block.append(make("div", cls, line || " "));
|
|
5151
|
+
}
|
|
5152
|
+
parent.append(block);
|
|
5153
|
+
return true;
|
|
5154
|
+
}
|
|
5155
|
+
|
|
5156
|
+
function normalizeToolExecution(message) {
|
|
5157
|
+
const result = toolExecutionResult(message);
|
|
5158
|
+
const args = message?.arguments ?? message?.args ?? {};
|
|
5159
|
+
const name = runIndicatorToolName(message?.toolName || message?.name || "tool");
|
|
5160
|
+
return {
|
|
5161
|
+
name,
|
|
5162
|
+
args,
|
|
5163
|
+
result,
|
|
5164
|
+
text: toolResultText(result),
|
|
5165
|
+
details: result?.details || message?.details || {},
|
|
5166
|
+
isPartial: !!message?.isPartial,
|
|
5167
|
+
isError: !!(message?.isError || result?.isError),
|
|
5168
|
+
startedAt: message?.startedAt || null,
|
|
5169
|
+
endedAt: message?.endedAt || null,
|
|
5170
|
+
};
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
function toolElapsedLabel(tool) {
|
|
5174
|
+
if (!tool.startedAt) return "";
|
|
5175
|
+
const end = tool.endedAt || Date.now();
|
|
5176
|
+
return `${tool.isPartial ? "elapsed" : "took"} ${formatDuration(end - tool.startedAt)}`;
|
|
5177
|
+
}
|
|
5178
|
+
|
|
5179
|
+
function toolStatusLabel(tool) {
|
|
5180
|
+
if (tool.isPartial) return "live";
|
|
5181
|
+
if (tool.isError) return "failed";
|
|
5182
|
+
if (tool.result) return "done";
|
|
5183
|
+
return "pending";
|
|
5184
|
+
}
|
|
5185
|
+
|
|
5186
|
+
function toolStateMeta(tool) {
|
|
5187
|
+
return [toolElapsedLabel(tool), toolStatusLabel(tool)];
|
|
5188
|
+
}
|
|
5189
|
+
|
|
5190
|
+
function toolLineCountLabel(text, label = "line") {
|
|
5191
|
+
const value = String(text || "").replace(/\s+$/g, "");
|
|
5192
|
+
if (!value) return "";
|
|
5193
|
+
const count = value.split(/\r?\n/).length;
|
|
5194
|
+
return `${count} ${label}${count === 1 ? "" : "s"}`;
|
|
5195
|
+
}
|
|
5196
|
+
|
|
5197
|
+
function toolRawDetailsReplacer(key, value) {
|
|
5198
|
+
if (typeof value === "string" && value.length > 4000) return `${value.slice(0, 4000)}… (${value.length - 4000} chars omitted)`;
|
|
5199
|
+
return value;
|
|
5200
|
+
}
|
|
5201
|
+
|
|
5202
|
+
function appendToolRawDetails(parent, tool) {
|
|
5203
|
+
const raw = JSON.stringify({ arguments: tool.args ?? {}, result: tool.result ?? null, details: tool.details ?? {} }, toolRawDetailsReplacer, 2);
|
|
5204
|
+
const details = make("details", "tool-raw-details");
|
|
5205
|
+
details.append(make("summary", "tool-raw-summary", "raw tool data"));
|
|
5206
|
+
appendText(details, raw, "code-block tool-raw-code");
|
|
5207
|
+
parent.append(details);
|
|
5208
|
+
}
|
|
5209
|
+
|
|
5210
|
+
function renderBashToolExecution(parent, tool) {
|
|
5211
|
+
const command = toolArgText(tool.args, "command", "");
|
|
5212
|
+
const timeout = toolArgValue(tool.args, "timeout");
|
|
5213
|
+
const meta = [timeout ? `timeout ${timeout}s` : "", ...toolStateMeta(tool)];
|
|
5214
|
+
appendToolCommand(parent, command, meta);
|
|
5215
|
+
appendToolOutput(parent, tool.text, { label: tool.isPartial ? "live output" : "output", previewLines: 5, previewFromEnd: true, open: tool.isError, emptyText: tool.isPartial ? "(no output yet)" : "" });
|
|
5216
|
+
appendToolWarnings(parent, tool.details);
|
|
5217
|
+
}
|
|
5218
|
+
|
|
5219
|
+
function renderReadToolExecution(parent, tool) {
|
|
5220
|
+
const path = toolArgText(tool.args, ["file_path", "path"], "");
|
|
5221
|
+
appendToolTitle(parent, "read", `${shortenToolPath(path)}${toolLineRange(tool.args)}`, [toolLineCountLabel(tool.text), ...toolStateMeta(tool)]);
|
|
5222
|
+
appendToolImages(parent, tool.result);
|
|
5223
|
+
appendToolOutput(parent, tool.text, { label: "file output", previewLines: 10, open: tool.isError });
|
|
5224
|
+
appendToolWarnings(parent, tool.details);
|
|
5225
|
+
}
|
|
5226
|
+
|
|
5227
|
+
function renderWriteToolExecution(parent, tool) {
|
|
5228
|
+
const path = toolArgText(tool.args, ["file_path", "path"], "");
|
|
5229
|
+
const content = toolArgText(tool.args, "content", "");
|
|
5230
|
+
const lineCount = content ? content.split(/\r?\n/).length : 0;
|
|
5231
|
+
appendToolTitle(parent, "write", shortenToolPath(path), [lineCount > 0 ? `${lineCount} line${lineCount === 1 ? "" : "s"}` : "", ...toolStateMeta(tool)]);
|
|
5232
|
+
appendToolOutput(parent, content, { label: "content", previewLines: 10 });
|
|
5233
|
+
appendToolOutput(parent, tool.text, { label: "result", previewLines: 6, open: tool.isError });
|
|
5234
|
+
}
|
|
5235
|
+
|
|
5236
|
+
function renderEditToolExecution(parent, tool) {
|
|
5237
|
+
const path = toolArgText(tool.args, ["file_path", "path"], "");
|
|
5238
|
+
const edits = Array.isArray(tool.args?.edits) ? tool.args.edits.length : 0;
|
|
5239
|
+
appendToolTitle(parent, "edit", shortenToolPath(path), [edits ? `${edits} replacement${edits === 1 ? "" : "s"}` : "", ...toolStateMeta(tool)]);
|
|
5240
|
+
const hasDiff = appendToolDiff(parent, tool.details?.diff || tool.details?.patch);
|
|
5241
|
+
appendToolOutput(parent, tool.text, { label: "result", previewLines: hasDiff ? 4 : 10, open: tool.isError });
|
|
5242
|
+
}
|
|
5243
|
+
|
|
5244
|
+
function renderGrepToolExecution(parent, tool) {
|
|
5245
|
+
const pattern = toolArgText(tool.args, "pattern", "");
|
|
5246
|
+
const path = toolArgText(tool.args, "path", ".");
|
|
5247
|
+
appendToolTitle(parent, "grep", `/${pattern || "…"}/ in ${shortenToolPath(path)}`, [tool.args?.glob ? `glob ${tool.args.glob}` : "", tool.args?.ignoreCase ? "ignore-case" : "", tool.args?.literal ? "literal" : "", toolLineCountLabel(tool.text, "match line"), ...toolStateMeta(tool)]);
|
|
5248
|
+
appendToolOutput(parent, tool.text, { label: "matches", previewLines: 10, open: tool.isError });
|
|
5249
|
+
appendToolWarnings(parent, tool.details);
|
|
5250
|
+
}
|
|
5251
|
+
|
|
5252
|
+
function renderFindToolExecution(parent, tool) {
|
|
5253
|
+
const pattern = toolArgText(tool.args, "pattern", "");
|
|
5254
|
+
const path = toolArgText(tool.args, "path", ".");
|
|
5255
|
+
appendToolTitle(parent, "find", `${pattern || "…"} in ${shortenToolPath(path)}`, [tool.args?.limit ? `limit ${tool.args.limit}` : "", toolLineCountLabel(tool.text, "result"), ...toolStateMeta(tool)]);
|
|
5256
|
+
appendToolOutput(parent, tool.text, { label: "results", previewLines: 10, open: tool.isError });
|
|
5257
|
+
appendToolWarnings(parent, tool.details);
|
|
5258
|
+
}
|
|
5259
|
+
|
|
5260
|
+
function renderLsToolExecution(parent, tool) {
|
|
5261
|
+
const path = toolArgText(tool.args, "path", ".");
|
|
5262
|
+
appendToolTitle(parent, "ls", shortenToolPath(path), [tool.args?.limit ? `limit ${tool.args.limit}` : "", toolLineCountLabel(tool.text, "entry"), ...toolStateMeta(tool)]);
|
|
5263
|
+
appendToolOutput(parent, tool.text, { label: "entries", previewLines: 20, open: tool.isError });
|
|
5264
|
+
appendToolWarnings(parent, tool.details);
|
|
5265
|
+
}
|
|
5266
|
+
|
|
5267
|
+
function renderGenericToolExecution(parent, tool) {
|
|
5268
|
+
appendToolTitle(parent, tool.name, "", toolStateMeta(tool));
|
|
5269
|
+
appendToolOutput(parent, JSON.stringify(tool.args ?? {}, null, 2), { label: "arguments", previewLines: 12 });
|
|
5270
|
+
appendToolImages(parent, tool.result);
|
|
5271
|
+
appendToolOutput(parent, tool.text, { label: "result", previewLines: 10, open: tool.isError });
|
|
5272
|
+
appendToolWarnings(parent, tool.details);
|
|
5273
|
+
}
|
|
5274
|
+
|
|
5275
|
+
const WEBUI_TOOL_RENDERERS = {
|
|
5276
|
+
bash: renderBashToolExecution,
|
|
5277
|
+
read: renderReadToolExecution,
|
|
5278
|
+
write: renderWriteToolExecution,
|
|
5279
|
+
edit: renderEditToolExecution,
|
|
5280
|
+
grep: renderGrepToolExecution,
|
|
5281
|
+
find: renderFindToolExecution,
|
|
5282
|
+
ls: renderLsToolExecution,
|
|
5283
|
+
};
|
|
5284
|
+
|
|
5285
|
+
function renderToolExecution(parent, message) {
|
|
5286
|
+
const tool = normalizeToolExecution(message);
|
|
5287
|
+
const renderer = WEBUI_TOOL_RENDERERS[tool.name] || renderGenericToolExecution;
|
|
5288
|
+
renderer(parent, tool);
|
|
5289
|
+
appendToolRawDetails(parent, tool);
|
|
5290
|
+
}
|
|
5291
|
+
|
|
5292
|
+
function liveToolRunMessage(run) {
|
|
5293
|
+
return {
|
|
5294
|
+
role: "toolExecution",
|
|
5295
|
+
title: toolExecutionTitle(run),
|
|
5296
|
+
toolName: run.toolName,
|
|
5297
|
+
toolCallId: run.toolCallId,
|
|
5298
|
+
arguments: run.arguments,
|
|
5299
|
+
result: run.result,
|
|
5300
|
+
isPartial: run.isPartial,
|
|
5301
|
+
isError: run.isError,
|
|
5302
|
+
startedAt: run.startedAt,
|
|
5303
|
+
endedAt: run.endedAt,
|
|
5304
|
+
timestamp: run.timestamp,
|
|
5305
|
+
live: true,
|
|
5306
|
+
};
|
|
5307
|
+
}
|
|
5308
|
+
|
|
5309
|
+
function renderLiveToolRun(run, { scroll = true } = {}) {
|
|
5310
|
+
if (!run?.toolCallId) return;
|
|
5311
|
+
const existing = liveToolCards.get(run.toolCallId);
|
|
5312
|
+
const shouldFollow = scroll && (autoFollowChat || isChatNearBottom());
|
|
5313
|
+
const created = appendMessage(liveToolRunMessage(run), { transient: true, animateEntry: !existing });
|
|
5314
|
+
if (existing?.isConnected && existing !== created.bubble) existing.replaceWith(created.bubble);
|
|
5315
|
+
renderRunIndicator({ scroll: false });
|
|
5316
|
+
if (shouldFollow) scrollChatToBottom();
|
|
5317
|
+
}
|
|
5318
|
+
|
|
5319
|
+
function upsertLiveToolRun(event, patch = {}) {
|
|
5320
|
+
const id = String(event.toolCallId || "");
|
|
5321
|
+
if (!id) return null;
|
|
5322
|
+
const existing = liveToolRuns.get(id) || {};
|
|
5323
|
+
const now = Date.now();
|
|
5324
|
+
const run = {
|
|
5325
|
+
...existing,
|
|
5326
|
+
role: "toolExecution",
|
|
5327
|
+
live: true,
|
|
5328
|
+
toolCallId: id,
|
|
5329
|
+
toolName: event.toolName || existing.toolName || "tool",
|
|
5330
|
+
arguments: event.args ?? existing.arguments ?? {},
|
|
5331
|
+
timestamp: existing.timestamp || now,
|
|
5332
|
+
startedAt: existing.startedAt || now,
|
|
5333
|
+
updatedAt: now,
|
|
5334
|
+
...patch,
|
|
5335
|
+
};
|
|
5336
|
+
liveToolRuns.set(id, run);
|
|
5337
|
+
return run;
|
|
5338
|
+
}
|
|
5339
|
+
|
|
5340
|
+
function handleToolExecutionStart(event) {
|
|
5341
|
+
const run = upsertLiveToolRun(event, { isPartial: true, isError: false });
|
|
5342
|
+
if (run) renderLiveToolRun(run);
|
|
5343
|
+
}
|
|
5344
|
+
|
|
5345
|
+
function handleToolExecutionUpdate(event) {
|
|
5346
|
+
const result = { ...(event.partialResult || {}), isError: false };
|
|
5347
|
+
const run = upsertLiveToolRun(event, { result, isPartial: true, isError: false });
|
|
5348
|
+
if (run) renderLiveToolRun(run, { scroll: false });
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5351
|
+
function handleToolExecutionEnd(event) {
|
|
5352
|
+
const result = { ...(event.result || {}), isError: !!event.isError };
|
|
5353
|
+
const run = upsertLiveToolRun(event, { result, isPartial: false, isError: !!event.isError, endedAt: Date.now() });
|
|
5354
|
+
if (run) renderLiveToolRun(run);
|
|
5355
|
+
}
|
|
5356
|
+
|
|
5357
|
+
function toolResultPreviewText(message, lineLimit = 10) {
|
|
5358
|
+
const text = textFromContent(message?.content).replace(/\s+$/g, "");
|
|
5359
|
+
if (!text) return "(empty tool result)";
|
|
5360
|
+
const lines = text.split(/\r?\n/);
|
|
5361
|
+
const preview = lines.slice(0, lineLimit).join("\n");
|
|
5362
|
+
const remaining = Math.max(0, lines.length - lineLimit);
|
|
5363
|
+
return remaining > 0 ? `${preview}\n… ${remaining} more line${remaining === 1 ? "" : "s"}; expand for full output` : preview;
|
|
5364
|
+
}
|
|
5365
|
+
|
|
3523
5366
|
function jumpToStickyUserPrompt() {
|
|
3524
5367
|
const button = elements.stickyUserPromptButton;
|
|
3525
5368
|
const index = Number(button?.dataset.messageIndex);
|
|
@@ -3534,16 +5377,27 @@ function jumpToStickyUserPrompt() {
|
|
|
3534
5377
|
requestAnimationFrame(updateStickyUserPromptButton);
|
|
3535
5378
|
}
|
|
3536
5379
|
|
|
3537
|
-
function appendMessage(message, { streaming = false, messageIndex = -1, transient = false } = {}) {
|
|
5380
|
+
function appendMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false } = {}) {
|
|
3538
5381
|
const role = String(message.role || "message");
|
|
3539
5382
|
const safeRole = role.replace(/[^a-z0-9_-]/gi, "");
|
|
3540
|
-
const bubble = make("article", `message ${safeRole}${message.level ? ` ${message.level}` : ""}${streaming ? " streaming" : ""}`);
|
|
5383
|
+
const bubble = make("article", `message ${safeRole}${message.level ? ` ${message.level}` : ""}${streaming ? " streaming" : ""}${animateEntry ? " action-enter" : ""}`);
|
|
5384
|
+
if (message.role === "toolExecution") {
|
|
5385
|
+
const status = toolExecutionStatus(message);
|
|
5386
|
+
bubble.classList.add(`tool-${status}`);
|
|
5387
|
+
if (message.isError || status === "error") bubble.classList.add("error");
|
|
5388
|
+
if (message.toolCallId) {
|
|
5389
|
+
bubble.dataset.toolCallId = String(message.toolCallId);
|
|
5390
|
+
if (message.live) liveToolCards.set(String(message.toolCallId), bubble);
|
|
5391
|
+
}
|
|
5392
|
+
}
|
|
3541
5393
|
if (!transient && messageIndex >= 0) {
|
|
3542
5394
|
bubble.dataset.messageIndex = String(messageIndex);
|
|
3543
5395
|
if (role === "user") bubble.dataset.userPrompt = "true";
|
|
3544
5396
|
}
|
|
3545
5397
|
const isCollapsibleOutput = !streaming && (message.role === "toolResult" || message.role === "bashExecution" || message.role === "compactionSummary");
|
|
3546
5398
|
|
|
5399
|
+
const hideMessageHeader = message.role === "assistant" && !isCollapsibleOutput;
|
|
5400
|
+
if (hideMessageHeader) bubble.setAttribute("aria-label", messageTitle(message));
|
|
3547
5401
|
const header = make(isCollapsibleOutput ? "summary" : "div", "message-header");
|
|
3548
5402
|
header.append(make("span", "message-role", messageTitle(message)));
|
|
3549
5403
|
header.append(make("span", "muted", formatDate(message.timestamp)));
|
|
@@ -3556,14 +5410,17 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
3556
5410
|
} else if (message.role === "toolResult") {
|
|
3557
5411
|
renderContent(body, message.content);
|
|
3558
5412
|
if (message.isError) bubble.classList.add("error");
|
|
5413
|
+
} else if (message.role === "toolExecution") {
|
|
5414
|
+
renderToolExecution(body, message);
|
|
3559
5415
|
} else if (message.role === "thinking") {
|
|
3560
|
-
|
|
5416
|
+
const thinkingText = message.thinking || textFromContent(message.content);
|
|
5417
|
+
if (thinkingOutputVisible && (thinkingText || !streaming)) appendText(body, thinkingText || "No thinking content was exposed by the provider.", "thinking-text");
|
|
3561
5418
|
} else if (message.role === "toolCall") {
|
|
3562
5419
|
appendText(body, JSON.stringify(message.arguments ?? message.content ?? {}, null, 2), "code-block");
|
|
3563
5420
|
} else if (message.role === "assistantEvent") {
|
|
3564
5421
|
appendText(body, typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? {}, null, 2), "code-block");
|
|
3565
5422
|
} else {
|
|
3566
|
-
renderContent(body, message.content);
|
|
5423
|
+
renderContent(body, message.content, { markdown: message.role === "assistant" });
|
|
3567
5424
|
}
|
|
3568
5425
|
|
|
3569
5426
|
if (isCollapsibleOutput) {
|
|
@@ -3571,6 +5428,13 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
3571
5428
|
if (message.isError) details.open = true;
|
|
3572
5429
|
details.append(header, body);
|
|
3573
5430
|
bubble.append(details);
|
|
5431
|
+
if (message.role === "toolResult" && !message.isError) {
|
|
5432
|
+
const preview = make("div", "tool-result-preview");
|
|
5433
|
+
appendText(preview, toolResultPreviewText(message, 10), "code-block tool-result-preview-text");
|
|
5434
|
+
bubble.append(preview);
|
|
5435
|
+
}
|
|
5436
|
+
} else if (hideMessageHeader) {
|
|
5437
|
+
bubble.append(body);
|
|
3574
5438
|
} else {
|
|
3575
5439
|
bubble.append(header, body);
|
|
3576
5440
|
}
|
|
@@ -3579,20 +5443,39 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
3579
5443
|
return { bubble, body };
|
|
3580
5444
|
}
|
|
3581
5445
|
|
|
3582
|
-
function appendTranscriptMessage(message, { streaming = false, messageIndex = -1, transient = false } = {}) {
|
|
5446
|
+
function appendTranscriptMessage(message, { streaming = false, messageIndex = -1, transient = false, animateEntry = false } = {}) {
|
|
3583
5447
|
if (streaming || transient || message?.role !== "assistant") {
|
|
3584
|
-
return appendMessage(message, { streaming, messageIndex, transient });
|
|
5448
|
+
return appendMessage(message, { streaming, messageIndex, transient, animateEntry });
|
|
3585
5449
|
}
|
|
3586
5450
|
|
|
3587
5451
|
let finalOutput = null;
|
|
3588
5452
|
const displayMessages = assistantDisplayMessages(message);
|
|
3589
5453
|
displayMessages.forEach((displayMessage) => {
|
|
3590
|
-
|
|
5454
|
+
let transcriptMessage = displayMessage;
|
|
5455
|
+
if (displayMessage.role === "toolCall" && displayMessage.toolCallId) {
|
|
5456
|
+
const result = toolResultForCallId(displayMessage.toolCallId);
|
|
5457
|
+
const liveRun = liveToolRuns.get(displayMessage.toolCallId);
|
|
5458
|
+
transcriptMessage = {
|
|
5459
|
+
...displayMessage,
|
|
5460
|
+
role: "toolExecution",
|
|
5461
|
+
title: `tool: ${displayMessage.toolName || "unknown"}`,
|
|
5462
|
+
arguments: liveRun?.arguments ?? displayMessage.arguments,
|
|
5463
|
+
result: result || liveRun?.result || null,
|
|
5464
|
+
isPartial: !result && !!liveRun?.isPartial,
|
|
5465
|
+
isError: !!(result?.isError || liveRun?.isError),
|
|
5466
|
+
startedAt: liveRun?.startedAt || null,
|
|
5467
|
+
endedAt: liveRun?.endedAt || null,
|
|
5468
|
+
live: !!liveRun && !result,
|
|
5469
|
+
};
|
|
5470
|
+
}
|
|
5471
|
+
if (transcriptMessage.role === "thinking" && !thinkingOutputVisible) return;
|
|
5472
|
+
const created = appendMessage(transcriptMessage, {
|
|
3591
5473
|
streaming: false,
|
|
3592
|
-
messageIndex:
|
|
5474
|
+
messageIndex: ["assistant", "toolExecution"].includes(transcriptMessage.role) ? messageIndex : -1,
|
|
3593
5475
|
transient: false,
|
|
5476
|
+
animateEntry: animateEntry && isActionTranscriptMessage(transcriptMessage),
|
|
3594
5477
|
});
|
|
3595
|
-
if (
|
|
5478
|
+
if (transcriptMessage.role === "assistant") finalOutput = created;
|
|
3596
5479
|
});
|
|
3597
5480
|
return finalOutput;
|
|
3598
5481
|
}
|
|
@@ -3610,25 +5493,29 @@ function clearRunIndicatorGraceCheck() {
|
|
|
3610
5493
|
runIndicatorGraceCheckTimer = null;
|
|
3611
5494
|
}
|
|
3612
5495
|
|
|
3613
|
-
function scheduleRunIndicatorGraceCheck() {
|
|
5496
|
+
function scheduleRunIndicatorGraceCheck(tabContext = activeTabContext()) {
|
|
3614
5497
|
if (!runIndicatorLocallyActive || stateHasRunIndicatorActivity(currentState) || !runIndicatorStartedAt) return;
|
|
3615
5498
|
const elapsedMs = performance.now() - runIndicatorStartedAt;
|
|
3616
5499
|
const delayMs = Math.max(120, RUN_INDICATOR_START_GRACE_MS - elapsedMs + 120);
|
|
3617
5500
|
clearRunIndicatorGraceCheck();
|
|
3618
5501
|
runIndicatorGraceCheckTimer = setTimeout(() => {
|
|
3619
5502
|
runIndicatorGraceCheckTimer = null;
|
|
3620
|
-
if (!runIndicatorLocallyActive || stateHasRunIndicatorActivity(currentState)) return;
|
|
5503
|
+
if (!isCurrentTabContext(tabContext) || !runIndicatorLocallyActive || stateHasRunIndicatorActivity(currentState)) return;
|
|
3621
5504
|
runIndicatorLastStateCheckAt = performance.now();
|
|
3622
|
-
refreshState().catch((error) =>
|
|
5505
|
+
refreshState(tabContext).catch((error) => {
|
|
5506
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5507
|
+
});
|
|
3623
5508
|
}, delayMs);
|
|
3624
5509
|
}
|
|
3625
5510
|
|
|
3626
|
-
function maybeRefreshRunIndicatorState() {
|
|
5511
|
+
function maybeRefreshRunIndicatorState(tabContext = activeTabContext()) {
|
|
3627
5512
|
if (!runIndicatorIsActive()) return;
|
|
3628
5513
|
const now = performance.now();
|
|
3629
5514
|
if (now - runIndicatorLastStateCheckAt < RUN_INDICATOR_STATE_RECHECK_MS) return;
|
|
3630
5515
|
runIndicatorLastStateCheckAt = now;
|
|
3631
|
-
refreshState().catch((error) =>
|
|
5516
|
+
refreshState(tabContext).catch((error) => {
|
|
5517
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5518
|
+
});
|
|
3632
5519
|
}
|
|
3633
5520
|
|
|
3634
5521
|
function formatRunIndicatorElapsed() {
|
|
@@ -3641,7 +5528,7 @@ function formatRunIndicatorElapsed() {
|
|
|
3641
5528
|
|
|
3642
5529
|
function runIndicatorHeadline() {
|
|
3643
5530
|
if (currentState?.isCompacting && !currentState?.isStreaming) return "Pi is compacting context:";
|
|
3644
|
-
return "Agent is
|
|
5531
|
+
return "Agent is running: ";
|
|
3645
5532
|
}
|
|
3646
5533
|
|
|
3647
5534
|
function runIndicatorShowsElapsed() {
|
|
@@ -3675,7 +5562,7 @@ function ensureRunIndicatorBubble() {
|
|
|
3675
5562
|
if (runIndicatorBubble?.parentElement !== elements.chat) {
|
|
3676
5563
|
runIndicatorBubble = make("article", "message runIndicator run-indicator-message streaming");
|
|
3677
5564
|
runIndicatorBubble.setAttribute("aria-live", "polite");
|
|
3678
|
-
runIndicatorBubble.setAttribute("aria-label", "Agent is
|
|
5565
|
+
runIndicatorBubble.setAttribute("aria-label", "Agent is running:");
|
|
3679
5566
|
|
|
3680
5567
|
const body = make("div", "message-body");
|
|
3681
5568
|
const row = make("div", "run-indicator-row");
|
|
@@ -3725,6 +5612,7 @@ function setRunIndicatorActivity(activity, { active = true, scroll = true } = {}
|
|
|
3725
5612
|
}
|
|
3726
5613
|
runIndicatorActivity = activity || runIndicatorActivity || "Waiting for output or action…";
|
|
3727
5614
|
renderRunIndicator({ scroll });
|
|
5615
|
+
updateComposerModeButtons();
|
|
3728
5616
|
if (active) scheduleRunIndicatorGraceCheck();
|
|
3729
5617
|
}
|
|
3730
5618
|
|
|
@@ -3735,6 +5623,7 @@ function clearRunIndicatorActivity({ render = true } = {}) {
|
|
|
3735
5623
|
runIndicatorStartedAt = null;
|
|
3736
5624
|
runIndicatorActivity = "Waiting for output or action…";
|
|
3737
5625
|
if (render) renderRunIndicator();
|
|
5626
|
+
updateComposerModeButtons();
|
|
3738
5627
|
}
|
|
3739
5628
|
|
|
3740
5629
|
function syncRunIndicatorFromState(state = currentState) {
|
|
@@ -3754,15 +5643,21 @@ function syncRunIndicatorFromState(state = currentState) {
|
|
|
3754
5643
|
} else {
|
|
3755
5644
|
renderRunIndicator();
|
|
3756
5645
|
}
|
|
5646
|
+
updateComposerModeButtons();
|
|
3757
5647
|
}
|
|
3758
5648
|
|
|
3759
5649
|
function runIndicatorToolName(name) {
|
|
3760
5650
|
return cleanStatusText(name || "tool") || "tool";
|
|
3761
5651
|
}
|
|
3762
5652
|
|
|
3763
|
-
function scheduleAbortStateChecks() {
|
|
5653
|
+
function scheduleAbortStateChecks(tabContext = activeTabContext()) {
|
|
3764
5654
|
for (const delay of [250, 900, 1800, 3600]) {
|
|
3765
|
-
setTimeout(() =>
|
|
5655
|
+
setTimeout(() => {
|
|
5656
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5657
|
+
refreshState(tabContext).catch((error) => {
|
|
5658
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5659
|
+
});
|
|
5660
|
+
}, delay);
|
|
3766
5661
|
}
|
|
3767
5662
|
}
|
|
3768
5663
|
|
|
@@ -3773,14 +5668,75 @@ function messageTimestampMs(message) {
|
|
|
3773
5668
|
return Number.isFinite(time) ? time : 0;
|
|
3774
5669
|
}
|
|
3775
5670
|
|
|
5671
|
+
function isActionTranscriptMessage(message) {
|
|
5672
|
+
return ["assistantEvent", "bashExecution", "toolCall", "toolExecution", "toolResult"].includes(message?.role);
|
|
5673
|
+
}
|
|
5674
|
+
|
|
5675
|
+
function assistantMessageHasActionContent(message) {
|
|
5676
|
+
return message?.role === "assistant" && Array.isArray(message.content) && message.content.some(isAssistantToolCallPart);
|
|
5677
|
+
}
|
|
5678
|
+
|
|
5679
|
+
function isActionEntryItem(item) {
|
|
5680
|
+
return isActionTranscriptMessage(item?.message) || assistantMessageHasActionContent(item?.message);
|
|
5681
|
+
}
|
|
5682
|
+
|
|
5683
|
+
function actionEntrySeenKeys(tabId = activeTabId) {
|
|
5684
|
+
if (!tabId) return new Set();
|
|
5685
|
+
let keys = actionEntrySeenKeysByTab.get(tabId);
|
|
5686
|
+
if (!keys) {
|
|
5687
|
+
keys = new Set();
|
|
5688
|
+
actionEntrySeenKeysByTab.set(tabId, keys);
|
|
5689
|
+
}
|
|
5690
|
+
return keys;
|
|
5691
|
+
}
|
|
5692
|
+
|
|
5693
|
+
function actionEntryKey(item) {
|
|
5694
|
+
const message = item?.message || {};
|
|
5695
|
+
return [
|
|
5696
|
+
item?.transient ? "transient" : "message",
|
|
5697
|
+
item?.messageIndex ?? -1,
|
|
5698
|
+
message.role || "message",
|
|
5699
|
+
message.toolName || "",
|
|
5700
|
+
message.toolCallId || "",
|
|
5701
|
+
message.command || "",
|
|
5702
|
+
message.title || "",
|
|
5703
|
+
message.timestamp || "",
|
|
5704
|
+
textFromContent(message.content).slice(0, 240),
|
|
5705
|
+
].join("|");
|
|
5706
|
+
}
|
|
5707
|
+
|
|
5708
|
+
function shouldAnimateActionEntry(item) {
|
|
5709
|
+
if (!activeTabId || !actionEntryAnimationPrimedTabs.has(activeTabId) || !isActionEntryItem(item)) return false;
|
|
5710
|
+
return !actionEntrySeenKeys(activeTabId).has(actionEntryKey(item));
|
|
5711
|
+
}
|
|
5712
|
+
|
|
5713
|
+
function rememberActionEntries(items) {
|
|
5714
|
+
if (!activeTabId) return;
|
|
5715
|
+
const keys = actionEntrySeenKeys(activeTabId);
|
|
5716
|
+
for (const item of items) {
|
|
5717
|
+
if (isActionEntryItem(item)) keys.add(actionEntryKey(item));
|
|
5718
|
+
}
|
|
5719
|
+
actionEntryAnimationPrimedTabs.add(activeTabId);
|
|
5720
|
+
}
|
|
5721
|
+
|
|
3776
5722
|
function orderedTranscriptItems() {
|
|
3777
5723
|
const items = [];
|
|
5724
|
+
const assistantToolCallIds = buildAssistantToolCallIdSet(latestMessages);
|
|
5725
|
+
const toolResults = buildToolResultMap(latestMessages);
|
|
3778
5726
|
latestMessages.forEach((message, index) => {
|
|
5727
|
+
const resultId = message?.role === "toolResult" ? toolResultCallId(message) : "";
|
|
5728
|
+
if (resultId && assistantToolCallIds.has(resultId)) return;
|
|
3779
5729
|
items.push({ message, messageIndex: index, transient: false, timestampMs: messageTimestampMs(message), order: index });
|
|
3780
5730
|
});
|
|
3781
5731
|
transientMessages.forEach((message, index) => {
|
|
3782
5732
|
items.push({ message, messageIndex: index, transient: true, timestampMs: messageTimestampMs(message), order: latestMessages.length + index });
|
|
3783
5733
|
});
|
|
5734
|
+
let liveOrder = latestMessages.length + transientMessages.length;
|
|
5735
|
+
for (const [toolCallId, run] of liveToolRuns.entries()) {
|
|
5736
|
+
if (assistantToolCallIds.has(toolCallId) || toolResults.has(toolCallId)) continue;
|
|
5737
|
+
const message = liveToolRunMessage(run);
|
|
5738
|
+
items.push({ message, messageIndex: -1, transient: true, timestampMs: messageTimestampMs(message), order: liveOrder++ });
|
|
5739
|
+
}
|
|
3784
5740
|
return items.sort((a, b) => a.timestampMs - b.timestampMs || a.order - b.order);
|
|
3785
5741
|
}
|
|
3786
5742
|
|
|
@@ -3788,9 +5744,15 @@ function renderAllMessages({ preserveScroll = false } = {}) {
|
|
|
3788
5744
|
const shouldFollow = !preserveScroll && (autoFollowChat || isChatNearBottom());
|
|
3789
5745
|
const previousScrollTop = elements.chat.scrollTop;
|
|
3790
5746
|
resetChatOutput();
|
|
3791
|
-
|
|
3792
|
-
|
|
5747
|
+
const transcriptItems = orderedTranscriptItems();
|
|
5748
|
+
for (const item of transcriptItems) {
|
|
5749
|
+
appendTranscriptMessage(item.message, {
|
|
5750
|
+
messageIndex: item.messageIndex,
|
|
5751
|
+
transient: item.transient,
|
|
5752
|
+
animateEntry: shouldAnimateActionEntry(item),
|
|
5753
|
+
});
|
|
3793
5754
|
}
|
|
5755
|
+
rememberActionEntries(transcriptItems);
|
|
3794
5756
|
renderRunIndicator({ scroll: false });
|
|
3795
5757
|
updateStickyUserPromptButton();
|
|
3796
5758
|
if (shouldFollow) scrollChatToBottom({ force: true });
|
|
@@ -3926,7 +5888,7 @@ function showComposerButtonTooltip(button) {
|
|
|
3926
5888
|
}
|
|
3927
5889
|
|
|
3928
5890
|
function sendPromptFromModeButton(kind, button) {
|
|
3929
|
-
if (!
|
|
5891
|
+
if (!hasComposerPayload()) {
|
|
3930
5892
|
showComposerButtonTooltip(button);
|
|
3931
5893
|
return;
|
|
3932
5894
|
}
|
|
@@ -3940,12 +5902,677 @@ function setPublishMenuOpen(open) {
|
|
|
3940
5902
|
elements.publishButton.parentElement?.classList.toggle("open", publishMenuOpen);
|
|
3941
5903
|
}
|
|
3942
5904
|
|
|
5905
|
+
function optionalFeatureIdForCommand(name) {
|
|
5906
|
+
if (OPTIONAL_COMMAND_FEATURES.has(name)) return OPTIONAL_COMMAND_FEATURES.get(name);
|
|
5907
|
+
if (name === "release-toggle" || name === "release-abort" || name === "release-npm-logs") return "releaseNpm";
|
|
5908
|
+
if (name === "release-aur" || name.startsWith("release-aur-")) return "releaseAur";
|
|
5909
|
+
if (name === "stats" || name.startsWith("stats-") || name === "calibrate") return "statsCommand";
|
|
5910
|
+
return null;
|
|
5911
|
+
}
|
|
5912
|
+
|
|
5913
|
+
function isCommandVisible(command) {
|
|
5914
|
+
if (HIDDEN_COMMAND_NAMES.has(command.name)) return false;
|
|
5915
|
+
const featureId = optionalFeatureIdForCommand(command.name);
|
|
5916
|
+
return !featureId || isOptionalFeatureEnabled(featureId);
|
|
5917
|
+
}
|
|
5918
|
+
|
|
5919
|
+
function visibleCommands() {
|
|
5920
|
+
return availableCommands.filter(isCommandVisible);
|
|
5921
|
+
}
|
|
5922
|
+
|
|
5923
|
+
function hasAvailableCommand(name) {
|
|
5924
|
+
return availableCommands.some((command) => command.name === name);
|
|
5925
|
+
}
|
|
5926
|
+
|
|
5927
|
+
function optionalFeatureUnavailableMessage(featureId) {
|
|
5928
|
+
const feature = OPTIONAL_FEATURE_BY_ID.get(featureId);
|
|
5929
|
+
if (!feature) return "Optional feature unavailable.";
|
|
5930
|
+
if (isOptionalFeatureDisabled(featureId)) return `${feature.label} is disabled in the Web UI optional-features panel.`;
|
|
5931
|
+
return `${feature.label} unavailable: ${feature.capabilityLabel} is not loaded. Install or enable ${feature.packageName}.`;
|
|
5932
|
+
}
|
|
5933
|
+
|
|
5934
|
+
function rememberOptionalControlDefault(button, key, value) {
|
|
5935
|
+
if (!(key in button.dataset)) button.dataset[key] = value || "";
|
|
5936
|
+
}
|
|
5937
|
+
|
|
5938
|
+
function setOptionalControlState(button, available, unavailableTitle) {
|
|
5939
|
+
if (!button) return;
|
|
5940
|
+
rememberOptionalControlDefault(button, "defaultTitle", button.getAttribute("title"));
|
|
5941
|
+
rememberOptionalControlDefault(button, "defaultAriaLabel", button.getAttribute("aria-label"));
|
|
5942
|
+
if (button.hasAttribute("data-tooltip")) rememberOptionalControlDefault(button, "defaultTooltip", button.getAttribute("data-tooltip"));
|
|
5943
|
+
|
|
5944
|
+
const nextTitle = available ? button.dataset.defaultTitle : unavailableTitle;
|
|
5945
|
+
const nextAriaLabel = available ? button.dataset.defaultAriaLabel : unavailableTitle;
|
|
5946
|
+
const nextTooltip = available ? button.dataset.defaultTooltip : unavailableTitle;
|
|
5947
|
+
|
|
5948
|
+
button.disabled = !available;
|
|
5949
|
+
button.setAttribute("aria-disabled", available ? "false" : "true");
|
|
5950
|
+
button.classList.toggle("feature-unavailable", !available);
|
|
5951
|
+
if (nextTitle) button.setAttribute("title", nextTitle);
|
|
5952
|
+
else button.removeAttribute("title");
|
|
5953
|
+
if (nextAriaLabel) button.setAttribute("aria-label", nextAriaLabel);
|
|
5954
|
+
else button.removeAttribute("aria-label");
|
|
5955
|
+
if (button.dataset.defaultTooltip !== undefined) {
|
|
5956
|
+
if (nextTooltip) button.setAttribute("data-tooltip", nextTooltip);
|
|
5957
|
+
else button.removeAttribute("data-tooltip");
|
|
5958
|
+
}
|
|
5959
|
+
}
|
|
5960
|
+
|
|
5961
|
+
function resetOptionalFeatureAvailability() {
|
|
5962
|
+
for (const key of Object.keys(optionalFeatureAvailability)) optionalFeatureAvailability[key] = false;
|
|
5963
|
+
optionalFeatureAvailability.themeBundle = availableThemes.length > 0;
|
|
5964
|
+
renderOptionalFeatureControls();
|
|
5965
|
+
}
|
|
5966
|
+
|
|
5967
|
+
function updateOptionalFeatureAvailability() {
|
|
5968
|
+
optionalFeatureAvailability.gitWorkflow = hasAvailableCommand("git-staged-msg");
|
|
5969
|
+
optionalFeatureAvailability.releaseNpm = hasAvailableCommand("release-npm");
|
|
5970
|
+
optionalFeatureAvailability.releaseAur = hasAvailableCommand("release-aur");
|
|
5971
|
+
optionalFeatureAvailability.statsCommand = hasAvailableCommand("stats");
|
|
5972
|
+
optionalFeatureAvailability.gitFooterStatus = hasAvailableCommand("git-footer-refresh") || optionalFeatureAvailability.gitFooterStatus || statusEntries.has("git-footer");
|
|
5973
|
+
optionalFeatureAvailability.todoProgressWidget = hasAvailableCommand("todo-progress-status") || optionalFeatureAvailability.todoProgressWidget || widgets.has("todo-progress");
|
|
5974
|
+
optionalFeatureAvailability.themeBundle = availableThemes.length > 0;
|
|
5975
|
+
renderOptionalFeatureControls();
|
|
5976
|
+
}
|
|
5977
|
+
|
|
5978
|
+
function optionalFeatureStatus(featureId) {
|
|
5979
|
+
const detected = isOptionalFeatureDetected(featureId);
|
|
5980
|
+
const disabled = isOptionalFeatureDisabled(featureId);
|
|
5981
|
+
if (detected && !disabled) return { label: "Enabled", className: "enabled", detail: "Detected and enabled in Web UI" };
|
|
5982
|
+
if (detected && disabled) return { label: "Disabled", className: "disabled", detail: "Detected, but disabled in Web UI" };
|
|
5983
|
+
return { label: "Install needed", className: "missing", detail: "Not detected in the active Pi tab" };
|
|
5984
|
+
}
|
|
5985
|
+
|
|
5986
|
+
function optionalFeatureWidgetFeatureId(key) {
|
|
5987
|
+
if (key.startsWith("release-npm:")) return "releaseNpm";
|
|
5988
|
+
if (key.startsWith("release-aur:")) return "releaseAur";
|
|
5989
|
+
if (key === "todo-progress") return "todoProgressWidget";
|
|
5990
|
+
return null;
|
|
5991
|
+
}
|
|
5992
|
+
|
|
5993
|
+
function renderOptionalFeaturePanel() {
|
|
5994
|
+
if (!elements.optionalFeaturesBox) return;
|
|
5995
|
+
elements.optionalFeaturesBox.replaceChildren();
|
|
5996
|
+
elements.optionalFeaturesBox.classList.remove("muted");
|
|
5997
|
+
|
|
5998
|
+
for (const feature of OPTIONAL_FEATURES) {
|
|
5999
|
+
const detected = isOptionalFeatureDetected(feature.id);
|
|
6000
|
+
const enabled = isOptionalFeatureEnabled(feature.id);
|
|
6001
|
+
const installing = optionalFeatureInstallInProgress.has(feature.id);
|
|
6002
|
+
const status = optionalFeatureStatus(feature.id);
|
|
6003
|
+
const row = make("div", `optional-feature-row ${status.className}`);
|
|
6004
|
+
|
|
6005
|
+
const main = make("div", "optional-feature-main");
|
|
6006
|
+
const title = make("div", "optional-feature-title");
|
|
6007
|
+
title.append(make("strong", undefined, feature.label), make("span", `optional-feature-pill ${status.className}`, status.label));
|
|
6008
|
+
const detail = make("div", "optional-feature-detail", `${status.detail} · checks ${feature.capabilityLabel}`);
|
|
6009
|
+
const description = make("div", "optional-feature-description", feature.description);
|
|
6010
|
+
const packageLine = make("code", "optional-feature-package", feature.packageName);
|
|
6011
|
+
main.append(title, detail, description, packageLine);
|
|
6012
|
+
|
|
6013
|
+
const action = make("button", "optional-feature-action");
|
|
6014
|
+
action.type = "button";
|
|
6015
|
+
action.disabled = installing;
|
|
6016
|
+
if (installing) {
|
|
6017
|
+
action.textContent = "Installing…";
|
|
6018
|
+
} else if (detected) {
|
|
6019
|
+
action.textContent = enabled ? "Disable" : "Enable";
|
|
6020
|
+
action.addEventListener("click", () => setOptionalFeatureDisabled(feature.id, enabled));
|
|
6021
|
+
} else {
|
|
6022
|
+
action.textContent = "Install…";
|
|
6023
|
+
action.classList.add("install");
|
|
6024
|
+
action.addEventListener("click", () => installOptionalFeature(feature.id));
|
|
6025
|
+
}
|
|
6026
|
+
|
|
6027
|
+
row.append(main, action);
|
|
6028
|
+
elements.optionalFeaturesBox.append(row);
|
|
6029
|
+
}
|
|
6030
|
+
}
|
|
6031
|
+
|
|
6032
|
+
function renderOptionalFeatureControls() {
|
|
6033
|
+
setOptionalControlState(
|
|
6034
|
+
elements.gitWorkflowButton,
|
|
6035
|
+
isOptionalFeatureEnabled("gitWorkflow"),
|
|
6036
|
+
optionalFeatureUnavailableMessage("gitWorkflow"),
|
|
6037
|
+
);
|
|
6038
|
+
|
|
6039
|
+
elements.releaseNpmButton.hidden = !isOptionalFeatureEnabled("releaseNpm");
|
|
6040
|
+
elements.releaseAurButton.hidden = !isOptionalFeatureEnabled("releaseAur");
|
|
6041
|
+
const hasPublishWorkflow = isOptionalFeatureEnabled("releaseNpm") || isOptionalFeatureEnabled("releaseAur");
|
|
6042
|
+
const publishContainer = elements.publishButton.parentElement;
|
|
6043
|
+
if (publishContainer) publishContainer.hidden = !hasPublishWorkflow;
|
|
6044
|
+
setOptionalControlState(
|
|
6045
|
+
elements.publishButton,
|
|
6046
|
+
hasPublishWorkflow,
|
|
6047
|
+
"Publish workflows unavailable: enable/install NPM Release and/or AUR Release in Optional features.",
|
|
6048
|
+
);
|
|
6049
|
+
if (!hasPublishWorkflow && publishMenuOpen) setPublishMenuOpen(false);
|
|
6050
|
+
|
|
6051
|
+
renderOptionalFeaturePanel();
|
|
6052
|
+
}
|
|
6053
|
+
|
|
6054
|
+
function commandUnavailableMessage(commandName) {
|
|
6055
|
+
const featureId = optionalFeatureIdForCommand(commandName);
|
|
6056
|
+
if (featureId) return optionalFeatureUnavailableMessage(featureId);
|
|
6057
|
+
return `Command unavailable: /${commandName} is not loaded in the active Pi tab.`;
|
|
6058
|
+
}
|
|
6059
|
+
|
|
6060
|
+
async function installOptionalFeature(featureId) {
|
|
6061
|
+
const feature = OPTIONAL_FEATURE_BY_ID.get(featureId);
|
|
6062
|
+
if (!feature || optionalFeatureInstallInProgress.has(featureId)) return;
|
|
6063
|
+
|
|
6064
|
+
const warning = [
|
|
6065
|
+
`Install optional feature: ${feature.label}?`,
|
|
6066
|
+
"",
|
|
6067
|
+
`This will run npm install for ${feature.packageName} in the Web UI package install root.`,
|
|
6068
|
+
"It can download code from npm and modify the local Pi/Web UI npm installation.",
|
|
6069
|
+
"If this feature is already installed but disabled in Pi settings, cancel and enable it there instead.",
|
|
6070
|
+
"",
|
|
6071
|
+
"Continue?",
|
|
6072
|
+
].join("\n");
|
|
6073
|
+
if (!confirm(warning)) return;
|
|
6074
|
+
|
|
6075
|
+
optionalFeatureInstallInProgress.add(featureId);
|
|
6076
|
+
renderOptionalFeatureControls();
|
|
6077
|
+
addEvent(`installing optional feature ${feature.label} (${feature.packageName})…`, "warn");
|
|
6078
|
+
try {
|
|
6079
|
+
const response = await api("/api/optional-feature-install", { method: "POST", body: { featureId }, scoped: false });
|
|
6080
|
+
disabledOptionalFeatures.delete(featureId);
|
|
6081
|
+
storeDisabledOptionalFeatures();
|
|
6082
|
+
addEvent(response.data?.message || `installed ${feature.packageName}`, "info");
|
|
6083
|
+
if (confirm(`${feature.label} install finished. Reload the active Pi tab now to enable newly loaded resources?`)) {
|
|
6084
|
+
sendPrompt("prompt", "/reload");
|
|
6085
|
+
} else {
|
|
6086
|
+
const tabContext = activeTabContext();
|
|
6087
|
+
await Promise.allSettled([refreshCommands(tabContext), initializeThemes()]);
|
|
6088
|
+
if (isCurrentTabContext(tabContext)) renderOptionalFeatureControls();
|
|
6089
|
+
}
|
|
6090
|
+
} catch (error) {
|
|
6091
|
+
addEvent(error.message || String(error), "error");
|
|
6092
|
+
} finally {
|
|
6093
|
+
optionalFeatureInstallInProgress.delete(featureId);
|
|
6094
|
+
renderOptionalFeatureControls();
|
|
6095
|
+
}
|
|
6096
|
+
}
|
|
6097
|
+
|
|
3943
6098
|
function runPublishWorkflow(command) {
|
|
3944
6099
|
setComposerActionsOpen(false);
|
|
3945
6100
|
setPublishMenuOpen(false);
|
|
6101
|
+
const commandName = String(command || "").replace(/^\//, "").split(/\s+/)[0];
|
|
6102
|
+
const featureId = OPTIONAL_COMMAND_FEATURES.get(commandName);
|
|
6103
|
+
if ((featureId && !isOptionalFeatureEnabled(featureId)) || !hasAvailableCommand(commandName)) {
|
|
6104
|
+
const tabContext = activeTabContext();
|
|
6105
|
+
addEvent(commandUnavailableMessage(commandName), "warn");
|
|
6106
|
+
refreshCommands(tabContext).catch((error) => {
|
|
6107
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
6108
|
+
});
|
|
6109
|
+
return;
|
|
6110
|
+
}
|
|
3946
6111
|
sendPrompt("prompt", command);
|
|
3947
6112
|
}
|
|
3948
6113
|
|
|
6114
|
+
function slashCommandName(message) {
|
|
6115
|
+
const match = String(message || "").trim().match(/^\/([^\s]+)$/);
|
|
6116
|
+
return match ? match[1] : "";
|
|
6117
|
+
}
|
|
6118
|
+
|
|
6119
|
+
function openNativeCommandDialog({ title, message = "", searchPlaceholder = "" } = {}) {
|
|
6120
|
+
nativeCommandTabId ||= activeTabId;
|
|
6121
|
+
elements.nativeCommandTitle.textContent = title || "Pi command";
|
|
6122
|
+
elements.nativeCommandMessage.textContent = message;
|
|
6123
|
+
elements.nativeCommandMessage.hidden = !message;
|
|
6124
|
+
elements.nativeCommandSearch.value = "";
|
|
6125
|
+
elements.nativeCommandSearch.placeholder = searchPlaceholder || "Filter choices…";
|
|
6126
|
+
elements.nativeCommandSearch.hidden = !searchPlaceholder;
|
|
6127
|
+
elements.nativeCommandSearch.oninput = null;
|
|
6128
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6129
|
+
elements.nativeCommandError.hidden = true;
|
|
6130
|
+
elements.nativeCommandError.textContent = "";
|
|
6131
|
+
elements.nativeCommandActions.replaceChildren();
|
|
6132
|
+
addNativeCommandAction("Cancel", closeNativeCommandDialog);
|
|
6133
|
+
if (!elements.nativeCommandDialog.open) elements.nativeCommandDialog.showModal();
|
|
6134
|
+
if (searchPlaceholder) queueMicrotask(() => elements.nativeCommandSearch.focus());
|
|
6135
|
+
}
|
|
6136
|
+
|
|
6137
|
+
function closeNativeCommandDialog() {
|
|
6138
|
+
if (elements.nativeCommandDialog.open) elements.nativeCommandDialog.close();
|
|
6139
|
+
elements.nativeCommandSearch.oninput = null;
|
|
6140
|
+
nativeCommandTabId = null;
|
|
6141
|
+
}
|
|
6142
|
+
|
|
6143
|
+
function nativeCommandApi(path, options = {}) {
|
|
6144
|
+
return api(path, { ...options, tabId: options.tabId || nativeCommandTabId || activeTabId });
|
|
6145
|
+
}
|
|
6146
|
+
|
|
6147
|
+
function setNativeCommandError(message) {
|
|
6148
|
+
elements.nativeCommandError.textContent = message || "";
|
|
6149
|
+
elements.nativeCommandError.hidden = !message;
|
|
6150
|
+
}
|
|
6151
|
+
|
|
6152
|
+
function addNativeCommandAction(label, handler, className) {
|
|
6153
|
+
const button = make("button", className, label);
|
|
6154
|
+
button.type = "button";
|
|
6155
|
+
button.addEventListener("click", handler);
|
|
6156
|
+
elements.nativeCommandActions.append(button);
|
|
6157
|
+
return button;
|
|
6158
|
+
}
|
|
6159
|
+
|
|
6160
|
+
function renderNativeLoading(label = "Loading…") {
|
|
6161
|
+
elements.nativeCommandBody.replaceChildren(make("div", "native-command-empty muted", label));
|
|
6162
|
+
}
|
|
6163
|
+
|
|
6164
|
+
function nativeSelectorMatches(item, query) {
|
|
6165
|
+
if (!query) return true;
|
|
6166
|
+
const needle = query.toLowerCase();
|
|
6167
|
+
return [item.label, item.description, item.meta, item.badge]
|
|
6168
|
+
.filter(Boolean)
|
|
6169
|
+
.some((value) => String(value).toLowerCase().includes(needle));
|
|
6170
|
+
}
|
|
6171
|
+
|
|
6172
|
+
function renderNativeSelectorItems(items, { emptyText = "No choices.", onSelect, activeId } = {}) {
|
|
6173
|
+
const query = elements.nativeCommandSearch.value.trim();
|
|
6174
|
+
const filtered = items.filter((item) => nativeSelectorMatches(item, query));
|
|
6175
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6176
|
+
if (!filtered.length) {
|
|
6177
|
+
elements.nativeCommandBody.append(make("div", "native-command-empty muted", emptyText));
|
|
6178
|
+
return;
|
|
6179
|
+
}
|
|
6180
|
+
const list = make("div", "native-selector-list");
|
|
6181
|
+
for (const item of filtered) {
|
|
6182
|
+
const button = make("button", `native-selector-item${item.id === activeId ? " active" : ""}`);
|
|
6183
|
+
button.type = "button";
|
|
6184
|
+
if (item.depth !== undefined) button.style.setProperty("--tree-depth", String(item.depth));
|
|
6185
|
+
button.disabled = item.disabled === true;
|
|
6186
|
+
button.addEventListener("click", () => onSelect?.(item));
|
|
6187
|
+
const title = make("span", "native-selector-title");
|
|
6188
|
+
title.append(make("strong", undefined, item.label || item.id || "choice"));
|
|
6189
|
+
if (item.badge) title.append(make("span", "native-selector-badge", item.badge));
|
|
6190
|
+
const detail = make("span", "native-selector-detail", item.description || "");
|
|
6191
|
+
const meta = make("span", "native-selector-meta", item.meta || "");
|
|
6192
|
+
button.append(title);
|
|
6193
|
+
if (item.description) button.append(detail);
|
|
6194
|
+
if (item.meta) button.append(meta);
|
|
6195
|
+
list.append(button);
|
|
6196
|
+
}
|
|
6197
|
+
elements.nativeCommandBody.append(list);
|
|
6198
|
+
}
|
|
6199
|
+
|
|
6200
|
+
function setNativeActionBusy(button, busy, label = "Working…") {
|
|
6201
|
+
if (!button) return;
|
|
6202
|
+
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent || "";
|
|
6203
|
+
button.disabled = busy;
|
|
6204
|
+
button.textContent = busy ? label : button.dataset.defaultLabel;
|
|
6205
|
+
}
|
|
6206
|
+
|
|
6207
|
+
function modelOptionLabel(model) {
|
|
6208
|
+
return `${model.provider}/${model.id}`;
|
|
6209
|
+
}
|
|
6210
|
+
|
|
6211
|
+
async function openNativeModelSelector() {
|
|
6212
|
+
openNativeCommandDialog({ title: "/model", message: "Select the active model for this Pi tab.", searchPlaceholder: "Filter models…" });
|
|
6213
|
+
renderNativeLoading("Loading models…");
|
|
6214
|
+
try {
|
|
6215
|
+
const response = await nativeCommandApi("/api/models");
|
|
6216
|
+
const models = Array.isArray(response.data?.models) ? response.data.models : [];
|
|
6217
|
+
const activeId = currentState?.model ? `${currentState.model.provider}/${currentState.model.id}` : "";
|
|
6218
|
+
const items = models.map((model) => ({
|
|
6219
|
+
id: modelOptionLabel(model),
|
|
6220
|
+
label: modelOptionLabel(model),
|
|
6221
|
+
description: model.name || model.description || "",
|
|
6222
|
+
meta: model.contextWindow ? `context ${model.contextWindow}` : model.provider,
|
|
6223
|
+
model,
|
|
6224
|
+
badge: modelOptionLabel(model) === activeId ? "current" : "",
|
|
6225
|
+
}));
|
|
6226
|
+
const render = () => renderNativeSelectorItems(items, {
|
|
6227
|
+
emptyText: "No models match this filter.",
|
|
6228
|
+
activeId,
|
|
6229
|
+
onSelect: async (item) => {
|
|
6230
|
+
setNativeCommandError("");
|
|
6231
|
+
try {
|
|
6232
|
+
await nativeCommandApi("/api/model", { method: "POST", body: { provider: item.model.provider, modelId: item.model.id } });
|
|
6233
|
+
addTransientMessage({ role: "native", title: "/model", content: `Model set to ${item.label}.`, level: "info" });
|
|
6234
|
+
closeNativeCommandDialog();
|
|
6235
|
+
await refreshState();
|
|
6236
|
+
} catch (error) {
|
|
6237
|
+
setNativeCommandError(error.message || String(error));
|
|
6238
|
+
}
|
|
6239
|
+
},
|
|
6240
|
+
});
|
|
6241
|
+
elements.nativeCommandSearch.oninput = render;
|
|
6242
|
+
render();
|
|
6243
|
+
} catch (error) {
|
|
6244
|
+
setNativeCommandError(error.message || String(error));
|
|
6245
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6246
|
+
}
|
|
6247
|
+
}
|
|
6248
|
+
|
|
6249
|
+
function openNativeThemeSelector() {
|
|
6250
|
+
openNativeCommandDialog({ title: "/theme", message: "Select the browser Web UI theme. Pi terminal theme changes remain native-TUI only.", searchPlaceholder: "Filter themes…" });
|
|
6251
|
+
const load = async () => {
|
|
6252
|
+
if (!availableThemes.length) await initializeThemes();
|
|
6253
|
+
const items = availableThemes.map((theme) => ({
|
|
6254
|
+
id: theme.name,
|
|
6255
|
+
label: theme.label || displayThemeName(theme.name) || theme.name,
|
|
6256
|
+
description: theme.name,
|
|
6257
|
+
meta: theme.author ? `by ${theme.author}` : "browser theme",
|
|
6258
|
+
theme,
|
|
6259
|
+
badge: theme.name === currentThemeName ? "current" : "",
|
|
6260
|
+
}));
|
|
6261
|
+
const render = () => renderNativeSelectorItems(items, {
|
|
6262
|
+
emptyText: "No themes match this filter.",
|
|
6263
|
+
activeId: currentThemeName,
|
|
6264
|
+
onSelect: async (item) => {
|
|
6265
|
+
try {
|
|
6266
|
+
await setThemeByName(item.theme.name, { persist: true, announce: true });
|
|
6267
|
+
addTransientMessage({ role: "native", title: "/theme", content: `Theme set to ${item.label}.`, level: "info" });
|
|
6268
|
+
closeNativeCommandDialog();
|
|
6269
|
+
} catch (error) {
|
|
6270
|
+
setNativeCommandError(error.message || String(error));
|
|
6271
|
+
}
|
|
6272
|
+
},
|
|
6273
|
+
});
|
|
6274
|
+
elements.nativeCommandSearch.oninput = render;
|
|
6275
|
+
render();
|
|
6276
|
+
};
|
|
6277
|
+
renderNativeLoading("Loading themes…");
|
|
6278
|
+
load().catch((error) => {
|
|
6279
|
+
setNativeCommandError(error.message || String(error));
|
|
6280
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6281
|
+
});
|
|
6282
|
+
}
|
|
6283
|
+
|
|
6284
|
+
function nativeSettingSelect(label, value, options) {
|
|
6285
|
+
const field = make("label", "native-settings-field");
|
|
6286
|
+
field.append(make("span", "native-settings-label", label));
|
|
6287
|
+
const select = make("select");
|
|
6288
|
+
for (const option of options) {
|
|
6289
|
+
const element = make("option", undefined, option.label || option.value);
|
|
6290
|
+
element.value = option.value;
|
|
6291
|
+
select.append(element);
|
|
6292
|
+
}
|
|
6293
|
+
select.value = value;
|
|
6294
|
+
field.append(select);
|
|
6295
|
+
return { field, select };
|
|
6296
|
+
}
|
|
6297
|
+
|
|
6298
|
+
function nativeSettingToggle(label, checked, hint) {
|
|
6299
|
+
const field = make("label", "native-settings-toggle");
|
|
6300
|
+
const input = make("input");
|
|
6301
|
+
input.type = "checkbox";
|
|
6302
|
+
input.checked = !!checked;
|
|
6303
|
+
const text = make("span");
|
|
6304
|
+
text.append(make("strong", undefined, label));
|
|
6305
|
+
if (hint) text.append(make("span", "native-settings-hint", hint));
|
|
6306
|
+
field.append(input, text);
|
|
6307
|
+
return { field, input };
|
|
6308
|
+
}
|
|
6309
|
+
|
|
6310
|
+
function openNativeSettingsDialog() {
|
|
6311
|
+
openNativeCommandDialog({ title: "/settings", message: "Quick Web UI settings for the active Pi tab." });
|
|
6312
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6313
|
+
const state = currentState || {};
|
|
6314
|
+
const body = make("div", "native-settings-grid");
|
|
6315
|
+
const thinking = nativeSettingSelect("Thinking level", state.thinkingLevel || "off", ["off", "minimal", "low", "medium", "high", "xhigh"].map((value) => ({ value })));
|
|
6316
|
+
const steering = nativeSettingSelect("Steering queue", state.steeringMode || "one-at-a-time", [
|
|
6317
|
+
{ value: "one-at-a-time", label: "one at a time" },
|
|
6318
|
+
{ value: "all", label: "all queued" },
|
|
6319
|
+
]);
|
|
6320
|
+
const followUp = nativeSettingSelect("Follow-up queue", state.followUpMode || "one-at-a-time", [
|
|
6321
|
+
{ value: "one-at-a-time", label: "one at a time" },
|
|
6322
|
+
{ value: "all", label: "all queued" },
|
|
6323
|
+
]);
|
|
6324
|
+
const autoCompact = nativeSettingToggle("Auto compaction", state.autoCompactionEnabled !== false, "Let Pi compact when context is nearly full.");
|
|
6325
|
+
const thinkingOutput = nativeSettingToggle("Show thinking output", thinkingOutputVisible, "Local browser transcript visibility.");
|
|
6326
|
+
const doneNotifications = nativeSettingToggle("Agent done notifications", agentDoneNotificationsEnabled, "Browser notification after background tab work completes.");
|
|
6327
|
+
const busyBehavior = nativeSettingSelect("Busy prompt behavior", elements.busyBehavior.value || "followUp", [
|
|
6328
|
+
{ value: "followUp", label: "follow-up" },
|
|
6329
|
+
{ value: "steer", label: "steer" },
|
|
6330
|
+
]);
|
|
6331
|
+
body.append(thinking.field, steering.field, followUp.field, busyBehavior.field, autoCompact.field, thinkingOutput.field, doneNotifications.field);
|
|
6332
|
+
elements.nativeCommandBody.append(body);
|
|
6333
|
+
elements.nativeCommandActions.replaceChildren();
|
|
6334
|
+
addNativeCommandAction("Model…", () => openNativeModelSelector());
|
|
6335
|
+
addNativeCommandAction("Theme…", () => openNativeThemeSelector());
|
|
6336
|
+
addNativeCommandAction("Cancel", closeNativeCommandDialog);
|
|
6337
|
+
const save = addNativeCommandAction("Apply", async () => {
|
|
6338
|
+
setNativeActionBusy(save, true, "Applying…");
|
|
6339
|
+
setNativeCommandError("");
|
|
6340
|
+
try {
|
|
6341
|
+
const requests = [];
|
|
6342
|
+
if (thinking.select.value !== state.thinkingLevel) requests.push(nativeCommandApi("/api/thinking", { method: "POST", body: { level: thinking.select.value } }));
|
|
6343
|
+
if (steering.select.value !== state.steeringMode) requests.push(nativeCommandApi("/api/steering-mode", { method: "POST", body: { mode: steering.select.value } }));
|
|
6344
|
+
if (followUp.select.value !== state.followUpMode) requests.push(nativeCommandApi("/api/follow-up-mode", { method: "POST", body: { mode: followUp.select.value } }));
|
|
6345
|
+
if (autoCompact.input.checked !== state.autoCompactionEnabled) requests.push(nativeCommandApi("/api/auto-compaction", { method: "POST", body: { enabled: autoCompact.input.checked } }));
|
|
6346
|
+
elements.busyBehavior.value = busyBehavior.select.value;
|
|
6347
|
+
if (thinkingOutput.input.checked !== thinkingOutputVisible) setThinkingOutputVisible(thinkingOutput.input.checked);
|
|
6348
|
+
if (doneNotifications.input.checked !== agentDoneNotificationsEnabled) await setAgentDoneNotificationsEnabled(doneNotifications.input.checked);
|
|
6349
|
+
await Promise.all(requests);
|
|
6350
|
+
addTransientMessage({ role: "native", title: "/settings", content: "Settings updated.", level: "info" });
|
|
6351
|
+
closeNativeCommandDialog();
|
|
6352
|
+
await refreshState();
|
|
6353
|
+
} catch (error) {
|
|
6354
|
+
setNativeCommandError(error.message || String(error));
|
|
6355
|
+
} finally {
|
|
6356
|
+
setNativeActionBusy(save, false);
|
|
6357
|
+
}
|
|
6358
|
+
}, "primary");
|
|
6359
|
+
}
|
|
6360
|
+
|
|
6361
|
+
async function openNativeForkSelector() {
|
|
6362
|
+
openNativeCommandDialog({ title: "/fork", message: "Choose a previous user message to fork before.", searchPlaceholder: "Filter fork points…" });
|
|
6363
|
+
renderNativeLoading("Loading fork points…");
|
|
6364
|
+
try {
|
|
6365
|
+
const response = await nativeCommandApi("/api/fork-messages");
|
|
6366
|
+
const items = (response.data?.messages || []).map((message, index) => ({
|
|
6367
|
+
id: message.entryId,
|
|
6368
|
+
label: `#${index + 1} user message`,
|
|
6369
|
+
description: message.text || "",
|
|
6370
|
+
meta: message.entryId,
|
|
6371
|
+
message,
|
|
6372
|
+
})).reverse();
|
|
6373
|
+
const render = () => renderNativeSelectorItems(items, {
|
|
6374
|
+
emptyText: "No user messages are available to fork from.",
|
|
6375
|
+
onSelect: async (item) => {
|
|
6376
|
+
setNativeCommandError("");
|
|
6377
|
+
try {
|
|
6378
|
+
const result = await nativeCommandApi("/api/fork", { method: "POST", body: { entryId: item.message.entryId } });
|
|
6379
|
+
applyResponseTab(result);
|
|
6380
|
+
const restoredText = result.data?.text || result.data?.result?.text || "";
|
|
6381
|
+
if (restoredText) {
|
|
6382
|
+
elements.promptInput.value = restoredText;
|
|
6383
|
+
resizePromptInput();
|
|
6384
|
+
focusPromptInput({ defer: true });
|
|
6385
|
+
}
|
|
6386
|
+
addTransientMessage({ role: "native", title: "/fork", content: result.data?.message || "Forked the current session.", level: "info" });
|
|
6387
|
+
closeNativeCommandDialog();
|
|
6388
|
+
await refreshAll();
|
|
6389
|
+
} catch (error) {
|
|
6390
|
+
setNativeCommandError(error.message || String(error));
|
|
6391
|
+
}
|
|
6392
|
+
},
|
|
6393
|
+
});
|
|
6394
|
+
elements.nativeCommandSearch.oninput = render;
|
|
6395
|
+
render();
|
|
6396
|
+
} catch (error) {
|
|
6397
|
+
setNativeCommandError(error.message || String(error));
|
|
6398
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
|
|
6402
|
+
function openNativeCloneDialog() {
|
|
6403
|
+
openNativeCommandDialog({ title: "/clone", message: "Duplicate the current session at the current position." });
|
|
6404
|
+
elements.nativeCommandBody.append(make("p", "native-command-note", "This creates a new forked session and switches this Web UI tab to it."));
|
|
6405
|
+
elements.nativeCommandActions.replaceChildren();
|
|
6406
|
+
addNativeCommandAction("Cancel", closeNativeCommandDialog);
|
|
6407
|
+
const clone = addNativeCommandAction("Clone session", async () => {
|
|
6408
|
+
setNativeActionBusy(clone, true, "Cloning…");
|
|
6409
|
+
try {
|
|
6410
|
+
const result = await nativeCommandApi("/api/clone", { method: "POST", body: {} });
|
|
6411
|
+
applyResponseTab(result);
|
|
6412
|
+
addTransientMessage({ role: "native", title: "/clone", content: result.data?.message || "Cloned the current session.", level: "info" });
|
|
6413
|
+
closeNativeCommandDialog();
|
|
6414
|
+
await refreshAll();
|
|
6415
|
+
} catch (error) {
|
|
6416
|
+
setNativeCommandError(error.message || String(error));
|
|
6417
|
+
} finally {
|
|
6418
|
+
setNativeActionBusy(clone, false);
|
|
6419
|
+
}
|
|
6420
|
+
}, "primary");
|
|
6421
|
+
}
|
|
6422
|
+
|
|
6423
|
+
async function openNativeResumeSelector(scope = "current") {
|
|
6424
|
+
openNativeCommandDialog({ title: "/resume", message: "Resume another persisted Pi session.", searchPlaceholder: "Filter sessions…" });
|
|
6425
|
+
renderNativeLoading("Loading sessions…");
|
|
6426
|
+
const selectedScope = scope === "all" ? "all" : "current";
|
|
6427
|
+
try {
|
|
6428
|
+
const response = await nativeCommandApi(`/api/sessions?scope=${encodeURIComponent(selectedScope)}`);
|
|
6429
|
+
const items = (response.data?.sessions || []).map((session) => ({
|
|
6430
|
+
id: session.path,
|
|
6431
|
+
label: session.name || session.firstMessage || session.id || session.path,
|
|
6432
|
+
description: session.firstMessage || "(no messages)",
|
|
6433
|
+
meta: `${session.cwd || "unknown cwd"} · ${session.messageCount || 0} messages · ${session.modified || "unknown time"}`,
|
|
6434
|
+
badge: session.current ? "current" : "",
|
|
6435
|
+
disabled: session.current,
|
|
6436
|
+
session,
|
|
6437
|
+
}));
|
|
6438
|
+
const render = () => renderNativeSelectorItems(items, {
|
|
6439
|
+
emptyText: selectedScope === "all" ? "No sessions match this filter." : "No sessions for this working directory match this filter.",
|
|
6440
|
+
onSelect: async (item) => {
|
|
6441
|
+
setNativeCommandError("");
|
|
6442
|
+
try {
|
|
6443
|
+
const result = await nativeCommandApi("/api/switch-session", { method: "POST", body: { sessionPath: item.session.path } });
|
|
6444
|
+
applyResponseTab(result);
|
|
6445
|
+
addTransientMessage({ role: "native", title: "/resume", content: result.data?.message || "Resumed selected session.", level: "info" });
|
|
6446
|
+
closeNativeCommandDialog();
|
|
6447
|
+
await refreshAll();
|
|
6448
|
+
} catch (error) {
|
|
6449
|
+
setNativeCommandError(error.message || String(error));
|
|
6450
|
+
}
|
|
6451
|
+
},
|
|
6452
|
+
});
|
|
6453
|
+
elements.nativeCommandSearch.oninput = render;
|
|
6454
|
+
elements.nativeCommandActions.replaceChildren();
|
|
6455
|
+
addNativeCommandAction(selectedScope === "all" ? "Current cwd" : "All sessions", () => openNativeResumeSelector(selectedScope === "all" ? "current" : "all"));
|
|
6456
|
+
addNativeCommandAction("Cancel", closeNativeCommandDialog);
|
|
6457
|
+
render();
|
|
6458
|
+
} catch (error) {
|
|
6459
|
+
setNativeCommandError(error.message || String(error));
|
|
6460
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6461
|
+
}
|
|
6462
|
+
}
|
|
6463
|
+
|
|
6464
|
+
async function openNativeTreeSelector() {
|
|
6465
|
+
openNativeCommandDialog({ title: "/tree", message: "Navigate the current session tree. Choosing a user message restores it into the editor.", searchPlaceholder: "Filter tree…" });
|
|
6466
|
+
renderNativeLoading("Loading session tree…");
|
|
6467
|
+
try {
|
|
6468
|
+
const response = await nativeCommandApi("/api/session-tree");
|
|
6469
|
+
const nodes = response.data?.nodes || [];
|
|
6470
|
+
const summarize = nativeSettingToggle("Summarize abandoned branch", false, "Optional; may call the active model before switching branches.");
|
|
6471
|
+
const labelField = make("label", "native-settings-field");
|
|
6472
|
+
labelField.append(make("span", "native-settings-label", "Optional label"));
|
|
6473
|
+
const labelInput = make("input", "dialog-input");
|
|
6474
|
+
labelInput.placeholder = "checkpoint label";
|
|
6475
|
+
labelField.append(labelInput);
|
|
6476
|
+
const options = make("div", "native-tree-options");
|
|
6477
|
+
options.append(summarize.field, labelField);
|
|
6478
|
+
const items = nodes.map((node) => ({
|
|
6479
|
+
id: node.id,
|
|
6480
|
+
label: `${node.title}${node.label ? ` · ${node.label}` : ""}`,
|
|
6481
|
+
description: node.text || "",
|
|
6482
|
+
meta: `${node.timestamp || ""}${node.childCount ? ` · ${node.childCount} child${node.childCount === 1 ? "" : "ren"}` : ""}`,
|
|
6483
|
+
badge: node.currentLeaf ? "leaf" : "",
|
|
6484
|
+
depth: node.depth || 0,
|
|
6485
|
+
node,
|
|
6486
|
+
}));
|
|
6487
|
+
const navigate = async (item) => {
|
|
6488
|
+
setNativeCommandError("");
|
|
6489
|
+
try {
|
|
6490
|
+
const result = await nativeCommandApi("/api/tree-navigate", {
|
|
6491
|
+
method: "POST",
|
|
6492
|
+
body: {
|
|
6493
|
+
entryId: item.node.id,
|
|
6494
|
+
summarize: summarize.input.checked,
|
|
6495
|
+
label: labelInput.value.trim() || undefined,
|
|
6496
|
+
},
|
|
6497
|
+
});
|
|
6498
|
+
applyResponseTab(result);
|
|
6499
|
+
addTransientMessage({ role: "native", title: "/tree", content: result.data?.message || "Navigated the session tree.", level: "info" });
|
|
6500
|
+
closeNativeCommandDialog();
|
|
6501
|
+
await refreshAll();
|
|
6502
|
+
} catch (error) {
|
|
6503
|
+
setNativeCommandError(error.message || String(error));
|
|
6504
|
+
}
|
|
6505
|
+
};
|
|
6506
|
+
const render = () => {
|
|
6507
|
+
renderNativeSelectorItems(items, { emptyText: "No session tree entries match this filter.", onSelect: navigate });
|
|
6508
|
+
elements.nativeCommandBody.prepend(options);
|
|
6509
|
+
};
|
|
6510
|
+
elements.nativeCommandSearch.oninput = render;
|
|
6511
|
+
render();
|
|
6512
|
+
} catch (error) {
|
|
6513
|
+
setNativeCommandError(error.message || String(error));
|
|
6514
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6515
|
+
}
|
|
6516
|
+
}
|
|
6517
|
+
|
|
6518
|
+
function openNativeScopedModelsInfo() {
|
|
6519
|
+
openNativeCommandDialog({ title: "/scoped-models", message: "Scoped model selection is available in the footer model picker." });
|
|
6520
|
+
elements.nativeCommandBody.append(make("p", "native-command-note", "Use the footer model chip to choose among scoped models. The full native scoped-models editor is still TUI-only."));
|
|
6521
|
+
}
|
|
6522
|
+
|
|
6523
|
+
function openNativeAuthInfo(mode) {
|
|
6524
|
+
const command = mode === "logout" ? "/logout" : "/login";
|
|
6525
|
+
openNativeCommandDialog({ title: command, message: "Provider credential entry is intentionally not implemented in the browser yet." });
|
|
6526
|
+
const note = [
|
|
6527
|
+
"Use native Pi TUI authentication for now, or configure provider credentials through environment variables or models.json.",
|
|
6528
|
+
"This avoids accepting or storing API keys in the Web UI until the credential flow has a dedicated security design.",
|
|
6529
|
+
].join("\n\n");
|
|
6530
|
+
elements.nativeCommandBody.append(make("p", "native-command-note", note));
|
|
6531
|
+
}
|
|
6532
|
+
|
|
6533
|
+
async function handleNativeSlashSelectorCommand(message, { usesPromptInput = false } = {}) {
|
|
6534
|
+
const name = slashCommandName(message);
|
|
6535
|
+
if (!NATIVE_SELECTOR_COMMANDS.has(name)) return false;
|
|
6536
|
+
setComposerActionsOpen(false);
|
|
6537
|
+
hideCommandSuggestions();
|
|
6538
|
+
if (usesPromptInput) {
|
|
6539
|
+
elements.promptInput.value = "";
|
|
6540
|
+
resizePromptInput();
|
|
6541
|
+
}
|
|
6542
|
+
switch (name) {
|
|
6543
|
+
case "model":
|
|
6544
|
+
await openNativeModelSelector();
|
|
6545
|
+
return true;
|
|
6546
|
+
case "settings":
|
|
6547
|
+
openNativeSettingsDialog();
|
|
6548
|
+
return true;
|
|
6549
|
+
case "theme":
|
|
6550
|
+
openNativeThemeSelector();
|
|
6551
|
+
return true;
|
|
6552
|
+
case "fork":
|
|
6553
|
+
await openNativeForkSelector();
|
|
6554
|
+
return true;
|
|
6555
|
+
case "clone":
|
|
6556
|
+
openNativeCloneDialog();
|
|
6557
|
+
return true;
|
|
6558
|
+
case "resume":
|
|
6559
|
+
await openNativeResumeSelector();
|
|
6560
|
+
return true;
|
|
6561
|
+
case "tree":
|
|
6562
|
+
await openNativeTreeSelector();
|
|
6563
|
+
return true;
|
|
6564
|
+
case "scoped-models":
|
|
6565
|
+
openNativeScopedModelsInfo();
|
|
6566
|
+
return true;
|
|
6567
|
+
case "login":
|
|
6568
|
+
case "logout":
|
|
6569
|
+
openNativeAuthInfo(name);
|
|
6570
|
+
return true;
|
|
6571
|
+
default:
|
|
6572
|
+
return false;
|
|
6573
|
+
}
|
|
6574
|
+
}
|
|
6575
|
+
|
|
3949
6576
|
function shouldSendPromptFromEnter(event) {
|
|
3950
6577
|
if (event.key !== "Enter" || event.shiftKey || event.isComposing) return false;
|
|
3951
6578
|
if (event.ctrlKey || event.metaKey) return true;
|
|
@@ -3954,6 +6581,7 @@ function shouldSendPromptFromEnter(event) {
|
|
|
3954
6581
|
|
|
3955
6582
|
function renderMessages(messages) {
|
|
3956
6583
|
latestMessages = messages || [];
|
|
6584
|
+
cleanupLiveToolRunsForMessages(latestMessages);
|
|
3957
6585
|
syncLastUserPromptFromMessages(latestMessages);
|
|
3958
6586
|
renderAllMessages();
|
|
3959
6587
|
renderFooter();
|
|
@@ -3965,7 +6593,13 @@ function cancelStreamBubbleHide() {
|
|
|
3965
6593
|
streamBubbleHideTimer = null;
|
|
3966
6594
|
}
|
|
3967
6595
|
|
|
6596
|
+
function cancelStreamingAssistantTextRender() {
|
|
6597
|
+
clearTimeout(streamTextRenderTimer);
|
|
6598
|
+
streamTextRenderTimer = null;
|
|
6599
|
+
}
|
|
6600
|
+
|
|
3968
6601
|
function removeStreamBubble() {
|
|
6602
|
+
cancelStreamingAssistantTextRender();
|
|
3969
6603
|
cancelStreamBubbleHide();
|
|
3970
6604
|
streamBubble?.remove();
|
|
3971
6605
|
streamBubble = null;
|
|
@@ -3986,37 +6620,65 @@ function scheduleStreamBubbleHide() {
|
|
|
3986
6620
|
}, delayMs);
|
|
3987
6621
|
}
|
|
3988
6622
|
|
|
6623
|
+
function renderStreamingAssistantText() {
|
|
6624
|
+
const assistantText = stripTodoProgressLines(streamRawText, { streaming: true });
|
|
6625
|
+
if (assistantText) {
|
|
6626
|
+
ensureStreamBubble();
|
|
6627
|
+
renderMarkdown(streamText, assistantText);
|
|
6628
|
+
} else {
|
|
6629
|
+
scheduleStreamBubbleHide();
|
|
6630
|
+
}
|
|
6631
|
+
}
|
|
6632
|
+
|
|
6633
|
+
function scheduleStreamingAssistantTextRender() {
|
|
6634
|
+
if (streamTextRenderTimer) return;
|
|
6635
|
+
streamTextRenderTimer = setTimeout(() => {
|
|
6636
|
+
streamTextRenderTimer = null;
|
|
6637
|
+
renderStreamingAssistantText();
|
|
6638
|
+
}, STREAM_OUTPUT_TOOLCALL_GUARD_MS);
|
|
6639
|
+
}
|
|
6640
|
+
|
|
6641
|
+
function suppressStreamingAssistantTextBeforeToolCall() {
|
|
6642
|
+
streamRawText = "";
|
|
6643
|
+
removeStreamBubble();
|
|
6644
|
+
}
|
|
6645
|
+
|
|
3989
6646
|
function ensureStreamBubble() {
|
|
3990
6647
|
cancelStreamBubbleHide();
|
|
3991
|
-
if (streamBubble) return;
|
|
3992
|
-
const created = appendMessage({ role: "assistant", title: "
|
|
6648
|
+
if (streamBubble?.parentElement === elements.chat) return;
|
|
6649
|
+
const created = appendMessage({ role: "assistant", title: "final output", timestamp: Date.now(), content: "" }, { streaming: true });
|
|
3993
6650
|
streamBubble = created.bubble;
|
|
3994
|
-
streamText =
|
|
6651
|
+
streamText = make("div", "markdown-body streaming-markdown");
|
|
6652
|
+
created.body.append(streamText);
|
|
3995
6653
|
streamBubbleVisibleSince = performance.now();
|
|
3996
6654
|
renderRunIndicator({ scroll: false });
|
|
3997
6655
|
scrollChatToBottom();
|
|
3998
6656
|
}
|
|
3999
6657
|
|
|
4000
6658
|
function ensureStreamingThinkingBubble() {
|
|
4001
|
-
if (
|
|
6659
|
+
if (!thinkingOutputVisible) return false;
|
|
6660
|
+
if (streamThinkingBubble?.parentElement === elements.chat) return true;
|
|
4002
6661
|
const created = appendMessage({ role: "thinking", title: "thinking", timestamp: Date.now(), content: "" }, { streaming: true });
|
|
4003
6662
|
streamThinkingBubble = created.bubble;
|
|
4004
6663
|
streamThinking = appendText(created.body, "", "thinking-text");
|
|
4005
6664
|
renderRunIndicator({ scroll: false });
|
|
4006
6665
|
scrollChatToBottom();
|
|
6666
|
+
return true;
|
|
4007
6667
|
}
|
|
4008
6668
|
|
|
4009
6669
|
function showStreamingThinking(placeholder = "Thinking…") {
|
|
4010
|
-
ensureStreamingThinkingBubble();
|
|
6670
|
+
if (!ensureStreamingThinkingBubble()) return;
|
|
4011
6671
|
if (!streamThinking.textContent) streamThinking.textContent = placeholder;
|
|
4012
6672
|
}
|
|
4013
6673
|
|
|
4014
6674
|
function resetStreamBubble() {
|
|
6675
|
+
cancelStreamingAssistantTextRender();
|
|
4015
6676
|
cancelStreamBubbleHide();
|
|
4016
6677
|
streamBubble = null;
|
|
4017
6678
|
streamText = null;
|
|
4018
6679
|
streamRawText = "";
|
|
4019
6680
|
streamBubbleVisibleSince = 0;
|
|
6681
|
+
streamToolCallSeen = false;
|
|
4020
6682
|
streamThinkingBubble = null;
|
|
4021
6683
|
streamThinking = null;
|
|
4022
6684
|
}
|
|
@@ -4035,9 +6697,12 @@ function assistantTextFromMessage(message) {
|
|
|
4035
6697
|
const content = message?.content;
|
|
4036
6698
|
if (typeof content === "string") return content;
|
|
4037
6699
|
if (!Array.isArray(content)) return null;
|
|
4038
|
-
const parts =
|
|
4039
|
-
|
|
4040
|
-
|
|
6700
|
+
const parts = [];
|
|
6701
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
6702
|
+
const part = content[index];
|
|
6703
|
+
const text = assistantTextPartText(part);
|
|
6704
|
+
if (text && !assistantHasToolCallAfter(content, index)) parts.push(text);
|
|
6705
|
+
}
|
|
4041
6706
|
return parts.length ? parts.join("\n\n") : "";
|
|
4042
6707
|
}
|
|
4043
6708
|
|
|
@@ -4052,11 +6717,13 @@ function assistantThinkingTextFromMessage(message) {
|
|
|
4052
6717
|
}
|
|
4053
6718
|
|
|
4054
6719
|
function setStreamingThinkingText(text) {
|
|
6720
|
+
if (!thinkingOutputVisible) return;
|
|
4055
6721
|
showStreamingThinking("");
|
|
4056
|
-
streamThinking.textContent = text;
|
|
6722
|
+
if (streamThinking) streamThinking.textContent = text;
|
|
4057
6723
|
}
|
|
4058
6724
|
|
|
4059
6725
|
function syncStreamingThinkingFromMessage(event, { placeholder = "" } = {}) {
|
|
6726
|
+
if (!thinkingOutputVisible) return true;
|
|
4060
6727
|
const text = assistantThinkingTextFromMessage(assistantStreamingMessage(event));
|
|
4061
6728
|
if (text === null) return false;
|
|
4062
6729
|
if (text || placeholder || streamThinkingBubble) setStreamingThinkingText(text || placeholder);
|
|
@@ -4074,10 +6741,10 @@ function handleMessageUpdate(event) {
|
|
|
4074
6741
|
currentRunStreamChars += delta.length;
|
|
4075
6742
|
setRunIndicatorActivity("Thinking…", { scroll: false });
|
|
4076
6743
|
const synced = syncStreamingThinkingFromMessage(event);
|
|
4077
|
-
if (!synced || (!streamThinking?.textContent && delta)) {
|
|
6744
|
+
if (thinkingOutputVisible && (!synced || (!streamThinking?.textContent && delta))) {
|
|
4078
6745
|
showStreamingThinking("");
|
|
4079
|
-
if (streamThinking
|
|
4080
|
-
streamThinking.textContent += delta;
|
|
6746
|
+
if (streamThinking?.textContent === "Thinking…") streamThinking.textContent = "";
|
|
6747
|
+
if (streamThinking) streamThinking.textContent += delta;
|
|
4081
6748
|
}
|
|
4082
6749
|
renderFooter();
|
|
4083
6750
|
scrollChatToBottom();
|
|
@@ -4093,17 +6760,14 @@ function handleMessageUpdate(event) {
|
|
|
4093
6760
|
if (typeof partialText === "string") streamRawText = partialText;
|
|
4094
6761
|
else if (update.type === "text_end" && typeof update.content === "string") streamRawText = update.content;
|
|
4095
6762
|
else streamRawText += delta;
|
|
4096
|
-
const assistantText = stripTodoProgressLines(streamRawText, { streaming: true });
|
|
4097
6763
|
setRunIndicatorActivity("Writing response…", { scroll: false });
|
|
4098
|
-
if (
|
|
4099
|
-
|
|
4100
|
-
streamText.textContent = assistantText;
|
|
4101
|
-
} else {
|
|
4102
|
-
scheduleStreamBubbleHide();
|
|
4103
|
-
}
|
|
6764
|
+
if (streamToolCallSeen || streamBubble) renderStreamingAssistantText();
|
|
6765
|
+
else scheduleStreamingAssistantTextRender();
|
|
4104
6766
|
renderFooter();
|
|
4105
6767
|
scrollChatToBottom();
|
|
4106
6768
|
} else if (update.type === "toolcall_start") {
|
|
6769
|
+
streamToolCallSeen = true;
|
|
6770
|
+
suppressStreamingAssistantTextBeforeToolCall();
|
|
4107
6771
|
const name = runIndicatorToolName(update.name || update.toolName || update.toolCall?.name);
|
|
4108
6772
|
setRunIndicatorActivity(`Preparing tool call: ${name}…`);
|
|
4109
6773
|
addEvent(`tool call started in assistant message`, "info");
|
|
@@ -4115,29 +6779,36 @@ function handleMessageUpdate(event) {
|
|
|
4115
6779
|
}
|
|
4116
6780
|
}
|
|
4117
6781
|
|
|
4118
|
-
async function refreshState() {
|
|
4119
|
-
|
|
6782
|
+
async function refreshState(tabContext = activeTabContext()) {
|
|
6783
|
+
if (!tabContext.tabId) return;
|
|
6784
|
+
const response = await api("/api/state", { tabId: tabContext.tabId });
|
|
6785
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
4120
6786
|
currentState = response.data || null;
|
|
4121
6787
|
syncActiveTabActivityFromState(currentState);
|
|
4122
6788
|
syncRunIndicatorFromState(currentState);
|
|
4123
6789
|
renderStatus();
|
|
4124
6790
|
}
|
|
4125
6791
|
|
|
4126
|
-
async function refreshStats() {
|
|
4127
|
-
|
|
6792
|
+
async function refreshStats(tabContext = activeTabContext()) {
|
|
6793
|
+
if (!tabContext.tabId) return;
|
|
6794
|
+
const response = await api("/api/stats", { tabId: tabContext.tabId });
|
|
6795
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
4128
6796
|
latestStats = response.data || null;
|
|
4129
6797
|
renderFooter();
|
|
4130
6798
|
}
|
|
4131
6799
|
|
|
4132
|
-
async function refreshWorkspace() {
|
|
6800
|
+
async function refreshWorkspace(tabContext = activeTabContext()) {
|
|
6801
|
+
if (!tabContext.tabId) return;
|
|
6802
|
+
let nextWorkspace = null;
|
|
4133
6803
|
try {
|
|
4134
|
-
const response = await api("/api/workspace");
|
|
4135
|
-
|
|
6804
|
+
const response = await api("/api/workspace", { tabId: tabContext.tabId });
|
|
6805
|
+
nextWorkspace = response.data || null;
|
|
4136
6806
|
} catch (error) {
|
|
6807
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
4137
6808
|
// Older webui server processes do not have /api/workspace. Fall back to /api/health,
|
|
4138
6809
|
// which has exposed cwd from the beginning, so the footer still shows the real path.
|
|
4139
|
-
const health = await api("/api/health");
|
|
4140
|
-
|
|
6810
|
+
const health = await api("/api/health", { tabId: tabContext.tabId });
|
|
6811
|
+
nextWorkspace = health.cwd
|
|
4141
6812
|
? {
|
|
4142
6813
|
cwd: health.cwd,
|
|
4143
6814
|
displayCwd: normalizeDisplayPath(health.cwd),
|
|
@@ -4146,6 +6817,8 @@ async function refreshWorkspace() {
|
|
|
4146
6817
|
}
|
|
4147
6818
|
: null;
|
|
4148
6819
|
}
|
|
6820
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
6821
|
+
latestWorkspace = nextWorkspace;
|
|
4149
6822
|
renderFooter();
|
|
4150
6823
|
}
|
|
4151
6824
|
|
|
@@ -4214,12 +6887,15 @@ async function refreshNetworkStatus() {
|
|
|
4214
6887
|
renderNetworkStatus();
|
|
4215
6888
|
}
|
|
4216
6889
|
|
|
4217
|
-
async function refreshFooterData() {
|
|
4218
|
-
|
|
6890
|
+
async function refreshFooterData(tabContext = activeTabContext()) {
|
|
6891
|
+
if (!tabContext.tabId) return;
|
|
6892
|
+
await Promise.allSettled([refreshStats(tabContext), refreshWorkspace(tabContext)]);
|
|
4219
6893
|
}
|
|
4220
6894
|
|
|
4221
|
-
async function refreshMessages() {
|
|
4222
|
-
|
|
6895
|
+
async function refreshMessages(tabContext = activeTabContext()) {
|
|
6896
|
+
if (!tabContext.tabId) return;
|
|
6897
|
+
const response = await api("/api/messages", { tabId: tabContext.tabId });
|
|
6898
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
4223
6899
|
latestMessages = response.data?.messages || [];
|
|
4224
6900
|
resetStreamBubble();
|
|
4225
6901
|
renderMessages(latestMessages);
|
|
@@ -4227,21 +6903,28 @@ async function refreshMessages() {
|
|
|
4227
6903
|
renderFooter();
|
|
4228
6904
|
}
|
|
4229
6905
|
|
|
4230
|
-
async function refreshModels() {
|
|
4231
|
-
|
|
6906
|
+
async function refreshModels(tabContext = activeTabContext()) {
|
|
6907
|
+
if (!tabContext.tabId) return;
|
|
6908
|
+
const response = await api("/api/models", { tabId: tabContext.tabId });
|
|
4232
6909
|
const models = response.data?.models || [];
|
|
4233
|
-
|
|
6910
|
+
let scopedModels = [];
|
|
6911
|
+
let scopedModelPatterns = [];
|
|
6912
|
+
let scopedModelSource = "none";
|
|
6913
|
+
let scopedModelError = null;
|
|
4234
6914
|
try {
|
|
4235
|
-
const scopedResponse = await api("/api/scoped-models");
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
6915
|
+
const scopedResponse = await api("/api/scoped-models", { tabId: tabContext.tabId });
|
|
6916
|
+
scopedModels = scopedResponse.data?.models || [];
|
|
6917
|
+
scopedModelPatterns = scopedResponse.data?.patterns || [];
|
|
6918
|
+
scopedModelSource = scopedResponse.data?.source || "none";
|
|
4239
6919
|
} catch (error) {
|
|
4240
|
-
|
|
4241
|
-
footerScopedModelPatterns = [];
|
|
4242
|
-
footerScopedModelSource = "none";
|
|
4243
|
-
addEvent(`failed to load scoped models: ${error.message}`, "warn");
|
|
6920
|
+
scopedModelError = error;
|
|
4244
6921
|
}
|
|
6922
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
6923
|
+
availableModels = models;
|
|
6924
|
+
footerScopedModels = scopedModels;
|
|
6925
|
+
footerScopedModelPatterns = scopedModelPatterns;
|
|
6926
|
+
footerScopedModelSource = scopedModelSource;
|
|
6927
|
+
if (scopedModelError) addEvent(`failed to load scoped models: ${scopedModelError.message}`, "warn");
|
|
4245
6928
|
elements.modelSelect.replaceChildren();
|
|
4246
6929
|
for (const model of models) {
|
|
4247
6930
|
const option = document.createElement("option");
|
|
@@ -4355,7 +7038,7 @@ function scoreCommandSuggestion(command, query) {
|
|
|
4355
7038
|
}
|
|
4356
7039
|
|
|
4357
7040
|
function getCommandMatches(query) {
|
|
4358
|
-
return
|
|
7041
|
+
return visibleCommands()
|
|
4359
7042
|
.map((command) => ({ command, score: scoreCommandSuggestion(command, query) }))
|
|
4360
7043
|
.filter((item) => Number.isFinite(item.score))
|
|
4361
7044
|
.sort((a, b) => a.score - b.score || a.command.name.localeCompare(b.command.name))
|
|
@@ -4619,9 +7302,7 @@ function insertPathSuggestion(index = commandSuggestIndex) {
|
|
|
4619
7302
|
return true;
|
|
4620
7303
|
}
|
|
4621
7304
|
|
|
4622
|
-
|
|
4623
|
-
const response = await api("/api/commands");
|
|
4624
|
-
availableCommands = normalizeCommands(response.data?.commands || []);
|
|
7305
|
+
function renderCommands() {
|
|
4625
7306
|
elements.commandsBox.replaceChildren();
|
|
4626
7307
|
if (!availableCommands.length) {
|
|
4627
7308
|
elements.commandsBox.textContent = "No RPC-visible commands.";
|
|
@@ -4629,8 +7310,15 @@ async function refreshCommands() {
|
|
|
4629
7310
|
hideCommandSuggestions();
|
|
4630
7311
|
return;
|
|
4631
7312
|
}
|
|
7313
|
+
const commandsToShow = visibleCommands();
|
|
7314
|
+
if (!commandsToShow.length) {
|
|
7315
|
+
elements.commandsBox.textContent = "No enabled commands visible. Re-enable optional features to show their commands.";
|
|
7316
|
+
elements.commandsBox.classList.add("muted");
|
|
7317
|
+
hideCommandSuggestions();
|
|
7318
|
+
return;
|
|
7319
|
+
}
|
|
4632
7320
|
elements.commandsBox.classList.remove("muted");
|
|
4633
|
-
for (const command of
|
|
7321
|
+
for (const command of commandsToShow.slice(0, 80)) {
|
|
4634
7322
|
const item = make("button", "command-item");
|
|
4635
7323
|
item.type = "button";
|
|
4636
7324
|
item.title = `Send /${command.name}`;
|
|
@@ -4645,8 +7333,27 @@ async function refreshCommands() {
|
|
|
4645
7333
|
renderCommandSuggestions();
|
|
4646
7334
|
}
|
|
4647
7335
|
|
|
4648
|
-
async function
|
|
4649
|
-
|
|
7336
|
+
async function refreshCommands(tabContext = activeTabContext()) {
|
|
7337
|
+
if (!tabContext.tabId) return;
|
|
7338
|
+
const response = await api("/api/commands", { tabId: tabContext.tabId });
|
|
7339
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
7340
|
+
availableCommands = normalizeCommands(response.data?.commands || []);
|
|
7341
|
+
updateOptionalFeatureAvailability();
|
|
7342
|
+
renderCommands();
|
|
7343
|
+
}
|
|
7344
|
+
|
|
7345
|
+
async function refreshAll(tabContext = activeTabContext()) {
|
|
7346
|
+
if (!tabContext.tabId) return;
|
|
7347
|
+
const results = await Promise.allSettled([
|
|
7348
|
+
refreshState(tabContext),
|
|
7349
|
+
refreshMessages(tabContext),
|
|
7350
|
+
refreshModels(tabContext),
|
|
7351
|
+
refreshCommands(tabContext),
|
|
7352
|
+
refreshStats(tabContext),
|
|
7353
|
+
refreshWorkspace(tabContext),
|
|
7354
|
+
refreshNetworkStatus(),
|
|
7355
|
+
]);
|
|
7356
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
4650
7357
|
for (const result of results) {
|
|
4651
7358
|
if (result.status === "rejected") addEvent(result.reason.message || String(result.reason), "error");
|
|
4652
7359
|
}
|
|
@@ -4729,42 +7436,56 @@ async function closeNetworkAccess() {
|
|
|
4729
7436
|
async function sendPrompt(kind = "prompt", explicitMessage) {
|
|
4730
7437
|
const usesPromptInput = explicitMessage === undefined;
|
|
4731
7438
|
const rawMessage = usesPromptInput ? elements.promptInput.value : explicitMessage;
|
|
4732
|
-
const
|
|
4733
|
-
if (!message) return;
|
|
4734
|
-
|
|
7439
|
+
const originalMessage = String(rawMessage || "").trim();
|
|
4735
7440
|
const targetTabId = activeTabId;
|
|
4736
|
-
|
|
4737
|
-
|
|
7441
|
+
if (!targetTabId) return;
|
|
7442
|
+
const tabContext = activeTabContext(targetTabId);
|
|
7443
|
+
const attachments = usesPromptInput ? [...attachmentsForTab(targetTabId)] : [];
|
|
7444
|
+
if (!originalMessage && attachments.length === 0) return;
|
|
7445
|
+
if (kind === "prompt" && attachments.length === 0 && await handleNativeSlashSelectorCommand(originalMessage, { usesPromptInput })) return;
|
|
7446
|
+
|
|
7447
|
+
const targetWasStreaming = !!currentState?.isStreaming;
|
|
7448
|
+
const busyBehavior = elements.busyBehavior.value || "followUp";
|
|
7449
|
+
const startsRun = kind === "prompt" && !targetWasStreaming;
|
|
4738
7450
|
autoFollowChat = true;
|
|
4739
7451
|
updateJumpToLatestButton();
|
|
4740
7452
|
setComposerActionsOpen(false);
|
|
4741
7453
|
if (startsRun) {
|
|
4742
7454
|
markTabWorkingLocally(targetTabId);
|
|
4743
|
-
setRunIndicatorActivity("Sending prompt to Pi…");
|
|
7455
|
+
setRunIndicatorActivity(attachments.length ? "Uploading attachments…" : "Sending prompt to Pi…");
|
|
4744
7456
|
}
|
|
4745
7457
|
|
|
7458
|
+
let message = originalMessage;
|
|
4746
7459
|
try {
|
|
7460
|
+
const prepared = attachments.length ? await prepareAttachmentsForPrompt(attachments, targetTabId) : { images: [], uploadedFiles: [], inlineImageIds: new Set() };
|
|
7461
|
+
message = composeMessageWithAttachments(originalMessage, prepared.uploadedFiles, prepared.inlineImageIds);
|
|
7462
|
+
const bodyBase = { message };
|
|
7463
|
+
if (prepared.images.length) bodyBase.images = prepared.images;
|
|
7464
|
+
if (kind === "prompt" && !message.startsWith("/")) rememberLastUserPrompt(message, { tabId: targetTabId });
|
|
7465
|
+
if (startsRun && isCurrentTabContext(tabContext)) setRunIndicatorActivity("Sending prompt to Pi…");
|
|
7466
|
+
|
|
4747
7467
|
let response;
|
|
4748
7468
|
if (kind === "steer") {
|
|
4749
|
-
response = await api("/api/steer", { method: "POST", body:
|
|
7469
|
+
response = await api("/api/steer", { method: "POST", body: bodyBase, tabId: targetTabId });
|
|
4750
7470
|
} else if (kind === "follow-up") {
|
|
4751
|
-
response = await api("/api/follow-up", { method: "POST", body:
|
|
7471
|
+
response = await api("/api/follow-up", { method: "POST", body: bodyBase, tabId: targetTabId });
|
|
4752
7472
|
} else {
|
|
4753
|
-
const body = {
|
|
4754
|
-
if (
|
|
7473
|
+
const body = { ...bodyBase };
|
|
7474
|
+
if (targetWasStreaming) body.streamingBehavior = busyBehavior;
|
|
4755
7475
|
response = await api("/api/prompt", { method: "POST", body, tabId: targetTabId });
|
|
4756
7476
|
}
|
|
4757
7477
|
applyResponseTab(response);
|
|
4758
7478
|
if (response?.command === "native_slash_command" && /^\/new(?:\s|$)/.test(message)) forgetLastUserPrompt(targetTabId);
|
|
7479
|
+
const targetStillActive = isCurrentTabContext(tabContext);
|
|
4759
7480
|
if (startsRun && response?.command === "native_slash_command") {
|
|
4760
7481
|
markTabIdleLocally(targetTabId);
|
|
4761
|
-
clearRunIndicatorActivity();
|
|
4762
|
-
} else if (kind === "steer" && currentState?.isStreaming) {
|
|
7482
|
+
if (targetStillActive) clearRunIndicatorActivity();
|
|
7483
|
+
} else if (targetStillActive && kind === "steer" && currentState?.isStreaming) {
|
|
4763
7484
|
setRunIndicatorActivity("Steering sent; waiting for the next output or action…");
|
|
4764
|
-
} else if (kind === "follow-up" && currentState?.isStreaming) {
|
|
7485
|
+
} else if (targetStillActive && kind === "follow-up" && currentState?.isStreaming) {
|
|
4765
7486
|
setRunIndicatorActivity("Follow-up queued; current agent run is still active…");
|
|
4766
7487
|
}
|
|
4767
|
-
if (response?.command === "native_slash_command" && response.data?.copyText) {
|
|
7488
|
+
if (targetStillActive && response?.command === "native_slash_command" && response.data?.copyText) {
|
|
4768
7489
|
try {
|
|
4769
7490
|
await navigator.clipboard.writeText(response.data.copyText);
|
|
4770
7491
|
} catch (error) {
|
|
@@ -4772,22 +7493,33 @@ async function sendPrompt(kind = "prompt", explicitMessage) {
|
|
|
4772
7493
|
response.data.level = "warn";
|
|
4773
7494
|
}
|
|
4774
7495
|
}
|
|
4775
|
-
if (response?.command === "native_slash_command" && response.data?.message) {
|
|
7496
|
+
if (targetStillActive && response?.command === "native_slash_command" && response.data?.message) {
|
|
4776
7497
|
addTransientMessage({ role: "native", title: message.split(/\s+/, 1)[0], content: response.data.message, level: response.data.level || "info" });
|
|
4777
7498
|
}
|
|
4778
7499
|
if (usesPromptInput) {
|
|
4779
|
-
|
|
4780
|
-
|
|
7500
|
+
clearAttachments(targetTabId);
|
|
7501
|
+
if (targetStillActive) {
|
|
7502
|
+
elements.promptInput.value = "";
|
|
7503
|
+
resizePromptInput();
|
|
7504
|
+
} else {
|
|
7505
|
+
tabDrafts.set(targetTabId, "");
|
|
7506
|
+
}
|
|
7507
|
+
}
|
|
7508
|
+
if (targetStillActive) {
|
|
7509
|
+
hideCommandSuggestions();
|
|
7510
|
+
scheduleRefreshState(120, tabContext);
|
|
7511
|
+
} else {
|
|
7512
|
+
scheduleRefreshTabs(300);
|
|
4781
7513
|
}
|
|
4782
|
-
hideCommandSuggestions();
|
|
4783
|
-
scheduleRefreshState();
|
|
4784
7514
|
} catch (error) {
|
|
4785
7515
|
if (startsRun) {
|
|
4786
7516
|
markTabIdleLocally(targetTabId);
|
|
4787
|
-
clearRunIndicatorActivity();
|
|
7517
|
+
if (isCurrentTabContext(tabContext)) clearRunIndicatorActivity();
|
|
7518
|
+
}
|
|
7519
|
+
if (isCurrentTabContext(tabContext)) {
|
|
7520
|
+
addEvent(error.message, "error");
|
|
7521
|
+
addTransientMessage({ role: "error", title: message.startsWith("/") ? message.split(/\s+/, 1)[0] : "error", content: error.message, level: "error" });
|
|
4788
7522
|
}
|
|
4789
|
-
addEvent(error.message, "error");
|
|
4790
|
-
addTransientMessage({ role: "error", title: message.startsWith("/") ? message.split(/\s+/, 1)[0] : "error", content: error.message, level: "error" });
|
|
4791
7523
|
}
|
|
4792
7524
|
}
|
|
4793
7525
|
|
|
@@ -4823,11 +7555,13 @@ function handleExtensionUiRequest(request) {
|
|
|
4823
7555
|
case "setStatus":
|
|
4824
7556
|
if (request.statusText) statusEntries.set(request.statusKey || "extension", request.statusText);
|
|
4825
7557
|
else statusEntries.delete(request.statusKey || "extension");
|
|
7558
|
+
updateOptionalFeatureAvailability();
|
|
4826
7559
|
renderStatus();
|
|
4827
7560
|
return;
|
|
4828
7561
|
case "setWidget":
|
|
4829
7562
|
if (Array.isArray(request.widgetLines)) widgets.set(request.widgetKey || request.id, request);
|
|
4830
7563
|
else widgets.delete(request.widgetKey || request.id);
|
|
7564
|
+
updateOptionalFeatureAvailability();
|
|
4831
7565
|
renderWidgets();
|
|
4832
7566
|
return;
|
|
4833
7567
|
case "setTitle":
|
|
@@ -4861,12 +7595,14 @@ function handleExtensionUiRequest(request) {
|
|
|
4861
7595
|
|
|
4862
7596
|
async function sendDialogResponse(payload) {
|
|
4863
7597
|
const { tabId = activeTabId, ...body } = payload;
|
|
7598
|
+
const tabContext = activeTabContext(tabId);
|
|
4864
7599
|
try {
|
|
4865
7600
|
const response = await api("/api/extension-ui-response", { method: "POST", body, tabId });
|
|
4866
7601
|
if (!applyResponseTab(response) && decrementTabPendingBlockerCount(tabId)) renderTabs();
|
|
4867
7602
|
} catch (error) {
|
|
4868
|
-
addEvent(error.message, "error");
|
|
7603
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
4869
7604
|
} finally {
|
|
7605
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
4870
7606
|
if (elements.dialog.open) elements.dialog.close();
|
|
4871
7607
|
activeDialog = null;
|
|
4872
7608
|
if (runIndicatorIsActive()) setRunIndicatorActivity("Continuing after your response…");
|
|
@@ -4888,7 +7624,8 @@ function showNextDialog() {
|
|
|
4888
7624
|
const request = activeDialog;
|
|
4889
7625
|
|
|
4890
7626
|
const prompt = normalizeDialogPrompt(request);
|
|
4891
|
-
const
|
|
7627
|
+
const detectedReleasePrompt = request.method === "select" ? releaseDialogPromptParts(prompt) : null;
|
|
7628
|
+
const releasePrompt = detectedReleasePrompt && isOptionalFeatureEnabled(detectedReleasePrompt.featureId) ? detectedReleasePrompt : null;
|
|
4892
7629
|
const displayPrompt = releasePrompt || prompt;
|
|
4893
7630
|
const isGuardrailDialog = isGuardrailDialogPrompt(displayPrompt);
|
|
4894
7631
|
const isReleaseDialog = !!releasePrompt;
|
|
@@ -4942,8 +7679,22 @@ function showNextDialog() {
|
|
|
4942
7679
|
elements.dialog.showModal();
|
|
4943
7680
|
}
|
|
4944
7681
|
|
|
7682
|
+
function handleInactiveTabEvent(event) {
|
|
7683
|
+
if (event.type === "extension_ui_request" && EXTENSION_UI_BLOCKING_METHODS.has(event.method)) {
|
|
7684
|
+
if (!event.replayed) notifyBlockedTab(event.tabId, { request: event, count: event.pendingExtensionUiRequestCount });
|
|
7685
|
+
renderTabs();
|
|
7686
|
+
} else if (event.type === "agent_end") {
|
|
7687
|
+
notifyAgentDone(event.tabId, { activity: event.tabActivity, tabTitle: event.tabTitle });
|
|
7688
|
+
}
|
|
7689
|
+
}
|
|
7690
|
+
|
|
4945
7691
|
function handleEvent(event) {
|
|
4946
7692
|
ingestEventTabActivity(event);
|
|
7693
|
+
if (!eventTargetsActiveTab(event)) {
|
|
7694
|
+
handleInactiveTabEvent(event);
|
|
7695
|
+
return;
|
|
7696
|
+
}
|
|
7697
|
+
const tabContext = activeTabContext(event.tabId || activeTabId);
|
|
4947
7698
|
switch (event.type) {
|
|
4948
7699
|
case "webui_connected":
|
|
4949
7700
|
addEvent(`connected to ${event.tabTitle || "terminal"} for ${event.cwd}`);
|
|
@@ -4967,8 +7718,18 @@ function handleEvent(event) {
|
|
|
4967
7718
|
case "webui_tab_reloaded":
|
|
4968
7719
|
addEvent(`${event.tabTitle || "terminal"} reloaded`);
|
|
4969
7720
|
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" });
|
|
7721
|
+
statusEntries.clear();
|
|
7722
|
+
widgets.clear();
|
|
7723
|
+
resetOptionalFeatureAvailability();
|
|
7724
|
+
renderStatus();
|
|
7725
|
+
renderWidgets();
|
|
4970
7726
|
refreshTabs().catch((error) => addEvent(error.message, "error"));
|
|
4971
|
-
setTimeout(() =>
|
|
7727
|
+
setTimeout(() => {
|
|
7728
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
7729
|
+
refreshAll(tabContext).catch((error) => {
|
|
7730
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
7731
|
+
});
|
|
7732
|
+
}, 500);
|
|
4972
7733
|
break;
|
|
4973
7734
|
case "webui_extension_ui_cancelled":
|
|
4974
7735
|
removeQueuedDialogRequests(event.ids || []);
|
|
@@ -5058,10 +7819,18 @@ function handleEvent(event) {
|
|
|
5058
7819
|
scheduleRefreshFooter();
|
|
5059
7820
|
break;
|
|
5060
7821
|
case "tool_execution_start":
|
|
7822
|
+
streamToolCallSeen = true;
|
|
7823
|
+
suppressStreamingAssistantTextBeforeToolCall();
|
|
7824
|
+
handleToolExecutionStart(event);
|
|
5061
7825
|
setRunIndicatorActivity(`Running tool: ${runIndicatorToolName(event.toolName)}…`);
|
|
5062
7826
|
addEvent(`tool ${event.toolName} started`);
|
|
5063
7827
|
break;
|
|
7828
|
+
case "tool_execution_update":
|
|
7829
|
+
handleToolExecutionUpdate(event);
|
|
7830
|
+
setRunIndicatorActivity(`Running tool: ${runIndicatorToolName(event.toolName)}…`, { scroll: false });
|
|
7831
|
+
break;
|
|
5064
7832
|
case "tool_execution_end":
|
|
7833
|
+
handleToolExecutionEnd(event);
|
|
5065
7834
|
setRunIndicatorActivity(`Tool ${runIndicatorToolName(event.toolName)} ${event.isError ? "failed" : "finished"}; waiting for the agent's next step…`);
|
|
5066
7835
|
addEvent(`tool ${event.toolName} ${event.isError ? "failed" : "finished"}`, event.isError ? "error" : "info");
|
|
5067
7836
|
scheduleRefreshMessages();
|
|
@@ -5079,6 +7848,29 @@ function handleEvent(event) {
|
|
|
5079
7848
|
markTabOutputSeen();
|
|
5080
7849
|
scheduleRefreshMessages();
|
|
5081
7850
|
break;
|
|
7851
|
+
case "auto_retry_start": {
|
|
7852
|
+
const seconds = Math.max(0, Math.ceil(Number(event.delayMs || 0) / 1000));
|
|
7853
|
+
const retryText = `Retrying (${event.attempt || "?"}/${event.maxAttempts || "?"}) in ${seconds}s after: ${event.errorMessage || "model/provider error"}`;
|
|
7854
|
+
setRunIndicatorActivity(retryText);
|
|
7855
|
+
addEvent(retryText, "warn");
|
|
7856
|
+
addTransientMessage({ role: "warn", title: "auto retry", content: retryText, level: "warn" });
|
|
7857
|
+
break;
|
|
7858
|
+
}
|
|
7859
|
+
case "auto_retry_end":
|
|
7860
|
+
if (event.success === false) {
|
|
7861
|
+
const retryError = `Retry failed after ${event.attempt || "?"} attempt(s): ${event.finalError || "Unknown error"}`;
|
|
7862
|
+
addEvent(retryError, "error");
|
|
7863
|
+
addTransientMessage({ role: "error", title: "auto retry failed", content: retryError, level: "error" });
|
|
7864
|
+
} else {
|
|
7865
|
+
addEvent(`retry recovered after ${event.attempt || "?"} attempt(s)`);
|
|
7866
|
+
}
|
|
7867
|
+
break;
|
|
7868
|
+
case "extension_error": {
|
|
7869
|
+
const message = `${event.extensionPath || "extension"}${event.event ? ` during ${event.event}` : ""}: ${event.error || "unknown extension error"}`;
|
|
7870
|
+
addEvent(message, "error");
|
|
7871
|
+
addTransientMessage({ role: "error", title: "extension error", content: message, level: "error" });
|
|
7872
|
+
break;
|
|
7873
|
+
}
|
|
5082
7874
|
case "extension_ui_request":
|
|
5083
7875
|
handleExtensionUiRequest(event);
|
|
5084
7876
|
break;
|
|
@@ -5101,18 +7893,23 @@ function handleEvent(event) {
|
|
|
5101
7893
|
}
|
|
5102
7894
|
}
|
|
5103
7895
|
|
|
5104
|
-
function connectEvents() {
|
|
7896
|
+
function connectEvents(tabContext = activeTabContext()) {
|
|
5105
7897
|
eventSource?.close();
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
7898
|
+
eventSource = null;
|
|
7899
|
+
if (!tabContext.tabId || !isCurrentTabContext(tabContext)) return;
|
|
7900
|
+
const source = new EventSource(`/api/events?tab=${encodeURIComponent(tabContext.tabId)}`);
|
|
7901
|
+
eventSource = source;
|
|
7902
|
+
source.onmessage = (message) => {
|
|
7903
|
+
if (eventSource !== source || !isCurrentTabContext(tabContext)) return;
|
|
5109
7904
|
try {
|
|
5110
7905
|
handleEvent(JSON.parse(message.data));
|
|
5111
7906
|
} catch (error) {
|
|
5112
|
-
addEvent(error.message, "error");
|
|
7907
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5113
7908
|
}
|
|
5114
7909
|
};
|
|
5115
|
-
|
|
7910
|
+
source.onerror = () => {
|
|
7911
|
+
if (eventSource === source && isCurrentTabContext(tabContext)) addEvent("event stream disconnected; browser will retry", "warn");
|
|
7912
|
+
};
|
|
5116
7913
|
}
|
|
5117
7914
|
|
|
5118
7915
|
elements.sendFeedbackButton.addEventListener("click", () => submitQueuedActionFeedback());
|
|
@@ -5129,6 +7926,7 @@ elements.terminalTabsToggleButton.addEventListener("click", () => {
|
|
|
5129
7926
|
setMobileTabsExpanded(!document.body.classList.contains("mobile-tabs-expanded"));
|
|
5130
7927
|
});
|
|
5131
7928
|
elements.newTabButton.addEventListener("click", () => createTerminalTab());
|
|
7929
|
+
elements.closeAllTabsButton.addEventListener("click", () => closeAllTerminalTabs());
|
|
5132
7930
|
elements.gitWorkflowButton.addEventListener("click", () => {
|
|
5133
7931
|
setComposerActionsOpen(false);
|
|
5134
7932
|
startGitWorkflow();
|
|
@@ -5148,70 +7946,143 @@ publishMenuContainer?.addEventListener("focusout", () => {
|
|
|
5148
7946
|
elements.releaseNpmButton.addEventListener("click", () => runPublishWorkflow("/release-npm"));
|
|
5149
7947
|
elements.releaseAurButton.addEventListener("click", () => runPublishWorkflow("/release-aur"));
|
|
5150
7948
|
elements.gitWorkflowCancelButton.addEventListener("click", cancelGitWorkflow);
|
|
5151
|
-
elements.
|
|
7949
|
+
elements.nativeCommandDialog.addEventListener("close", () => {
|
|
7950
|
+
elements.nativeCommandSearch.oninput = null;
|
|
7951
|
+
nativeCommandTabId = null;
|
|
7952
|
+
});
|
|
7953
|
+
|
|
7954
|
+
function resetAbortLongPressAffordance() {
|
|
7955
|
+
clearTimeout(abortLongPressTimer);
|
|
7956
|
+
abortLongPressTimer = null;
|
|
7957
|
+
elements.abortButton.classList.remove("long-pressing");
|
|
7958
|
+
if (!abortRequestInFlight) elements.abortButton.textContent = "Abort";
|
|
7959
|
+
}
|
|
7960
|
+
|
|
7961
|
+
async function abortActiveRun({ source = "button" } = {}) {
|
|
7962
|
+
if (abortRequestInFlight || !isAbortAvailable()) return;
|
|
7963
|
+
const tabContext = activeTabContext();
|
|
7964
|
+
abortRequestInFlight = true;
|
|
7965
|
+
resetAbortLongPressAffordance();
|
|
7966
|
+
updateComposerModeButtons();
|
|
5152
7967
|
const hadActiveRun = runIndicatorIsActive();
|
|
5153
7968
|
try {
|
|
5154
|
-
if (hadActiveRun) setRunIndicatorActivity(
|
|
5155
|
-
await api("/api/abort", { method: "POST", body: {} });
|
|
7969
|
+
if (hadActiveRun) setRunIndicatorActivity(`Abort requested${source === "escape" ? " from Esc" : source === "long-press" ? " from long-press" : ""}; checking whether Pi stopped…`);
|
|
7970
|
+
await api("/api/abort", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
7971
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5156
7972
|
addAbortTranscriptNotice({ activeRun: hadActiveRun });
|
|
5157
|
-
scheduleAbortStateChecks();
|
|
7973
|
+
scheduleAbortStateChecks(tabContext);
|
|
5158
7974
|
} catch (error) {
|
|
5159
|
-
|
|
5160
|
-
|
|
7975
|
+
if (isCurrentTabContext(tabContext)) {
|
|
7976
|
+
addEvent(error.message, "error");
|
|
7977
|
+
addAbortTranscriptNotice({ errorMessage: error.message });
|
|
7978
|
+
}
|
|
7979
|
+
} finally {
|
|
7980
|
+
abortRequestInFlight = false;
|
|
7981
|
+
updateComposerModeButtons();
|
|
7982
|
+
}
|
|
7983
|
+
}
|
|
7984
|
+
|
|
7985
|
+
function startAbortLongPress(event) {
|
|
7986
|
+
if (!isAbortAvailable() || abortRequestInFlight) return;
|
|
7987
|
+
if (event.button !== undefined && event.button !== 0) return;
|
|
7988
|
+
resetAbortLongPressAffordance();
|
|
7989
|
+
abortLongPressHandled = false;
|
|
7990
|
+
elements.abortButton.classList.add("long-pressing");
|
|
7991
|
+
elements.abortButton.textContent = "Hold…";
|
|
7992
|
+
abortLongPressTimer = setTimeout(() => {
|
|
7993
|
+
abortLongPressTimer = null;
|
|
7994
|
+
abortLongPressHandled = true;
|
|
7995
|
+
abortActiveRun({ source: "long-press" });
|
|
7996
|
+
}, ABORT_LONG_PRESS_MS);
|
|
7997
|
+
}
|
|
7998
|
+
|
|
7999
|
+
elements.abortButton.addEventListener("pointerdown", startAbortLongPress);
|
|
8000
|
+
for (const eventName of ["pointerup", "pointerleave", "pointercancel", "blur"]) {
|
|
8001
|
+
elements.abortButton.addEventListener(eventName, resetAbortLongPressAffordance);
|
|
8002
|
+
}
|
|
8003
|
+
elements.abortButton.addEventListener("click", (event) => {
|
|
8004
|
+
if (abortLongPressHandled) {
|
|
8005
|
+
event.preventDefault();
|
|
8006
|
+
abortLongPressHandled = false;
|
|
8007
|
+
return;
|
|
5161
8008
|
}
|
|
8009
|
+
abortActiveRun({ source: "button" });
|
|
5162
8010
|
});
|
|
5163
8011
|
elements.newSessionButton.addEventListener("click", async () => {
|
|
5164
8012
|
setComposerActionsOpen(false);
|
|
8013
|
+
const tabContext = activeTabContext();
|
|
5165
8014
|
if (!confirm("Start a new Pi session?")) return;
|
|
5166
8015
|
try {
|
|
5167
|
-
const response = await api("/api/new-session", { method: "POST", body: {} });
|
|
8016
|
+
const response = await api("/api/new-session", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
5168
8017
|
applyResponseTab(response);
|
|
5169
|
-
forgetLastUserPrompt(
|
|
5170
|
-
|
|
5171
|
-
|
|
8018
|
+
forgetLastUserPrompt(tabContext.tabId);
|
|
8019
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
8020
|
+
await refreshAll(tabContext);
|
|
8021
|
+
if (isCurrentTabContext(tabContext)) focusPromptInput({ defer: true });
|
|
5172
8022
|
} catch (error) {
|
|
5173
|
-
addEvent(error.message, "error");
|
|
8023
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5174
8024
|
}
|
|
5175
8025
|
});
|
|
5176
8026
|
elements.compactButton.addEventListener("click", async () => {
|
|
5177
8027
|
setComposerActionsOpen(false);
|
|
8028
|
+
const tabContext = activeTabContext();
|
|
5178
8029
|
try {
|
|
5179
8030
|
elements.compactButton.disabled = true;
|
|
5180
8031
|
elements.compactButton.textContent = "Compacting…";
|
|
5181
8032
|
setRunIndicatorActivity("Requesting context compaction…");
|
|
5182
8033
|
scrollChatToBottom({ force: true });
|
|
5183
8034
|
addEvent("manual compaction requested");
|
|
5184
|
-
await api("/api/compact", { method: "POST", body: {} });
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
8035
|
+
await api("/api/compact", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
8036
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
8037
|
+
scheduleRefreshState(120, tabContext);
|
|
8038
|
+
scheduleRefreshMessages(600, tabContext);
|
|
8039
|
+
scheduleRefreshFooter(600, tabContext);
|
|
5188
8040
|
} catch (error) {
|
|
5189
|
-
|
|
5190
|
-
|
|
8041
|
+
if (isCurrentTabContext(tabContext)) {
|
|
8042
|
+
clearRunIndicatorActivity();
|
|
8043
|
+
addEvent(error.message, "error");
|
|
8044
|
+
}
|
|
5191
8045
|
} finally {
|
|
5192
|
-
|
|
5193
|
-
|
|
8046
|
+
if (isCurrentTabContext(tabContext)) {
|
|
8047
|
+
elements.compactButton.disabled = !!currentState?.isCompacting;
|
|
8048
|
+
elements.compactButton.textContent = currentState?.isCompacting ? "Compacting…" : "Compact";
|
|
8049
|
+
}
|
|
5194
8050
|
}
|
|
5195
8051
|
});
|
|
5196
8052
|
elements.setModelButton.addEventListener("click", async () => {
|
|
5197
8053
|
if (!elements.modelSelect.value) return;
|
|
8054
|
+
const tabContext = activeTabContext();
|
|
5198
8055
|
try {
|
|
5199
8056
|
const selected = JSON.parse(elements.modelSelect.value);
|
|
5200
|
-
await api("/api/model", { method: "POST", body: selected });
|
|
5201
|
-
await refreshState();
|
|
8057
|
+
await api("/api/model", { method: "POST", body: selected, tabId: tabContext.tabId });
|
|
8058
|
+
if (isCurrentTabContext(tabContext)) await refreshState(tabContext);
|
|
5202
8059
|
} catch (error) {
|
|
5203
|
-
addEvent(error.message, "error");
|
|
8060
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5204
8061
|
}
|
|
5205
8062
|
});
|
|
5206
8063
|
elements.setThinkingButton.addEventListener("click", async () => {
|
|
8064
|
+
const tabContext = activeTabContext();
|
|
5207
8065
|
try {
|
|
5208
|
-
await api("/api/thinking", { method: "POST", body: { level: elements.thinkingSelect.value } });
|
|
5209
|
-
await refreshState();
|
|
8066
|
+
await api("/api/thinking", { method: "POST", body: { level: elements.thinkingSelect.value }, tabId: tabContext.tabId });
|
|
8067
|
+
if (isCurrentTabContext(tabContext)) await refreshState(tabContext);
|
|
5210
8068
|
} catch (error) {
|
|
5211
|
-
addEvent(error.message, "error");
|
|
8069
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5212
8070
|
}
|
|
5213
8071
|
});
|
|
5214
|
-
elements.themeSelect.addEventListener("change", () =>
|
|
8072
|
+
elements.themeSelect.addEventListener("change", () => {
|
|
8073
|
+
setThemeByName(elements.themeSelect.value, { persist: true, announce: true }).catch((error) => addEvent(error.message || String(error), "error"));
|
|
8074
|
+
});
|
|
8075
|
+
if (elements.backgroundChooseButton && elements.backgroundInput) {
|
|
8076
|
+
elements.backgroundChooseButton.addEventListener("click", () => elements.backgroundInput.click());
|
|
8077
|
+
elements.backgroundInput.addEventListener("change", () => {
|
|
8078
|
+
const [file] = Array.from(elements.backgroundInput.files || []);
|
|
8079
|
+
elements.backgroundInput.value = "";
|
|
8080
|
+
setCustomBackgroundFromFile(file).catch((error) => addEvent(error.message || String(error), "error"));
|
|
8081
|
+
});
|
|
8082
|
+
}
|
|
8083
|
+
if (elements.backgroundClearButton) {
|
|
8084
|
+
elements.backgroundClearButton.addEventListener("click", () => clearCustomBackground().catch((error) => addEvent(error.message || String(error), "error")));
|
|
8085
|
+
}
|
|
5215
8086
|
elements.openNetworkButton.addEventListener("click", openToNetwork);
|
|
5216
8087
|
elements.agentDoneNotificationsToggle.addEventListener("change", () => {
|
|
5217
8088
|
setAgentDoneNotificationsEnabled(elements.agentDoneNotificationsToggle.checked, {
|
|
@@ -5222,6 +8093,11 @@ elements.agentDoneNotificationsToggle.addEventListener("change", () => {
|
|
|
5222
8093
|
renderAgentDoneNotificationsToggle();
|
|
5223
8094
|
});
|
|
5224
8095
|
});
|
|
8096
|
+
if (elements.thinkingVisibilityToggle) {
|
|
8097
|
+
elements.thinkingVisibilityToggle.addEventListener("change", () => {
|
|
8098
|
+
setThinkingOutputVisible(elements.thinkingVisibilityToggle.checked, { announce: true });
|
|
8099
|
+
});
|
|
8100
|
+
}
|
|
5225
8101
|
elements.toggleSidePanelButton.addEventListener("click", () => {
|
|
5226
8102
|
setSidePanelCollapsed(true);
|
|
5227
8103
|
});
|
|
@@ -5268,6 +8144,7 @@ document.addEventListener("pointermove", (event) => {
|
|
|
5268
8144
|
}, { passive: true });
|
|
5269
8145
|
window.addEventListener("keydown", (event) => {
|
|
5270
8146
|
if (event.key !== "Escape") return;
|
|
8147
|
+
if (elements.dialog?.open || elements.pathPickerDialog?.open) return;
|
|
5271
8148
|
if (publishMenuOpen) {
|
|
5272
8149
|
setPublishMenuOpen(false);
|
|
5273
8150
|
return;
|
|
@@ -5284,8 +8161,17 @@ window.addEventListener("keydown", (event) => {
|
|
|
5284
8161
|
setFooterModelPickerOpen(false);
|
|
5285
8162
|
return;
|
|
5286
8163
|
}
|
|
8164
|
+
if (!elements.commandSuggest.hidden) {
|
|
8165
|
+
hideCommandSuggestions();
|
|
8166
|
+
return;
|
|
8167
|
+
}
|
|
5287
8168
|
if (isMobileView() && !document.body.classList.contains("side-panel-collapsed")) {
|
|
5288
8169
|
setSidePanelCollapsed(true);
|
|
8170
|
+
return;
|
|
8171
|
+
}
|
|
8172
|
+
if (isAbortAvailable()) {
|
|
8173
|
+
event.preventDefault();
|
|
8174
|
+
abortActiveRun({ source: "escape" });
|
|
5289
8175
|
}
|
|
5290
8176
|
});
|
|
5291
8177
|
|
|
@@ -5300,6 +8186,18 @@ elements.pathPickerDialog.addEventListener("close", () => {
|
|
|
5300
8186
|
if (pathPickerState) closePathPicker(null);
|
|
5301
8187
|
});
|
|
5302
8188
|
|
|
8189
|
+
if (elements.attachButton && elements.attachmentInput) {
|
|
8190
|
+
elements.attachButton.addEventListener("click", () => elements.attachmentInput.click());
|
|
8191
|
+
elements.attachmentInput.addEventListener("change", () => {
|
|
8192
|
+
addAttachmentFiles(elements.attachmentInput.files, "picker");
|
|
8193
|
+
elements.attachmentInput.value = "";
|
|
8194
|
+
});
|
|
8195
|
+
}
|
|
8196
|
+
elements.promptInput.addEventListener("paste", handleAttachmentPaste);
|
|
8197
|
+
elements.composer.addEventListener("dragover", handleComposerDragOver);
|
|
8198
|
+
elements.composer.addEventListener("dragleave", handleComposerDragLeave);
|
|
8199
|
+
elements.composer.addEventListener("drop", handleComposerDrop);
|
|
8200
|
+
|
|
5303
8201
|
elements.promptInput.addEventListener("keydown", (event) => {
|
|
5304
8202
|
if (shouldSendPromptFromEnter(event)) {
|
|
5305
8203
|
event.preventDefault();
|
|
@@ -5358,11 +8256,20 @@ elements.promptInput.addEventListener("blur", () => {
|
|
|
5358
8256
|
resizePromptInput();
|
|
5359
8257
|
focusPromptInput({ defer: true });
|
|
5360
8258
|
updateComposerModeButtons();
|
|
8259
|
+
updateOptionalFeatureAvailability();
|
|
5361
8260
|
loadLastUserPromptCache();
|
|
5362
8261
|
installViewportHandlers();
|
|
5363
|
-
|
|
8262
|
+
currentThemeName = storedThemeName();
|
|
8263
|
+
renderBackgroundControl();
|
|
8264
|
+
initializeThemes().catch((error) => {
|
|
8265
|
+
addEvent(`failed to load themes: ${error.message}`, "warn");
|
|
8266
|
+
initializeCustomBackground().catch((backgroundError) => addEvent(`failed to initialize background: ${backgroundError.message}`, "warn"));
|
|
8267
|
+
});
|
|
5364
8268
|
initializeFastPicks().catch((error) => addEvent(`failed to initialize path fast picks: ${error.message}`, "error"));
|
|
5365
8269
|
restoreAgentDoneNotificationsSetting();
|
|
8270
|
+
restoreThinkingVisibilitySetting();
|
|
8271
|
+
restoreSidePanelSectionState();
|
|
8272
|
+
bindSidePanelSectionToggles();
|
|
5366
8273
|
restoreSidePanelState();
|
|
5367
8274
|
bindMobileViewChanges();
|
|
5368
8275
|
registerPwaServiceWorker();
|