@firstpick/pi-package-webui 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/bin/pi-webui.mjs +425 -13
- package/index.ts +82 -10
- package/package.json +1 -1
- package/public/app.js +2853 -234
- package/public/catppuccin-mocha-background.png +0 -0
- package/public/index.html +165 -54
- package/public/matrix-background.webp +0 -0
- package/public/service-worker.js +3 -1
- package/public/styles.css +695 -16
- package/tests/mobile-static.test.mjs +155 -30
package/public/app.js
CHANGED
|
@@ -7,6 +7,11 @@ const elements = {
|
|
|
7
7
|
newTabButton: $("#newTabButton"),
|
|
8
8
|
closeAllTabsButton: $("#closeAllTabsButton"),
|
|
9
9
|
statusBar: $("#statusBar"),
|
|
10
|
+
serverOfflinePanel: $("#serverOfflinePanel"),
|
|
11
|
+
serverOfflineCommand: $("#serverOfflineCommand"),
|
|
12
|
+
serverOfflineSlashCommand: $("#serverOfflineSlashCommand"),
|
|
13
|
+
copyServerCommandButton: $("#copyServerCommandButton"),
|
|
14
|
+
retryServerConnectionButton: $("#retryServerConnectionButton"),
|
|
10
15
|
widgetArea: $("#widgetArea"),
|
|
11
16
|
stickyUserPromptButton: $("#stickyUserPromptButton"),
|
|
12
17
|
chat: $("#chat"),
|
|
@@ -21,6 +26,9 @@ const elements = {
|
|
|
21
26
|
promptInput: $("#promptInput"),
|
|
22
27
|
sendButton: $("#sendButton"),
|
|
23
28
|
commandSuggest: $("#commandSuggest"),
|
|
29
|
+
attachmentTray: $("#attachmentTray"),
|
|
30
|
+
attachButton: $("#attachButton"),
|
|
31
|
+
attachmentInput: $("#attachmentInput"),
|
|
24
32
|
busyBehavior: $("#busyBehavior"),
|
|
25
33
|
steerButton: $("#steerButton"),
|
|
26
34
|
followUpButton: $("#followUpButton"),
|
|
@@ -43,7 +51,13 @@ const elements = {
|
|
|
43
51
|
setModelButton: $("#setModelButton"),
|
|
44
52
|
thinkingSelect: $("#thinkingSelect"),
|
|
45
53
|
setThinkingButton: $("#setThinkingButton"),
|
|
54
|
+
thinkingVisibilityToggle: $("#thinkingVisibilityToggle"),
|
|
55
|
+
thinkingVisibilityStatus: $("#thinkingVisibilityStatus"),
|
|
46
56
|
themeSelect: $("#themeSelect"),
|
|
57
|
+
backgroundInput: $("#backgroundInput"),
|
|
58
|
+
backgroundChooseButton: $("#backgroundChooseButton"),
|
|
59
|
+
backgroundClearButton: $("#backgroundClearButton"),
|
|
60
|
+
backgroundStatus: $("#backgroundStatus"),
|
|
47
61
|
networkStatus: $("#networkStatus"),
|
|
48
62
|
openNetworkButton: $("#openNetworkButton"),
|
|
49
63
|
agentDoneNotificationsToggle: $("#agentDoneNotificationsToggle"),
|
|
@@ -72,12 +86,21 @@ const elements = {
|
|
|
72
86
|
pathPickerError: $("#pathPickerError"),
|
|
73
87
|
pathPickerCancelButton: $("#pathPickerCancelButton"),
|
|
74
88
|
pathPickerChooseButton: $("#pathPickerChooseButton"),
|
|
89
|
+
nativeCommandDialog: $("#nativeCommandDialog"),
|
|
90
|
+
nativeCommandTitle: $("#nativeCommandTitle"),
|
|
91
|
+
nativeCommandMessage: $("#nativeCommandMessage"),
|
|
92
|
+
nativeCommandSearch: $("#nativeCommandSearch"),
|
|
93
|
+
nativeCommandBody: $("#nativeCommandBody"),
|
|
94
|
+
nativeCommandError: $("#nativeCommandError"),
|
|
95
|
+
nativeCommandActions: $("#nativeCommandActions"),
|
|
75
96
|
};
|
|
76
97
|
|
|
77
98
|
let currentState = null;
|
|
78
99
|
let tabs = [];
|
|
79
100
|
let activeTabId = null;
|
|
101
|
+
let activeTabGeneration = 0;
|
|
80
102
|
let tabDrafts = new Map();
|
|
103
|
+
let tabAttachments = new Map();
|
|
81
104
|
let tabActivities = new Map();
|
|
82
105
|
let tabSeenCompletionSerials = new Map();
|
|
83
106
|
let streamBubble = null;
|
|
@@ -104,6 +127,7 @@ let refreshFooterTimer = null;
|
|
|
104
127
|
let refreshTabsTimer = null;
|
|
105
128
|
let eventSource = null;
|
|
106
129
|
let activeDialog = null;
|
|
130
|
+
let nativeCommandTabId = null;
|
|
107
131
|
let pathPickerState = null;
|
|
108
132
|
let pathFastPicks = [];
|
|
109
133
|
let pathFastPicksReady = false;
|
|
@@ -122,6 +146,8 @@ let pathSuggestAbortController = null;
|
|
|
122
146
|
let latestStats = null;
|
|
123
147
|
let latestWorkspace = null;
|
|
124
148
|
let latestNetwork = null;
|
|
149
|
+
let backendOffline = false;
|
|
150
|
+
let backendOfflineNoticeShown = false;
|
|
125
151
|
let latestMessages = [];
|
|
126
152
|
let transientMessages = [];
|
|
127
153
|
let actionEntrySeenKeysByTab = new Map();
|
|
@@ -133,12 +159,16 @@ let blockedTabNotificationKeys = new Set();
|
|
|
133
159
|
let blockedTabNotificationPermissionRequested = false;
|
|
134
160
|
let blockedTabNotificationFallbackNoted = false;
|
|
135
161
|
let agentDoneNotificationsEnabled = false;
|
|
162
|
+
let thinkingOutputVisible = true;
|
|
136
163
|
let agentDoneNotificationPermissionRequested = false;
|
|
137
164
|
let agentDoneNotificationFallbackNoted = false;
|
|
138
165
|
let agentDoneNotificationKeys = new Set();
|
|
139
166
|
let availableModels = [];
|
|
140
167
|
let availableThemes = [];
|
|
141
168
|
let currentThemeName = "catppuccin-mocha";
|
|
169
|
+
let customBackground = null;
|
|
170
|
+
let customBackgroundObjectUrl = null;
|
|
171
|
+
let customBackgroundLoading = false;
|
|
142
172
|
let footerScopedModels = [];
|
|
143
173
|
let footerScopedModelPatterns = [];
|
|
144
174
|
let footerScopedModelSource = "none";
|
|
@@ -154,14 +184,34 @@ let maxVisualViewportHeight = 0;
|
|
|
154
184
|
let currentRunStartedAt = null;
|
|
155
185
|
let currentRunStreamChars = 0;
|
|
156
186
|
let latestTokPerSecond = null;
|
|
187
|
+
let abortRequestInFlight = false;
|
|
188
|
+
let abortLongPressTimer = null;
|
|
189
|
+
let abortLongPressHandled = false;
|
|
157
190
|
const dialogQueue = [];
|
|
158
191
|
const SIDE_PANEL_STORAGE_KEY = "pi-webui-side-panel-collapsed";
|
|
192
|
+
const SIDE_PANEL_SECTION_STORAGE_KEY = "pi-webui-side-panel-sections-collapsed";
|
|
159
193
|
const TAB_STORAGE_KEY = "pi-webui-active-tab";
|
|
160
194
|
const PATH_FAST_PICKS_STORAGE_KEY = "pi-webui-path-fast-picks";
|
|
161
195
|
const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications";
|
|
196
|
+
const THINKING_VISIBILITY_STORAGE_KEY = "pi-webui-thinking-visible";
|
|
162
197
|
const THEME_STORAGE_KEY = "pi-webui-theme";
|
|
198
|
+
const CUSTOM_BACKGROUND_STORAGE_KEY = "pi-webui-custom-background";
|
|
199
|
+
const CUSTOM_BACKGROUNDS_STORAGE_KEY = "pi-webui-custom-backgrounds";
|
|
200
|
+
const CUSTOM_BACKGROUND_IDB_NAME = "pi-webui-custom-background";
|
|
201
|
+
const CUSTOM_BACKGROUND_IDB_STORE = "backgrounds";
|
|
202
|
+
const CUSTOM_BACKGROUND_LEGACY_ID = "active";
|
|
203
|
+
const SERVER_START_CWD_STORAGE_KEY = "pi-webui-last-server-cwd";
|
|
204
|
+
const DEFAULT_WEBUI_PORT = "31415";
|
|
205
|
+
const CUSTOM_BACKGROUND_MAX_FILE_BYTES = 24 * 1024 * 1024;
|
|
163
206
|
const OPTIONAL_FEATURES_STORAGE_KEY = "pi-webui-optional-features-disabled";
|
|
164
207
|
const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
|
|
208
|
+
const ATTACHMENT_MAX_FILES = 12;
|
|
209
|
+
const ATTACHMENT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
210
|
+
const ATTACHMENT_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
211
|
+
const ATTACHMENT_INLINE_IMAGE_MAX_BYTES = 8 * 1024 * 1024;
|
|
212
|
+
const ATTACHMENT_INLINE_IMAGE_TOTAL_MAX_BYTES = 16 * 1024 * 1024;
|
|
213
|
+
const INLINE_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
214
|
+
const BACKGROUND_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
165
215
|
const DEFAULT_THEME_NAME = "catppuccin-mocha";
|
|
166
216
|
const MOBILE_VIEW_QUERY = "(max-width: 720px), (max-device-width: 720px), (pointer: coarse) and (hover: none)";
|
|
167
217
|
const CHAT_BOTTOM_THRESHOLD_PX = 96;
|
|
@@ -173,9 +223,11 @@ const CHAT_USER_SCROLL_INTENT_MS = 700;
|
|
|
173
223
|
const RUN_INDICATOR_TICK_MS = 1000;
|
|
174
224
|
const RUN_INDICATOR_START_GRACE_MS = 2500;
|
|
175
225
|
const RUN_INDICATOR_STATE_RECHECK_MS = 5000;
|
|
226
|
+
const ABORT_LONG_PRESS_MS = 700;
|
|
176
227
|
const STREAM_OUTPUT_HIDE_DELAY_MS = 300;
|
|
177
228
|
const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220;
|
|
178
229
|
const STREAM_OUTPUT_MIN_VISIBLE_MS = 900;
|
|
230
|
+
const TOOL_LIVE_UPDATE_THROTTLE_MS = 80;
|
|
179
231
|
const TODO_PROGRESS_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)\]\s+.+$/;
|
|
180
232
|
const TODO_PROGRESS_PARTIAL_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)?\]?\s*.*$/;
|
|
181
233
|
const CHAT_SCROLL_KEYS = new Set(["ArrowDown", "ArrowUp", "End", "Home", "PageDown", "PageUp", " "]);
|
|
@@ -188,6 +240,10 @@ const BLOCKED_TAB_NOTIFICATION_ICON = "/icon-192.png";
|
|
|
188
240
|
const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
|
|
189
241
|
const statusEntries = new Map();
|
|
190
242
|
const widgets = new Map();
|
|
243
|
+
const liveToolRuns = new Map();
|
|
244
|
+
const liveToolCards = new Map();
|
|
245
|
+
const liveToolRenderQueue = new Map();
|
|
246
|
+
let liveToolRenderTimer = null;
|
|
191
247
|
// Optional feature detection intentionally checks loaded Pi capabilities (RPC-visible
|
|
192
248
|
// commands and live widget events), not npm package folders. This keeps local dev
|
|
193
249
|
// symlinks and independently installed packages working.
|
|
@@ -260,6 +316,8 @@ const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
|
260
316
|
["git-footer-refresh", "gitFooterStatus"],
|
|
261
317
|
["todo-progress-status", "todoProgressWidget"],
|
|
262
318
|
]);
|
|
319
|
+
const HIDDEN_COMMAND_NAMES = new Set(["webui-tree-navigate"]);
|
|
320
|
+
const NATIVE_SELECTOR_COMMANDS = new Set(["model", "settings", "theme", "fork", "clone", "resume", "tree", "login", "logout", "scoped-models"]);
|
|
263
321
|
const optionalFeatureInstallInProgress = new Set();
|
|
264
322
|
const gitWorkflow = {
|
|
265
323
|
active: false,
|
|
@@ -313,6 +371,63 @@ function readStoredSidePanelCollapsed() {
|
|
|
313
371
|
}
|
|
314
372
|
}
|
|
315
373
|
|
|
374
|
+
function sidePanelSectionRecords() {
|
|
375
|
+
return Array.from(elements.sidePanel.querySelectorAll("[data-side-panel-section]"))
|
|
376
|
+
.map((section) => {
|
|
377
|
+
const id = section.dataset.sidePanelSection || "";
|
|
378
|
+
const button = section.querySelector("[data-side-panel-section-toggle]");
|
|
379
|
+
const contentId = button?.getAttribute("aria-controls") || "";
|
|
380
|
+
const content = contentId ? document.getElementById(contentId) : null;
|
|
381
|
+
return { id, section, button, content };
|
|
382
|
+
})
|
|
383
|
+
.filter((record) => record.id && record.button && record.content);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function readStoredSidePanelSectionCollapsedIds() {
|
|
387
|
+
try {
|
|
388
|
+
const parsed = JSON.parse(localStorage.getItem(SIDE_PANEL_SECTION_STORAGE_KEY) || "[]");
|
|
389
|
+
return new Set(Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : []);
|
|
390
|
+
} catch {
|
|
391
|
+
return new Set();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function persistSidePanelSectionState() {
|
|
396
|
+
try {
|
|
397
|
+
const collapsed = sidePanelSectionRecords()
|
|
398
|
+
.filter(({ section }) => section.classList.contains("collapsed"))
|
|
399
|
+
.map(({ id }) => id);
|
|
400
|
+
localStorage.setItem(SIDE_PANEL_SECTION_STORAGE_KEY, JSON.stringify(collapsed));
|
|
401
|
+
} catch {
|
|
402
|
+
// Ignore storage failures; section toggles should still work for this page load.
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function setSidePanelSectionCollapsed(record, collapsed, { persist = true } = {}) {
|
|
407
|
+
const label = record.button.querySelector(".side-panel-section-label")?.textContent?.trim() || "side panel";
|
|
408
|
+
record.section.classList.toggle("collapsed", collapsed);
|
|
409
|
+
record.content.hidden = collapsed;
|
|
410
|
+
record.button.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
411
|
+
record.button.setAttribute("aria-label", `${collapsed ? "Expand" : "Collapse"} ${label} section`);
|
|
412
|
+
record.button.setAttribute("title", `${collapsed ? "Expand" : "Collapse"} ${label} section`);
|
|
413
|
+
if (persist) persistSidePanelSectionState();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function restoreSidePanelSectionState() {
|
|
417
|
+
const collapsedIds = readStoredSidePanelSectionCollapsedIds();
|
|
418
|
+
for (const record of sidePanelSectionRecords()) {
|
|
419
|
+
setSidePanelSectionCollapsed(record, collapsedIds.has(record.id), { persist: false });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function bindSidePanelSectionToggles() {
|
|
424
|
+
for (const record of sidePanelSectionRecords()) {
|
|
425
|
+
record.button.addEventListener("click", () => {
|
|
426
|
+
setSidePanelSectionCollapsed(record, !record.section.classList.contains("collapsed"));
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
316
431
|
function readStoredAgentDoneNotificationsEnabled() {
|
|
317
432
|
try {
|
|
318
433
|
return localStorage.getItem(AGENT_DONE_NOTIFICATIONS_STORAGE_KEY) === "1";
|
|
@@ -379,6 +494,55 @@ function restoreAgentDoneNotificationsSetting() {
|
|
|
379
494
|
renderAgentDoneNotificationsToggle();
|
|
380
495
|
}
|
|
381
496
|
|
|
497
|
+
function readStoredThinkingOutputVisible() {
|
|
498
|
+
try {
|
|
499
|
+
const stored = localStorage.getItem(THINKING_VISIBILITY_STORAGE_KEY);
|
|
500
|
+
return stored === null ? true : stored === "1";
|
|
501
|
+
} catch {
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function persistThinkingOutputVisible(visible) {
|
|
507
|
+
try {
|
|
508
|
+
localStorage.setItem(THINKING_VISIBILITY_STORAGE_KEY, visible ? "1" : "0");
|
|
509
|
+
} catch {
|
|
510
|
+
// Ignore storage failures; the toggle should still work for this page load.
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function thinkingVisibilityStatusText() {
|
|
515
|
+
return thinkingOutputVisible ? "Visible" : "Hidden from transcript";
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function renderThinkingVisibilityToggle() {
|
|
519
|
+
if (!elements.thinkingVisibilityToggle) return;
|
|
520
|
+
elements.thinkingVisibilityToggle.checked = thinkingOutputVisible;
|
|
521
|
+
elements.thinkingVisibilityToggle.setAttribute("aria-describedby", "thinkingVisibilityStatus");
|
|
522
|
+
if (elements.thinkingVisibilityStatus) elements.thinkingVisibilityStatus.textContent = thinkingVisibilityStatusText();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function removeStreamingThinkingBubble() {
|
|
526
|
+
streamThinkingBubble?.remove();
|
|
527
|
+
streamThinkingBubble = null;
|
|
528
|
+
streamThinking = null;
|
|
529
|
+
renderRunIndicator({ scroll: false });
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function setThinkingOutputVisible(visible, { announce = false } = {}) {
|
|
533
|
+
thinkingOutputVisible = !!visible;
|
|
534
|
+
persistThinkingOutputVisible(thinkingOutputVisible);
|
|
535
|
+
renderThinkingVisibilityToggle();
|
|
536
|
+
if (!thinkingOutputVisible) removeStreamingThinkingBubble();
|
|
537
|
+
renderAllMessages({ preserveScroll: true });
|
|
538
|
+
if (announce) addEvent(thinkingOutputVisible ? "thinking output shown" : "thinking output hidden", thinkingOutputVisible ? "info" : "warn");
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function restoreThinkingVisibilitySetting() {
|
|
542
|
+
thinkingOutputVisible = readStoredThinkingOutputVisible();
|
|
543
|
+
renderThinkingVisibilityToggle();
|
|
544
|
+
}
|
|
545
|
+
|
|
382
546
|
function setComposerActionsOpen(open) {
|
|
383
547
|
const shouldOpen = open && isMobileView();
|
|
384
548
|
document.body.classList.toggle("composer-actions-open", shouldOpen);
|
|
@@ -387,7 +551,11 @@ function setComposerActionsOpen(open) {
|
|
|
387
551
|
}
|
|
388
552
|
|
|
389
553
|
function isRunActive() {
|
|
390
|
-
return !!currentState?.isStreaming;
|
|
554
|
+
return !!currentState?.isStreaming || (runIndicatorLocallyActive && !currentState?.isCompacting);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function isAbortAvailable() {
|
|
558
|
+
return runIndicatorIsActive();
|
|
391
559
|
}
|
|
392
560
|
|
|
393
561
|
function resizePromptInput() {
|
|
@@ -401,12 +569,20 @@ function resizePromptInput() {
|
|
|
401
569
|
|
|
402
570
|
function updateComposerModeButtons() {
|
|
403
571
|
const runActive = isRunActive();
|
|
572
|
+
const abortAvailable = isAbortAvailable();
|
|
404
573
|
const target = runActive ? elements.composerRow : elements.composerActionsPanel;
|
|
405
|
-
const before = runActive ? elements.
|
|
574
|
+
const before = runActive ? elements.abortButton : null;
|
|
406
575
|
for (const button of [elements.steerButton, elements.followUpButton]) {
|
|
407
576
|
if (button.parentElement !== target) target.insertBefore(button, before);
|
|
577
|
+
button.hidden = !runActive;
|
|
578
|
+
button.disabled = !runActive;
|
|
408
579
|
}
|
|
409
|
-
|
|
580
|
+
elements.abortButton.hidden = !abortAvailable;
|
|
581
|
+
elements.abortButton.disabled = !abortAvailable || abortRequestInFlight;
|
|
582
|
+
elements.abortButton.textContent = abortRequestInFlight ? "Aborting…" : "Abort";
|
|
583
|
+
elements.abortButton.title = abortAvailable ? "Abort the active Pi run (Esc or hold)" : "Abort is available while Pi is running";
|
|
584
|
+
elements.abortButton.setAttribute("aria-label", elements.abortButton.title);
|
|
585
|
+
document.body.classList.toggle("pi-run-active", runActive || abortAvailable);
|
|
410
586
|
}
|
|
411
587
|
|
|
412
588
|
function updateFooterModelPickerPosition() {
|
|
@@ -538,6 +714,120 @@ function registerPwaServiceWorker() {
|
|
|
538
714
|
});
|
|
539
715
|
}
|
|
540
716
|
|
|
717
|
+
function readStoredServerStartCwd() {
|
|
718
|
+
try {
|
|
719
|
+
return localStorage.getItem(SERVER_START_CWD_STORAGE_KEY) || "";
|
|
720
|
+
} catch {
|
|
721
|
+
return "";
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function rememberServerStartCwd(cwd) {
|
|
726
|
+
const value = typeof cwd === "string" ? cwd.trim() : "";
|
|
727
|
+
if (!value) return;
|
|
728
|
+
try {
|
|
729
|
+
localStorage.setItem(SERVER_START_CWD_STORAGE_KEY, value);
|
|
730
|
+
} catch {
|
|
731
|
+
// Ignore storage failures; the offline start helper can still show a generic command.
|
|
732
|
+
}
|
|
733
|
+
if (backendOffline) renderServerOfflinePanel();
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function quoteCommandArg(value) {
|
|
737
|
+
const text = String(value || ".");
|
|
738
|
+
if (!/[\s"'`$]/.test(text)) return text;
|
|
739
|
+
if (!text.includes("'")) return `'${text}'`;
|
|
740
|
+
return `"${text.replace(/(["`$])/g, "\\$1")}"`;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function currentPortArg() {
|
|
744
|
+
const port = window.location.port || "";
|
|
745
|
+
return port && port !== DEFAULT_WEBUI_PORT ? ` --port ${port}` : "";
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function serverStartCommandText() {
|
|
749
|
+
const cwd = readStoredServerStartCwd() || ".";
|
|
750
|
+
return `pi-webui --cwd ${quoteCommandArg(cwd)}${currentPortArg()}`;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function serverStartSlashCommandText() {
|
|
754
|
+
return `/webui-start${currentPortArg()}`;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function renderServerOfflinePanel() {
|
|
758
|
+
if (elements.serverOfflineCommand) elements.serverOfflineCommand.textContent = serverStartCommandText();
|
|
759
|
+
if (elements.serverOfflineSlashCommand) elements.serverOfflineSlashCommand.textContent = serverStartSlashCommandText();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function setBackendOffline(offline, error) {
|
|
763
|
+
backendOffline = !!offline;
|
|
764
|
+
document.body.classList.toggle("server-offline", backendOffline);
|
|
765
|
+
if (elements.serverOfflinePanel) elements.serverOfflinePanel.hidden = !backendOffline;
|
|
766
|
+
renderServerOfflinePanel();
|
|
767
|
+
if (backendOffline) {
|
|
768
|
+
if (!backendOfflineNoticeShown) {
|
|
769
|
+
backendOfflineNoticeShown = true;
|
|
770
|
+
addEvent(`Pi Web UI server is offline${error?.message ? `: ${error.message}` : ""}`, "warn");
|
|
771
|
+
}
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
if (backendOfflineNoticeShown) addEvent("Pi Web UI server is back online", "info");
|
|
775
|
+
backendOfflineNoticeShown = false;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async function copyText(text) {
|
|
779
|
+
if (navigator.clipboard?.writeText && window.isSecureContext) {
|
|
780
|
+
await navigator.clipboard.writeText(text);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const textarea = document.createElement("textarea");
|
|
784
|
+
textarea.value = text;
|
|
785
|
+
textarea.setAttribute("readonly", "");
|
|
786
|
+
textarea.style.position = "fixed";
|
|
787
|
+
textarea.style.opacity = "0";
|
|
788
|
+
document.body.append(textarea);
|
|
789
|
+
textarea.select();
|
|
790
|
+
const copied = document.execCommand("copy");
|
|
791
|
+
textarea.remove();
|
|
792
|
+
if (!copied) throw new Error("Clipboard copy failed");
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function copyServerStartCommand() {
|
|
796
|
+
const command = serverStartCommandText();
|
|
797
|
+
try {
|
|
798
|
+
await copyText(command);
|
|
799
|
+
addEvent("copied Pi Web UI start command", "info");
|
|
800
|
+
} catch (error) {
|
|
801
|
+
addEvent(`copy failed; manually run: ${command}`, "warn");
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
async function retryServerConnection() {
|
|
806
|
+
const button = elements.retryServerConnectionButton;
|
|
807
|
+
if (button) {
|
|
808
|
+
button.disabled = true;
|
|
809
|
+
button.textContent = "Retrying…";
|
|
810
|
+
}
|
|
811
|
+
try {
|
|
812
|
+
await api("/api/health", { scoped: false });
|
|
813
|
+
} catch (error) {
|
|
814
|
+
setBackendOffline(true, error);
|
|
815
|
+
addEvent("Pi Web UI server is still offline", "warn");
|
|
816
|
+
return;
|
|
817
|
+
} finally {
|
|
818
|
+
if (button) {
|
|
819
|
+
button.disabled = false;
|
|
820
|
+
button.textContent = "Retry connection";
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
await initializeTabs();
|
|
826
|
+
} catch (error) {
|
|
827
|
+
addEvent(error.message || String(error), "error");
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
541
831
|
function scopedApiPath(path, tabId = activeTabId) {
|
|
542
832
|
if (!tabId || !path.startsWith("/api/") || path === "/api/tabs" || path.startsWith("/api/tabs?") || path.startsWith("/api/tabs/")) return path;
|
|
543
833
|
const url = new URL(path, window.location.origin);
|
|
@@ -546,12 +836,21 @@ function scopedApiPath(path, tabId = activeTabId) {
|
|
|
546
836
|
}
|
|
547
837
|
|
|
548
838
|
async function api(path, { method = "GET", body, tabId = activeTabId, scoped = true, signal } = {}) {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
839
|
+
let response;
|
|
840
|
+
try {
|
|
841
|
+
response = await fetch(scoped ? scopedApiPath(path, tabId) : path, {
|
|
842
|
+
method,
|
|
843
|
+
headers: body === undefined ? undefined : { "content-type": "application/json" },
|
|
844
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
845
|
+
signal,
|
|
846
|
+
});
|
|
847
|
+
} catch (error) {
|
|
848
|
+
const offlineError = error instanceof Error ? error : new Error(String(error));
|
|
849
|
+
offlineError.backendOffline = true;
|
|
850
|
+
setBackendOffline(true, offlineError);
|
|
851
|
+
throw offlineError;
|
|
852
|
+
}
|
|
853
|
+
setBackendOffline(false);
|
|
555
854
|
const data = await response.json().catch(() => ({}));
|
|
556
855
|
if (!response.ok) {
|
|
557
856
|
const error = new Error(data.error || data.message || JSON.stringify(data));
|
|
@@ -562,6 +861,633 @@ async function api(path, { method = "GET", body, tabId = activeTabId, scoped = t
|
|
|
562
861
|
return data;
|
|
563
862
|
}
|
|
564
863
|
|
|
864
|
+
function formatBytes(bytes) {
|
|
865
|
+
const value = Number(bytes) || 0;
|
|
866
|
+
if (value < 1024) return `${value} B`;
|
|
867
|
+
const units = ["KB", "MB", "GB"];
|
|
868
|
+
let scaled = value / 1024;
|
|
869
|
+
for (const unit of units) {
|
|
870
|
+
if (scaled < 1024 || unit === units[units.length - 1]) return `${scaled.toFixed(scaled >= 10 ? 1 : 2)} ${unit}`;
|
|
871
|
+
scaled /= 1024;
|
|
872
|
+
}
|
|
873
|
+
return `${value} B`;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function inferMimeTypeFromName(name = "") {
|
|
877
|
+
const ext = String(name).split(".").pop()?.toLowerCase() || "";
|
|
878
|
+
const map = {
|
|
879
|
+
md: "text/markdown",
|
|
880
|
+
markdown: "text/markdown",
|
|
881
|
+
txt: "text/plain",
|
|
882
|
+
log: "text/plain",
|
|
883
|
+
csv: "text/csv",
|
|
884
|
+
json: "application/json",
|
|
885
|
+
xml: "application/xml",
|
|
886
|
+
yaml: "application/x-yaml",
|
|
887
|
+
yml: "application/x-yaml",
|
|
888
|
+
toml: "application/toml",
|
|
889
|
+
pdf: "application/pdf",
|
|
890
|
+
doc: "application/msword",
|
|
891
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
892
|
+
mp3: "audio/mpeg",
|
|
893
|
+
wav: "audio/wav",
|
|
894
|
+
m4a: "audio/mp4",
|
|
895
|
+
mp4: "video/mp4",
|
|
896
|
+
mov: "video/quicktime",
|
|
897
|
+
webm: "video/webm",
|
|
898
|
+
};
|
|
899
|
+
return map[ext] || "application/octet-stream";
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function attachmentKind(mimeType = "", name = "") {
|
|
903
|
+
const type = String(mimeType || inferMimeTypeFromName(name));
|
|
904
|
+
if (type.startsWith("image/")) return "image";
|
|
905
|
+
if (type.startsWith("video/")) return "video";
|
|
906
|
+
if (type.startsWith("audio/")) return "audio";
|
|
907
|
+
if (type.startsWith("text/") || /(?:json|xml|pdf|word|excel|powerpoint|document|spreadsheet|presentation|markdown|csv)/i.test(type)) return "doc";
|
|
908
|
+
return "file";
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function attachmentIcon(kind) {
|
|
912
|
+
return kind === "image" ? "🖼️" : kind === "video" ? "🎞️" : kind === "audio" ? "🎵" : kind === "doc" ? "📄" : "📎";
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function attachmentsForTab(tabId = activeTabId) {
|
|
916
|
+
return tabId ? tabAttachments.get(tabId) || [] : [];
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function ensureAttachmentsForTab(tabId = activeTabId) {
|
|
920
|
+
if (!tabId) return [];
|
|
921
|
+
if (!tabAttachments.has(tabId)) tabAttachments.set(tabId, []);
|
|
922
|
+
return tabAttachments.get(tabId);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function hasComposerPayload() {
|
|
926
|
+
return !!elements.promptInput.value.trim() || attachmentsForTab().length > 0;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function renderAttachmentTray() {
|
|
930
|
+
const tray = elements.attachmentTray;
|
|
931
|
+
if (!tray) return;
|
|
932
|
+
const attachments = attachmentsForTab();
|
|
933
|
+
tray.innerHTML = "";
|
|
934
|
+
tray.hidden = attachments.length === 0;
|
|
935
|
+
if (attachments.length === 0) return;
|
|
936
|
+
|
|
937
|
+
for (const attachment of attachments) {
|
|
938
|
+
const pill = make("span", "attachment-pill");
|
|
939
|
+
pill.title = `${attachment.name}\n${attachment.mimeType}\n${formatBytes(attachment.size)}`;
|
|
940
|
+
const icon = make("span", "attachment-pill-icon", attachmentIcon(attachment.kind));
|
|
941
|
+
const name = make("span", "attachment-pill-name", attachment.name);
|
|
942
|
+
const meta = make("span", "attachment-pill-meta", `${attachment.kind} · ${formatBytes(attachment.size)}`);
|
|
943
|
+
const remove = make("button", "attachment-remove-button", "×");
|
|
944
|
+
remove.type = "button";
|
|
945
|
+
remove.setAttribute("aria-label", `Remove ${attachment.name}`);
|
|
946
|
+
remove.addEventListener("click", () => removeAttachment(attachment.id));
|
|
947
|
+
pill.append(icon, name, meta, remove);
|
|
948
|
+
tray.append(pill);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function removeAttachment(id, tabId = activeTabId) {
|
|
953
|
+
const attachments = attachmentsForTab(tabId);
|
|
954
|
+
const index = attachments.findIndex((attachment) => attachment.id === id);
|
|
955
|
+
if (index === -1) return;
|
|
956
|
+
const [removed] = attachments.splice(index, 1);
|
|
957
|
+
if (removed?.previewUrl) URL.revokeObjectURL(removed.previewUrl);
|
|
958
|
+
if (attachments.length === 0) tabAttachments.delete(tabId);
|
|
959
|
+
if (tabId === activeTabId) renderAttachmentTray();
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function clearAttachments(tabId = activeTabId) {
|
|
963
|
+
const attachments = attachmentsForTab(tabId);
|
|
964
|
+
for (const attachment of attachments) {
|
|
965
|
+
if (attachment.previewUrl) URL.revokeObjectURL(attachment.previewUrl);
|
|
966
|
+
}
|
|
967
|
+
if (tabId) tabAttachments.delete(tabId);
|
|
968
|
+
if (tabId === activeTabId) renderAttachmentTray();
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function addAttachmentFiles(fileList, source = "picker") {
|
|
972
|
+
const files = Array.from(fileList || []).filter(Boolean);
|
|
973
|
+
if (!files.length) return;
|
|
974
|
+
const attachments = ensureAttachmentsForTab();
|
|
975
|
+
if (!attachments.length && !activeTabId) return;
|
|
976
|
+
let totalBytes = attachments.reduce((sum, attachment) => sum + attachment.size, 0);
|
|
977
|
+
let added = 0;
|
|
978
|
+
const skipped = [];
|
|
979
|
+
|
|
980
|
+
for (const file of files) {
|
|
981
|
+
const name = file.name || `${source}-attachment`;
|
|
982
|
+
if (attachments.length >= ATTACHMENT_MAX_FILES) {
|
|
983
|
+
skipped.push(`${name}: attachment limit is ${ATTACHMENT_MAX_FILES}`);
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
if (file.size > ATTACHMENT_MAX_FILE_BYTES) {
|
|
987
|
+
skipped.push(`${name}: larger than ${formatBytes(ATTACHMENT_MAX_FILE_BYTES)}`);
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
if (totalBytes + file.size > ATTACHMENT_MAX_TOTAL_BYTES) {
|
|
991
|
+
skipped.push(`${name}: total attachment limit is ${formatBytes(ATTACHMENT_MAX_TOTAL_BYTES)}`);
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
const mimeType = file.type || inferMimeTypeFromName(name);
|
|
995
|
+
const kind = attachmentKind(mimeType, name);
|
|
996
|
+
attachments.push({
|
|
997
|
+
id: `att-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
998
|
+
file,
|
|
999
|
+
name,
|
|
1000
|
+
mimeType,
|
|
1001
|
+
size: file.size || 0,
|
|
1002
|
+
source,
|
|
1003
|
+
kind,
|
|
1004
|
+
previewUrl: kind === "image" ? URL.createObjectURL(file) : undefined,
|
|
1005
|
+
});
|
|
1006
|
+
totalBytes += file.size || 0;
|
|
1007
|
+
added++;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
renderAttachmentTray();
|
|
1011
|
+
if (added) addEvent(`attached ${added} ${added === 1 ? "file" : "files"} from ${source}`, "info");
|
|
1012
|
+
if (skipped.length) addEvent(`skipped attachments: ${skipped.join("; ")}`, "warn");
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function clipboardFiles(dataTransfer) {
|
|
1016
|
+
const files = [];
|
|
1017
|
+
const seen = new Set();
|
|
1018
|
+
for (const file of Array.from(dataTransfer?.files || [])) {
|
|
1019
|
+
const key = `${file.name}:${file.size}:${file.type}`;
|
|
1020
|
+
if (!seen.has(key)) {
|
|
1021
|
+
seen.add(key);
|
|
1022
|
+
files.push(file);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
for (const item of Array.from(dataTransfer?.items || [])) {
|
|
1026
|
+
if (item.kind !== "file") continue;
|
|
1027
|
+
const file = item.getAsFile?.();
|
|
1028
|
+
if (!file) continue;
|
|
1029
|
+
const key = `${file.name}:${file.size}:${file.type}`;
|
|
1030
|
+
if (!seen.has(key)) {
|
|
1031
|
+
seen.add(key);
|
|
1032
|
+
files.push(file);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return files;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function handleAttachmentPaste(event) {
|
|
1039
|
+
const files = clipboardFiles(event.clipboardData);
|
|
1040
|
+
if (!files.length) return;
|
|
1041
|
+
event.preventDefault();
|
|
1042
|
+
addAttachmentFiles(files, "clipboard");
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function isFileDrag(event) {
|
|
1046
|
+
return Array.from(event.dataTransfer?.types || []).includes("Files");
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function handleComposerDragOver(event) {
|
|
1050
|
+
if (!isFileDrag(event)) return;
|
|
1051
|
+
event.preventDefault();
|
|
1052
|
+
elements.composer.classList.add("drag-over");
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function handleComposerDragLeave(event) {
|
|
1056
|
+
if (!elements.composer.contains(event.relatedTarget)) elements.composer.classList.remove("drag-over");
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function handleComposerDrop(event) {
|
|
1060
|
+
if (!isFileDrag(event)) return;
|
|
1061
|
+
event.preventDefault();
|
|
1062
|
+
elements.composer.classList.remove("drag-over");
|
|
1063
|
+
addAttachmentFiles(event.dataTransfer?.files, "drop");
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function readFileAsBase64(file) {
|
|
1067
|
+
return new Promise((resolve, reject) => {
|
|
1068
|
+
const reader = new FileReader();
|
|
1069
|
+
reader.onerror = () => reject(reader.error || new Error("Failed to read attachment"));
|
|
1070
|
+
reader.onload = () => {
|
|
1071
|
+
const result = String(reader.result || "");
|
|
1072
|
+
const comma = result.indexOf(",");
|
|
1073
|
+
resolve(comma === -1 ? result : result.slice(comma + 1));
|
|
1074
|
+
};
|
|
1075
|
+
reader.readAsDataURL(file);
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function readFileAsDataUrl(file) {
|
|
1080
|
+
return new Promise((resolve, reject) => {
|
|
1081
|
+
const reader = new FileReader();
|
|
1082
|
+
reader.onerror = () => reject(reader.error || new Error("Failed to read background image"));
|
|
1083
|
+
reader.onload = () => resolve(String(reader.result || ""));
|
|
1084
|
+
reader.readAsDataURL(file);
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function sanitizeBackgroundName(name) {
|
|
1089
|
+
const safe = String(name || "custom background").replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
1090
|
+
return safe || "custom background";
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function backgroundMimeType(file) {
|
|
1094
|
+
const declared = String(file?.type || "").split(";", 1)[0].trim().toLowerCase();
|
|
1095
|
+
if (BACKGROUND_IMAGE_MIME_TYPES.has(declared)) return declared;
|
|
1096
|
+
const ext = String(file?.name || "").split(".").pop()?.toLowerCase() || "";
|
|
1097
|
+
const byExt = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp", gif: "image/gif" };
|
|
1098
|
+
return byExt[ext] || declared || "application/octet-stream";
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function normalizeCustomBackgroundRecord(value) {
|
|
1102
|
+
if (!value || typeof value !== "object") return null;
|
|
1103
|
+
const dataUrl = String(value.dataUrl || "");
|
|
1104
|
+
const match = dataUrl.match(/^data:(image\/(?:png|jpeg|webp|gif));base64,[A-Za-z0-9+/]+={0,2}$/i);
|
|
1105
|
+
if (!match) return null;
|
|
1106
|
+
return {
|
|
1107
|
+
name: sanitizeBackgroundName(value.name),
|
|
1108
|
+
mimeType: match[1].toLowerCase(),
|
|
1109
|
+
size: Math.max(0, Number(value.size) || 0),
|
|
1110
|
+
dataUrl,
|
|
1111
|
+
updatedAt: Number(value.updatedAt) || Date.now(),
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function dataUrlToBlob(dataUrl) {
|
|
1116
|
+
const match = String(dataUrl || "").match(/^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/]+={0,2})$/i);
|
|
1117
|
+
if (!match) throw new Error("Invalid background data URL");
|
|
1118
|
+
const binary = atob(match[2]);
|
|
1119
|
+
const bytes = new Uint8Array(binary.length);
|
|
1120
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
1121
|
+
return new Blob([bytes], { type: match[1].toLowerCase() });
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function revokeCustomBackgroundObjectUrl() {
|
|
1125
|
+
if (!customBackgroundObjectUrl) return;
|
|
1126
|
+
URL.revokeObjectURL(customBackgroundObjectUrl);
|
|
1127
|
+
customBackgroundObjectUrl = null;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function setCustomBackgroundRecord(background, { objectUrl = null } = {}) {
|
|
1131
|
+
const record = normalizeCustomBackgroundRecord(background);
|
|
1132
|
+
revokeCustomBackgroundObjectUrl();
|
|
1133
|
+
customBackground = record;
|
|
1134
|
+
if (!record) return null;
|
|
1135
|
+
if (objectUrl) customBackgroundObjectUrl = objectUrl;
|
|
1136
|
+
else {
|
|
1137
|
+
try {
|
|
1138
|
+
customBackgroundObjectUrl = URL.createObjectURL(dataUrlToBlob(record.dataUrl));
|
|
1139
|
+
} catch {
|
|
1140
|
+
customBackgroundObjectUrl = null;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return record;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function idbRequest(request) {
|
|
1147
|
+
return new Promise((resolve, reject) => {
|
|
1148
|
+
request.onsuccess = () => resolve(request.result);
|
|
1149
|
+
request.onerror = () => reject(request.error || new Error("IndexedDB request failed"));
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function idbTransactionDone(transaction) {
|
|
1154
|
+
return new Promise((resolve, reject) => {
|
|
1155
|
+
transaction.oncomplete = () => resolve();
|
|
1156
|
+
transaction.onerror = () => reject(transaction.error || new Error("IndexedDB transaction failed"));
|
|
1157
|
+
transaction.onabort = () => reject(transaction.error || new Error("IndexedDB transaction aborted"));
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function openCustomBackgroundDb() {
|
|
1162
|
+
return new Promise((resolve, reject) => {
|
|
1163
|
+
const indexedDb = window.indexedDB;
|
|
1164
|
+
if (!indexedDb) {
|
|
1165
|
+
reject(new Error("IndexedDB unavailable"));
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const request = indexedDb.open(CUSTOM_BACKGROUND_IDB_NAME, 1);
|
|
1169
|
+
request.onupgradeneeded = () => {
|
|
1170
|
+
const db = request.result;
|
|
1171
|
+
if (!db.objectStoreNames.contains(CUSTOM_BACKGROUND_IDB_STORE)) db.createObjectStore(CUSTOM_BACKGROUND_IDB_STORE);
|
|
1172
|
+
};
|
|
1173
|
+
request.onsuccess = () => resolve(request.result);
|
|
1174
|
+
request.onerror = () => reject(request.error || new Error("Failed to open background storage"));
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function customBackgroundThemeKey(themeName = currentThemeName) {
|
|
1179
|
+
return String(themeName || DEFAULT_THEME_NAME).trim() || DEFAULT_THEME_NAME;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
async function readCustomBackgroundFromIndexedDb(themeName = currentThemeName) {
|
|
1183
|
+
const db = await openCustomBackgroundDb();
|
|
1184
|
+
try {
|
|
1185
|
+
return await idbRequest(db.transaction(CUSTOM_BACKGROUND_IDB_STORE, "readonly").objectStore(CUSTOM_BACKGROUND_IDB_STORE).get(customBackgroundThemeKey(themeName)));
|
|
1186
|
+
} finally {
|
|
1187
|
+
db.close();
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
async function readLegacyCustomBackgroundFromIndexedDb() {
|
|
1192
|
+
const db = await openCustomBackgroundDb();
|
|
1193
|
+
try {
|
|
1194
|
+
return await idbRequest(db.transaction(CUSTOM_BACKGROUND_IDB_STORE, "readonly").objectStore(CUSTOM_BACKGROUND_IDB_STORE).get(CUSTOM_BACKGROUND_LEGACY_ID));
|
|
1195
|
+
} finally {
|
|
1196
|
+
db.close();
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
async function writeCustomBackgroundToIndexedDb(background, themeName = currentThemeName) {
|
|
1201
|
+
const db = await openCustomBackgroundDb();
|
|
1202
|
+
try {
|
|
1203
|
+
const transaction = db.transaction(CUSTOM_BACKGROUND_IDB_STORE, "readwrite");
|
|
1204
|
+
transaction.objectStore(CUSTOM_BACKGROUND_IDB_STORE).put(background, customBackgroundThemeKey(themeName));
|
|
1205
|
+
await idbTransactionDone(transaction);
|
|
1206
|
+
} finally {
|
|
1207
|
+
db.close();
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
async function deleteCustomBackgroundFromIndexedDb(themeName = currentThemeName) {
|
|
1212
|
+
const db = await openCustomBackgroundDb();
|
|
1213
|
+
try {
|
|
1214
|
+
const transaction = db.transaction(CUSTOM_BACKGROUND_IDB_STORE, "readwrite");
|
|
1215
|
+
transaction.objectStore(CUSTOM_BACKGROUND_IDB_STORE).delete(customBackgroundThemeKey(themeName));
|
|
1216
|
+
await idbTransactionDone(transaction);
|
|
1217
|
+
} finally {
|
|
1218
|
+
db.close();
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
async function deleteLegacyCustomBackgroundFromIndexedDb() {
|
|
1223
|
+
const db = await openCustomBackgroundDb();
|
|
1224
|
+
try {
|
|
1225
|
+
const transaction = db.transaction(CUSTOM_BACKGROUND_IDB_STORE, "readwrite");
|
|
1226
|
+
transaction.objectStore(CUSTOM_BACKGROUND_IDB_STORE).delete(CUSTOM_BACKGROUND_LEGACY_ID);
|
|
1227
|
+
await idbTransactionDone(transaction);
|
|
1228
|
+
} finally {
|
|
1229
|
+
db.close();
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function readCustomBackgroundFromLocalStorage(themeName = currentThemeName, { includeLegacy = false } = {}) {
|
|
1234
|
+
try {
|
|
1235
|
+
const parsed = JSON.parse(localStorage.getItem(CUSTOM_BACKGROUNDS_STORAGE_KEY) || "{}");
|
|
1236
|
+
const record = parsed && typeof parsed === "object" ? normalizeCustomBackgroundRecord(parsed[customBackgroundThemeKey(themeName)]) : null;
|
|
1237
|
+
if (record) return record;
|
|
1238
|
+
} catch {
|
|
1239
|
+
// Fall through to legacy storage below.
|
|
1240
|
+
}
|
|
1241
|
+
if (!includeLegacy) return null;
|
|
1242
|
+
try {
|
|
1243
|
+
return normalizeCustomBackgroundRecord(JSON.parse(localStorage.getItem(CUSTOM_BACKGROUND_STORAGE_KEY) || "null"));
|
|
1244
|
+
} catch {
|
|
1245
|
+
return null;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function writeCustomBackgroundToLocalStorage(background, themeName = currentThemeName) {
|
|
1250
|
+
const record = normalizeCustomBackgroundRecord(background);
|
|
1251
|
+
if (!record) throw new Error("Invalid background image data");
|
|
1252
|
+
const key = customBackgroundThemeKey(themeName);
|
|
1253
|
+
const parsed = JSON.parse(localStorage.getItem(CUSTOM_BACKGROUNDS_STORAGE_KEY) || "{}");
|
|
1254
|
+
const backgrounds = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1255
|
+
backgrounds[key] = record;
|
|
1256
|
+
localStorage.setItem(CUSTOM_BACKGROUNDS_STORAGE_KEY, JSON.stringify(backgrounds));
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function removeCustomBackgroundFromLocalStorage(themeName = currentThemeName, { includeLegacy = false } = {}) {
|
|
1260
|
+
const key = customBackgroundThemeKey(themeName);
|
|
1261
|
+
try {
|
|
1262
|
+
const parsed = JSON.parse(localStorage.getItem(CUSTOM_BACKGROUNDS_STORAGE_KEY) || "{}");
|
|
1263
|
+
const backgrounds = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1264
|
+
delete backgrounds[key];
|
|
1265
|
+
localStorage.setItem(CUSTOM_BACKGROUNDS_STORAGE_KEY, JSON.stringify(backgrounds));
|
|
1266
|
+
} catch {
|
|
1267
|
+
// Ignore fallback cleanup failures.
|
|
1268
|
+
}
|
|
1269
|
+
if (includeLegacy) {
|
|
1270
|
+
try {
|
|
1271
|
+
localStorage.removeItem(CUSTOM_BACKGROUND_STORAGE_KEY);
|
|
1272
|
+
} catch {
|
|
1273
|
+
// Ignore legacy cleanup failures.
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
async function readStoredCustomBackground(themeName = currentThemeName, { includeLegacy = false } = {}) {
|
|
1279
|
+
try {
|
|
1280
|
+
const stored = normalizeCustomBackgroundRecord(await readCustomBackgroundFromIndexedDb(themeName));
|
|
1281
|
+
if (stored) return stored;
|
|
1282
|
+
if (includeLegacy) {
|
|
1283
|
+
const legacy = normalizeCustomBackgroundRecord(await readLegacyCustomBackgroundFromIndexedDb());
|
|
1284
|
+
if (legacy) return legacy;
|
|
1285
|
+
}
|
|
1286
|
+
} catch {
|
|
1287
|
+
// Fall back to localStorage for older browsers or private browsing modes.
|
|
1288
|
+
}
|
|
1289
|
+
return readCustomBackgroundFromLocalStorage(themeName, { includeLegacy });
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
async function persistCustomBackground(background, themeName = currentThemeName) {
|
|
1293
|
+
const record = normalizeCustomBackgroundRecord(background);
|
|
1294
|
+
if (!record) throw new Error("Invalid background image data");
|
|
1295
|
+
try {
|
|
1296
|
+
await writeCustomBackgroundToIndexedDb(record, themeName);
|
|
1297
|
+
removeCustomBackgroundFromLocalStorage(themeName);
|
|
1298
|
+
return;
|
|
1299
|
+
} catch {
|
|
1300
|
+
// Fall back to localStorage when IndexedDB is unavailable.
|
|
1301
|
+
}
|
|
1302
|
+
writeCustomBackgroundToLocalStorage(record, themeName);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
async function clearStoredCustomBackground(themeName = currentThemeName, { includeLegacy = false } = {}) {
|
|
1306
|
+
await Promise.allSettled([
|
|
1307
|
+
deleteCustomBackgroundFromIndexedDb(themeName),
|
|
1308
|
+
includeLegacy ? deleteLegacyCustomBackgroundFromIndexedDb() : Promise.resolve(),
|
|
1309
|
+
Promise.resolve().then(() => removeCustomBackgroundFromLocalStorage(themeName, { includeLegacy })),
|
|
1310
|
+
]);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function customBackgroundCssImage(background = customBackground) {
|
|
1314
|
+
if (!background?.dataUrl) return null;
|
|
1315
|
+
return `url("${customBackgroundObjectUrl || background.dataUrl}")`;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function renderBackgroundControl() {
|
|
1319
|
+
if (!elements.backgroundStatus) return;
|
|
1320
|
+
const active = !!customBackground?.dataUrl;
|
|
1321
|
+
const themeLabel = displayThemeName(currentThemeName) || currentThemeName || "theme";
|
|
1322
|
+
elements.backgroundStatus.textContent = customBackgroundLoading
|
|
1323
|
+
? `Loading ${themeLabel} background…`
|
|
1324
|
+
: active
|
|
1325
|
+
? `${themeLabel}: ${customBackground.name || "background"}`
|
|
1326
|
+
: `${themeLabel}: theme default`;
|
|
1327
|
+
if (elements.backgroundChooseButton) {
|
|
1328
|
+
elements.backgroundChooseButton.disabled = customBackgroundLoading;
|
|
1329
|
+
elements.backgroundChooseButton.textContent = active ? "Change background" : "Add background";
|
|
1330
|
+
}
|
|
1331
|
+
if (elements.backgroundInput) elements.backgroundInput.disabled = customBackgroundLoading;
|
|
1332
|
+
if (elements.backgroundClearButton) {
|
|
1333
|
+
elements.backgroundClearButton.hidden = !active;
|
|
1334
|
+
elements.backgroundClearButton.disabled = customBackgroundLoading;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function applyCustomBackgroundOverride({ render = true } = {}) {
|
|
1339
|
+
const activeImage = customBackgroundCssImage();
|
|
1340
|
+
document.body.classList.toggle("custom-background-active", !!activeImage);
|
|
1341
|
+
if (activeImage) document.documentElement.style.setProperty("--theme-background-image", activeImage);
|
|
1342
|
+
if (render) renderBackgroundControl();
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function reapplyCurrentThemeBackground() {
|
|
1346
|
+
const theme = availableThemes.find((item) => item.name === currentThemeName);
|
|
1347
|
+
if (theme && isOptionalFeatureEnabled("themeBundle")) applyTheme(theme, { persist: false });
|
|
1348
|
+
else {
|
|
1349
|
+
document.documentElement.style.setProperty("--theme-background-image", "none");
|
|
1350
|
+
applyCustomBackgroundOverride();
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
async function loadCustomBackgroundForTheme(themeName = currentThemeName, { includeLegacy = false } = {}) {
|
|
1355
|
+
const themeKey = customBackgroundThemeKey(themeName);
|
|
1356
|
+
customBackgroundLoading = true;
|
|
1357
|
+
renderBackgroundControl();
|
|
1358
|
+
try {
|
|
1359
|
+
const background = await readStoredCustomBackground(themeKey, { includeLegacy });
|
|
1360
|
+
if (customBackgroundThemeKey(currentThemeName) !== themeKey) return;
|
|
1361
|
+
setCustomBackgroundRecord(background);
|
|
1362
|
+
if (background && includeLegacy) {
|
|
1363
|
+
persistCustomBackground(background, themeKey).catch(() => {});
|
|
1364
|
+
}
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
if (customBackgroundThemeKey(currentThemeName) === themeKey) {
|
|
1367
|
+
addEvent(`failed to load ${displayThemeName(themeKey) || themeKey} background: ${error.message || String(error)}`, "warn");
|
|
1368
|
+
setCustomBackgroundRecord(null);
|
|
1369
|
+
}
|
|
1370
|
+
} finally {
|
|
1371
|
+
if (customBackgroundThemeKey(currentThemeName) === themeKey) {
|
|
1372
|
+
customBackgroundLoading = false;
|
|
1373
|
+
applyCustomBackgroundOverride();
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
async function setCustomBackgroundFromFile(file) {
|
|
1379
|
+
if (!file) return;
|
|
1380
|
+
const mimeType = backgroundMimeType(file);
|
|
1381
|
+
if (!BACKGROUND_IMAGE_MIME_TYPES.has(mimeType)) {
|
|
1382
|
+
addEvent("background must be a PNG, JPEG, WebP, or GIF image", "error");
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
if ((file.size || 0) > CUSTOM_BACKGROUND_MAX_FILE_BYTES) {
|
|
1386
|
+
addEvent(`background image is larger than ${formatBytes(CUSTOM_BACKGROUND_MAX_FILE_BYTES)}`, "error");
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
const themeName = customBackgroundThemeKey(currentThemeName);
|
|
1391
|
+
customBackgroundLoading = true;
|
|
1392
|
+
renderBackgroundControl();
|
|
1393
|
+
try {
|
|
1394
|
+
const rawDataUrl = await readFileAsDataUrl(file);
|
|
1395
|
+
const dataUrl = rawDataUrl.replace(/^data:;base64,/i, `data:${mimeType};base64,`);
|
|
1396
|
+
const background = normalizeCustomBackgroundRecord({
|
|
1397
|
+
name: file.name,
|
|
1398
|
+
mimeType,
|
|
1399
|
+
size: file.size || 0,
|
|
1400
|
+
dataUrl,
|
|
1401
|
+
updatedAt: Date.now(),
|
|
1402
|
+
});
|
|
1403
|
+
if (!background) throw new Error("Unsupported or invalid background image data");
|
|
1404
|
+
let objectUrl = null;
|
|
1405
|
+
try {
|
|
1406
|
+
objectUrl = URL.createObjectURL(file);
|
|
1407
|
+
} catch {
|
|
1408
|
+
objectUrl = null;
|
|
1409
|
+
}
|
|
1410
|
+
const targetStillActive = customBackgroundThemeKey(currentThemeName) === themeName;
|
|
1411
|
+
if (targetStillActive) {
|
|
1412
|
+
setCustomBackgroundRecord(background, { objectUrl });
|
|
1413
|
+
applyCustomBackgroundOverride({ render: false });
|
|
1414
|
+
} else if (objectUrl) {
|
|
1415
|
+
URL.revokeObjectURL(objectUrl);
|
|
1416
|
+
}
|
|
1417
|
+
try {
|
|
1418
|
+
await persistCustomBackground(background, themeName);
|
|
1419
|
+
addEvent(`custom background saved for ${displayThemeName(themeName) || themeName}: ${background.name}`);
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
addEvent(`background changed for this page, but persistent save failed: ${error.message || String(error)}`, "warn");
|
|
1422
|
+
}
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
addEvent(`failed to set background: ${error.message || String(error)}`, "error");
|
|
1425
|
+
} finally {
|
|
1426
|
+
if (customBackgroundThemeKey(currentThemeName) === themeName) {
|
|
1427
|
+
customBackgroundLoading = false;
|
|
1428
|
+
renderBackgroundControl();
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
async function clearCustomBackground() {
|
|
1434
|
+
const themeName = customBackgroundThemeKey(currentThemeName);
|
|
1435
|
+
const hadBackground = !!customBackground?.dataUrl;
|
|
1436
|
+
setCustomBackgroundRecord(null);
|
|
1437
|
+
customBackgroundLoading = true;
|
|
1438
|
+
renderBackgroundControl();
|
|
1439
|
+
await clearStoredCustomBackground(themeName, { includeLegacy: true });
|
|
1440
|
+
customBackgroundLoading = false;
|
|
1441
|
+
reapplyCurrentThemeBackground();
|
|
1442
|
+
renderBackgroundControl();
|
|
1443
|
+
if (hadBackground) addEvent(`custom background removed for ${displayThemeName(themeName) || themeName}`);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
async function initializeCustomBackground() {
|
|
1447
|
+
await loadCustomBackgroundForTheme(currentThemeName, { includeLegacy: true });
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
async function prepareAttachmentsForPrompt(attachments, tabId) {
|
|
1451
|
+
if (!attachments.length) return { images: [], uploadedFiles: [], inlineImageIds: new Set() };
|
|
1452
|
+
const files = [];
|
|
1453
|
+
const images = [];
|
|
1454
|
+
const inlineImageIds = new Set();
|
|
1455
|
+
let inlineImageBytes = 0;
|
|
1456
|
+
|
|
1457
|
+
for (const attachment of attachments) {
|
|
1458
|
+
const data = await readFileAsBase64(attachment.file);
|
|
1459
|
+
files.push({
|
|
1460
|
+
id: attachment.id,
|
|
1461
|
+
name: attachment.name,
|
|
1462
|
+
mimeType: attachment.mimeType,
|
|
1463
|
+
size: attachment.size,
|
|
1464
|
+
data,
|
|
1465
|
+
});
|
|
1466
|
+
if (
|
|
1467
|
+
INLINE_IMAGE_MIME_TYPES.has(attachment.mimeType) &&
|
|
1468
|
+
attachment.size <= ATTACHMENT_INLINE_IMAGE_MAX_BYTES &&
|
|
1469
|
+
inlineImageBytes + attachment.size <= ATTACHMENT_INLINE_IMAGE_TOTAL_MAX_BYTES
|
|
1470
|
+
) {
|
|
1471
|
+
images.push({ type: "image", data, mimeType: attachment.mimeType });
|
|
1472
|
+
inlineImageIds.add(attachment.id);
|
|
1473
|
+
inlineImageBytes += attachment.size;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
const response = await api("/api/attachments", { method: "POST", body: { files }, tabId });
|
|
1478
|
+
return { images, uploadedFiles: response.data?.files || [], inlineImageIds };
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function composeMessageWithAttachments(message, uploadedFiles, inlineImageIds) {
|
|
1482
|
+
if (!uploadedFiles.length) return message;
|
|
1483
|
+
const baseMessage = message || "Please inspect the attached file(s).";
|
|
1484
|
+
const lines = uploadedFiles.map((file, index) => {
|
|
1485
|
+
const inlineNote = inlineImageIds.has(file.id) ? "sent inline and saved at" : "saved at";
|
|
1486
|
+
return `- ${index + 1}. ${file.name || "attachment"} (${file.mimeType || "application/octet-stream"}, ${formatBytes(file.size)}): ${inlineNote} ${file.path}`;
|
|
1487
|
+
});
|
|
1488
|
+
return `${baseMessage}\n\nAttached files:\n${lines.join("\n")}`;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
565
1491
|
function storedThemeName() {
|
|
566
1492
|
try {
|
|
567
1493
|
return localStorage.getItem(THEME_STORAGE_KEY) || DEFAULT_THEME_NAME;
|
|
@@ -609,16 +1535,32 @@ function isOptionalFeatureEnabled(featureId) {
|
|
|
609
1535
|
return isOptionalFeatureDetected(featureId) && !isOptionalFeatureDisabled(featureId);
|
|
610
1536
|
}
|
|
611
1537
|
|
|
1538
|
+
function renderOptionalFeatureDependentDisplays() {
|
|
1539
|
+
renderOptionalFeatureControls();
|
|
1540
|
+
renderThemeSelect();
|
|
1541
|
+
renderWidgets();
|
|
1542
|
+
renderStatus();
|
|
1543
|
+
renderCommands();
|
|
1544
|
+
cancelStreamingAssistantTextRender();
|
|
1545
|
+
cancelStreamBubbleHide();
|
|
1546
|
+
streamBubble?.remove();
|
|
1547
|
+
streamBubble = null;
|
|
1548
|
+
streamText = null;
|
|
1549
|
+
streamBubbleVisibleSince = 0;
|
|
1550
|
+
renderAllMessages({ preserveScroll: true });
|
|
1551
|
+
if (streamRawText) renderStreamingAssistantText();
|
|
1552
|
+
}
|
|
1553
|
+
|
|
612
1554
|
function setOptionalFeatureDisabled(featureId, disabled) {
|
|
613
1555
|
if (!OPTIONAL_FEATURE_BY_ID.has(featureId)) return;
|
|
614
1556
|
if (disabled) disabledOptionalFeatures.add(featureId);
|
|
615
1557
|
else disabledOptionalFeatures.delete(featureId);
|
|
616
1558
|
storeDisabledOptionalFeatures();
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
1559
|
+
renderOptionalFeatureDependentDisplays();
|
|
1560
|
+
const tabContext = activeTabContext();
|
|
1561
|
+
refreshCommands(tabContext).catch((error) => {
|
|
1562
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
1563
|
+
});
|
|
622
1564
|
}
|
|
623
1565
|
|
|
624
1566
|
function displayThemeName(name) {
|
|
@@ -646,6 +1588,16 @@ function themeExportColor(theme, key, fallback) {
|
|
|
646
1588
|
return resolveThemeValue(theme, theme?.export?.[key], fallback);
|
|
647
1589
|
}
|
|
648
1590
|
|
|
1591
|
+
const LOCAL_BACKGROUND_IMAGE_PATTERN = /^(?:none|url\(["']?\/(?!\/)[A-Za-z0-9._~!$&'()*+,=:@%\/-]+["']?\))$/i;
|
|
1592
|
+
const BACKGROUND_OVERLAY_PATTERN = /^(?:none|linear-gradient\([^;\r\n{}<>]+\))$/i;
|
|
1593
|
+
const SAFE_BACKGROUND_TOKEN_PATTERN = /^[A-Za-z0-9%._ -]+$/;
|
|
1594
|
+
|
|
1595
|
+
function themeExportCssValue(theme, key, fallback, pattern = /^[^;\r\n{}<>]+$/) {
|
|
1596
|
+
const raw = String(theme?.export?.[key] ?? "").trim();
|
|
1597
|
+
if (!raw) return fallback;
|
|
1598
|
+
return pattern.test(raw) ? raw : fallback;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
649
1601
|
function hexToRgb(color) {
|
|
650
1602
|
const raw = String(color || "").trim();
|
|
651
1603
|
const match = raw.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
|
@@ -793,9 +1745,15 @@ function applyTheme(theme, { persist = false, announce = false } = {}) {
|
|
|
793
1745
|
"--background-glow-pink": colorWithAlpha(pink, isLight ? 0.16 : 0.34, pink),
|
|
794
1746
|
"--background-glow-blue": colorWithAlpha(accent, isLight ? 0.15 : 0.32, accent),
|
|
795
1747
|
"--background-glow-teal": colorWithAlpha(accent2, isLight ? 0.12 : 0.20, accent2),
|
|
1748
|
+
"--theme-background-image": themeExportCssValue(theme, "backgroundImage", "none", LOCAL_BACKGROUND_IMAGE_PATTERN),
|
|
1749
|
+
"--theme-background-overlay": themeExportCssValue(theme, "backgroundOverlay", "linear-gradient(180deg, rgba(17, 17, 27, 0), rgba(17, 17, 27, 0))", BACKGROUND_OVERLAY_PATTERN),
|
|
1750
|
+
"--theme-background-size": themeExportCssValue(theme, "backgroundSize", "cover", SAFE_BACKGROUND_TOKEN_PATTERN),
|
|
1751
|
+
"--theme-background-position": themeExportCssValue(theme, "backgroundPosition", "center", SAFE_BACKGROUND_TOKEN_PATTERN),
|
|
1752
|
+
"--theme-background-repeat": themeExportCssValue(theme, "backgroundRepeat", "no-repeat", SAFE_BACKGROUND_TOKEN_PATTERN),
|
|
796
1753
|
};
|
|
797
1754
|
|
|
798
1755
|
for (const [name, value] of Object.entries(vars)) root.style.setProperty(name, value);
|
|
1756
|
+
applyCustomBackgroundOverride({ render: false });
|
|
799
1757
|
root.style.colorScheme = isLight ? "light" : "dark";
|
|
800
1758
|
document.body.classList.toggle("theme-light", isLight);
|
|
801
1759
|
document.body.classList.toggle("theme-dark", !isLight);
|
|
@@ -832,11 +1790,17 @@ function renderThemeSelect({ unavailableLabel = "Theme bundle unavailable" } = {
|
|
|
832
1790
|
elements.themeSelect.value = currentThemeName;
|
|
833
1791
|
}
|
|
834
1792
|
|
|
835
|
-
function setThemeByName(name, options = {}) {
|
|
1793
|
+
async function setThemeByName(name, options = {}) {
|
|
836
1794
|
if (!isOptionalFeatureEnabled("themeBundle")) return;
|
|
837
1795
|
const theme = availableThemes.find((item) => item.name === name);
|
|
838
1796
|
if (!theme) return;
|
|
1797
|
+
currentThemeName = theme.name;
|
|
1798
|
+
if (elements.themeSelect && elements.themeSelect.value !== theme.name) elements.themeSelect.value = theme.name;
|
|
1799
|
+
setCustomBackgroundRecord(null);
|
|
1800
|
+
customBackgroundLoading = true;
|
|
839
1801
|
applyTheme(theme, options);
|
|
1802
|
+
renderBackgroundControl();
|
|
1803
|
+
await loadCustomBackgroundForTheme(theme.name, { includeLegacy: !!options.includeLegacy });
|
|
840
1804
|
}
|
|
841
1805
|
|
|
842
1806
|
async function initializeThemes() {
|
|
@@ -857,8 +1821,8 @@ async function initializeThemes() {
|
|
|
857
1821
|
const stored = storedThemeName();
|
|
858
1822
|
currentThemeName = availableThemes.some((theme) => theme.name === stored) ? stored : DEFAULT_THEME_NAME;
|
|
859
1823
|
renderThemeSelect();
|
|
860
|
-
setThemeByName(currentThemeName, { persist: false });
|
|
861
|
-
if (isOptionalFeatureEnabled("themeBundle") && !availableThemes.some((theme) => theme.name === currentThemeName) && availableThemes[0])
|
|
1824
|
+
await setThemeByName(currentThemeName, { persist: false, includeLegacy: true });
|
|
1825
|
+
if (isOptionalFeatureEnabled("themeBundle") && !availableThemes.some((theme) => theme.name === currentThemeName) && availableThemes[0]) await setThemeByName(availableThemes[0].name, { persist: false });
|
|
862
1826
|
if (!availableThemes.length) addEvent("theme bundle unavailable; using built-in default theme", "warn");
|
|
863
1827
|
}
|
|
864
1828
|
|
|
@@ -866,6 +1830,26 @@ function activeTab() {
|
|
|
866
1830
|
return tabs.find((tab) => tab.id === activeTabId) || null;
|
|
867
1831
|
}
|
|
868
1832
|
|
|
1833
|
+
function activeTabContext(tabId = activeTabId) {
|
|
1834
|
+
return { tabId: tabId || null, generation: activeTabGeneration };
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
function setActiveTabId(tabId, { remember = false } = {}) {
|
|
1838
|
+
const nextTabId = tabId || null;
|
|
1839
|
+
if (nextTabId !== activeTabId) activeTabGeneration += 1;
|
|
1840
|
+
activeTabId = nextTabId;
|
|
1841
|
+
if (remember) rememberActiveTab();
|
|
1842
|
+
return activeTabContext(nextTabId);
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
function isCurrentTabContext(context) {
|
|
1846
|
+
return !!context && context.tabId === activeTabId && context.generation === activeTabGeneration;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
function eventTargetsActiveTab(event) {
|
|
1850
|
+
return !event?.tabId || event.tabId === activeTabId;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
869
1853
|
function normalizeTabActivity(activity = {}) {
|
|
870
1854
|
const status = activity.status === "working" || activity.isWorking ? "working" : activity.status === "done" ? "done" : "idle";
|
|
871
1855
|
const completionSerial = Number(activity.completionSerial);
|
|
@@ -1123,11 +2107,12 @@ function restoreActiveDraft() {
|
|
|
1123
2107
|
elements.promptInput.value = activeTabId ? tabDrafts.get(activeTabId) || "" : "";
|
|
1124
2108
|
resizePromptInput();
|
|
1125
2109
|
renderCommandSuggestions();
|
|
2110
|
+
renderAttachmentTray();
|
|
1126
2111
|
}
|
|
1127
2112
|
|
|
1128
2113
|
function focusPromptInput({ defer = false } = {}) {
|
|
1129
2114
|
const focus = () => {
|
|
1130
|
-
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || document.visibilityState === "hidden") return;
|
|
2115
|
+
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || elements.nativeCommandDialog.open || document.visibilityState === "hidden") return;
|
|
1131
2116
|
try {
|
|
1132
2117
|
elements.promptInput.focus({ preventScroll: true });
|
|
1133
2118
|
} catch {
|
|
@@ -1166,6 +2151,7 @@ function cancelPendingDialogs() {
|
|
|
1166
2151
|
|
|
1167
2152
|
function resetActiveTabUi() {
|
|
1168
2153
|
clearRefreshTimers();
|
|
2154
|
+
clearLiveToolRenderQueue();
|
|
1169
2155
|
eventSource?.close();
|
|
1170
2156
|
eventSource = null;
|
|
1171
2157
|
currentState = null;
|
|
@@ -1179,6 +2165,8 @@ function resetActiveTabUi() {
|
|
|
1179
2165
|
statusEntries.clear();
|
|
1180
2166
|
widgets.clear();
|
|
1181
2167
|
transientMessages = [];
|
|
2168
|
+
liveToolRuns.clear();
|
|
2169
|
+
liveToolCards.clear();
|
|
1182
2170
|
availableCommands = [];
|
|
1183
2171
|
resetOptionalFeatureAvailability();
|
|
1184
2172
|
commandSuggestions = [];
|
|
@@ -1189,6 +2177,8 @@ function resetActiveTabUi() {
|
|
|
1189
2177
|
removeRunIndicatorBubble();
|
|
1190
2178
|
hideCommandSuggestions();
|
|
1191
2179
|
cancelPendingDialogs();
|
|
2180
|
+
if (elements.nativeCommandDialog.open) closeNativeCommandDialog();
|
|
2181
|
+
if (pathPickerState) closePathPicker(null);
|
|
1192
2182
|
Object.assign(gitWorkflow, {
|
|
1193
2183
|
active: false,
|
|
1194
2184
|
step: "idle",
|
|
@@ -1450,9 +2440,9 @@ async function refreshTabs({ selectStored = false } = {}) {
|
|
|
1450
2440
|
syncAgentDoneNotificationsFromTabs(tabs, previousTabs);
|
|
1451
2441
|
const stored = selectStored ? restoreStoredTabId() : null;
|
|
1452
2442
|
if (!activeTabId || !tabs.some((tab) => tab.id === activeTabId)) {
|
|
1453
|
-
|
|
1454
|
-
rememberActiveTab();
|
|
2443
|
+
setActiveTabId((stored && tabs.some((tab) => tab.id === stored) ? stored : tabs[0]?.id) || null, { remember: true });
|
|
1455
2444
|
}
|
|
2445
|
+
rememberServerStartCwd(tabs.find((tab) => tab.id === activeTabId)?.cwd || tabs[0]?.cwd);
|
|
1456
2446
|
renderTabs();
|
|
1457
2447
|
return tabs;
|
|
1458
2448
|
}
|
|
@@ -1463,15 +2453,14 @@ async function switchTab(tabId) {
|
|
|
1463
2453
|
setMobileTabsExpanded(false);
|
|
1464
2454
|
footerModelPickerOpen = false;
|
|
1465
2455
|
saveActiveDraft();
|
|
1466
|
-
|
|
1467
|
-
rememberActiveTab();
|
|
2456
|
+
const tabContext = setActiveTabId(tabId, { remember: true });
|
|
1468
2457
|
resetActiveTabUi();
|
|
1469
2458
|
renderTabs();
|
|
1470
2459
|
restoreActiveDraft();
|
|
1471
2460
|
focusPromptInput({ defer: true });
|
|
1472
|
-
connectEvents();
|
|
1473
|
-
await refreshAll();
|
|
1474
|
-
markTabOutputSeen();
|
|
2461
|
+
connectEvents(tabContext);
|
|
2462
|
+
await refreshAll(tabContext);
|
|
2463
|
+
if (isCurrentTabContext(tabContext)) markTabOutputSeen();
|
|
1475
2464
|
}
|
|
1476
2465
|
|
|
1477
2466
|
async function createTerminalTab(cwd = activeTab()?.cwd, { triggerButton = elements.newTabButton } = {}) {
|
|
@@ -1540,22 +2529,25 @@ async function closeTerminalTabs(tabIds, { label = "selected terminal tabs" } =
|
|
|
1540
2529
|
const closedIds = response.data?.closedIds || targetIds;
|
|
1541
2530
|
tabs = response.data?.tabs || tabs.filter((item) => !closedIds.includes(item.id));
|
|
1542
2531
|
syncTabMetadata(tabs);
|
|
1543
|
-
for (const id of closedIds)
|
|
2532
|
+
for (const id of closedIds) {
|
|
2533
|
+
tabDrafts.delete(id);
|
|
2534
|
+
clearAttachments(id);
|
|
2535
|
+
}
|
|
1544
2536
|
clearOpenTerminalTabGroup(null, { force: true });
|
|
1545
2537
|
|
|
1546
|
-
|
|
1547
|
-
|
|
2538
|
+
const activeTabNeedsFallback = closedIds.includes(activeTabId) || !tabs.some((item) => item.id === activeTabId);
|
|
2539
|
+
if (activeTabNeedsFallback) {
|
|
2540
|
+
const tabContext = setActiveTabId((response.data?.activeTabId && tabs.some((item) => item.id === response.data.activeTabId)
|
|
1548
2541
|
? response.data.activeTabId
|
|
1549
|
-
: (fallbackTabId && tabs.some((item) => item.id === fallbackTabId) ? fallbackTabId : tabs[0]?.id)) || null;
|
|
1550
|
-
rememberActiveTab();
|
|
2542
|
+
: (fallbackTabId && tabs.some((item) => item.id === fallbackTabId) ? fallbackTabId : tabs[0]?.id)) || null, { remember: true });
|
|
1551
2543
|
resetActiveTabUi();
|
|
1552
2544
|
renderTabs();
|
|
1553
2545
|
restoreActiveDraft();
|
|
1554
2546
|
focusPromptInput({ defer: true });
|
|
1555
|
-
connectEvents();
|
|
2547
|
+
connectEvents(tabContext);
|
|
1556
2548
|
if (activeTabId) {
|
|
1557
|
-
await refreshAll();
|
|
1558
|
-
markTabOutputSeen();
|
|
2549
|
+
await refreshAll(tabContext);
|
|
2550
|
+
if (isCurrentTabContext(tabContext)) markTabOutputSeen();
|
|
1559
2551
|
}
|
|
1560
2552
|
} else {
|
|
1561
2553
|
renderTabs();
|
|
@@ -1587,10 +2579,11 @@ async function initializeTabs() {
|
|
|
1587
2579
|
renderTabs();
|
|
1588
2580
|
restoreActiveDraft();
|
|
1589
2581
|
focusPromptInput({ defer: true });
|
|
1590
|
-
|
|
2582
|
+
const tabContext = activeTabContext();
|
|
2583
|
+
connectEvents(tabContext);
|
|
1591
2584
|
if (activeTabId) {
|
|
1592
|
-
await refreshAll();
|
|
1593
|
-
markTabOutputSeen();
|
|
2585
|
+
await refreshAll(tabContext);
|
|
2586
|
+
if (isCurrentTabContext(tabContext)) markTabOutputSeen();
|
|
1594
2587
|
}
|
|
1595
2588
|
}
|
|
1596
2589
|
|
|
@@ -2143,16 +3136,20 @@ function setFooterModelPickerOpen(open) {
|
|
|
2143
3136
|
|
|
2144
3137
|
async function applyFooterModel(model) {
|
|
2145
3138
|
if (!model?.provider || !model?.id) return;
|
|
3139
|
+
const tabContext = activeTabContext();
|
|
2146
3140
|
try {
|
|
2147
3141
|
footerModelPickerOpen = false;
|
|
2148
|
-
await api("/api/model", { method: "POST", body: { provider: model.provider, modelId: model.id } });
|
|
2149
|
-
|
|
2150
|
-
await
|
|
3142
|
+
await api("/api/model", { method: "POST", body: { provider: model.provider, modelId: model.id }, tabId: tabContext.tabId });
|
|
3143
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
3144
|
+
await refreshState(tabContext);
|
|
3145
|
+
await refreshModels(tabContext);
|
|
2151
3146
|
} catch (error) {
|
|
2152
|
-
addEvent(error.message, "error");
|
|
3147
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
2153
3148
|
} finally {
|
|
2154
|
-
|
|
2155
|
-
|
|
3149
|
+
if (isCurrentTabContext(tabContext)) {
|
|
3150
|
+
document.body.classList.toggle("footer-model-picker-open", footerModelPickerOpen);
|
|
3151
|
+
renderFooter();
|
|
3152
|
+
}
|
|
2156
3153
|
}
|
|
2157
3154
|
}
|
|
2158
3155
|
|
|
@@ -2422,10 +3419,11 @@ function pickCwd(tab, initialCwd) {
|
|
|
2422
3419
|
async function changeActiveTabCwd() {
|
|
2423
3420
|
const tab = activeTab();
|
|
2424
3421
|
if (!tab) return;
|
|
3422
|
+
const tabContext = activeTabContext(tab.id);
|
|
2425
3423
|
|
|
2426
3424
|
const currentCwd = latestWorkspace?.cwd || tab.cwd || "";
|
|
2427
3425
|
const cwd = await pickCwd(tab, currentCwd);
|
|
2428
|
-
if (!cwd || cwd === currentCwd) return;
|
|
3426
|
+
if (!isCurrentTabContext(tabContext) || !cwd || cwd === currentCwd) return;
|
|
2429
3427
|
if (!window.confirm(`Restart ${tab.title} in:\n${cwd}\n\nCurrent in-flight work in this tab will be stopped.`)) return;
|
|
2430
3428
|
|
|
2431
3429
|
saveActiveDraft();
|
|
@@ -2433,16 +3431,21 @@ async function changeActiveTabCwd() {
|
|
|
2433
3431
|
const response = await api(`/api/tabs/${encodeURIComponent(tab.id)}`, { method: "PATCH", body: { cwd }, scoped: false });
|
|
2434
3432
|
tabs = response.data?.tabs || tabs;
|
|
2435
3433
|
syncTabMetadata(tabs);
|
|
2436
|
-
|
|
3434
|
+
if (!isCurrentTabContext(tabContext)) {
|
|
3435
|
+
renderTabs();
|
|
3436
|
+
return;
|
|
3437
|
+
}
|
|
3438
|
+
const nextContext = setActiveTabId(response.data?.tab?.id || activeTabId);
|
|
2437
3439
|
resetActiveTabUi();
|
|
2438
3440
|
renderTabs();
|
|
2439
3441
|
restoreActiveDraft();
|
|
2440
|
-
connectEvents();
|
|
2441
|
-
await refreshAll();
|
|
3442
|
+
connectEvents(nextContext);
|
|
3443
|
+
await refreshAll(nextContext);
|
|
3444
|
+
if (!isCurrentTabContext(nextContext)) return;
|
|
2442
3445
|
const changedCwd = response.data?.tab?.cwd || cwd;
|
|
2443
3446
|
addEvent(response.data?.changed === false ? `cwd unchanged: ${changedCwd}` : `changed ${tab.title} cwd to ${changedCwd}`, "info");
|
|
2444
3447
|
} catch (error) {
|
|
2445
|
-
addEvent(error.message, "error");
|
|
3448
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
2446
3449
|
}
|
|
2447
3450
|
}
|
|
2448
3451
|
|
|
@@ -2505,19 +3508,34 @@ function renderFooter() {
|
|
|
2505
3508
|
updateFooterModelPickerPosition();
|
|
2506
3509
|
}
|
|
2507
3510
|
|
|
2508
|
-
function scheduleRefreshMessages(delay = 120) {
|
|
3511
|
+
function scheduleRefreshMessages(delay = 120, tabContext = activeTabContext()) {
|
|
2509
3512
|
clearTimeout(refreshMessagesTimer);
|
|
2510
|
-
refreshMessagesTimer = setTimeout(() =>
|
|
3513
|
+
refreshMessagesTimer = setTimeout(() => {
|
|
3514
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
3515
|
+
refreshMessages(tabContext).catch((error) => {
|
|
3516
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
3517
|
+
});
|
|
3518
|
+
}, delay);
|
|
2511
3519
|
}
|
|
2512
3520
|
|
|
2513
|
-
function scheduleRefreshState(delay = 120) {
|
|
3521
|
+
function scheduleRefreshState(delay = 120, tabContext = activeTabContext()) {
|
|
2514
3522
|
clearTimeout(refreshStateTimer);
|
|
2515
|
-
refreshStateTimer = setTimeout(() =>
|
|
3523
|
+
refreshStateTimer = setTimeout(() => {
|
|
3524
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
3525
|
+
refreshState(tabContext).catch((error) => {
|
|
3526
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
3527
|
+
});
|
|
3528
|
+
}, delay);
|
|
2516
3529
|
}
|
|
2517
3530
|
|
|
2518
|
-
function scheduleRefreshFooter(delay = 300) {
|
|
3531
|
+
function scheduleRefreshFooter(delay = 300, tabContext = activeTabContext()) {
|
|
2519
3532
|
clearTimeout(refreshFooterTimer);
|
|
2520
|
-
refreshFooterTimer = setTimeout(() =>
|
|
3533
|
+
refreshFooterTimer = setTimeout(() => {
|
|
3534
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
3535
|
+
refreshFooterData(tabContext).catch((error) => {
|
|
3536
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
3537
|
+
});
|
|
3538
|
+
}, delay);
|
|
2521
3539
|
}
|
|
2522
3540
|
|
|
2523
3541
|
function renderStatus() {
|
|
@@ -2608,6 +3626,7 @@ function releaseDialogPromptParts(prompt) {
|
|
|
2608
3626
|
title: question,
|
|
2609
3627
|
message,
|
|
2610
3628
|
plainMessage: stripAnsi(message),
|
|
3629
|
+
featureId: isAurReleasePrompt ? "releaseAur" : "releaseNpm",
|
|
2611
3630
|
};
|
|
2612
3631
|
}
|
|
2613
3632
|
|
|
@@ -2763,13 +3782,17 @@ function appendReleaseNpmTerminalLine(parent, line) {
|
|
|
2763
3782
|
}
|
|
2764
3783
|
|
|
2765
3784
|
async function sendReleaseNpmCommand(command) {
|
|
3785
|
+
const tabContext = activeTabContext();
|
|
2766
3786
|
try {
|
|
2767
|
-
await api("/api/prompt", { method: "POST", body: { message: command }, tabId:
|
|
3787
|
+
await api("/api/prompt", { method: "POST", body: { message: command }, tabId: tabContext.tabId });
|
|
3788
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
2768
3789
|
addEvent(`${command} sent`, "info");
|
|
2769
|
-
scheduleRefreshState();
|
|
3790
|
+
scheduleRefreshState(120, tabContext);
|
|
2770
3791
|
} catch (error) {
|
|
2771
|
-
|
|
2772
|
-
|
|
3792
|
+
if (isCurrentTabContext(tabContext)) {
|
|
3793
|
+
addEvent(error.message, "error");
|
|
3794
|
+
addTransientMessage({ role: "error", title: command, content: error.message, level: "error" });
|
|
3795
|
+
}
|
|
2773
3796
|
}
|
|
2774
3797
|
}
|
|
2775
3798
|
|
|
@@ -3077,8 +4100,11 @@ function failGitWorkflow(error, step = gitWorkflow.step) {
|
|
|
3077
4100
|
|
|
3078
4101
|
function startGitWorkflow() {
|
|
3079
4102
|
if (!isOptionalFeatureEnabled("gitWorkflow")) {
|
|
4103
|
+
const tabContext = activeTabContext();
|
|
3080
4104
|
addEvent(commandUnavailableMessage("git-staged-msg"), "warn");
|
|
3081
|
-
refreshCommands().catch((error) =>
|
|
4105
|
+
refreshCommands(tabContext).catch((error) => {
|
|
4106
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
4107
|
+
});
|
|
3082
4108
|
return;
|
|
3083
4109
|
}
|
|
3084
4110
|
if (gitWorkflow.active && !["done", "cancelled", "error"].includes(gitWorkflow.step) && !confirm("Restart the active git workflow?")) return;
|
|
@@ -3225,6 +4251,287 @@ function appendText(parent, text, className = "text-block") {
|
|
|
3225
4251
|
return block;
|
|
3226
4252
|
}
|
|
3227
4253
|
|
|
4254
|
+
function safeMarkdownLinkHref(url) {
|
|
4255
|
+
const href = String(url || "").trim();
|
|
4256
|
+
if (!href || /[\u0000-\u001f\u007f]/.test(href)) return "";
|
|
4257
|
+
if (/^(?:https?:|mailto:)/i.test(href)) return href;
|
|
4258
|
+
if (/^(?:#|\/(?!\/)|\.\/|\.\.\/)/.test(href)) return href;
|
|
4259
|
+
return "";
|
|
4260
|
+
}
|
|
4261
|
+
|
|
4262
|
+
function appendInlineMarkdown(parent, text, depth = 0) {
|
|
4263
|
+
const value = String(text || "");
|
|
4264
|
+
if (!value) return;
|
|
4265
|
+
if (depth > 6) {
|
|
4266
|
+
parent.append(document.createTextNode(value));
|
|
4267
|
+
return;
|
|
4268
|
+
}
|
|
4269
|
+
let index = 0;
|
|
4270
|
+
const appendPlain = (end) => {
|
|
4271
|
+
if (end > index) parent.append(document.createTextNode(value.slice(index, end)));
|
|
4272
|
+
index = end;
|
|
4273
|
+
};
|
|
4274
|
+
while (index < value.length) {
|
|
4275
|
+
if (value[index] === "`") {
|
|
4276
|
+
const end = value.indexOf("`", index + 1);
|
|
4277
|
+
if (end > index + 1) {
|
|
4278
|
+
const code = make("code", "markdown-inline-code", value.slice(index + 1, end));
|
|
4279
|
+
parent.append(code);
|
|
4280
|
+
index = end + 1;
|
|
4281
|
+
continue;
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
if (value[index] === "[") {
|
|
4285
|
+
const labelEnd = value.indexOf("](", index + 1);
|
|
4286
|
+
const linkEnd = labelEnd === -1 ? -1 : value.indexOf(")", labelEnd + 2);
|
|
4287
|
+
if (labelEnd !== -1 && linkEnd !== -1) {
|
|
4288
|
+
const label = value.slice(index + 1, labelEnd);
|
|
4289
|
+
const href = safeMarkdownLinkHref(value.slice(labelEnd + 2, linkEnd));
|
|
4290
|
+
if (href) {
|
|
4291
|
+
const link = make("a");
|
|
4292
|
+
link.href = href;
|
|
4293
|
+
if (/^https?:/i.test(href)) {
|
|
4294
|
+
link.target = "_blank";
|
|
4295
|
+
link.rel = "noopener noreferrer";
|
|
4296
|
+
}
|
|
4297
|
+
appendInlineMarkdown(link, label, depth + 1);
|
|
4298
|
+
parent.append(link);
|
|
4299
|
+
} else {
|
|
4300
|
+
parent.append(document.createTextNode(value.slice(index, linkEnd + 1)));
|
|
4301
|
+
}
|
|
4302
|
+
index = linkEnd + 1;
|
|
4303
|
+
continue;
|
|
4304
|
+
}
|
|
4305
|
+
}
|
|
4306
|
+
const strongMarker = value.startsWith("**", index) ? "**" : value.startsWith("__", index) ? "__" : "";
|
|
4307
|
+
if (strongMarker) {
|
|
4308
|
+
const end = value.indexOf(strongMarker, index + 2);
|
|
4309
|
+
if (end > index + 2) {
|
|
4310
|
+
const strong = make("strong");
|
|
4311
|
+
appendInlineMarkdown(strong, value.slice(index + 2, end), depth + 1);
|
|
4312
|
+
parent.append(strong);
|
|
4313
|
+
index = end + 2;
|
|
4314
|
+
continue;
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
if (value.startsWith("~~", index)) {
|
|
4318
|
+
const end = value.indexOf("~~", index + 2);
|
|
4319
|
+
if (end > index + 2) {
|
|
4320
|
+
const del = make("del");
|
|
4321
|
+
appendInlineMarkdown(del, value.slice(index + 2, end), depth + 1);
|
|
4322
|
+
parent.append(del);
|
|
4323
|
+
index = end + 2;
|
|
4324
|
+
continue;
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
const emphasisMarker = value[index] === "*" || value[index] === "_" ? value[index] : "";
|
|
4328
|
+
if (emphasisMarker && value[index + 1] !== emphasisMarker) {
|
|
4329
|
+
const end = value.indexOf(emphasisMarker, index + 1);
|
|
4330
|
+
if (end > index + 1) {
|
|
4331
|
+
const em = make("em");
|
|
4332
|
+
appendInlineMarkdown(em, value.slice(index + 1, end), depth + 1);
|
|
4333
|
+
parent.append(em);
|
|
4334
|
+
index = end + 1;
|
|
4335
|
+
continue;
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
const nextSpecials = ["`", "[", "**", "__", "~~", "*", "_"]
|
|
4339
|
+
.map((marker) => value.indexOf(marker, index + 1))
|
|
4340
|
+
.filter((pos) => pos !== -1);
|
|
4341
|
+
appendPlain(nextSpecials.length ? Math.min(...nextSpecials) : value.length);
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
function appendMarkdownParagraph(parent, lines) {
|
|
4346
|
+
const paragraph = make("p");
|
|
4347
|
+
lines.forEach((line, index) => {
|
|
4348
|
+
if (index > 0) paragraph.append(make("br"));
|
|
4349
|
+
appendInlineMarkdown(paragraph, line);
|
|
4350
|
+
});
|
|
4351
|
+
parent.append(paragraph);
|
|
4352
|
+
}
|
|
4353
|
+
|
|
4354
|
+
function appendMarkdownCodeBlock(parent, code, language = "") {
|
|
4355
|
+
const wrapper = make("div", "markdown-code-block");
|
|
4356
|
+
if (language) wrapper.append(make("div", "markdown-code-language", language));
|
|
4357
|
+
const pre = make("pre", "code-block markdown-code");
|
|
4358
|
+
const codeNode = make("code", language ? `language-${language.replace(/[^a-z0-9_-]/gi, "")}` : "");
|
|
4359
|
+
codeNode.textContent = code.replace(/\n+$/g, "");
|
|
4360
|
+
pre.append(codeNode);
|
|
4361
|
+
wrapper.append(pre);
|
|
4362
|
+
parent.append(wrapper);
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
function markdownTableSeparator(line) {
|
|
4366
|
+
return /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line || "");
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4369
|
+
function splitMarkdownTableRow(line) {
|
|
4370
|
+
let row = String(line || "").trim();
|
|
4371
|
+
if (row.startsWith("|")) row = row.slice(1);
|
|
4372
|
+
if (row.endsWith("|")) row = row.slice(0, -1);
|
|
4373
|
+
return row.split(/(?<!\\)\|/).map((cell) => cell.replace(/\\\|/g, "|").trim());
|
|
4374
|
+
}
|
|
4375
|
+
|
|
4376
|
+
function appendMarkdownTable(parent, rows) {
|
|
4377
|
+
const wrapper = make("div", "markdown-table-wrapper");
|
|
4378
|
+
const table = make("table", "markdown-table");
|
|
4379
|
+
const thead = make("thead");
|
|
4380
|
+
const tbody = make("tbody");
|
|
4381
|
+
const headerRow = make("tr");
|
|
4382
|
+
for (const cell of rows[0] || []) {
|
|
4383
|
+
const th = make("th");
|
|
4384
|
+
appendInlineMarkdown(th, cell);
|
|
4385
|
+
headerRow.append(th);
|
|
4386
|
+
}
|
|
4387
|
+
thead.append(headerRow);
|
|
4388
|
+
for (const row of rows.slice(1)) {
|
|
4389
|
+
const tr = make("tr");
|
|
4390
|
+
for (const cell of row) {
|
|
4391
|
+
const td = make("td");
|
|
4392
|
+
appendInlineMarkdown(td, cell);
|
|
4393
|
+
tr.append(td);
|
|
4394
|
+
}
|
|
4395
|
+
tbody.append(tr);
|
|
4396
|
+
}
|
|
4397
|
+
table.append(thead, tbody);
|
|
4398
|
+
wrapper.append(table);
|
|
4399
|
+
parent.append(wrapper);
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4402
|
+
function markdownListMatch(line) {
|
|
4403
|
+
const unordered = line.match(/^\s{0,3}[-*+]\s+(.+)$/);
|
|
4404
|
+
if (unordered) return { ordered: false, text: unordered[1] };
|
|
4405
|
+
const ordered = line.match(/^\s{0,3}(\d+)[.)]\s+(.+)$/);
|
|
4406
|
+
if (ordered) return { ordered: true, start: Number(ordered[1]), text: ordered[2] };
|
|
4407
|
+
return null;
|
|
4408
|
+
}
|
|
4409
|
+
|
|
4410
|
+
function appendMarkdownList(parent, items, ordered = false, start = null) {
|
|
4411
|
+
const list = make(ordered ? "ol" : "ul", "markdown-list");
|
|
4412
|
+
if (ordered && Number.isFinite(start) && start > 1) list.start = start;
|
|
4413
|
+
for (const itemText of items) {
|
|
4414
|
+
const li = make("li");
|
|
4415
|
+
const task = String(itemText).match(/^\[( |x|X|-)\]\s+(.+)$/);
|
|
4416
|
+
if (task) {
|
|
4417
|
+
li.classList.add("markdown-task-item");
|
|
4418
|
+
const checkbox = make("input", "markdown-task-checkbox");
|
|
4419
|
+
checkbox.type = "checkbox";
|
|
4420
|
+
checkbox.disabled = true;
|
|
4421
|
+
checkbox.checked = task[1].toLowerCase() === "x";
|
|
4422
|
+
li.append(checkbox);
|
|
4423
|
+
appendInlineMarkdown(li, task[2]);
|
|
4424
|
+
} else {
|
|
4425
|
+
appendInlineMarkdown(li, itemText);
|
|
4426
|
+
}
|
|
4427
|
+
list.append(li);
|
|
4428
|
+
}
|
|
4429
|
+
parent.append(list);
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
function renderMarkdownInto(parent, text) {
|
|
4433
|
+
const raw = String(text || "").replace(/\r\n?/g, "\n");
|
|
4434
|
+
const lines = raw.split("\n");
|
|
4435
|
+
let index = 0;
|
|
4436
|
+
let paragraph = [];
|
|
4437
|
+
const flushParagraph = () => {
|
|
4438
|
+
if (paragraph.length) appendMarkdownParagraph(parent, paragraph);
|
|
4439
|
+
paragraph = [];
|
|
4440
|
+
};
|
|
4441
|
+
|
|
4442
|
+
while (index < lines.length) {
|
|
4443
|
+
const line = lines[index];
|
|
4444
|
+
if (!line.trim()) {
|
|
4445
|
+
flushParagraph();
|
|
4446
|
+
index += 1;
|
|
4447
|
+
continue;
|
|
4448
|
+
}
|
|
4449
|
+
const fence = line.match(/^\s*```\s*([\w.+-]*)\s*$/);
|
|
4450
|
+
if (fence) {
|
|
4451
|
+
flushParagraph();
|
|
4452
|
+
const language = fence[1] || "";
|
|
4453
|
+
const codeLines = [];
|
|
4454
|
+
index += 1;
|
|
4455
|
+
while (index < lines.length && !/^\s*```\s*$/.test(lines[index])) {
|
|
4456
|
+
codeLines.push(lines[index]);
|
|
4457
|
+
index += 1;
|
|
4458
|
+
}
|
|
4459
|
+
if (index < lines.length) index += 1;
|
|
4460
|
+
appendMarkdownCodeBlock(parent, codeLines.join("\n"), language);
|
|
4461
|
+
continue;
|
|
4462
|
+
}
|
|
4463
|
+
if (markdownTableSeparator(lines[index + 1]) && line.includes("|")) {
|
|
4464
|
+
flushParagraph();
|
|
4465
|
+
const rows = [splitMarkdownTableRow(line)];
|
|
4466
|
+
index += 2;
|
|
4467
|
+
while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
|
|
4468
|
+
rows.push(splitMarkdownTableRow(lines[index]));
|
|
4469
|
+
index += 1;
|
|
4470
|
+
}
|
|
4471
|
+
appendMarkdownTable(parent, rows);
|
|
4472
|
+
continue;
|
|
4473
|
+
}
|
|
4474
|
+
const heading = line.match(/^\s{0,3}(#{1,6})\s+(.+?)\s*#*\s*$/);
|
|
4475
|
+
if (heading) {
|
|
4476
|
+
flushParagraph();
|
|
4477
|
+
const level = Math.min(6, heading[1].length);
|
|
4478
|
+
const node = make(`h${level}`, `markdown-heading markdown-heading-${level}`);
|
|
4479
|
+
appendInlineMarkdown(node, heading[2]);
|
|
4480
|
+
parent.append(node);
|
|
4481
|
+
index += 1;
|
|
4482
|
+
continue;
|
|
4483
|
+
}
|
|
4484
|
+
if (/^\s{0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
|
|
4485
|
+
flushParagraph();
|
|
4486
|
+
parent.append(make("hr", "markdown-rule"));
|
|
4487
|
+
index += 1;
|
|
4488
|
+
continue;
|
|
4489
|
+
}
|
|
4490
|
+
if (/^\s{0,3}>\s?/.test(line)) {
|
|
4491
|
+
flushParagraph();
|
|
4492
|
+
const quoteLines = [];
|
|
4493
|
+
while (index < lines.length && /^\s{0,3}>\s?/.test(lines[index])) {
|
|
4494
|
+
quoteLines.push(lines[index].replace(/^\s{0,3}>\s?/, ""));
|
|
4495
|
+
index += 1;
|
|
4496
|
+
}
|
|
4497
|
+
const quote = make("blockquote", "markdown-blockquote");
|
|
4498
|
+
renderMarkdownInto(quote, quoteLines.join("\n"));
|
|
4499
|
+
parent.append(quote);
|
|
4500
|
+
continue;
|
|
4501
|
+
}
|
|
4502
|
+
const listMatch = markdownListMatch(line);
|
|
4503
|
+
if (listMatch) {
|
|
4504
|
+
flushParagraph();
|
|
4505
|
+
const ordered = listMatch.ordered;
|
|
4506
|
+
const start = listMatch.start || null;
|
|
4507
|
+
const items = [];
|
|
4508
|
+
while (index < lines.length) {
|
|
4509
|
+
const item = markdownListMatch(lines[index]);
|
|
4510
|
+
if (!item || item.ordered !== ordered) break;
|
|
4511
|
+
items.push(item.text);
|
|
4512
|
+
index += 1;
|
|
4513
|
+
}
|
|
4514
|
+
appendMarkdownList(parent, items, ordered, start);
|
|
4515
|
+
continue;
|
|
4516
|
+
}
|
|
4517
|
+
paragraph.push(line);
|
|
4518
|
+
index += 1;
|
|
4519
|
+
}
|
|
4520
|
+
flushParagraph();
|
|
4521
|
+
}
|
|
4522
|
+
|
|
4523
|
+
function appendMarkdown(parent, text) {
|
|
4524
|
+
const block = make("div", "markdown-body");
|
|
4525
|
+
renderMarkdownInto(block, text);
|
|
4526
|
+
parent.append(block);
|
|
4527
|
+
return block;
|
|
4528
|
+
}
|
|
4529
|
+
|
|
4530
|
+
function renderMarkdown(block, text) {
|
|
4531
|
+
block.replaceChildren();
|
|
4532
|
+
renderMarkdownInto(block, text);
|
|
4533
|
+
}
|
|
4534
|
+
|
|
3228
4535
|
function appendImage(parent, part) {
|
|
3229
4536
|
const wrapper = make("div", "image-block");
|
|
3230
4537
|
const img = document.createElement("img");
|
|
@@ -3238,7 +4545,7 @@ function appendImage(parent, part) {
|
|
|
3238
4545
|
}
|
|
3239
4546
|
|
|
3240
4547
|
function isActionFeedbackMessage(message) {
|
|
3241
|
-
return message?.role === "assistant" || message?.role === "toolResult" || message?.role === "bashExecution";
|
|
4548
|
+
return message?.role === "assistant" || message?.role === "toolExecution" || message?.role === "toolResult" || message?.role === "bashExecution";
|
|
3242
4549
|
}
|
|
3243
4550
|
|
|
3244
4551
|
function truncateActionFeedbackText(text, limit = ACTION_FEEDBACK_SNIPPET_LIMIT) {
|
|
@@ -3253,6 +4560,7 @@ function actionFeedbackKey(message, messageIndex) {
|
|
|
3253
4560
|
messageIndex,
|
|
3254
4561
|
message?.role || "message",
|
|
3255
4562
|
message?.toolName || "",
|
|
4563
|
+
message?.toolCallId || "",
|
|
3256
4564
|
message?.command || "",
|
|
3257
4565
|
message?.timestamp || "",
|
|
3258
4566
|
].join("|");
|
|
@@ -3270,6 +4578,12 @@ function actionFeedbackSummary(message) {
|
|
|
3270
4578
|
snippet: truncateActionFeedbackText(`$ ${message.command || ""}\n\n${message.output || ""}`),
|
|
3271
4579
|
};
|
|
3272
4580
|
}
|
|
4581
|
+
if (message?.role === "toolExecution") {
|
|
4582
|
+
const result = toolExecutionResult(message);
|
|
4583
|
+
const args = message.arguments === undefined ? "" : JSON.stringify(message.arguments, null, 2);
|
|
4584
|
+
const output = toolResultText(result);
|
|
4585
|
+
return { kind: "action", title, snippet: truncateActionFeedbackText([args, output].filter(Boolean).join("\n\n")) };
|
|
4586
|
+
}
|
|
3273
4587
|
return { kind: "action", title, snippet: truncateActionFeedbackText(textFromContent(message?.content)) };
|
|
3274
4588
|
}
|
|
3275
4589
|
|
|
@@ -3298,9 +4612,10 @@ function actionFeedbackSteerMessage(item) {
|
|
|
3298
4612
|
}
|
|
3299
4613
|
|
|
3300
4614
|
async function sendLiveActionFeedback(item) {
|
|
4615
|
+
const tabContext = activeTabContext(item.tabId);
|
|
3301
4616
|
if (!isRunActive()) return;
|
|
3302
4617
|
await api("/api/steer", { method: "POST", body: { message: actionFeedbackSteerMessage(item) }, tabId: item.tabId });
|
|
3303
|
-
addEvent(`sent ${ACTION_FEEDBACK_REACTIONS[item.reaction]?.icon || "feedback"} action feedback as live steering`);
|
|
4618
|
+
if (isCurrentTabContext(tabContext)) addEvent(`sent ${ACTION_FEEDBACK_REACTIONS[item.reaction]?.icon || "feedback"} action feedback as live steering`);
|
|
3304
4619
|
}
|
|
3305
4620
|
|
|
3306
4621
|
function setActionFeedback(message, messageIndex, reaction) {
|
|
@@ -3382,13 +4697,13 @@ function isMissingActionFeedbackEndpoint(error) {
|
|
|
3382
4697
|
return error?.statusCode === 404 || /not found/i.test(error?.message || "");
|
|
3383
4698
|
}
|
|
3384
4699
|
|
|
3385
|
-
async function postQueuedFeedback(tabId, items) {
|
|
4700
|
+
async function postQueuedFeedback(tabId, items, tabContext = activeTabContext(tabId)) {
|
|
3386
4701
|
const feedback = items.map(serializeActionFeedback);
|
|
3387
4702
|
try {
|
|
3388
4703
|
await api("/api/action-feedback", { method: "POST", body: { feedback }, tabId });
|
|
3389
4704
|
} catch (error) {
|
|
3390
4705
|
if (!isMissingActionFeedbackEndpoint(error)) throw error;
|
|
3391
|
-
addEvent("/api/action-feedback not found; falling back to a normal prompt. Restart Web UI to use the dedicated endpoint.", "warn");
|
|
4706
|
+
if (isCurrentTabContext(tabContext)) addEvent("/api/action-feedback not found; falling back to a normal prompt. Restart Web UI to use the dedicated endpoint.", "warn");
|
|
3392
4707
|
await api("/api/prompt", { method: "POST", body: { message: formatActionFeedbackLearningPrompt(feedback) }, tabId });
|
|
3393
4708
|
}
|
|
3394
4709
|
}
|
|
@@ -3433,6 +4748,7 @@ function renderFeedbackTray() {
|
|
|
3433
4748
|
|
|
3434
4749
|
async function submitQueuedActionFeedback() {
|
|
3435
4750
|
const tabId = activeTabId;
|
|
4751
|
+
const tabContext = activeTabContext(tabId);
|
|
3436
4752
|
const items = queuedActionFeedback(tabId);
|
|
3437
4753
|
if (!tabId || items.length === 0 || actionFeedbackSendBusy) return;
|
|
3438
4754
|
if (isRunActive()) {
|
|
@@ -3446,28 +4762,32 @@ async function submitQueuedActionFeedback() {
|
|
|
3446
4762
|
setRunIndicatorActivity("Sending action feedback to Pi…");
|
|
3447
4763
|
renderFeedbackTray();
|
|
3448
4764
|
try {
|
|
3449
|
-
await postQueuedFeedback(tabId, items);
|
|
4765
|
+
await postQueuedFeedback(tabId, items, tabContext);
|
|
3450
4766
|
actionFeedbackByTab.get(tabId)?.clear();
|
|
4767
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
3451
4768
|
renderAllMessages({ preserveScroll: true });
|
|
3452
4769
|
addEvent("feedback sent; Pi will create a LEARNING");
|
|
3453
|
-
scheduleRefreshState();
|
|
3454
|
-
scheduleRefreshMessages();
|
|
3455
|
-
scheduleRefreshFooter();
|
|
4770
|
+
scheduleRefreshState(120, tabContext);
|
|
4771
|
+
scheduleRefreshMessages(120, tabContext);
|
|
4772
|
+
scheduleRefreshFooter(300, tabContext);
|
|
3456
4773
|
} catch (error) {
|
|
3457
4774
|
markTabIdleLocally(tabId);
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
4775
|
+
if (isCurrentTabContext(tabContext)) {
|
|
4776
|
+
clearRunIndicatorActivity();
|
|
4777
|
+
addEvent(error.message, "error");
|
|
4778
|
+
addTransientMessage({ role: "error", title: "feedback", content: error.message, level: "error" });
|
|
4779
|
+
}
|
|
3461
4780
|
} finally {
|
|
3462
4781
|
actionFeedbackSendBusy = false;
|
|
3463
4782
|
renderFeedbackTray();
|
|
3464
4783
|
}
|
|
3465
4784
|
}
|
|
3466
4785
|
|
|
3467
|
-
function renderContent(parent, content) {
|
|
4786
|
+
function renderContent(parent, content, { markdown = false } = {}) {
|
|
3468
4787
|
if (content === undefined || content === null) return;
|
|
3469
4788
|
if (typeof content === "string") {
|
|
3470
|
-
|
|
4789
|
+
if (markdown) appendMarkdown(parent, stripTodoProgressLines(content));
|
|
4790
|
+
else appendText(parent, content);
|
|
3471
4791
|
return;
|
|
3472
4792
|
}
|
|
3473
4793
|
if (!Array.isArray(content)) {
|
|
@@ -3481,8 +4801,11 @@ function renderContent(parent, content) {
|
|
|
3481
4801
|
continue;
|
|
3482
4802
|
}
|
|
3483
4803
|
if (part.type === "text") {
|
|
3484
|
-
|
|
4804
|
+
const text = assistantTextPartText(part);
|
|
4805
|
+
if (markdown) appendMarkdown(parent, stripTodoProgressLines(text));
|
|
4806
|
+
else appendText(parent, text);
|
|
3485
4807
|
} else if (part.type === "thinking") {
|
|
4808
|
+
if (!thinkingOutputVisible) continue;
|
|
3486
4809
|
const details = make("details", "thinking-block");
|
|
3487
4810
|
details.open = true;
|
|
3488
4811
|
details.append(make("summary", undefined, "thinking"));
|
|
@@ -3503,10 +4826,11 @@ function renderContent(parent, content) {
|
|
|
3503
4826
|
}
|
|
3504
4827
|
|
|
3505
4828
|
function messageTitle(message) {
|
|
3506
|
-
if (message.role === "assistant") return "
|
|
4829
|
+
if (message.role === "assistant") return message.title || "final output";
|
|
3507
4830
|
if (message.title) return message.title;
|
|
3508
4831
|
if (message.role === "thinking") return "thinking";
|
|
3509
4832
|
if (message.role === "toolCall") return `tool call: ${message.toolName || "unknown"}`;
|
|
4833
|
+
if (message.role === "toolExecution") return toolExecutionTitle(message);
|
|
3510
4834
|
if (message.role === "assistantEvent") return "assistant event";
|
|
3511
4835
|
if (message.role === "toolResult") return `tool result: ${message.toolName || "unknown"}`;
|
|
3512
4836
|
if (message.role === "bashExecution") return `bash: ${message.command || ""}`;
|
|
@@ -3537,13 +4861,26 @@ function assistantToolCallArguments(part) {
|
|
|
3537
4861
|
return part?.arguments || part?.args || part?.input || part?.toolCall?.arguments || {};
|
|
3538
4862
|
}
|
|
3539
4863
|
|
|
4864
|
+
function assistantTextPartText(part) {
|
|
4865
|
+
if (!part || typeof part !== "object" || part.type !== "text") return "";
|
|
4866
|
+
if (typeof part.text === "string") return part.text;
|
|
4867
|
+
return typeof part.content === "string" ? part.content : "";
|
|
4868
|
+
}
|
|
4869
|
+
|
|
4870
|
+
function isEmptyAssistantTextPart(part) {
|
|
4871
|
+
return !!(part && typeof part === "object" && part.type === "text" && !assistantTextPartText(part).trim());
|
|
4872
|
+
}
|
|
4873
|
+
|
|
3540
4874
|
function assistantFinalOutputPart(part) {
|
|
3541
4875
|
if (part === undefined || part === null) return null;
|
|
3542
4876
|
if (typeof part !== "object") {
|
|
3543
4877
|
const text = String(part);
|
|
3544
4878
|
return text.trim() ? { type: "text", text } : null;
|
|
3545
4879
|
}
|
|
3546
|
-
if (part.type === "text")
|
|
4880
|
+
if (part.type === "text") {
|
|
4881
|
+
const text = assistantTextPartText(part);
|
|
4882
|
+
return text.trim() ? { ...part, type: "text", text } : null;
|
|
4883
|
+
}
|
|
3547
4884
|
if (typeof part.text === "string") return part.text.trim() ? { ...part, type: "text", text: part.text } : null;
|
|
3548
4885
|
if (part.type === "image") return part;
|
|
3549
4886
|
if (typeof part.content === "string" && part.type !== "thinking" && part.type !== "toolCall" && typeof part.thinking !== "string") {
|
|
@@ -3557,10 +4894,10 @@ function assistantDisplayMessages(message) {
|
|
|
3557
4894
|
const base = { timestamp: message.timestamp };
|
|
3558
4895
|
const content = message.content;
|
|
3559
4896
|
if (typeof content === "string") {
|
|
3560
|
-
return content.trim() ? [{ ...message, title: "
|
|
4897
|
+
return content.trim() ? [{ ...message, title: "final output" }] : [];
|
|
3561
4898
|
}
|
|
3562
4899
|
if (!Array.isArray(content)) {
|
|
3563
|
-
return content === undefined || content === null ? [] : [{ ...message, title: "
|
|
4900
|
+
return content === undefined || content === null ? [] : [{ ...message, title: "final output" }];
|
|
3564
4901
|
}
|
|
3565
4902
|
|
|
3566
4903
|
const displayMessages = [];
|
|
@@ -3576,7 +4913,8 @@ function assistantDisplayMessages(message) {
|
|
|
3576
4913
|
if (isAssistantToolCallPart(part)) {
|
|
3577
4914
|
const toolName = assistantToolCallName(part);
|
|
3578
4915
|
const args = assistantToolCallArguments(part);
|
|
3579
|
-
|
|
4916
|
+
const toolCallId = assistantToolCallId(part);
|
|
4917
|
+
displayMessages.push({ ...base, role: "toolCall", title: `tool call: ${toolName}`, toolName, toolCallId, arguments: args, content: args });
|
|
3580
4918
|
continue;
|
|
3581
4919
|
}
|
|
3582
4920
|
const finalPart = assistantFinalOutputPart(part);
|
|
@@ -3584,13 +4922,14 @@ function assistantDisplayMessages(message) {
|
|
|
3584
4922
|
if (!assistantHasToolCallAfter(content, index)) finalParts.push(finalPart);
|
|
3585
4923
|
continue;
|
|
3586
4924
|
}
|
|
4925
|
+
if (isEmptyAssistantTextPart(part)) continue;
|
|
3587
4926
|
if (part !== undefined && part !== null) {
|
|
3588
4927
|
displayMessages.push({ ...base, role: "assistantEvent", title: part?.type ? `assistant ${part.type}` : "assistant event", content: part });
|
|
3589
4928
|
}
|
|
3590
4929
|
}
|
|
3591
4930
|
|
|
3592
4931
|
if (finalParts.length > 0) {
|
|
3593
|
-
displayMessages.push({ ...message, title: "
|
|
4932
|
+
displayMessages.push({ ...message, title: "final output", content: finalParts });
|
|
3594
4933
|
}
|
|
3595
4934
|
return displayMessages;
|
|
3596
4935
|
}
|
|
@@ -3684,6 +5023,7 @@ function stickyUserPromptViewportGap() {
|
|
|
3684
5023
|
}
|
|
3685
5024
|
|
|
3686
5025
|
function resetChatOutput() {
|
|
5026
|
+
liveToolCards.clear();
|
|
3687
5027
|
elements.chat.replaceChildren();
|
|
3688
5028
|
if (elements.stickyUserPromptButton) elements.chat.append(elements.stickyUserPromptButton);
|
|
3689
5029
|
}
|
|
@@ -3701,47 +5041,556 @@ function userPromptTargets() {
|
|
|
3701
5041
|
.sort((a, b) => a.index - b.index);
|
|
3702
5042
|
}
|
|
3703
5043
|
|
|
3704
|
-
function findStickyUserPromptTarget(targets = userPromptTargets()) {
|
|
3705
|
-
if (targets.length === 0) return cachedLastUserPromptTarget();
|
|
3706
|
-
const viewportTop = elements.chat.scrollTop + stickyUserPromptViewportGap();
|
|
3707
|
-
const previousPrompt = targets.filter((target) => target.top < viewportTop - STICKY_USER_PROMPT_TOP_GAP_PX).at(-1);
|
|
3708
|
-
if (previousPrompt) return previousPrompt;
|
|
5044
|
+
function findStickyUserPromptTarget(targets = userPromptTargets()) {
|
|
5045
|
+
if (targets.length === 0) return cachedLastUserPromptTarget();
|
|
5046
|
+
const viewportTop = elements.chat.scrollTop + stickyUserPromptViewportGap();
|
|
5047
|
+
const previousPrompt = targets.filter((target) => target.top < viewportTop - STICKY_USER_PROMPT_TOP_GAP_PX).at(-1);
|
|
5048
|
+
if (previousPrompt) return previousPrompt;
|
|
5049
|
+
|
|
5050
|
+
const latestPrompt = targets.at(-1);
|
|
5051
|
+
const latestTopInView = latestPrompt.top - elements.chat.scrollTop;
|
|
5052
|
+
const latestVisibleNearTop = latestTopInView >= 0 && latestTopInView <= Math.min(elements.chat.clientHeight * 0.55, 180);
|
|
5053
|
+
if (targets.length === 1 && latestVisibleNearTop) return null;
|
|
5054
|
+
return latestPrompt;
|
|
5055
|
+
}
|
|
5056
|
+
|
|
5057
|
+
function updateStickyUserPromptButton() {
|
|
5058
|
+
const button = elements.stickyUserPromptButton;
|
|
5059
|
+
if (!button) return;
|
|
5060
|
+
const targets = userPromptTargets();
|
|
5061
|
+
const target = findStickyUserPromptTarget(targets);
|
|
5062
|
+
if (!target) {
|
|
5063
|
+
button.hidden = true;
|
|
5064
|
+
button.removeAttribute("data-message-index");
|
|
5065
|
+
button.removeAttribute("data-compacted");
|
|
5066
|
+
button.replaceChildren();
|
|
5067
|
+
return;
|
|
5068
|
+
}
|
|
5069
|
+
|
|
5070
|
+
const ordinal = target.compacted ? 1 : targets.findIndex((item) => item.index === target.index) + 1;
|
|
5071
|
+
const isLatest = target.compacted || ordinal === targets.length;
|
|
5072
|
+
const label = target.compacted ? "Last user prompt (compacted)" : isLatest ? "Last user prompt" : "Previous user prompt";
|
|
5073
|
+
const meta = target.compacted ? "summary ↑" : `${ordinal}/${targets.length} ↑`;
|
|
5074
|
+
button.hidden = false;
|
|
5075
|
+
button.dataset.compacted = target.compacted ? "true" : "false";
|
|
5076
|
+
if (Number.isInteger(target.index) && target.index >= 0) button.dataset.messageIndex = String(target.index);
|
|
5077
|
+
else button.removeAttribute("data-message-index");
|
|
5078
|
+
button.title = target.compacted ? `Prompt was compacted; jump to compaction summary: ${target.preview}` : `Jump to ${label.toLowerCase()}: ${target.preview}`;
|
|
5079
|
+
button.setAttribute("aria-label", target.compacted ? `Prompt was compacted; jump to compaction summary: ${target.preview}` : `Jump to ${label.toLowerCase()} (${ordinal} of ${targets.length}): ${target.preview}`);
|
|
5080
|
+
button.replaceChildren(
|
|
5081
|
+
make("span", "sticky-user-prompt-label", label),
|
|
5082
|
+
make("span", "sticky-user-prompt-text", target.preview),
|
|
5083
|
+
make("span", "sticky-user-prompt-meta", meta),
|
|
5084
|
+
);
|
|
5085
|
+
}
|
|
5086
|
+
|
|
5087
|
+
function assistantToolCallId(part) {
|
|
5088
|
+
const id = part?.id || part?.toolCallId || part?.tool_call_id || part?.toolCall?.id || part?.toolCall?.toolCallId || part?.toolCall?.tool_call_id;
|
|
5089
|
+
return id === undefined || id === null ? "" : String(id);
|
|
5090
|
+
}
|
|
5091
|
+
|
|
5092
|
+
function toolResultCallId(message) {
|
|
5093
|
+
const id = message?.toolCallId || message?.tool_call_id;
|
|
5094
|
+
return id === undefined || id === null ? "" : String(id);
|
|
5095
|
+
}
|
|
5096
|
+
|
|
5097
|
+
function buildToolResultMap(messages = latestMessages) {
|
|
5098
|
+
const results = new Map();
|
|
5099
|
+
for (const message of messages || []) {
|
|
5100
|
+
if (message?.role !== "toolResult") continue;
|
|
5101
|
+
const id = toolResultCallId(message);
|
|
5102
|
+
if (id && !results.has(id)) results.set(id, message);
|
|
5103
|
+
}
|
|
5104
|
+
return results;
|
|
5105
|
+
}
|
|
5106
|
+
|
|
5107
|
+
function buildAssistantToolCallIdSet(messages = latestMessages) {
|
|
5108
|
+
const ids = new Set();
|
|
5109
|
+
for (const message of messages || []) {
|
|
5110
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
5111
|
+
for (const part of message.content) {
|
|
5112
|
+
if (!isAssistantToolCallPart(part)) continue;
|
|
5113
|
+
const id = assistantToolCallId(part);
|
|
5114
|
+
if (id) ids.add(id);
|
|
5115
|
+
}
|
|
5116
|
+
}
|
|
5117
|
+
return ids;
|
|
5118
|
+
}
|
|
5119
|
+
|
|
5120
|
+
function toolResultForCallId(toolCallId, messages = latestMessages) {
|
|
5121
|
+
const id = String(toolCallId || "");
|
|
5122
|
+
if (!id) return null;
|
|
5123
|
+
for (const message of messages || []) {
|
|
5124
|
+
if (message?.role === "toolResult" && toolResultCallId(message) === id) return message;
|
|
5125
|
+
}
|
|
5126
|
+
return null;
|
|
5127
|
+
}
|
|
5128
|
+
|
|
5129
|
+
function cleanupLiveToolRunsForMessages(messages = latestMessages) {
|
|
5130
|
+
const results = buildToolResultMap(messages);
|
|
5131
|
+
for (const id of liveToolRuns.keys()) {
|
|
5132
|
+
if (results.has(id)) {
|
|
5133
|
+
liveToolRuns.delete(id);
|
|
5134
|
+
cancelQueuedLiveToolRunRender(id);
|
|
5135
|
+
}
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
|
|
5139
|
+
function shortenToolPath(value, fallback = ".") {
|
|
5140
|
+
const path = normalizeDisplayPath(value || fallback);
|
|
5141
|
+
if (path.length <= 96) return path;
|
|
5142
|
+
return `…${path.slice(-95)}`;
|
|
5143
|
+
}
|
|
5144
|
+
|
|
5145
|
+
function toolArgValue(args, keys) {
|
|
5146
|
+
const keyList = Array.isArray(keys) ? keys : [keys];
|
|
5147
|
+
for (const key of keyList) {
|
|
5148
|
+
if (args && Object.prototype.hasOwnProperty.call(args, key)) return args[key];
|
|
5149
|
+
}
|
|
5150
|
+
return undefined;
|
|
5151
|
+
}
|
|
5152
|
+
|
|
5153
|
+
function toolArgText(args, keys, fallback = "") {
|
|
5154
|
+
const value = toolArgValue(args, keys);
|
|
5155
|
+
if (value === undefined || value === null) return fallback;
|
|
5156
|
+
if (typeof value === "string") return value;
|
|
5157
|
+
return String(value);
|
|
5158
|
+
}
|
|
5159
|
+
|
|
5160
|
+
function toolExecutionResult(message) {
|
|
5161
|
+
if (message?.result) return message.result;
|
|
5162
|
+
if (message?.partialResult) return { ...message.partialResult, isError: false };
|
|
5163
|
+
if (message?.role === "toolResult") return message;
|
|
5164
|
+
return null;
|
|
5165
|
+
}
|
|
5166
|
+
|
|
5167
|
+
function toolResultText(result) {
|
|
5168
|
+
if (!result) return "";
|
|
5169
|
+
return stripAnsi(textFromContent(result.content)).replace(/\s+$/g, "");
|
|
5170
|
+
}
|
|
5171
|
+
|
|
5172
|
+
function toolExecutionStatus(message) {
|
|
5173
|
+
const result = toolExecutionResult(message);
|
|
5174
|
+
if (message?.isPartial) return "running";
|
|
5175
|
+
if (!result) return "pending";
|
|
5176
|
+
return message?.isError || result?.isError ? "error" : "success";
|
|
5177
|
+
}
|
|
5178
|
+
|
|
5179
|
+
function toolExecutionTitle(message) {
|
|
5180
|
+
const name = runIndicatorToolName(message?.toolName || message?.name || "tool");
|
|
5181
|
+
const status = toolExecutionStatus(message);
|
|
5182
|
+
if (status === "running") return `tool: ${name} (running)`;
|
|
5183
|
+
if (status === "pending") return `tool: ${name} (pending)`;
|
|
5184
|
+
if (status === "error") return `tool: ${name} (failed)`;
|
|
5185
|
+
return `tool: ${name}`;
|
|
5186
|
+
}
|
|
5187
|
+
|
|
5188
|
+
function toolLineRange(args) {
|
|
5189
|
+
const offset = toolArgValue(args, "offset");
|
|
5190
|
+
const limit = toolArgValue(args, "limit");
|
|
5191
|
+
const start = Number.isFinite(Number(offset)) ? Number(offset) : null;
|
|
5192
|
+
const count = Number.isFinite(Number(limit)) ? Number(limit) : null;
|
|
5193
|
+
if (start === null && count === null) return "";
|
|
5194
|
+
const first = start ?? 1;
|
|
5195
|
+
const last = count === null ? "" : first + count - 1;
|
|
5196
|
+
return `:${first}${last ? `-${last}` : ""}`;
|
|
5197
|
+
}
|
|
5198
|
+
|
|
5199
|
+
function appendToolTitle(parent, name, subject = "", meta = []) {
|
|
5200
|
+
const line = make("div", "tool-title-line");
|
|
5201
|
+
line.append(make("span", "tool-name", name));
|
|
5202
|
+
if (subject) line.append(make("span", "tool-subject", subject));
|
|
5203
|
+
parent.append(line);
|
|
5204
|
+
const items = meta.filter(Boolean);
|
|
5205
|
+
if (items.length > 0) {
|
|
5206
|
+
const metaLine = make("div", "tool-meta-line");
|
|
5207
|
+
for (const item of items) metaLine.append(make("span", "tool-meta-pill", item));
|
|
5208
|
+
parent.append(metaLine);
|
|
5209
|
+
}
|
|
5210
|
+
}
|
|
5211
|
+
|
|
5212
|
+
function appendToolCommand(parent, command, meta = []) {
|
|
5213
|
+
const line = make("pre", "tool-command-line");
|
|
5214
|
+
line.textContent = `$ ${command || "..."}`;
|
|
5215
|
+
parent.append(line);
|
|
5216
|
+
const items = meta.filter(Boolean);
|
|
5217
|
+
if (items.length > 0) {
|
|
5218
|
+
const metaLine = make("div", "tool-meta-line");
|
|
5219
|
+
for (const item of items) metaLine.append(make("span", "tool-meta-pill", item));
|
|
5220
|
+
parent.append(metaLine);
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
5223
|
+
|
|
5224
|
+
function appendToolImages(parent, result) {
|
|
5225
|
+
if (!Array.isArray(result?.content)) return;
|
|
5226
|
+
for (const part of result.content) {
|
|
5227
|
+
if (part?.type === "image") appendImage(parent, part);
|
|
5228
|
+
}
|
|
5229
|
+
}
|
|
5230
|
+
|
|
5231
|
+
function appendToolOutput(parent, text, { label = "output", previewLines = 10, previewFromEnd = false, open = false, emptyText = "" } = {}) {
|
|
5232
|
+
const clean = stripAnsi(text).replace(/\s+$/g, "");
|
|
5233
|
+
if (!clean) {
|
|
5234
|
+
if (emptyText) appendText(parent, emptyText, "code-block tool-output-code muted-output");
|
|
5235
|
+
return;
|
|
5236
|
+
}
|
|
5237
|
+
const lines = clean.split(/\r?\n/);
|
|
5238
|
+
if (lines.length > previewLines) {
|
|
5239
|
+
const details = make("details", "tool-output-details");
|
|
5240
|
+
details.open = open;
|
|
5241
|
+
details.append(make("summary", "tool-output-summary", `${label} (${lines.length} lines; expand)`));
|
|
5242
|
+
appendText(details, clean, "code-block tool-output-code");
|
|
5243
|
+
parent.append(details);
|
|
5244
|
+
|
|
5245
|
+
const preview = make("div", "tool-output-preview");
|
|
5246
|
+
const visibleLines = previewFromEnd ? lines.slice(-previewLines) : lines.slice(0, previewLines);
|
|
5247
|
+
const omitted = lines.length - visibleLines.length;
|
|
5248
|
+
const hint = previewFromEnd
|
|
5249
|
+
? `… ${omitted} earlier line${omitted === 1 ? "" : "s"}; expand for full output`
|
|
5250
|
+
: `… ${omitted} more line${omitted === 1 ? "" : "s"}; expand for full output`;
|
|
5251
|
+
appendText(preview, `${visibleLines.join("\n")}\n${hint}`, "code-block tool-output-code tool-output-preview-text");
|
|
5252
|
+
parent.append(preview);
|
|
5253
|
+
return;
|
|
5254
|
+
}
|
|
5255
|
+
appendText(parent, clean, "code-block tool-output-code");
|
|
5256
|
+
}
|
|
5257
|
+
|
|
5258
|
+
function appendToolWarnings(parent, details = {}) {
|
|
5259
|
+
const warnings = [];
|
|
5260
|
+
if (details.fullOutputPath) warnings.push(`Full output: ${details.fullOutputPath}`);
|
|
5261
|
+
const truncation = details.truncation;
|
|
5262
|
+
if (truncation?.truncated) {
|
|
5263
|
+
if (truncation.truncatedBy === "lines") warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
|
|
5264
|
+
else if (truncation.outputLines) warnings.push(`Truncated: ${truncation.outputLines} lines shown`);
|
|
5265
|
+
else warnings.push("Output truncated");
|
|
5266
|
+
}
|
|
5267
|
+
if (details.matchLimitReached) warnings.push(`Match limit reached: ${details.matchLimitReached}`);
|
|
5268
|
+
if (details.resultLimitReached) warnings.push(`Result limit reached: ${details.resultLimitReached}`);
|
|
5269
|
+
if (details.entryLimitReached) warnings.push(`Entry limit reached: ${details.entryLimitReached}`);
|
|
5270
|
+
if (warnings.length === 0) return;
|
|
5271
|
+
const box = make("div", "tool-warnings");
|
|
5272
|
+
for (const warning of warnings) box.append(make("div", "tool-warning", warning));
|
|
5273
|
+
parent.append(box);
|
|
5274
|
+
}
|
|
5275
|
+
|
|
5276
|
+
function appendToolDiff(parent, diff) {
|
|
5277
|
+
const value = String(diff || "").replace(/\s+$/g, "");
|
|
5278
|
+
if (!value) return false;
|
|
5279
|
+
const block = make("div", "tool-diff");
|
|
5280
|
+
for (const line of value.split(/\r?\n/)) {
|
|
5281
|
+
const cls = /^@@/.test(line)
|
|
5282
|
+
? "diff-hunk"
|
|
5283
|
+
: /^\+/.test(line) && !/^\+\+\+/.test(line)
|
|
5284
|
+
? "diff-added"
|
|
5285
|
+
: /^-/.test(line) && !/^---/.test(line)
|
|
5286
|
+
? "diff-removed"
|
|
5287
|
+
: /^(?:\+\+\+|---)/.test(line)
|
|
5288
|
+
? "diff-file"
|
|
5289
|
+
: "diff-context";
|
|
5290
|
+
block.append(make("div", cls, line || " "));
|
|
5291
|
+
}
|
|
5292
|
+
parent.append(block);
|
|
5293
|
+
return true;
|
|
5294
|
+
}
|
|
5295
|
+
|
|
5296
|
+
function normalizeToolExecution(message) {
|
|
5297
|
+
const result = toolExecutionResult(message);
|
|
5298
|
+
const args = message?.arguments ?? message?.args ?? {};
|
|
5299
|
+
const name = runIndicatorToolName(message?.toolName || message?.name || "tool");
|
|
5300
|
+
return {
|
|
5301
|
+
name,
|
|
5302
|
+
args,
|
|
5303
|
+
result,
|
|
5304
|
+
text: toolResultText(result),
|
|
5305
|
+
details: result?.details || message?.details || {},
|
|
5306
|
+
isPartial: !!message?.isPartial,
|
|
5307
|
+
isError: !!(message?.isError || result?.isError),
|
|
5308
|
+
startedAt: message?.startedAt || null,
|
|
5309
|
+
endedAt: message?.endedAt || null,
|
|
5310
|
+
};
|
|
5311
|
+
}
|
|
5312
|
+
|
|
5313
|
+
function toolElapsedLabel(tool) {
|
|
5314
|
+
if (!tool.startedAt) return "";
|
|
5315
|
+
const end = tool.endedAt || Date.now();
|
|
5316
|
+
return `${tool.isPartial ? "elapsed" : "took"} ${formatDuration(end - tool.startedAt)}`;
|
|
5317
|
+
}
|
|
5318
|
+
|
|
5319
|
+
function toolStatusLabel(tool) {
|
|
5320
|
+
if (tool.isPartial) return "live";
|
|
5321
|
+
if (tool.isError) return "failed";
|
|
5322
|
+
if (tool.result) return "done";
|
|
5323
|
+
return "pending";
|
|
5324
|
+
}
|
|
5325
|
+
|
|
5326
|
+
function toolStateMeta(tool) {
|
|
5327
|
+
return [toolElapsedLabel(tool), toolStatusLabel(tool)];
|
|
5328
|
+
}
|
|
5329
|
+
|
|
5330
|
+
function toolLineCountLabel(text, label = "line") {
|
|
5331
|
+
const value = String(text || "").replace(/\s+$/g, "");
|
|
5332
|
+
if (!value) return "";
|
|
5333
|
+
const count = value.split(/\r?\n/).length;
|
|
5334
|
+
return `${count} ${label}${count === 1 ? "" : "s"}`;
|
|
5335
|
+
}
|
|
5336
|
+
|
|
5337
|
+
function toolRawDetailsReplacer(key, value) {
|
|
5338
|
+
if (typeof value === "string" && value.length > 4000) return `${value.slice(0, 4000)}… (${value.length - 4000} chars omitted)`;
|
|
5339
|
+
return value;
|
|
5340
|
+
}
|
|
5341
|
+
|
|
5342
|
+
function appendToolRawDetails(parent, tool) {
|
|
5343
|
+
const raw = JSON.stringify({ arguments: tool.args ?? {}, result: tool.result ?? null, details: tool.details ?? {} }, toolRawDetailsReplacer, 2);
|
|
5344
|
+
const details = make("details", "tool-raw-details");
|
|
5345
|
+
details.append(make("summary", "tool-raw-summary", "raw tool data"));
|
|
5346
|
+
appendText(details, raw, "code-block tool-raw-code");
|
|
5347
|
+
parent.append(details);
|
|
5348
|
+
}
|
|
5349
|
+
|
|
5350
|
+
function renderBashToolExecution(parent, tool) {
|
|
5351
|
+
const command = toolArgText(tool.args, "command", "");
|
|
5352
|
+
const timeout = toolArgValue(tool.args, "timeout");
|
|
5353
|
+
const meta = [timeout ? `timeout ${timeout}s` : "", ...toolStateMeta(tool)];
|
|
5354
|
+
appendToolCommand(parent, command, meta);
|
|
5355
|
+
appendToolOutput(parent, tool.text, { label: tool.isPartial ? "live output" : "output", previewLines: 5, previewFromEnd: true, open: tool.isError, emptyText: tool.isPartial ? "(no output yet)" : "" });
|
|
5356
|
+
appendToolWarnings(parent, tool.details);
|
|
5357
|
+
}
|
|
5358
|
+
|
|
5359
|
+
function renderReadToolExecution(parent, tool) {
|
|
5360
|
+
const path = toolArgText(tool.args, ["file_path", "path"], "");
|
|
5361
|
+
appendToolTitle(parent, "read", `${shortenToolPath(path)}${toolLineRange(tool.args)}`, [toolLineCountLabel(tool.text), ...toolStateMeta(tool)]);
|
|
5362
|
+
appendToolImages(parent, tool.result);
|
|
5363
|
+
appendToolOutput(parent, tool.text, { label: "file output", previewLines: 10, open: tool.isError });
|
|
5364
|
+
appendToolWarnings(parent, tool.details);
|
|
5365
|
+
}
|
|
5366
|
+
|
|
5367
|
+
function renderWriteToolExecution(parent, tool) {
|
|
5368
|
+
const path = toolArgText(tool.args, ["file_path", "path"], "");
|
|
5369
|
+
const content = toolArgText(tool.args, "content", "");
|
|
5370
|
+
const lineCount = content ? content.split(/\r?\n/).length : 0;
|
|
5371
|
+
appendToolTitle(parent, "write", shortenToolPath(path), [lineCount > 0 ? `${lineCount} line${lineCount === 1 ? "" : "s"}` : "", ...toolStateMeta(tool)]);
|
|
5372
|
+
appendToolOutput(parent, content, { label: "content", previewLines: 10 });
|
|
5373
|
+
appendToolOutput(parent, tool.text, { label: "result", previewLines: 6, open: tool.isError });
|
|
5374
|
+
}
|
|
5375
|
+
|
|
5376
|
+
function renderEditToolExecution(parent, tool) {
|
|
5377
|
+
const path = toolArgText(tool.args, ["file_path", "path"], "");
|
|
5378
|
+
const edits = Array.isArray(tool.args?.edits) ? tool.args.edits.length : 0;
|
|
5379
|
+
appendToolTitle(parent, "edit", shortenToolPath(path), [edits ? `${edits} replacement${edits === 1 ? "" : "s"}` : "", ...toolStateMeta(tool)]);
|
|
5380
|
+
const hasDiff = appendToolDiff(parent, tool.details?.diff || tool.details?.patch);
|
|
5381
|
+
appendToolOutput(parent, tool.text, { label: "result", previewLines: hasDiff ? 4 : 10, open: tool.isError });
|
|
5382
|
+
}
|
|
5383
|
+
|
|
5384
|
+
function renderGrepToolExecution(parent, tool) {
|
|
5385
|
+
const pattern = toolArgText(tool.args, "pattern", "");
|
|
5386
|
+
const path = toolArgText(tool.args, "path", ".");
|
|
5387
|
+
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)]);
|
|
5388
|
+
appendToolOutput(parent, tool.text, { label: "matches", previewLines: 10, open: tool.isError });
|
|
5389
|
+
appendToolWarnings(parent, tool.details);
|
|
5390
|
+
}
|
|
5391
|
+
|
|
5392
|
+
function renderFindToolExecution(parent, tool) {
|
|
5393
|
+
const pattern = toolArgText(tool.args, "pattern", "");
|
|
5394
|
+
const path = toolArgText(tool.args, "path", ".");
|
|
5395
|
+
appendToolTitle(parent, "find", `${pattern || "…"} in ${shortenToolPath(path)}`, [tool.args?.limit ? `limit ${tool.args.limit}` : "", toolLineCountLabel(tool.text, "result"), ...toolStateMeta(tool)]);
|
|
5396
|
+
appendToolOutput(parent, tool.text, { label: "results", previewLines: 10, open: tool.isError });
|
|
5397
|
+
appendToolWarnings(parent, tool.details);
|
|
5398
|
+
}
|
|
5399
|
+
|
|
5400
|
+
function renderLsToolExecution(parent, tool) {
|
|
5401
|
+
const path = toolArgText(tool.args, "path", ".");
|
|
5402
|
+
appendToolTitle(parent, "ls", shortenToolPath(path), [tool.args?.limit ? `limit ${tool.args.limit}` : "", toolLineCountLabel(tool.text, "entry"), ...toolStateMeta(tool)]);
|
|
5403
|
+
appendToolOutput(parent, tool.text, { label: "entries", previewLines: 20, open: tool.isError });
|
|
5404
|
+
appendToolWarnings(parent, tool.details);
|
|
5405
|
+
}
|
|
5406
|
+
|
|
5407
|
+
function renderGenericToolExecution(parent, tool) {
|
|
5408
|
+
appendToolTitle(parent, tool.name, "", toolStateMeta(tool));
|
|
5409
|
+
appendToolOutput(parent, JSON.stringify(tool.args ?? {}, null, 2), { label: "arguments", previewLines: 12 });
|
|
5410
|
+
appendToolImages(parent, tool.result);
|
|
5411
|
+
appendToolOutput(parent, tool.text, { label: "result", previewLines: 10, open: tool.isError });
|
|
5412
|
+
appendToolWarnings(parent, tool.details);
|
|
5413
|
+
}
|
|
5414
|
+
|
|
5415
|
+
const WEBUI_TOOL_RENDERERS = {
|
|
5416
|
+
bash: renderBashToolExecution,
|
|
5417
|
+
read: renderReadToolExecution,
|
|
5418
|
+
write: renderWriteToolExecution,
|
|
5419
|
+
edit: renderEditToolExecution,
|
|
5420
|
+
grep: renderGrepToolExecution,
|
|
5421
|
+
find: renderFindToolExecution,
|
|
5422
|
+
ls: renderLsToolExecution,
|
|
5423
|
+
};
|
|
5424
|
+
|
|
5425
|
+
function renderToolExecution(parent, message) {
|
|
5426
|
+
const tool = normalizeToolExecution(message);
|
|
5427
|
+
const renderer = WEBUI_TOOL_RENDERERS[tool.name] || renderGenericToolExecution;
|
|
5428
|
+
renderer(parent, tool);
|
|
5429
|
+
appendToolRawDetails(parent, tool);
|
|
5430
|
+
}
|
|
5431
|
+
|
|
5432
|
+
function liveToolRunMessage(run) {
|
|
5433
|
+
return {
|
|
5434
|
+
role: "toolExecution",
|
|
5435
|
+
title: toolExecutionTitle(run),
|
|
5436
|
+
toolName: run.toolName,
|
|
5437
|
+
toolCallId: run.toolCallId,
|
|
5438
|
+
arguments: run.arguments,
|
|
5439
|
+
result: run.result,
|
|
5440
|
+
isPartial: run.isPartial,
|
|
5441
|
+
isError: run.isError,
|
|
5442
|
+
startedAt: run.startedAt,
|
|
5443
|
+
endedAt: run.endedAt,
|
|
5444
|
+
timestamp: run.timestamp,
|
|
5445
|
+
live: true,
|
|
5446
|
+
};
|
|
5447
|
+
}
|
|
5448
|
+
|
|
5449
|
+
function applyToolExecutionBubbleState(bubble, message) {
|
|
5450
|
+
const status = toolExecutionStatus(message);
|
|
5451
|
+
bubble.classList.remove("tool-pending", "tool-running", "tool-success", "tool-error", "error");
|
|
5452
|
+
bubble.classList.add(`tool-${status}`);
|
|
5453
|
+
if (message.isError || status === "error") bubble.classList.add("error");
|
|
5454
|
+
if (message.toolCallId) {
|
|
5455
|
+
const id = String(message.toolCallId);
|
|
5456
|
+
bubble.dataset.toolCallId = id;
|
|
5457
|
+
if (message.live) liveToolCards.set(id, bubble);
|
|
5458
|
+
}
|
|
5459
|
+
}
|
|
5460
|
+
|
|
5461
|
+
function toolDetailsStateKey(details, counts) {
|
|
5462
|
+
const classKey = Array.from(details.classList || []).sort().join(".") || "details";
|
|
5463
|
+
const summaryText = details.querySelector("summary")?.textContent || "";
|
|
5464
|
+
const summaryKey = summaryText.replace(/\s*\([^)]*\)\s*$/g, "").trim();
|
|
5465
|
+
const base = `${classKey}|${summaryKey}`;
|
|
5466
|
+
const index = counts.get(base) || 0;
|
|
5467
|
+
counts.set(base, index + 1);
|
|
5468
|
+
return `${base}|${index}`;
|
|
5469
|
+
}
|
|
5470
|
+
|
|
5471
|
+
function captureToolDetailsOpenState(root) {
|
|
5472
|
+
const state = new Set();
|
|
5473
|
+
const counts = new Map();
|
|
5474
|
+
for (const details of root.querySelectorAll("details")) {
|
|
5475
|
+
const key = toolDetailsStateKey(details, counts);
|
|
5476
|
+
if (details.open) state.add(key);
|
|
5477
|
+
}
|
|
5478
|
+
return state;
|
|
5479
|
+
}
|
|
5480
|
+
|
|
5481
|
+
function restoreToolDetailsOpenState(root, state) {
|
|
5482
|
+
if (!state?.size) return;
|
|
5483
|
+
const counts = new Map();
|
|
5484
|
+
for (const details of root.querySelectorAll("details")) {
|
|
5485
|
+
if (state.has(toolDetailsStateKey(details, counts))) details.open = true;
|
|
5486
|
+
}
|
|
5487
|
+
}
|
|
5488
|
+
|
|
5489
|
+
function updateLiveToolCard(bubble, message) {
|
|
5490
|
+
if (!bubble?.isConnected) return false;
|
|
5491
|
+
const header = bubble.querySelector(":scope > .message-header");
|
|
5492
|
+
const body = bubble.querySelector(":scope > .message-body");
|
|
5493
|
+
if (!body) return false;
|
|
5494
|
+
applyToolExecutionBubbleState(bubble, message);
|
|
5495
|
+
const role = header?.querySelector(".message-role");
|
|
5496
|
+
if (role) role.textContent = messageTitle(message);
|
|
5497
|
+
const timestamp = header?.querySelector(".muted");
|
|
5498
|
+
if (timestamp) timestamp.textContent = formatDate(message.timestamp);
|
|
5499
|
+
const detailsOpenState = captureToolDetailsOpenState(body);
|
|
5500
|
+
body.replaceChildren();
|
|
5501
|
+
renderToolExecution(body, message);
|
|
5502
|
+
restoreToolDetailsOpenState(body, detailsOpenState);
|
|
5503
|
+
return true;
|
|
5504
|
+
}
|
|
3709
5505
|
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
if (
|
|
3714
|
-
|
|
5506
|
+
function cancelQueuedLiveToolRunRender(toolCallId = "") {
|
|
5507
|
+
if (toolCallId) liveToolRenderQueue.delete(String(toolCallId));
|
|
5508
|
+
else liveToolRenderQueue.clear();
|
|
5509
|
+
if (liveToolRenderQueue.size === 0) {
|
|
5510
|
+
clearTimeout(liveToolRenderTimer);
|
|
5511
|
+
liveToolRenderTimer = null;
|
|
5512
|
+
}
|
|
3715
5513
|
}
|
|
3716
5514
|
|
|
3717
|
-
function
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
5515
|
+
function clearLiveToolRenderQueue() {
|
|
5516
|
+
cancelQueuedLiveToolRunRender();
|
|
5517
|
+
}
|
|
5518
|
+
|
|
5519
|
+
function flushLiveToolRunRenderQueue() {
|
|
5520
|
+
const entries = Array.from(liveToolRenderQueue.values());
|
|
5521
|
+
clearLiveToolRenderQueue();
|
|
5522
|
+
for (const entry of entries) renderLiveToolRun(entry.run, { scroll: entry.scroll });
|
|
5523
|
+
}
|
|
5524
|
+
|
|
5525
|
+
function scheduleLiveToolRunRender(run, { scroll = false } = {}) {
|
|
5526
|
+
if (!run?.toolCallId) return;
|
|
5527
|
+
const id = String(run.toolCallId);
|
|
5528
|
+
const existing = liveToolRenderQueue.get(id);
|
|
5529
|
+
liveToolRenderQueue.set(id, { run, scroll: !!(existing?.scroll || scroll) });
|
|
5530
|
+
if (liveToolRenderTimer) return;
|
|
5531
|
+
liveToolRenderTimer = setTimeout(() => {
|
|
5532
|
+
liveToolRenderTimer = null;
|
|
5533
|
+
const flush = () => flushLiveToolRunRenderQueue();
|
|
5534
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(flush);
|
|
5535
|
+
else flush();
|
|
5536
|
+
}, TOOL_LIVE_UPDATE_THROTTLE_MS);
|
|
5537
|
+
}
|
|
5538
|
+
|
|
5539
|
+
function renderLiveToolRun(run, { scroll = true } = {}) {
|
|
5540
|
+
if (!run?.toolCallId) return;
|
|
5541
|
+
const id = String(run.toolCallId);
|
|
5542
|
+
cancelQueuedLiveToolRunRender(id);
|
|
5543
|
+
const existing = liveToolCards.get(id);
|
|
5544
|
+
const existingConnected = !!(existing?.isConnected && existing.parentElement === elements.chat);
|
|
5545
|
+
const shouldFollow = scroll && (autoFollowChat || isChatNearBottom());
|
|
5546
|
+
const message = liveToolRunMessage(run);
|
|
5547
|
+
if (existingConnected && updateLiveToolCard(existing, message)) {
|
|
5548
|
+
renderRunIndicator({ scroll: false });
|
|
5549
|
+
if (shouldFollow) scrollChatToBottom();
|
|
3727
5550
|
return;
|
|
3728
5551
|
}
|
|
5552
|
+
const created = appendMessage(message, { transient: true, animateEntry: !existingConnected });
|
|
5553
|
+
if (existingConnected && existing !== created.bubble) existing.replaceWith(created.bubble);
|
|
5554
|
+
renderRunIndicator({ scroll: false });
|
|
5555
|
+
if (shouldFollow) scrollChatToBottom();
|
|
5556
|
+
}
|
|
3729
5557
|
|
|
3730
|
-
|
|
3731
|
-
const
|
|
3732
|
-
|
|
3733
|
-
const
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
5558
|
+
function upsertLiveToolRun(event, patch = {}) {
|
|
5559
|
+
const id = String(event.toolCallId || "");
|
|
5560
|
+
if (!id) return null;
|
|
5561
|
+
const existing = liveToolRuns.get(id) || {};
|
|
5562
|
+
const now = Date.now();
|
|
5563
|
+
const run = {
|
|
5564
|
+
...existing,
|
|
5565
|
+
role: "toolExecution",
|
|
5566
|
+
live: true,
|
|
5567
|
+
toolCallId: id,
|
|
5568
|
+
toolName: event.toolName || existing.toolName || "tool",
|
|
5569
|
+
arguments: event.args ?? existing.arguments ?? {},
|
|
5570
|
+
timestamp: existing.timestamp || now,
|
|
5571
|
+
startedAt: existing.startedAt || now,
|
|
5572
|
+
updatedAt: now,
|
|
5573
|
+
...patch,
|
|
5574
|
+
};
|
|
5575
|
+
liveToolRuns.set(id, run);
|
|
5576
|
+
return run;
|
|
5577
|
+
}
|
|
5578
|
+
|
|
5579
|
+
function handleToolExecutionStart(event) {
|
|
5580
|
+
const run = upsertLiveToolRun(event, { isPartial: true, isError: false });
|
|
5581
|
+
if (run) renderLiveToolRun(run);
|
|
5582
|
+
}
|
|
5583
|
+
|
|
5584
|
+
function handleToolExecutionUpdate(event) {
|
|
5585
|
+
const result = { ...(event.partialResult || {}), isError: false };
|
|
5586
|
+
const run = upsertLiveToolRun(event, { result, isPartial: true, isError: false });
|
|
5587
|
+
if (run) scheduleLiveToolRunRender(run, { scroll: false });
|
|
5588
|
+
}
|
|
5589
|
+
|
|
5590
|
+
function handleToolExecutionEnd(event) {
|
|
5591
|
+
const result = { ...(event.result || {}), isError: !!event.isError };
|
|
5592
|
+
const run = upsertLiveToolRun(event, { result, isPartial: false, isError: !!event.isError, endedAt: Date.now() });
|
|
5593
|
+
if (run) renderLiveToolRun(run);
|
|
3745
5594
|
}
|
|
3746
5595
|
|
|
3747
5596
|
function toolResultPreviewText(message, lineLimit = 10) {
|
|
@@ -3771,12 +5620,15 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
3771
5620
|
const role = String(message.role || "message");
|
|
3772
5621
|
const safeRole = role.replace(/[^a-z0-9_-]/gi, "");
|
|
3773
5622
|
const bubble = make("article", `message ${safeRole}${message.level ? ` ${message.level}` : ""}${streaming ? " streaming" : ""}${animateEntry ? " action-enter" : ""}`);
|
|
5623
|
+
if (message.role === "toolExecution") applyToolExecutionBubbleState(bubble, message);
|
|
3774
5624
|
if (!transient && messageIndex >= 0) {
|
|
3775
5625
|
bubble.dataset.messageIndex = String(messageIndex);
|
|
3776
5626
|
if (role === "user") bubble.dataset.userPrompt = "true";
|
|
3777
5627
|
}
|
|
3778
5628
|
const isCollapsibleOutput = !streaming && (message.role === "toolResult" || message.role === "bashExecution" || message.role === "compactionSummary");
|
|
3779
5629
|
|
|
5630
|
+
const hideMessageHeader = message.role === "assistant" && !isCollapsibleOutput;
|
|
5631
|
+
if (hideMessageHeader) bubble.setAttribute("aria-label", messageTitle(message));
|
|
3780
5632
|
const header = make(isCollapsibleOutput ? "summary" : "div", "message-header");
|
|
3781
5633
|
header.append(make("span", "message-role", messageTitle(message)));
|
|
3782
5634
|
header.append(make("span", "muted", formatDate(message.timestamp)));
|
|
@@ -3789,15 +5641,17 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
3789
5641
|
} else if (message.role === "toolResult") {
|
|
3790
5642
|
renderContent(body, message.content);
|
|
3791
5643
|
if (message.isError) bubble.classList.add("error");
|
|
5644
|
+
} else if (message.role === "toolExecution") {
|
|
5645
|
+
renderToolExecution(body, message);
|
|
3792
5646
|
} else if (message.role === "thinking") {
|
|
3793
5647
|
const thinkingText = message.thinking || textFromContent(message.content);
|
|
3794
|
-
if (thinkingText || !streaming) appendText(body, thinkingText || "No thinking content was exposed by the provider.", "thinking-text");
|
|
5648
|
+
if (thinkingOutputVisible && (thinkingText || !streaming)) appendText(body, thinkingText || "No thinking content was exposed by the provider.", "thinking-text");
|
|
3795
5649
|
} else if (message.role === "toolCall") {
|
|
3796
5650
|
appendText(body, JSON.stringify(message.arguments ?? message.content ?? {}, null, 2), "code-block");
|
|
3797
5651
|
} else if (message.role === "assistantEvent") {
|
|
3798
5652
|
appendText(body, typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? {}, null, 2), "code-block");
|
|
3799
5653
|
} else {
|
|
3800
|
-
renderContent(body, message.content);
|
|
5654
|
+
renderContent(body, message.content, { markdown: message.role === "assistant" });
|
|
3801
5655
|
}
|
|
3802
5656
|
|
|
3803
5657
|
if (isCollapsibleOutput) {
|
|
@@ -3810,6 +5664,8 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
3810
5664
|
appendText(preview, toolResultPreviewText(message, 10), "code-block tool-result-preview-text");
|
|
3811
5665
|
bubble.append(preview);
|
|
3812
5666
|
}
|
|
5667
|
+
} else if (hideMessageHeader) {
|
|
5668
|
+
bubble.append(body);
|
|
3813
5669
|
} else {
|
|
3814
5670
|
bubble.append(header, body);
|
|
3815
5671
|
}
|
|
@@ -3826,13 +5682,31 @@ function appendTranscriptMessage(message, { streaming = false, messageIndex = -1
|
|
|
3826
5682
|
let finalOutput = null;
|
|
3827
5683
|
const displayMessages = assistantDisplayMessages(message);
|
|
3828
5684
|
displayMessages.forEach((displayMessage) => {
|
|
3829
|
-
|
|
5685
|
+
let transcriptMessage = displayMessage;
|
|
5686
|
+
if (displayMessage.role === "toolCall" && displayMessage.toolCallId) {
|
|
5687
|
+
const result = toolResultForCallId(displayMessage.toolCallId);
|
|
5688
|
+
const liveRun = liveToolRuns.get(displayMessage.toolCallId);
|
|
5689
|
+
transcriptMessage = {
|
|
5690
|
+
...displayMessage,
|
|
5691
|
+
role: "toolExecution",
|
|
5692
|
+
title: `tool: ${displayMessage.toolName || "unknown"}`,
|
|
5693
|
+
arguments: liveRun?.arguments ?? displayMessage.arguments,
|
|
5694
|
+
result: result || liveRun?.result || null,
|
|
5695
|
+
isPartial: !result && !!liveRun?.isPartial,
|
|
5696
|
+
isError: !!(result?.isError || liveRun?.isError),
|
|
5697
|
+
startedAt: liveRun?.startedAt || null,
|
|
5698
|
+
endedAt: liveRun?.endedAt || null,
|
|
5699
|
+
live: !!liveRun && !result,
|
|
5700
|
+
};
|
|
5701
|
+
}
|
|
5702
|
+
if (transcriptMessage.role === "thinking" && !thinkingOutputVisible) return;
|
|
5703
|
+
const created = appendMessage(transcriptMessage, {
|
|
3830
5704
|
streaming: false,
|
|
3831
|
-
messageIndex:
|
|
5705
|
+
messageIndex: ["assistant", "toolExecution"].includes(transcriptMessage.role) ? messageIndex : -1,
|
|
3832
5706
|
transient: false,
|
|
3833
|
-
animateEntry: animateEntry && isActionTranscriptMessage(
|
|
5707
|
+
animateEntry: animateEntry && isActionTranscriptMessage(transcriptMessage),
|
|
3834
5708
|
});
|
|
3835
|
-
if (
|
|
5709
|
+
if (transcriptMessage.role === "assistant") finalOutput = created;
|
|
3836
5710
|
});
|
|
3837
5711
|
return finalOutput;
|
|
3838
5712
|
}
|
|
@@ -3850,25 +5724,29 @@ function clearRunIndicatorGraceCheck() {
|
|
|
3850
5724
|
runIndicatorGraceCheckTimer = null;
|
|
3851
5725
|
}
|
|
3852
5726
|
|
|
3853
|
-
function scheduleRunIndicatorGraceCheck() {
|
|
5727
|
+
function scheduleRunIndicatorGraceCheck(tabContext = activeTabContext()) {
|
|
3854
5728
|
if (!runIndicatorLocallyActive || stateHasRunIndicatorActivity(currentState) || !runIndicatorStartedAt) return;
|
|
3855
5729
|
const elapsedMs = performance.now() - runIndicatorStartedAt;
|
|
3856
5730
|
const delayMs = Math.max(120, RUN_INDICATOR_START_GRACE_MS - elapsedMs + 120);
|
|
3857
5731
|
clearRunIndicatorGraceCheck();
|
|
3858
5732
|
runIndicatorGraceCheckTimer = setTimeout(() => {
|
|
3859
5733
|
runIndicatorGraceCheckTimer = null;
|
|
3860
|
-
if (!runIndicatorLocallyActive || stateHasRunIndicatorActivity(currentState)) return;
|
|
5734
|
+
if (!isCurrentTabContext(tabContext) || !runIndicatorLocallyActive || stateHasRunIndicatorActivity(currentState)) return;
|
|
3861
5735
|
runIndicatorLastStateCheckAt = performance.now();
|
|
3862
|
-
refreshState().catch((error) =>
|
|
5736
|
+
refreshState(tabContext).catch((error) => {
|
|
5737
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5738
|
+
});
|
|
3863
5739
|
}, delayMs);
|
|
3864
5740
|
}
|
|
3865
5741
|
|
|
3866
|
-
function maybeRefreshRunIndicatorState() {
|
|
5742
|
+
function maybeRefreshRunIndicatorState(tabContext = activeTabContext()) {
|
|
3867
5743
|
if (!runIndicatorIsActive()) return;
|
|
3868
5744
|
const now = performance.now();
|
|
3869
5745
|
if (now - runIndicatorLastStateCheckAt < RUN_INDICATOR_STATE_RECHECK_MS) return;
|
|
3870
5746
|
runIndicatorLastStateCheckAt = now;
|
|
3871
|
-
refreshState().catch((error) =>
|
|
5747
|
+
refreshState(tabContext).catch((error) => {
|
|
5748
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5749
|
+
});
|
|
3872
5750
|
}
|
|
3873
5751
|
|
|
3874
5752
|
function formatRunIndicatorElapsed() {
|
|
@@ -3965,6 +5843,7 @@ function setRunIndicatorActivity(activity, { active = true, scroll = true } = {}
|
|
|
3965
5843
|
}
|
|
3966
5844
|
runIndicatorActivity = activity || runIndicatorActivity || "Waiting for output or action…";
|
|
3967
5845
|
renderRunIndicator({ scroll });
|
|
5846
|
+
updateComposerModeButtons();
|
|
3968
5847
|
if (active) scheduleRunIndicatorGraceCheck();
|
|
3969
5848
|
}
|
|
3970
5849
|
|
|
@@ -3975,6 +5854,7 @@ function clearRunIndicatorActivity({ render = true } = {}) {
|
|
|
3975
5854
|
runIndicatorStartedAt = null;
|
|
3976
5855
|
runIndicatorActivity = "Waiting for output or action…";
|
|
3977
5856
|
if (render) renderRunIndicator();
|
|
5857
|
+
updateComposerModeButtons();
|
|
3978
5858
|
}
|
|
3979
5859
|
|
|
3980
5860
|
function syncRunIndicatorFromState(state = currentState) {
|
|
@@ -3994,15 +5874,21 @@ function syncRunIndicatorFromState(state = currentState) {
|
|
|
3994
5874
|
} else {
|
|
3995
5875
|
renderRunIndicator();
|
|
3996
5876
|
}
|
|
5877
|
+
updateComposerModeButtons();
|
|
3997
5878
|
}
|
|
3998
5879
|
|
|
3999
5880
|
function runIndicatorToolName(name) {
|
|
4000
5881
|
return cleanStatusText(name || "tool") || "tool";
|
|
4001
5882
|
}
|
|
4002
5883
|
|
|
4003
|
-
function scheduleAbortStateChecks() {
|
|
5884
|
+
function scheduleAbortStateChecks(tabContext = activeTabContext()) {
|
|
4004
5885
|
for (const delay of [250, 900, 1800, 3600]) {
|
|
4005
|
-
setTimeout(() =>
|
|
5886
|
+
setTimeout(() => {
|
|
5887
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5888
|
+
refreshState(tabContext).catch((error) => {
|
|
5889
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5890
|
+
});
|
|
5891
|
+
}, delay);
|
|
4006
5892
|
}
|
|
4007
5893
|
}
|
|
4008
5894
|
|
|
@@ -4014,7 +5900,7 @@ function messageTimestampMs(message) {
|
|
|
4014
5900
|
}
|
|
4015
5901
|
|
|
4016
5902
|
function isActionTranscriptMessage(message) {
|
|
4017
|
-
return ["assistantEvent", "bashExecution", "toolCall", "toolResult"].includes(message?.role);
|
|
5903
|
+
return ["assistantEvent", "bashExecution", "toolCall", "toolExecution", "toolResult"].includes(message?.role);
|
|
4018
5904
|
}
|
|
4019
5905
|
|
|
4020
5906
|
function assistantMessageHasActionContent(message) {
|
|
@@ -4042,6 +5928,7 @@ function actionEntryKey(item) {
|
|
|
4042
5928
|
item?.messageIndex ?? -1,
|
|
4043
5929
|
message.role || "message",
|
|
4044
5930
|
message.toolName || "",
|
|
5931
|
+
message.toolCallId || "",
|
|
4045
5932
|
message.command || "",
|
|
4046
5933
|
message.title || "",
|
|
4047
5934
|
message.timestamp || "",
|
|
@@ -4065,12 +5952,22 @@ function rememberActionEntries(items) {
|
|
|
4065
5952
|
|
|
4066
5953
|
function orderedTranscriptItems() {
|
|
4067
5954
|
const items = [];
|
|
5955
|
+
const assistantToolCallIds = buildAssistantToolCallIdSet(latestMessages);
|
|
5956
|
+
const toolResults = buildToolResultMap(latestMessages);
|
|
4068
5957
|
latestMessages.forEach((message, index) => {
|
|
5958
|
+
const resultId = message?.role === "toolResult" ? toolResultCallId(message) : "";
|
|
5959
|
+
if (resultId && assistantToolCallIds.has(resultId)) return;
|
|
4069
5960
|
items.push({ message, messageIndex: index, transient: false, timestampMs: messageTimestampMs(message), order: index });
|
|
4070
5961
|
});
|
|
4071
5962
|
transientMessages.forEach((message, index) => {
|
|
4072
5963
|
items.push({ message, messageIndex: index, transient: true, timestampMs: messageTimestampMs(message), order: latestMessages.length + index });
|
|
4073
5964
|
});
|
|
5965
|
+
let liveOrder = latestMessages.length + transientMessages.length;
|
|
5966
|
+
for (const [toolCallId, run] of liveToolRuns.entries()) {
|
|
5967
|
+
if (assistantToolCallIds.has(toolCallId) || toolResults.has(toolCallId)) continue;
|
|
5968
|
+
const message = liveToolRunMessage(run);
|
|
5969
|
+
items.push({ message, messageIndex: -1, transient: true, timestampMs: messageTimestampMs(message), order: liveOrder++ });
|
|
5970
|
+
}
|
|
4074
5971
|
return items.sort((a, b) => a.timestampMs - b.timestampMs || a.order - b.order);
|
|
4075
5972
|
}
|
|
4076
5973
|
|
|
@@ -4222,7 +6119,7 @@ function showComposerButtonTooltip(button) {
|
|
|
4222
6119
|
}
|
|
4223
6120
|
|
|
4224
6121
|
function sendPromptFromModeButton(kind, button) {
|
|
4225
|
-
if (!
|
|
6122
|
+
if (!hasComposerPayload()) {
|
|
4226
6123
|
showComposerButtonTooltip(button);
|
|
4227
6124
|
return;
|
|
4228
6125
|
}
|
|
@@ -4245,6 +6142,7 @@ function optionalFeatureIdForCommand(name) {
|
|
|
4245
6142
|
}
|
|
4246
6143
|
|
|
4247
6144
|
function isCommandVisible(command) {
|
|
6145
|
+
if (HIDDEN_COMMAND_NAMES.has(command.name)) return false;
|
|
4248
6146
|
const featureId = optionalFeatureIdForCommand(command.name);
|
|
4249
6147
|
return !featureId || isOptionalFeatureEnabled(featureId);
|
|
4250
6148
|
}
|
|
@@ -4264,13 +6162,31 @@ function optionalFeatureUnavailableMessage(featureId) {
|
|
|
4264
6162
|
return `${feature.label} unavailable: ${feature.capabilityLabel} is not loaded. Install or enable ${feature.packageName}.`;
|
|
4265
6163
|
}
|
|
4266
6164
|
|
|
6165
|
+
function rememberOptionalControlDefault(button, key, value) {
|
|
6166
|
+
if (!(key in button.dataset)) button.dataset[key] = value || "";
|
|
6167
|
+
}
|
|
6168
|
+
|
|
4267
6169
|
function setOptionalControlState(button, available, unavailableTitle) {
|
|
4268
6170
|
if (!button) return;
|
|
4269
|
-
|
|
6171
|
+
rememberOptionalControlDefault(button, "defaultTitle", button.getAttribute("title"));
|
|
6172
|
+
rememberOptionalControlDefault(button, "defaultAriaLabel", button.getAttribute("aria-label"));
|
|
6173
|
+
if (button.hasAttribute("data-tooltip")) rememberOptionalControlDefault(button, "defaultTooltip", button.getAttribute("data-tooltip"));
|
|
6174
|
+
|
|
6175
|
+
const nextTitle = available ? button.dataset.defaultTitle : unavailableTitle;
|
|
6176
|
+
const nextAriaLabel = available ? button.dataset.defaultAriaLabel : unavailableTitle;
|
|
6177
|
+
const nextTooltip = available ? button.dataset.defaultTooltip : unavailableTitle;
|
|
6178
|
+
|
|
4270
6179
|
button.disabled = !available;
|
|
4271
6180
|
button.setAttribute("aria-disabled", available ? "false" : "true");
|
|
4272
6181
|
button.classList.toggle("feature-unavailable", !available);
|
|
4273
|
-
button.setAttribute("title",
|
|
6182
|
+
if (nextTitle) button.setAttribute("title", nextTitle);
|
|
6183
|
+
else button.removeAttribute("title");
|
|
6184
|
+
if (nextAriaLabel) button.setAttribute("aria-label", nextAriaLabel);
|
|
6185
|
+
else button.removeAttribute("aria-label");
|
|
6186
|
+
if (button.dataset.defaultTooltip !== undefined) {
|
|
6187
|
+
if (nextTooltip) button.setAttribute("data-tooltip", nextTooltip);
|
|
6188
|
+
else button.removeAttribute("data-tooltip");
|
|
6189
|
+
}
|
|
4274
6190
|
}
|
|
4275
6191
|
|
|
4276
6192
|
function resetOptionalFeatureAvailability() {
|
|
@@ -4398,8 +6314,9 @@ async function installOptionalFeature(featureId) {
|
|
|
4398
6314
|
if (confirm(`${feature.label} install finished. Reload the active Pi tab now to enable newly loaded resources?`)) {
|
|
4399
6315
|
sendPrompt("prompt", "/reload");
|
|
4400
6316
|
} else {
|
|
4401
|
-
|
|
4402
|
-
|
|
6317
|
+
const tabContext = activeTabContext();
|
|
6318
|
+
await Promise.allSettled([refreshCommands(tabContext), initializeThemes()]);
|
|
6319
|
+
if (isCurrentTabContext(tabContext)) renderOptionalFeatureControls();
|
|
4403
6320
|
}
|
|
4404
6321
|
} catch (error) {
|
|
4405
6322
|
addEvent(error.message || String(error), "error");
|
|
@@ -4415,13 +6332,478 @@ function runPublishWorkflow(command) {
|
|
|
4415
6332
|
const commandName = String(command || "").replace(/^\//, "").split(/\s+/)[0];
|
|
4416
6333
|
const featureId = OPTIONAL_COMMAND_FEATURES.get(commandName);
|
|
4417
6334
|
if ((featureId && !isOptionalFeatureEnabled(featureId)) || !hasAvailableCommand(commandName)) {
|
|
6335
|
+
const tabContext = activeTabContext();
|
|
4418
6336
|
addEvent(commandUnavailableMessage(commandName), "warn");
|
|
4419
|
-
refreshCommands().catch((error) =>
|
|
6337
|
+
refreshCommands(tabContext).catch((error) => {
|
|
6338
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
6339
|
+
});
|
|
4420
6340
|
return;
|
|
4421
6341
|
}
|
|
4422
6342
|
sendPrompt("prompt", command);
|
|
4423
6343
|
}
|
|
4424
6344
|
|
|
6345
|
+
function slashCommandName(message) {
|
|
6346
|
+
const match = String(message || "").trim().match(/^\/([^\s]+)$/);
|
|
6347
|
+
return match ? match[1] : "";
|
|
6348
|
+
}
|
|
6349
|
+
|
|
6350
|
+
function openNativeCommandDialog({ title, message = "", searchPlaceholder = "" } = {}) {
|
|
6351
|
+
nativeCommandTabId ||= activeTabId;
|
|
6352
|
+
elements.nativeCommandTitle.textContent = title || "Pi command";
|
|
6353
|
+
elements.nativeCommandMessage.textContent = message;
|
|
6354
|
+
elements.nativeCommandMessage.hidden = !message;
|
|
6355
|
+
elements.nativeCommandSearch.value = "";
|
|
6356
|
+
elements.nativeCommandSearch.placeholder = searchPlaceholder || "Filter choices…";
|
|
6357
|
+
elements.nativeCommandSearch.hidden = !searchPlaceholder;
|
|
6358
|
+
elements.nativeCommandSearch.oninput = null;
|
|
6359
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6360
|
+
elements.nativeCommandError.hidden = true;
|
|
6361
|
+
elements.nativeCommandError.textContent = "";
|
|
6362
|
+
elements.nativeCommandActions.replaceChildren();
|
|
6363
|
+
addNativeCommandAction("Cancel", closeNativeCommandDialog);
|
|
6364
|
+
if (!elements.nativeCommandDialog.open) elements.nativeCommandDialog.showModal();
|
|
6365
|
+
if (searchPlaceholder) queueMicrotask(() => elements.nativeCommandSearch.focus());
|
|
6366
|
+
}
|
|
6367
|
+
|
|
6368
|
+
function closeNativeCommandDialog() {
|
|
6369
|
+
if (elements.nativeCommandDialog.open) elements.nativeCommandDialog.close();
|
|
6370
|
+
elements.nativeCommandSearch.oninput = null;
|
|
6371
|
+
nativeCommandTabId = null;
|
|
6372
|
+
}
|
|
6373
|
+
|
|
6374
|
+
function nativeCommandApi(path, options = {}) {
|
|
6375
|
+
return api(path, { ...options, tabId: options.tabId || nativeCommandTabId || activeTabId });
|
|
6376
|
+
}
|
|
6377
|
+
|
|
6378
|
+
function setNativeCommandError(message) {
|
|
6379
|
+
elements.nativeCommandError.textContent = message || "";
|
|
6380
|
+
elements.nativeCommandError.hidden = !message;
|
|
6381
|
+
}
|
|
6382
|
+
|
|
6383
|
+
function addNativeCommandAction(label, handler, className) {
|
|
6384
|
+
const button = make("button", className, label);
|
|
6385
|
+
button.type = "button";
|
|
6386
|
+
button.addEventListener("click", handler);
|
|
6387
|
+
elements.nativeCommandActions.append(button);
|
|
6388
|
+
return button;
|
|
6389
|
+
}
|
|
6390
|
+
|
|
6391
|
+
function renderNativeLoading(label = "Loading…") {
|
|
6392
|
+
elements.nativeCommandBody.replaceChildren(make("div", "native-command-empty muted", label));
|
|
6393
|
+
}
|
|
6394
|
+
|
|
6395
|
+
function nativeSelectorMatches(item, query) {
|
|
6396
|
+
if (!query) return true;
|
|
6397
|
+
const needle = query.toLowerCase();
|
|
6398
|
+
return [item.label, item.description, item.meta, item.badge]
|
|
6399
|
+
.filter(Boolean)
|
|
6400
|
+
.some((value) => String(value).toLowerCase().includes(needle));
|
|
6401
|
+
}
|
|
6402
|
+
|
|
6403
|
+
function renderNativeSelectorItems(items, { emptyText = "No choices.", onSelect, activeId } = {}) {
|
|
6404
|
+
const query = elements.nativeCommandSearch.value.trim();
|
|
6405
|
+
const filtered = items.filter((item) => nativeSelectorMatches(item, query));
|
|
6406
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6407
|
+
if (!filtered.length) {
|
|
6408
|
+
elements.nativeCommandBody.append(make("div", "native-command-empty muted", emptyText));
|
|
6409
|
+
return;
|
|
6410
|
+
}
|
|
6411
|
+
const list = make("div", "native-selector-list");
|
|
6412
|
+
for (const item of filtered) {
|
|
6413
|
+
const button = make("button", `native-selector-item${item.id === activeId ? " active" : ""}`);
|
|
6414
|
+
button.type = "button";
|
|
6415
|
+
if (item.depth !== undefined) button.style.setProperty("--tree-depth", String(item.depth));
|
|
6416
|
+
button.disabled = item.disabled === true;
|
|
6417
|
+
button.addEventListener("click", () => onSelect?.(item));
|
|
6418
|
+
const title = make("span", "native-selector-title");
|
|
6419
|
+
title.append(make("strong", undefined, item.label || item.id || "choice"));
|
|
6420
|
+
if (item.badge) title.append(make("span", "native-selector-badge", item.badge));
|
|
6421
|
+
const detail = make("span", "native-selector-detail", item.description || "");
|
|
6422
|
+
const meta = make("span", "native-selector-meta", item.meta || "");
|
|
6423
|
+
button.append(title);
|
|
6424
|
+
if (item.description) button.append(detail);
|
|
6425
|
+
if (item.meta) button.append(meta);
|
|
6426
|
+
list.append(button);
|
|
6427
|
+
}
|
|
6428
|
+
elements.nativeCommandBody.append(list);
|
|
6429
|
+
}
|
|
6430
|
+
|
|
6431
|
+
function setNativeActionBusy(button, busy, label = "Working…") {
|
|
6432
|
+
if (!button) return;
|
|
6433
|
+
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent || "";
|
|
6434
|
+
button.disabled = busy;
|
|
6435
|
+
button.textContent = busy ? label : button.dataset.defaultLabel;
|
|
6436
|
+
}
|
|
6437
|
+
|
|
6438
|
+
function modelOptionLabel(model) {
|
|
6439
|
+
return `${model.provider}/${model.id}`;
|
|
6440
|
+
}
|
|
6441
|
+
|
|
6442
|
+
async function openNativeModelSelector() {
|
|
6443
|
+
openNativeCommandDialog({ title: "/model", message: "Select the active model for this Pi tab.", searchPlaceholder: "Filter models…" });
|
|
6444
|
+
renderNativeLoading("Loading models…");
|
|
6445
|
+
try {
|
|
6446
|
+
const response = await nativeCommandApi("/api/models");
|
|
6447
|
+
const models = Array.isArray(response.data?.models) ? response.data.models : [];
|
|
6448
|
+
const activeId = currentState?.model ? `${currentState.model.provider}/${currentState.model.id}` : "";
|
|
6449
|
+
const items = models.map((model) => ({
|
|
6450
|
+
id: modelOptionLabel(model),
|
|
6451
|
+
label: modelOptionLabel(model),
|
|
6452
|
+
description: model.name || model.description || "",
|
|
6453
|
+
meta: model.contextWindow ? `context ${model.contextWindow}` : model.provider,
|
|
6454
|
+
model,
|
|
6455
|
+
badge: modelOptionLabel(model) === activeId ? "current" : "",
|
|
6456
|
+
}));
|
|
6457
|
+
const render = () => renderNativeSelectorItems(items, {
|
|
6458
|
+
emptyText: "No models match this filter.",
|
|
6459
|
+
activeId,
|
|
6460
|
+
onSelect: async (item) => {
|
|
6461
|
+
setNativeCommandError("");
|
|
6462
|
+
try {
|
|
6463
|
+
await nativeCommandApi("/api/model", { method: "POST", body: { provider: item.model.provider, modelId: item.model.id } });
|
|
6464
|
+
addTransientMessage({ role: "native", title: "/model", content: `Model set to ${item.label}.`, level: "info" });
|
|
6465
|
+
closeNativeCommandDialog();
|
|
6466
|
+
await refreshState();
|
|
6467
|
+
} catch (error) {
|
|
6468
|
+
setNativeCommandError(error.message || String(error));
|
|
6469
|
+
}
|
|
6470
|
+
},
|
|
6471
|
+
});
|
|
6472
|
+
elements.nativeCommandSearch.oninput = render;
|
|
6473
|
+
render();
|
|
6474
|
+
} catch (error) {
|
|
6475
|
+
setNativeCommandError(error.message || String(error));
|
|
6476
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6477
|
+
}
|
|
6478
|
+
}
|
|
6479
|
+
|
|
6480
|
+
function openNativeThemeSelector() {
|
|
6481
|
+
openNativeCommandDialog({ title: "/theme", message: "Select the browser Web UI theme. Pi terminal theme changes remain native-TUI only.", searchPlaceholder: "Filter themes…" });
|
|
6482
|
+
const load = async () => {
|
|
6483
|
+
if (!availableThemes.length) await initializeThemes();
|
|
6484
|
+
const items = availableThemes.map((theme) => ({
|
|
6485
|
+
id: theme.name,
|
|
6486
|
+
label: theme.label || displayThemeName(theme.name) || theme.name,
|
|
6487
|
+
description: theme.name,
|
|
6488
|
+
meta: theme.author ? `by ${theme.author}` : "browser theme",
|
|
6489
|
+
theme,
|
|
6490
|
+
badge: theme.name === currentThemeName ? "current" : "",
|
|
6491
|
+
}));
|
|
6492
|
+
const render = () => renderNativeSelectorItems(items, {
|
|
6493
|
+
emptyText: "No themes match this filter.",
|
|
6494
|
+
activeId: currentThemeName,
|
|
6495
|
+
onSelect: async (item) => {
|
|
6496
|
+
try {
|
|
6497
|
+
await setThemeByName(item.theme.name, { persist: true, announce: true });
|
|
6498
|
+
addTransientMessage({ role: "native", title: "/theme", content: `Theme set to ${item.label}.`, level: "info" });
|
|
6499
|
+
closeNativeCommandDialog();
|
|
6500
|
+
} catch (error) {
|
|
6501
|
+
setNativeCommandError(error.message || String(error));
|
|
6502
|
+
}
|
|
6503
|
+
},
|
|
6504
|
+
});
|
|
6505
|
+
elements.nativeCommandSearch.oninput = render;
|
|
6506
|
+
render();
|
|
6507
|
+
};
|
|
6508
|
+
renderNativeLoading("Loading themes…");
|
|
6509
|
+
load().catch((error) => {
|
|
6510
|
+
setNativeCommandError(error.message || String(error));
|
|
6511
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6512
|
+
});
|
|
6513
|
+
}
|
|
6514
|
+
|
|
6515
|
+
function nativeSettingSelect(label, value, options) {
|
|
6516
|
+
const field = make("label", "native-settings-field");
|
|
6517
|
+
field.append(make("span", "native-settings-label", label));
|
|
6518
|
+
const select = make("select");
|
|
6519
|
+
for (const option of options) {
|
|
6520
|
+
const element = make("option", undefined, option.label || option.value);
|
|
6521
|
+
element.value = option.value;
|
|
6522
|
+
select.append(element);
|
|
6523
|
+
}
|
|
6524
|
+
select.value = value;
|
|
6525
|
+
field.append(select);
|
|
6526
|
+
return { field, select };
|
|
6527
|
+
}
|
|
6528
|
+
|
|
6529
|
+
function nativeSettingToggle(label, checked, hint) {
|
|
6530
|
+
const field = make("label", "native-settings-toggle");
|
|
6531
|
+
const input = make("input");
|
|
6532
|
+
input.type = "checkbox";
|
|
6533
|
+
input.checked = !!checked;
|
|
6534
|
+
const text = make("span");
|
|
6535
|
+
text.append(make("strong", undefined, label));
|
|
6536
|
+
if (hint) text.append(make("span", "native-settings-hint", hint));
|
|
6537
|
+
field.append(input, text);
|
|
6538
|
+
return { field, input };
|
|
6539
|
+
}
|
|
6540
|
+
|
|
6541
|
+
function openNativeSettingsDialog() {
|
|
6542
|
+
openNativeCommandDialog({ title: "/settings", message: "Quick Web UI settings for the active Pi tab." });
|
|
6543
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6544
|
+
const state = currentState || {};
|
|
6545
|
+
const body = make("div", "native-settings-grid");
|
|
6546
|
+
const thinking = nativeSettingSelect("Thinking level", state.thinkingLevel || "off", ["off", "minimal", "low", "medium", "high", "xhigh"].map((value) => ({ value })));
|
|
6547
|
+
const steering = nativeSettingSelect("Steering queue", state.steeringMode || "one-at-a-time", [
|
|
6548
|
+
{ value: "one-at-a-time", label: "one at a time" },
|
|
6549
|
+
{ value: "all", label: "all queued" },
|
|
6550
|
+
]);
|
|
6551
|
+
const followUp = nativeSettingSelect("Follow-up queue", state.followUpMode || "one-at-a-time", [
|
|
6552
|
+
{ value: "one-at-a-time", label: "one at a time" },
|
|
6553
|
+
{ value: "all", label: "all queued" },
|
|
6554
|
+
]);
|
|
6555
|
+
const autoCompact = nativeSettingToggle("Auto compaction", state.autoCompactionEnabled !== false, "Let Pi compact when context is nearly full.");
|
|
6556
|
+
const thinkingOutput = nativeSettingToggle("Show thinking output", thinkingOutputVisible, "Local browser transcript visibility.");
|
|
6557
|
+
const doneNotifications = nativeSettingToggle("Agent done notifications", agentDoneNotificationsEnabled, "Browser notification after background tab work completes.");
|
|
6558
|
+
const busyBehavior = nativeSettingSelect("Busy prompt behavior", elements.busyBehavior.value || "followUp", [
|
|
6559
|
+
{ value: "followUp", label: "follow-up" },
|
|
6560
|
+
{ value: "steer", label: "steer" },
|
|
6561
|
+
]);
|
|
6562
|
+
body.append(thinking.field, steering.field, followUp.field, busyBehavior.field, autoCompact.field, thinkingOutput.field, doneNotifications.field);
|
|
6563
|
+
elements.nativeCommandBody.append(body);
|
|
6564
|
+
elements.nativeCommandActions.replaceChildren();
|
|
6565
|
+
addNativeCommandAction("Model…", () => openNativeModelSelector());
|
|
6566
|
+
addNativeCommandAction("Theme…", () => openNativeThemeSelector());
|
|
6567
|
+
addNativeCommandAction("Cancel", closeNativeCommandDialog);
|
|
6568
|
+
const save = addNativeCommandAction("Apply", async () => {
|
|
6569
|
+
setNativeActionBusy(save, true, "Applying…");
|
|
6570
|
+
setNativeCommandError("");
|
|
6571
|
+
try {
|
|
6572
|
+
const requests = [];
|
|
6573
|
+
if (thinking.select.value !== state.thinkingLevel) requests.push(nativeCommandApi("/api/thinking", { method: "POST", body: { level: thinking.select.value } }));
|
|
6574
|
+
if (steering.select.value !== state.steeringMode) requests.push(nativeCommandApi("/api/steering-mode", { method: "POST", body: { mode: steering.select.value } }));
|
|
6575
|
+
if (followUp.select.value !== state.followUpMode) requests.push(nativeCommandApi("/api/follow-up-mode", { method: "POST", body: { mode: followUp.select.value } }));
|
|
6576
|
+
if (autoCompact.input.checked !== state.autoCompactionEnabled) requests.push(nativeCommandApi("/api/auto-compaction", { method: "POST", body: { enabled: autoCompact.input.checked } }));
|
|
6577
|
+
elements.busyBehavior.value = busyBehavior.select.value;
|
|
6578
|
+
if (thinkingOutput.input.checked !== thinkingOutputVisible) setThinkingOutputVisible(thinkingOutput.input.checked);
|
|
6579
|
+
if (doneNotifications.input.checked !== agentDoneNotificationsEnabled) await setAgentDoneNotificationsEnabled(doneNotifications.input.checked);
|
|
6580
|
+
await Promise.all(requests);
|
|
6581
|
+
addTransientMessage({ role: "native", title: "/settings", content: "Settings updated.", level: "info" });
|
|
6582
|
+
closeNativeCommandDialog();
|
|
6583
|
+
await refreshState();
|
|
6584
|
+
} catch (error) {
|
|
6585
|
+
setNativeCommandError(error.message || String(error));
|
|
6586
|
+
} finally {
|
|
6587
|
+
setNativeActionBusy(save, false);
|
|
6588
|
+
}
|
|
6589
|
+
}, "primary");
|
|
6590
|
+
}
|
|
6591
|
+
|
|
6592
|
+
async function openNativeForkSelector() {
|
|
6593
|
+
openNativeCommandDialog({ title: "/fork", message: "Choose a previous user message to fork before.", searchPlaceholder: "Filter fork points…" });
|
|
6594
|
+
renderNativeLoading("Loading fork points…");
|
|
6595
|
+
try {
|
|
6596
|
+
const response = await nativeCommandApi("/api/fork-messages");
|
|
6597
|
+
const items = (response.data?.messages || []).map((message, index) => ({
|
|
6598
|
+
id: message.entryId,
|
|
6599
|
+
label: `#${index + 1} user message`,
|
|
6600
|
+
description: message.text || "",
|
|
6601
|
+
meta: message.entryId,
|
|
6602
|
+
message,
|
|
6603
|
+
})).reverse();
|
|
6604
|
+
const render = () => renderNativeSelectorItems(items, {
|
|
6605
|
+
emptyText: "No user messages are available to fork from.",
|
|
6606
|
+
onSelect: async (item) => {
|
|
6607
|
+
setNativeCommandError("");
|
|
6608
|
+
try {
|
|
6609
|
+
const result = await nativeCommandApi("/api/fork", { method: "POST", body: { entryId: item.message.entryId } });
|
|
6610
|
+
applyResponseTab(result);
|
|
6611
|
+
const restoredText = result.data?.text || result.data?.result?.text || "";
|
|
6612
|
+
if (restoredText) {
|
|
6613
|
+
elements.promptInput.value = restoredText;
|
|
6614
|
+
resizePromptInput();
|
|
6615
|
+
focusPromptInput({ defer: true });
|
|
6616
|
+
}
|
|
6617
|
+
addTransientMessage({ role: "native", title: "/fork", content: result.data?.message || "Forked the current session.", level: "info" });
|
|
6618
|
+
closeNativeCommandDialog();
|
|
6619
|
+
await refreshAll();
|
|
6620
|
+
} catch (error) {
|
|
6621
|
+
setNativeCommandError(error.message || String(error));
|
|
6622
|
+
}
|
|
6623
|
+
},
|
|
6624
|
+
});
|
|
6625
|
+
elements.nativeCommandSearch.oninput = render;
|
|
6626
|
+
render();
|
|
6627
|
+
} catch (error) {
|
|
6628
|
+
setNativeCommandError(error.message || String(error));
|
|
6629
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6630
|
+
}
|
|
6631
|
+
}
|
|
6632
|
+
|
|
6633
|
+
function openNativeCloneDialog() {
|
|
6634
|
+
openNativeCommandDialog({ title: "/clone", message: "Duplicate the current session at the current position." });
|
|
6635
|
+
elements.nativeCommandBody.append(make("p", "native-command-note", "This creates a new forked session and switches this Web UI tab to it."));
|
|
6636
|
+
elements.nativeCommandActions.replaceChildren();
|
|
6637
|
+
addNativeCommandAction("Cancel", closeNativeCommandDialog);
|
|
6638
|
+
const clone = addNativeCommandAction("Clone session", async () => {
|
|
6639
|
+
setNativeActionBusy(clone, true, "Cloning…");
|
|
6640
|
+
try {
|
|
6641
|
+
const result = await nativeCommandApi("/api/clone", { method: "POST", body: {} });
|
|
6642
|
+
applyResponseTab(result);
|
|
6643
|
+
addTransientMessage({ role: "native", title: "/clone", content: result.data?.message || "Cloned the current session.", level: "info" });
|
|
6644
|
+
closeNativeCommandDialog();
|
|
6645
|
+
await refreshAll();
|
|
6646
|
+
} catch (error) {
|
|
6647
|
+
setNativeCommandError(error.message || String(error));
|
|
6648
|
+
} finally {
|
|
6649
|
+
setNativeActionBusy(clone, false);
|
|
6650
|
+
}
|
|
6651
|
+
}, "primary");
|
|
6652
|
+
}
|
|
6653
|
+
|
|
6654
|
+
async function openNativeResumeSelector(scope = "current") {
|
|
6655
|
+
openNativeCommandDialog({ title: "/resume", message: "Resume another persisted Pi session.", searchPlaceholder: "Filter sessions…" });
|
|
6656
|
+
renderNativeLoading("Loading sessions…");
|
|
6657
|
+
const selectedScope = scope === "all" ? "all" : "current";
|
|
6658
|
+
try {
|
|
6659
|
+
const response = await nativeCommandApi(`/api/sessions?scope=${encodeURIComponent(selectedScope)}`);
|
|
6660
|
+
const items = (response.data?.sessions || []).map((session) => ({
|
|
6661
|
+
id: session.path,
|
|
6662
|
+
label: session.name || session.firstMessage || session.id || session.path,
|
|
6663
|
+
description: session.firstMessage || "(no messages)",
|
|
6664
|
+
meta: `${session.cwd || "unknown cwd"} · ${session.messageCount || 0} messages · ${session.modified || "unknown time"}`,
|
|
6665
|
+
badge: session.current ? "current" : "",
|
|
6666
|
+
disabled: session.current,
|
|
6667
|
+
session,
|
|
6668
|
+
}));
|
|
6669
|
+
const render = () => renderNativeSelectorItems(items, {
|
|
6670
|
+
emptyText: selectedScope === "all" ? "No sessions match this filter." : "No sessions for this working directory match this filter.",
|
|
6671
|
+
onSelect: async (item) => {
|
|
6672
|
+
setNativeCommandError("");
|
|
6673
|
+
try {
|
|
6674
|
+
const result = await nativeCommandApi("/api/switch-session", { method: "POST", body: { sessionPath: item.session.path } });
|
|
6675
|
+
applyResponseTab(result);
|
|
6676
|
+
addTransientMessage({ role: "native", title: "/resume", content: result.data?.message || "Resumed selected session.", level: "info" });
|
|
6677
|
+
closeNativeCommandDialog();
|
|
6678
|
+
await refreshAll();
|
|
6679
|
+
} catch (error) {
|
|
6680
|
+
setNativeCommandError(error.message || String(error));
|
|
6681
|
+
}
|
|
6682
|
+
},
|
|
6683
|
+
});
|
|
6684
|
+
elements.nativeCommandSearch.oninput = render;
|
|
6685
|
+
elements.nativeCommandActions.replaceChildren();
|
|
6686
|
+
addNativeCommandAction(selectedScope === "all" ? "Current cwd" : "All sessions", () => openNativeResumeSelector(selectedScope === "all" ? "current" : "all"));
|
|
6687
|
+
addNativeCommandAction("Cancel", closeNativeCommandDialog);
|
|
6688
|
+
render();
|
|
6689
|
+
} catch (error) {
|
|
6690
|
+
setNativeCommandError(error.message || String(error));
|
|
6691
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6692
|
+
}
|
|
6693
|
+
}
|
|
6694
|
+
|
|
6695
|
+
async function openNativeTreeSelector() {
|
|
6696
|
+
openNativeCommandDialog({ title: "/tree", message: "Navigate the current session tree. Choosing a user message restores it into the editor.", searchPlaceholder: "Filter tree…" });
|
|
6697
|
+
renderNativeLoading("Loading session tree…");
|
|
6698
|
+
try {
|
|
6699
|
+
const response = await nativeCommandApi("/api/session-tree");
|
|
6700
|
+
const nodes = response.data?.nodes || [];
|
|
6701
|
+
const summarize = nativeSettingToggle("Summarize abandoned branch", false, "Optional; may call the active model before switching branches.");
|
|
6702
|
+
const labelField = make("label", "native-settings-field");
|
|
6703
|
+
labelField.append(make("span", "native-settings-label", "Optional label"));
|
|
6704
|
+
const labelInput = make("input", "dialog-input");
|
|
6705
|
+
labelInput.placeholder = "checkpoint label";
|
|
6706
|
+
labelField.append(labelInput);
|
|
6707
|
+
const options = make("div", "native-tree-options");
|
|
6708
|
+
options.append(summarize.field, labelField);
|
|
6709
|
+
const items = nodes.map((node) => ({
|
|
6710
|
+
id: node.id,
|
|
6711
|
+
label: `${node.title}${node.label ? ` · ${node.label}` : ""}`,
|
|
6712
|
+
description: node.text || "",
|
|
6713
|
+
meta: `${node.timestamp || ""}${node.childCount ? ` · ${node.childCount} child${node.childCount === 1 ? "" : "ren"}` : ""}`,
|
|
6714
|
+
badge: node.currentLeaf ? "leaf" : "",
|
|
6715
|
+
depth: node.depth || 0,
|
|
6716
|
+
node,
|
|
6717
|
+
}));
|
|
6718
|
+
const navigate = async (item) => {
|
|
6719
|
+
setNativeCommandError("");
|
|
6720
|
+
try {
|
|
6721
|
+
const result = await nativeCommandApi("/api/tree-navigate", {
|
|
6722
|
+
method: "POST",
|
|
6723
|
+
body: {
|
|
6724
|
+
entryId: item.node.id,
|
|
6725
|
+
summarize: summarize.input.checked,
|
|
6726
|
+
label: labelInput.value.trim() || undefined,
|
|
6727
|
+
},
|
|
6728
|
+
});
|
|
6729
|
+
applyResponseTab(result);
|
|
6730
|
+
addTransientMessage({ role: "native", title: "/tree", content: result.data?.message || "Navigated the session tree.", level: "info" });
|
|
6731
|
+
closeNativeCommandDialog();
|
|
6732
|
+
await refreshAll();
|
|
6733
|
+
} catch (error) {
|
|
6734
|
+
setNativeCommandError(error.message || String(error));
|
|
6735
|
+
}
|
|
6736
|
+
};
|
|
6737
|
+
const render = () => {
|
|
6738
|
+
renderNativeSelectorItems(items, { emptyText: "No session tree entries match this filter.", onSelect: navigate });
|
|
6739
|
+
elements.nativeCommandBody.prepend(options);
|
|
6740
|
+
};
|
|
6741
|
+
elements.nativeCommandSearch.oninput = render;
|
|
6742
|
+
render();
|
|
6743
|
+
} catch (error) {
|
|
6744
|
+
setNativeCommandError(error.message || String(error));
|
|
6745
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6746
|
+
}
|
|
6747
|
+
}
|
|
6748
|
+
|
|
6749
|
+
function openNativeScopedModelsInfo() {
|
|
6750
|
+
openNativeCommandDialog({ title: "/scoped-models", message: "Scoped model selection is available in the footer model picker." });
|
|
6751
|
+
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."));
|
|
6752
|
+
}
|
|
6753
|
+
|
|
6754
|
+
function openNativeAuthInfo(mode) {
|
|
6755
|
+
const command = mode === "logout" ? "/logout" : "/login";
|
|
6756
|
+
openNativeCommandDialog({ title: command, message: "Provider credential entry is intentionally not implemented in the browser yet." });
|
|
6757
|
+
const note = [
|
|
6758
|
+
"Use native Pi TUI authentication for now, or configure provider credentials through environment variables or models.json.",
|
|
6759
|
+
"This avoids accepting or storing API keys in the Web UI until the credential flow has a dedicated security design.",
|
|
6760
|
+
].join("\n\n");
|
|
6761
|
+
elements.nativeCommandBody.append(make("p", "native-command-note", note));
|
|
6762
|
+
}
|
|
6763
|
+
|
|
6764
|
+
async function handleNativeSlashSelectorCommand(message, { usesPromptInput = false } = {}) {
|
|
6765
|
+
const name = slashCommandName(message);
|
|
6766
|
+
if (!NATIVE_SELECTOR_COMMANDS.has(name)) return false;
|
|
6767
|
+
setComposerActionsOpen(false);
|
|
6768
|
+
hideCommandSuggestions();
|
|
6769
|
+
if (usesPromptInput) {
|
|
6770
|
+
elements.promptInput.value = "";
|
|
6771
|
+
resizePromptInput();
|
|
6772
|
+
}
|
|
6773
|
+
switch (name) {
|
|
6774
|
+
case "model":
|
|
6775
|
+
await openNativeModelSelector();
|
|
6776
|
+
return true;
|
|
6777
|
+
case "settings":
|
|
6778
|
+
openNativeSettingsDialog();
|
|
6779
|
+
return true;
|
|
6780
|
+
case "theme":
|
|
6781
|
+
openNativeThemeSelector();
|
|
6782
|
+
return true;
|
|
6783
|
+
case "fork":
|
|
6784
|
+
await openNativeForkSelector();
|
|
6785
|
+
return true;
|
|
6786
|
+
case "clone":
|
|
6787
|
+
openNativeCloneDialog();
|
|
6788
|
+
return true;
|
|
6789
|
+
case "resume":
|
|
6790
|
+
await openNativeResumeSelector();
|
|
6791
|
+
return true;
|
|
6792
|
+
case "tree":
|
|
6793
|
+
await openNativeTreeSelector();
|
|
6794
|
+
return true;
|
|
6795
|
+
case "scoped-models":
|
|
6796
|
+
openNativeScopedModelsInfo();
|
|
6797
|
+
return true;
|
|
6798
|
+
case "login":
|
|
6799
|
+
case "logout":
|
|
6800
|
+
openNativeAuthInfo(name);
|
|
6801
|
+
return true;
|
|
6802
|
+
default:
|
|
6803
|
+
return false;
|
|
6804
|
+
}
|
|
6805
|
+
}
|
|
6806
|
+
|
|
4425
6807
|
function shouldSendPromptFromEnter(event) {
|
|
4426
6808
|
if (event.key !== "Enter" || event.shiftKey || event.isComposing) return false;
|
|
4427
6809
|
if (event.ctrlKey || event.metaKey) return true;
|
|
@@ -4430,6 +6812,7 @@ function shouldSendPromptFromEnter(event) {
|
|
|
4430
6812
|
|
|
4431
6813
|
function renderMessages(messages) {
|
|
4432
6814
|
latestMessages = messages || [];
|
|
6815
|
+
cleanupLiveToolRunsForMessages(latestMessages);
|
|
4433
6816
|
syncLastUserPromptFromMessages(latestMessages);
|
|
4434
6817
|
renderAllMessages();
|
|
4435
6818
|
renderFooter();
|
|
@@ -4472,7 +6855,7 @@ function renderStreamingAssistantText() {
|
|
|
4472
6855
|
const assistantText = stripTodoProgressLines(streamRawText, { streaming: true });
|
|
4473
6856
|
if (assistantText) {
|
|
4474
6857
|
ensureStreamBubble();
|
|
4475
|
-
streamText
|
|
6858
|
+
renderMarkdown(streamText, assistantText);
|
|
4476
6859
|
} else {
|
|
4477
6860
|
scheduleStreamBubbleHide();
|
|
4478
6861
|
}
|
|
@@ -4493,26 +6876,29 @@ function suppressStreamingAssistantTextBeforeToolCall() {
|
|
|
4493
6876
|
|
|
4494
6877
|
function ensureStreamBubble() {
|
|
4495
6878
|
cancelStreamBubbleHide();
|
|
4496
|
-
if (streamBubble) return;
|
|
4497
|
-
const created = appendMessage({ role: "assistant", title: "
|
|
6879
|
+
if (streamBubble?.parentElement === elements.chat) return;
|
|
6880
|
+
const created = appendMessage({ role: "assistant", title: "final output", timestamp: Date.now(), content: "" }, { streaming: true });
|
|
4498
6881
|
streamBubble = created.bubble;
|
|
4499
|
-
streamText =
|
|
6882
|
+
streamText = make("div", "markdown-body streaming-markdown");
|
|
6883
|
+
created.body.append(streamText);
|
|
4500
6884
|
streamBubbleVisibleSince = performance.now();
|
|
4501
6885
|
renderRunIndicator({ scroll: false });
|
|
4502
6886
|
scrollChatToBottom();
|
|
4503
6887
|
}
|
|
4504
6888
|
|
|
4505
6889
|
function ensureStreamingThinkingBubble() {
|
|
4506
|
-
if (
|
|
6890
|
+
if (!thinkingOutputVisible) return false;
|
|
6891
|
+
if (streamThinkingBubble?.parentElement === elements.chat) return true;
|
|
4507
6892
|
const created = appendMessage({ role: "thinking", title: "thinking", timestamp: Date.now(), content: "" }, { streaming: true });
|
|
4508
6893
|
streamThinkingBubble = created.bubble;
|
|
4509
6894
|
streamThinking = appendText(created.body, "", "thinking-text");
|
|
4510
6895
|
renderRunIndicator({ scroll: false });
|
|
4511
6896
|
scrollChatToBottom();
|
|
6897
|
+
return true;
|
|
4512
6898
|
}
|
|
4513
6899
|
|
|
4514
6900
|
function showStreamingThinking(placeholder = "Thinking…") {
|
|
4515
|
-
ensureStreamingThinkingBubble();
|
|
6901
|
+
if (!ensureStreamingThinkingBubble()) return;
|
|
4516
6902
|
if (!streamThinking.textContent) streamThinking.textContent = placeholder;
|
|
4517
6903
|
}
|
|
4518
6904
|
|
|
@@ -4545,9 +6931,8 @@ function assistantTextFromMessage(message) {
|
|
|
4545
6931
|
const parts = [];
|
|
4546
6932
|
for (let index = 0; index < content.length; index += 1) {
|
|
4547
6933
|
const part = content[index];
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
}
|
|
6934
|
+
const text = assistantTextPartText(part);
|
|
6935
|
+
if (text && !assistantHasToolCallAfter(content, index)) parts.push(text);
|
|
4551
6936
|
}
|
|
4552
6937
|
return parts.length ? parts.join("\n\n") : "";
|
|
4553
6938
|
}
|
|
@@ -4563,11 +6948,13 @@ function assistantThinkingTextFromMessage(message) {
|
|
|
4563
6948
|
}
|
|
4564
6949
|
|
|
4565
6950
|
function setStreamingThinkingText(text) {
|
|
6951
|
+
if (!thinkingOutputVisible) return;
|
|
4566
6952
|
showStreamingThinking("");
|
|
4567
|
-
streamThinking.textContent = text;
|
|
6953
|
+
if (streamThinking) streamThinking.textContent = text;
|
|
4568
6954
|
}
|
|
4569
6955
|
|
|
4570
6956
|
function syncStreamingThinkingFromMessage(event, { placeholder = "" } = {}) {
|
|
6957
|
+
if (!thinkingOutputVisible) return true;
|
|
4571
6958
|
const text = assistantThinkingTextFromMessage(assistantStreamingMessage(event));
|
|
4572
6959
|
if (text === null) return false;
|
|
4573
6960
|
if (text || placeholder || streamThinkingBubble) setStreamingThinkingText(text || placeholder);
|
|
@@ -4585,10 +6972,10 @@ function handleMessageUpdate(event) {
|
|
|
4585
6972
|
currentRunStreamChars += delta.length;
|
|
4586
6973
|
setRunIndicatorActivity("Thinking…", { scroll: false });
|
|
4587
6974
|
const synced = syncStreamingThinkingFromMessage(event);
|
|
4588
|
-
if (!synced || (!streamThinking?.textContent && delta)) {
|
|
6975
|
+
if (thinkingOutputVisible && (!synced || (!streamThinking?.textContent && delta))) {
|
|
4589
6976
|
showStreamingThinking("");
|
|
4590
|
-
if (streamThinking
|
|
4591
|
-
streamThinking.textContent += delta;
|
|
6977
|
+
if (streamThinking?.textContent === "Thinking…") streamThinking.textContent = "";
|
|
6978
|
+
if (streamThinking) streamThinking.textContent += delta;
|
|
4592
6979
|
}
|
|
4593
6980
|
renderFooter();
|
|
4594
6981
|
scrollChatToBottom();
|
|
@@ -4623,29 +7010,36 @@ function handleMessageUpdate(event) {
|
|
|
4623
7010
|
}
|
|
4624
7011
|
}
|
|
4625
7012
|
|
|
4626
|
-
async function refreshState() {
|
|
4627
|
-
|
|
7013
|
+
async function refreshState(tabContext = activeTabContext()) {
|
|
7014
|
+
if (!tabContext.tabId) return;
|
|
7015
|
+
const response = await api("/api/state", { tabId: tabContext.tabId });
|
|
7016
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
4628
7017
|
currentState = response.data || null;
|
|
4629
7018
|
syncActiveTabActivityFromState(currentState);
|
|
4630
7019
|
syncRunIndicatorFromState(currentState);
|
|
4631
7020
|
renderStatus();
|
|
4632
7021
|
}
|
|
4633
7022
|
|
|
4634
|
-
async function refreshStats() {
|
|
4635
|
-
|
|
7023
|
+
async function refreshStats(tabContext = activeTabContext()) {
|
|
7024
|
+
if (!tabContext.tabId) return;
|
|
7025
|
+
const response = await api("/api/stats", { tabId: tabContext.tabId });
|
|
7026
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
4636
7027
|
latestStats = response.data || null;
|
|
4637
7028
|
renderFooter();
|
|
4638
7029
|
}
|
|
4639
7030
|
|
|
4640
|
-
async function refreshWorkspace() {
|
|
7031
|
+
async function refreshWorkspace(tabContext = activeTabContext()) {
|
|
7032
|
+
if (!tabContext.tabId) return;
|
|
7033
|
+
let nextWorkspace = null;
|
|
4641
7034
|
try {
|
|
4642
|
-
const response = await api("/api/workspace");
|
|
4643
|
-
|
|
7035
|
+
const response = await api("/api/workspace", { tabId: tabContext.tabId });
|
|
7036
|
+
nextWorkspace = response.data || null;
|
|
4644
7037
|
} catch (error) {
|
|
7038
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
4645
7039
|
// Older webui server processes do not have /api/workspace. Fall back to /api/health,
|
|
4646
7040
|
// which has exposed cwd from the beginning, so the footer still shows the real path.
|
|
4647
|
-
const health = await api("/api/health");
|
|
4648
|
-
|
|
7041
|
+
const health = await api("/api/health", { tabId: tabContext.tabId });
|
|
7042
|
+
nextWorkspace = health.cwd
|
|
4649
7043
|
? {
|
|
4650
7044
|
cwd: health.cwd,
|
|
4651
7045
|
displayCwd: normalizeDisplayPath(health.cwd),
|
|
@@ -4654,6 +7048,9 @@ async function refreshWorkspace() {
|
|
|
4654
7048
|
}
|
|
4655
7049
|
: null;
|
|
4656
7050
|
}
|
|
7051
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
7052
|
+
latestWorkspace = nextWorkspace;
|
|
7053
|
+
rememberServerStartCwd(nextWorkspace?.cwd);
|
|
4657
7054
|
renderFooter();
|
|
4658
7055
|
}
|
|
4659
7056
|
|
|
@@ -4722,12 +7119,15 @@ async function refreshNetworkStatus() {
|
|
|
4722
7119
|
renderNetworkStatus();
|
|
4723
7120
|
}
|
|
4724
7121
|
|
|
4725
|
-
async function refreshFooterData() {
|
|
4726
|
-
|
|
7122
|
+
async function refreshFooterData(tabContext = activeTabContext()) {
|
|
7123
|
+
if (!tabContext.tabId) return;
|
|
7124
|
+
await Promise.allSettled([refreshStats(tabContext), refreshWorkspace(tabContext)]);
|
|
4727
7125
|
}
|
|
4728
7126
|
|
|
4729
|
-
async function refreshMessages() {
|
|
4730
|
-
|
|
7127
|
+
async function refreshMessages(tabContext = activeTabContext()) {
|
|
7128
|
+
if (!tabContext.tabId) return;
|
|
7129
|
+
const response = await api("/api/messages", { tabId: tabContext.tabId });
|
|
7130
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
4731
7131
|
latestMessages = response.data?.messages || [];
|
|
4732
7132
|
resetStreamBubble();
|
|
4733
7133
|
renderMessages(latestMessages);
|
|
@@ -4735,21 +7135,28 @@ async function refreshMessages() {
|
|
|
4735
7135
|
renderFooter();
|
|
4736
7136
|
}
|
|
4737
7137
|
|
|
4738
|
-
async function refreshModels() {
|
|
4739
|
-
|
|
7138
|
+
async function refreshModels(tabContext = activeTabContext()) {
|
|
7139
|
+
if (!tabContext.tabId) return;
|
|
7140
|
+
const response = await api("/api/models", { tabId: tabContext.tabId });
|
|
4740
7141
|
const models = response.data?.models || [];
|
|
4741
|
-
|
|
7142
|
+
let scopedModels = [];
|
|
7143
|
+
let scopedModelPatterns = [];
|
|
7144
|
+
let scopedModelSource = "none";
|
|
7145
|
+
let scopedModelError = null;
|
|
4742
7146
|
try {
|
|
4743
|
-
const scopedResponse = await api("/api/scoped-models");
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
7147
|
+
const scopedResponse = await api("/api/scoped-models", { tabId: tabContext.tabId });
|
|
7148
|
+
scopedModels = scopedResponse.data?.models || [];
|
|
7149
|
+
scopedModelPatterns = scopedResponse.data?.patterns || [];
|
|
7150
|
+
scopedModelSource = scopedResponse.data?.source || "none";
|
|
4747
7151
|
} catch (error) {
|
|
4748
|
-
|
|
4749
|
-
footerScopedModelPatterns = [];
|
|
4750
|
-
footerScopedModelSource = "none";
|
|
4751
|
-
addEvent(`failed to load scoped models: ${error.message}`, "warn");
|
|
7152
|
+
scopedModelError = error;
|
|
4752
7153
|
}
|
|
7154
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
7155
|
+
availableModels = models;
|
|
7156
|
+
footerScopedModels = scopedModels;
|
|
7157
|
+
footerScopedModelPatterns = scopedModelPatterns;
|
|
7158
|
+
footerScopedModelSource = scopedModelSource;
|
|
7159
|
+
if (scopedModelError) addEvent(`failed to load scoped models: ${scopedModelError.message}`, "warn");
|
|
4753
7160
|
elements.modelSelect.replaceChildren();
|
|
4754
7161
|
for (const model of models) {
|
|
4755
7162
|
const option = document.createElement("option");
|
|
@@ -5127,10 +7534,7 @@ function insertPathSuggestion(index = commandSuggestIndex) {
|
|
|
5127
7534
|
return true;
|
|
5128
7535
|
}
|
|
5129
7536
|
|
|
5130
|
-
|
|
5131
|
-
const response = await api("/api/commands");
|
|
5132
|
-
availableCommands = normalizeCommands(response.data?.commands || []);
|
|
5133
|
-
updateOptionalFeatureAvailability();
|
|
7537
|
+
function renderCommands() {
|
|
5134
7538
|
elements.commandsBox.replaceChildren();
|
|
5135
7539
|
if (!availableCommands.length) {
|
|
5136
7540
|
elements.commandsBox.textContent = "No RPC-visible commands.";
|
|
@@ -5161,8 +7565,27 @@ async function refreshCommands() {
|
|
|
5161
7565
|
renderCommandSuggestions();
|
|
5162
7566
|
}
|
|
5163
7567
|
|
|
5164
|
-
async function
|
|
5165
|
-
|
|
7568
|
+
async function refreshCommands(tabContext = activeTabContext()) {
|
|
7569
|
+
if (!tabContext.tabId) return;
|
|
7570
|
+
const response = await api("/api/commands", { tabId: tabContext.tabId });
|
|
7571
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
7572
|
+
availableCommands = normalizeCommands(response.data?.commands || []);
|
|
7573
|
+
updateOptionalFeatureAvailability();
|
|
7574
|
+
renderCommands();
|
|
7575
|
+
}
|
|
7576
|
+
|
|
7577
|
+
async function refreshAll(tabContext = activeTabContext()) {
|
|
7578
|
+
if (!tabContext.tabId) return;
|
|
7579
|
+
const results = await Promise.allSettled([
|
|
7580
|
+
refreshState(tabContext),
|
|
7581
|
+
refreshMessages(tabContext),
|
|
7582
|
+
refreshModels(tabContext),
|
|
7583
|
+
refreshCommands(tabContext),
|
|
7584
|
+
refreshStats(tabContext),
|
|
7585
|
+
refreshWorkspace(tabContext),
|
|
7586
|
+
refreshNetworkStatus(),
|
|
7587
|
+
]);
|
|
7588
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5166
7589
|
for (const result of results) {
|
|
5167
7590
|
if (result.status === "rejected") addEvent(result.reason.message || String(result.reason), "error");
|
|
5168
7591
|
}
|
|
@@ -5245,42 +7668,56 @@ async function closeNetworkAccess() {
|
|
|
5245
7668
|
async function sendPrompt(kind = "prompt", explicitMessage) {
|
|
5246
7669
|
const usesPromptInput = explicitMessage === undefined;
|
|
5247
7670
|
const rawMessage = usesPromptInput ? elements.promptInput.value : explicitMessage;
|
|
5248
|
-
const
|
|
5249
|
-
if (!message) return;
|
|
5250
|
-
|
|
7671
|
+
const originalMessage = String(rawMessage || "").trim();
|
|
5251
7672
|
const targetTabId = activeTabId;
|
|
5252
|
-
|
|
5253
|
-
|
|
7673
|
+
if (!targetTabId) return;
|
|
7674
|
+
const tabContext = activeTabContext(targetTabId);
|
|
7675
|
+
const attachments = usesPromptInput ? [...attachmentsForTab(targetTabId)] : [];
|
|
7676
|
+
if (!originalMessage && attachments.length === 0) return;
|
|
7677
|
+
if (kind === "prompt" && attachments.length === 0 && await handleNativeSlashSelectorCommand(originalMessage, { usesPromptInput })) return;
|
|
7678
|
+
|
|
7679
|
+
const targetWasStreaming = !!currentState?.isStreaming;
|
|
7680
|
+
const busyBehavior = elements.busyBehavior.value || "followUp";
|
|
7681
|
+
const startsRun = kind === "prompt" && !targetWasStreaming;
|
|
5254
7682
|
autoFollowChat = true;
|
|
5255
7683
|
updateJumpToLatestButton();
|
|
5256
7684
|
setComposerActionsOpen(false);
|
|
5257
7685
|
if (startsRun) {
|
|
5258
7686
|
markTabWorkingLocally(targetTabId);
|
|
5259
|
-
setRunIndicatorActivity("Sending prompt to Pi…");
|
|
7687
|
+
setRunIndicatorActivity(attachments.length ? "Uploading attachments…" : "Sending prompt to Pi…");
|
|
5260
7688
|
}
|
|
5261
7689
|
|
|
7690
|
+
let message = originalMessage;
|
|
5262
7691
|
try {
|
|
7692
|
+
const prepared = attachments.length ? await prepareAttachmentsForPrompt(attachments, targetTabId) : { images: [], uploadedFiles: [], inlineImageIds: new Set() };
|
|
7693
|
+
message = composeMessageWithAttachments(originalMessage, prepared.uploadedFiles, prepared.inlineImageIds);
|
|
7694
|
+
const bodyBase = { message };
|
|
7695
|
+
if (prepared.images.length) bodyBase.images = prepared.images;
|
|
7696
|
+
if (kind === "prompt" && !message.startsWith("/")) rememberLastUserPrompt(message, { tabId: targetTabId });
|
|
7697
|
+
if (startsRun && isCurrentTabContext(tabContext)) setRunIndicatorActivity("Sending prompt to Pi…");
|
|
7698
|
+
|
|
5263
7699
|
let response;
|
|
5264
7700
|
if (kind === "steer") {
|
|
5265
|
-
response = await api("/api/steer", { method: "POST", body:
|
|
7701
|
+
response = await api("/api/steer", { method: "POST", body: bodyBase, tabId: targetTabId });
|
|
5266
7702
|
} else if (kind === "follow-up") {
|
|
5267
|
-
response = await api("/api/follow-up", { method: "POST", body:
|
|
7703
|
+
response = await api("/api/follow-up", { method: "POST", body: bodyBase, tabId: targetTabId });
|
|
5268
7704
|
} else {
|
|
5269
|
-
const body = {
|
|
5270
|
-
if (
|
|
7705
|
+
const body = { ...bodyBase };
|
|
7706
|
+
if (targetWasStreaming) body.streamingBehavior = busyBehavior;
|
|
5271
7707
|
response = await api("/api/prompt", { method: "POST", body, tabId: targetTabId });
|
|
5272
7708
|
}
|
|
5273
7709
|
applyResponseTab(response);
|
|
5274
7710
|
if (response?.command === "native_slash_command" && /^\/new(?:\s|$)/.test(message)) forgetLastUserPrompt(targetTabId);
|
|
7711
|
+
const targetStillActive = isCurrentTabContext(tabContext);
|
|
5275
7712
|
if (startsRun && response?.command === "native_slash_command") {
|
|
5276
7713
|
markTabIdleLocally(targetTabId);
|
|
5277
|
-
clearRunIndicatorActivity();
|
|
5278
|
-
} else if (kind === "steer" && currentState?.isStreaming) {
|
|
7714
|
+
if (targetStillActive) clearRunIndicatorActivity();
|
|
7715
|
+
} else if (targetStillActive && kind === "steer" && currentState?.isStreaming) {
|
|
5279
7716
|
setRunIndicatorActivity("Steering sent; waiting for the next output or action…");
|
|
5280
|
-
} else if (kind === "follow-up" && currentState?.isStreaming) {
|
|
7717
|
+
} else if (targetStillActive && kind === "follow-up" && currentState?.isStreaming) {
|
|
5281
7718
|
setRunIndicatorActivity("Follow-up queued; current agent run is still active…");
|
|
5282
7719
|
}
|
|
5283
|
-
if (response?.command === "native_slash_command" && response.data?.copyText) {
|
|
7720
|
+
if (targetStillActive && response?.command === "native_slash_command" && response.data?.copyText) {
|
|
5284
7721
|
try {
|
|
5285
7722
|
await navigator.clipboard.writeText(response.data.copyText);
|
|
5286
7723
|
} catch (error) {
|
|
@@ -5288,22 +7725,33 @@ async function sendPrompt(kind = "prompt", explicitMessage) {
|
|
|
5288
7725
|
response.data.level = "warn";
|
|
5289
7726
|
}
|
|
5290
7727
|
}
|
|
5291
|
-
if (response?.command === "native_slash_command" && response.data?.message) {
|
|
7728
|
+
if (targetStillActive && response?.command === "native_slash_command" && response.data?.message) {
|
|
5292
7729
|
addTransientMessage({ role: "native", title: message.split(/\s+/, 1)[0], content: response.data.message, level: response.data.level || "info" });
|
|
5293
7730
|
}
|
|
5294
7731
|
if (usesPromptInput) {
|
|
5295
|
-
|
|
5296
|
-
|
|
7732
|
+
clearAttachments(targetTabId);
|
|
7733
|
+
if (targetStillActive) {
|
|
7734
|
+
elements.promptInput.value = "";
|
|
7735
|
+
resizePromptInput();
|
|
7736
|
+
} else {
|
|
7737
|
+
tabDrafts.set(targetTabId, "");
|
|
7738
|
+
}
|
|
7739
|
+
}
|
|
7740
|
+
if (targetStillActive) {
|
|
7741
|
+
hideCommandSuggestions();
|
|
7742
|
+
scheduleRefreshState(120, tabContext);
|
|
7743
|
+
} else {
|
|
7744
|
+
scheduleRefreshTabs(300);
|
|
5297
7745
|
}
|
|
5298
|
-
hideCommandSuggestions();
|
|
5299
|
-
scheduleRefreshState();
|
|
5300
7746
|
} catch (error) {
|
|
5301
7747
|
if (startsRun) {
|
|
5302
7748
|
markTabIdleLocally(targetTabId);
|
|
5303
|
-
clearRunIndicatorActivity();
|
|
7749
|
+
if (isCurrentTabContext(tabContext)) clearRunIndicatorActivity();
|
|
7750
|
+
}
|
|
7751
|
+
if (isCurrentTabContext(tabContext)) {
|
|
7752
|
+
addEvent(error.message, "error");
|
|
7753
|
+
addTransientMessage({ role: "error", title: message.startsWith("/") ? message.split(/\s+/, 1)[0] : "error", content: error.message, level: "error" });
|
|
5304
7754
|
}
|
|
5305
|
-
addEvent(error.message, "error");
|
|
5306
|
-
addTransientMessage({ role: "error", title: message.startsWith("/") ? message.split(/\s+/, 1)[0] : "error", content: error.message, level: "error" });
|
|
5307
7755
|
}
|
|
5308
7756
|
}
|
|
5309
7757
|
|
|
@@ -5379,12 +7827,14 @@ function handleExtensionUiRequest(request) {
|
|
|
5379
7827
|
|
|
5380
7828
|
async function sendDialogResponse(payload) {
|
|
5381
7829
|
const { tabId = activeTabId, ...body } = payload;
|
|
7830
|
+
const tabContext = activeTabContext(tabId);
|
|
5382
7831
|
try {
|
|
5383
7832
|
const response = await api("/api/extension-ui-response", { method: "POST", body, tabId });
|
|
5384
7833
|
if (!applyResponseTab(response) && decrementTabPendingBlockerCount(tabId)) renderTabs();
|
|
5385
7834
|
} catch (error) {
|
|
5386
|
-
addEvent(error.message, "error");
|
|
7835
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5387
7836
|
} finally {
|
|
7837
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5388
7838
|
if (elements.dialog.open) elements.dialog.close();
|
|
5389
7839
|
activeDialog = null;
|
|
5390
7840
|
if (runIndicatorIsActive()) setRunIndicatorActivity("Continuing after your response…");
|
|
@@ -5406,7 +7856,8 @@ function showNextDialog() {
|
|
|
5406
7856
|
const request = activeDialog;
|
|
5407
7857
|
|
|
5408
7858
|
const prompt = normalizeDialogPrompt(request);
|
|
5409
|
-
const
|
|
7859
|
+
const detectedReleasePrompt = request.method === "select" ? releaseDialogPromptParts(prompt) : null;
|
|
7860
|
+
const releasePrompt = detectedReleasePrompt && isOptionalFeatureEnabled(detectedReleasePrompt.featureId) ? detectedReleasePrompt : null;
|
|
5410
7861
|
const displayPrompt = releasePrompt || prompt;
|
|
5411
7862
|
const isGuardrailDialog = isGuardrailDialogPrompt(displayPrompt);
|
|
5412
7863
|
const isReleaseDialog = !!releasePrompt;
|
|
@@ -5460,8 +7911,22 @@ function showNextDialog() {
|
|
|
5460
7911
|
elements.dialog.showModal();
|
|
5461
7912
|
}
|
|
5462
7913
|
|
|
7914
|
+
function handleInactiveTabEvent(event) {
|
|
7915
|
+
if (event.type === "extension_ui_request" && EXTENSION_UI_BLOCKING_METHODS.has(event.method)) {
|
|
7916
|
+
if (!event.replayed) notifyBlockedTab(event.tabId, { request: event, count: event.pendingExtensionUiRequestCount });
|
|
7917
|
+
renderTabs();
|
|
7918
|
+
} else if (event.type === "agent_end") {
|
|
7919
|
+
notifyAgentDone(event.tabId, { activity: event.tabActivity, tabTitle: event.tabTitle });
|
|
7920
|
+
}
|
|
7921
|
+
}
|
|
7922
|
+
|
|
5463
7923
|
function handleEvent(event) {
|
|
5464
7924
|
ingestEventTabActivity(event);
|
|
7925
|
+
if (!eventTargetsActiveTab(event)) {
|
|
7926
|
+
handleInactiveTabEvent(event);
|
|
7927
|
+
return;
|
|
7928
|
+
}
|
|
7929
|
+
const tabContext = activeTabContext(event.tabId || activeTabId);
|
|
5465
7930
|
switch (event.type) {
|
|
5466
7931
|
case "webui_connected":
|
|
5467
7932
|
addEvent(`connected to ${event.tabTitle || "terminal"} for ${event.cwd}`);
|
|
@@ -5491,7 +7956,12 @@ function handleEvent(event) {
|
|
|
5491
7956
|
renderStatus();
|
|
5492
7957
|
renderWidgets();
|
|
5493
7958
|
refreshTabs().catch((error) => addEvent(error.message, "error"));
|
|
5494
|
-
setTimeout(() =>
|
|
7959
|
+
setTimeout(() => {
|
|
7960
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
7961
|
+
refreshAll(tabContext).catch((error) => {
|
|
7962
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
7963
|
+
});
|
|
7964
|
+
}, 500);
|
|
5495
7965
|
break;
|
|
5496
7966
|
case "webui_extension_ui_cancelled":
|
|
5497
7967
|
removeQueuedDialogRequests(event.ids || []);
|
|
@@ -5581,10 +8051,18 @@ function handleEvent(event) {
|
|
|
5581
8051
|
scheduleRefreshFooter();
|
|
5582
8052
|
break;
|
|
5583
8053
|
case "tool_execution_start":
|
|
8054
|
+
streamToolCallSeen = true;
|
|
8055
|
+
suppressStreamingAssistantTextBeforeToolCall();
|
|
8056
|
+
handleToolExecutionStart(event);
|
|
5584
8057
|
setRunIndicatorActivity(`Running tool: ${runIndicatorToolName(event.toolName)}…`);
|
|
5585
8058
|
addEvent(`tool ${event.toolName} started`);
|
|
5586
8059
|
break;
|
|
8060
|
+
case "tool_execution_update":
|
|
8061
|
+
handleToolExecutionUpdate(event);
|
|
8062
|
+
setRunIndicatorActivity(`Running tool: ${runIndicatorToolName(event.toolName)}…`, { scroll: false });
|
|
8063
|
+
break;
|
|
5587
8064
|
case "tool_execution_end":
|
|
8065
|
+
handleToolExecutionEnd(event);
|
|
5588
8066
|
setRunIndicatorActivity(`Tool ${runIndicatorToolName(event.toolName)} ${event.isError ? "failed" : "finished"}; waiting for the agent's next step…`);
|
|
5589
8067
|
addEvent(`tool ${event.toolName} ${event.isError ? "failed" : "finished"}`, event.isError ? "error" : "info");
|
|
5590
8068
|
scheduleRefreshMessages();
|
|
@@ -5602,6 +8080,29 @@ function handleEvent(event) {
|
|
|
5602
8080
|
markTabOutputSeen();
|
|
5603
8081
|
scheduleRefreshMessages();
|
|
5604
8082
|
break;
|
|
8083
|
+
case "auto_retry_start": {
|
|
8084
|
+
const seconds = Math.max(0, Math.ceil(Number(event.delayMs || 0) / 1000));
|
|
8085
|
+
const retryText = `Retrying (${event.attempt || "?"}/${event.maxAttempts || "?"}) in ${seconds}s after: ${event.errorMessage || "model/provider error"}`;
|
|
8086
|
+
setRunIndicatorActivity(retryText);
|
|
8087
|
+
addEvent(retryText, "warn");
|
|
8088
|
+
addTransientMessage({ role: "warn", title: "auto retry", content: retryText, level: "warn" });
|
|
8089
|
+
break;
|
|
8090
|
+
}
|
|
8091
|
+
case "auto_retry_end":
|
|
8092
|
+
if (event.success === false) {
|
|
8093
|
+
const retryError = `Retry failed after ${event.attempt || "?"} attempt(s): ${event.finalError || "Unknown error"}`;
|
|
8094
|
+
addEvent(retryError, "error");
|
|
8095
|
+
addTransientMessage({ role: "error", title: "auto retry failed", content: retryError, level: "error" });
|
|
8096
|
+
} else {
|
|
8097
|
+
addEvent(`retry recovered after ${event.attempt || "?"} attempt(s)`);
|
|
8098
|
+
}
|
|
8099
|
+
break;
|
|
8100
|
+
case "extension_error": {
|
|
8101
|
+
const message = `${event.extensionPath || "extension"}${event.event ? ` during ${event.event}` : ""}: ${event.error || "unknown extension error"}`;
|
|
8102
|
+
addEvent(message, "error");
|
|
8103
|
+
addTransientMessage({ role: "error", title: "extension error", content: message, level: "error" });
|
|
8104
|
+
break;
|
|
8105
|
+
}
|
|
5605
8106
|
case "extension_ui_request":
|
|
5606
8107
|
handleExtensionUiRequest(event);
|
|
5607
8108
|
break;
|
|
@@ -5624,20 +8125,29 @@ function handleEvent(event) {
|
|
|
5624
8125
|
}
|
|
5625
8126
|
}
|
|
5626
8127
|
|
|
5627
|
-
function connectEvents() {
|
|
8128
|
+
function connectEvents(tabContext = activeTabContext()) {
|
|
5628
8129
|
eventSource?.close();
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
8130
|
+
eventSource = null;
|
|
8131
|
+
if (!tabContext.tabId || !isCurrentTabContext(tabContext)) return;
|
|
8132
|
+
const source = new EventSource(`/api/events?tab=${encodeURIComponent(tabContext.tabId)}`);
|
|
8133
|
+
eventSource = source;
|
|
8134
|
+
source.onmessage = (message) => {
|
|
8135
|
+
if (eventSource !== source || !isCurrentTabContext(tabContext)) return;
|
|
5632
8136
|
try {
|
|
5633
8137
|
handleEvent(JSON.parse(message.data));
|
|
5634
8138
|
} catch (error) {
|
|
5635
|
-
addEvent(error.message, "error");
|
|
8139
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5636
8140
|
}
|
|
5637
8141
|
};
|
|
5638
|
-
|
|
8142
|
+
source.onerror = () => {
|
|
8143
|
+
if (eventSource !== source || !isCurrentTabContext(tabContext)) return;
|
|
8144
|
+
addEvent("event stream disconnected; browser will retry", "warn");
|
|
8145
|
+
fetch("/api/health", { cache: "no-store" }).catch((error) => setBackendOffline(true, error));
|
|
8146
|
+
};
|
|
5639
8147
|
}
|
|
5640
8148
|
|
|
8149
|
+
elements.copyServerCommandButton?.addEventListener("click", copyServerStartCommand);
|
|
8150
|
+
elements.retryServerConnectionButton?.addEventListener("click", retryServerConnection);
|
|
5641
8151
|
elements.sendFeedbackButton.addEventListener("click", () => submitQueuedActionFeedback());
|
|
5642
8152
|
elements.composer.addEventListener("submit", (event) => {
|
|
5643
8153
|
event.preventDefault();
|
|
@@ -5672,70 +8182,143 @@ publishMenuContainer?.addEventListener("focusout", () => {
|
|
|
5672
8182
|
elements.releaseNpmButton.addEventListener("click", () => runPublishWorkflow("/release-npm"));
|
|
5673
8183
|
elements.releaseAurButton.addEventListener("click", () => runPublishWorkflow("/release-aur"));
|
|
5674
8184
|
elements.gitWorkflowCancelButton.addEventListener("click", cancelGitWorkflow);
|
|
5675
|
-
elements.
|
|
8185
|
+
elements.nativeCommandDialog.addEventListener("close", () => {
|
|
8186
|
+
elements.nativeCommandSearch.oninput = null;
|
|
8187
|
+
nativeCommandTabId = null;
|
|
8188
|
+
});
|
|
8189
|
+
|
|
8190
|
+
function resetAbortLongPressAffordance() {
|
|
8191
|
+
clearTimeout(abortLongPressTimer);
|
|
8192
|
+
abortLongPressTimer = null;
|
|
8193
|
+
elements.abortButton.classList.remove("long-pressing");
|
|
8194
|
+
if (!abortRequestInFlight) elements.abortButton.textContent = "Abort";
|
|
8195
|
+
}
|
|
8196
|
+
|
|
8197
|
+
async function abortActiveRun({ source = "button" } = {}) {
|
|
8198
|
+
if (abortRequestInFlight || !isAbortAvailable()) return;
|
|
8199
|
+
const tabContext = activeTabContext();
|
|
8200
|
+
abortRequestInFlight = true;
|
|
8201
|
+
resetAbortLongPressAffordance();
|
|
8202
|
+
updateComposerModeButtons();
|
|
5676
8203
|
const hadActiveRun = runIndicatorIsActive();
|
|
5677
8204
|
try {
|
|
5678
|
-
if (hadActiveRun) setRunIndicatorActivity(
|
|
5679
|
-
await api("/api/abort", { method: "POST", body: {} });
|
|
8205
|
+
if (hadActiveRun) setRunIndicatorActivity(`Abort requested${source === "escape" ? " from Esc" : source === "long-press" ? " from long-press" : ""}; checking whether Pi stopped…`);
|
|
8206
|
+
await api("/api/abort", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
8207
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5680
8208
|
addAbortTranscriptNotice({ activeRun: hadActiveRun });
|
|
5681
|
-
scheduleAbortStateChecks();
|
|
8209
|
+
scheduleAbortStateChecks(tabContext);
|
|
5682
8210
|
} catch (error) {
|
|
5683
|
-
|
|
5684
|
-
|
|
8211
|
+
if (isCurrentTabContext(tabContext)) {
|
|
8212
|
+
addEvent(error.message, "error");
|
|
8213
|
+
addAbortTranscriptNotice({ errorMessage: error.message });
|
|
8214
|
+
}
|
|
8215
|
+
} finally {
|
|
8216
|
+
abortRequestInFlight = false;
|
|
8217
|
+
updateComposerModeButtons();
|
|
8218
|
+
}
|
|
8219
|
+
}
|
|
8220
|
+
|
|
8221
|
+
function startAbortLongPress(event) {
|
|
8222
|
+
if (!isAbortAvailable() || abortRequestInFlight) return;
|
|
8223
|
+
if (event.button !== undefined && event.button !== 0) return;
|
|
8224
|
+
resetAbortLongPressAffordance();
|
|
8225
|
+
abortLongPressHandled = false;
|
|
8226
|
+
elements.abortButton.classList.add("long-pressing");
|
|
8227
|
+
elements.abortButton.textContent = "Hold…";
|
|
8228
|
+
abortLongPressTimer = setTimeout(() => {
|
|
8229
|
+
abortLongPressTimer = null;
|
|
8230
|
+
abortLongPressHandled = true;
|
|
8231
|
+
abortActiveRun({ source: "long-press" });
|
|
8232
|
+
}, ABORT_LONG_PRESS_MS);
|
|
8233
|
+
}
|
|
8234
|
+
|
|
8235
|
+
elements.abortButton.addEventListener("pointerdown", startAbortLongPress);
|
|
8236
|
+
for (const eventName of ["pointerup", "pointerleave", "pointercancel", "blur"]) {
|
|
8237
|
+
elements.abortButton.addEventListener(eventName, resetAbortLongPressAffordance);
|
|
8238
|
+
}
|
|
8239
|
+
elements.abortButton.addEventListener("click", (event) => {
|
|
8240
|
+
if (abortLongPressHandled) {
|
|
8241
|
+
event.preventDefault();
|
|
8242
|
+
abortLongPressHandled = false;
|
|
8243
|
+
return;
|
|
5685
8244
|
}
|
|
8245
|
+
abortActiveRun({ source: "button" });
|
|
5686
8246
|
});
|
|
5687
8247
|
elements.newSessionButton.addEventListener("click", async () => {
|
|
5688
8248
|
setComposerActionsOpen(false);
|
|
8249
|
+
const tabContext = activeTabContext();
|
|
5689
8250
|
if (!confirm("Start a new Pi session?")) return;
|
|
5690
8251
|
try {
|
|
5691
|
-
const response = await api("/api/new-session", { method: "POST", body: {} });
|
|
8252
|
+
const response = await api("/api/new-session", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
5692
8253
|
applyResponseTab(response);
|
|
5693
|
-
forgetLastUserPrompt(
|
|
5694
|
-
|
|
5695
|
-
|
|
8254
|
+
forgetLastUserPrompt(tabContext.tabId);
|
|
8255
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
8256
|
+
await refreshAll(tabContext);
|
|
8257
|
+
if (isCurrentTabContext(tabContext)) focusPromptInput({ defer: true });
|
|
5696
8258
|
} catch (error) {
|
|
5697
|
-
addEvent(error.message, "error");
|
|
8259
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5698
8260
|
}
|
|
5699
8261
|
});
|
|
5700
8262
|
elements.compactButton.addEventListener("click", async () => {
|
|
5701
8263
|
setComposerActionsOpen(false);
|
|
8264
|
+
const tabContext = activeTabContext();
|
|
5702
8265
|
try {
|
|
5703
8266
|
elements.compactButton.disabled = true;
|
|
5704
8267
|
elements.compactButton.textContent = "Compacting…";
|
|
5705
8268
|
setRunIndicatorActivity("Requesting context compaction…");
|
|
5706
8269
|
scrollChatToBottom({ force: true });
|
|
5707
8270
|
addEvent("manual compaction requested");
|
|
5708
|
-
await api("/api/compact", { method: "POST", body: {} });
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
8271
|
+
await api("/api/compact", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
8272
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
8273
|
+
scheduleRefreshState(120, tabContext);
|
|
8274
|
+
scheduleRefreshMessages(600, tabContext);
|
|
8275
|
+
scheduleRefreshFooter(600, tabContext);
|
|
5712
8276
|
} catch (error) {
|
|
5713
|
-
|
|
5714
|
-
|
|
8277
|
+
if (isCurrentTabContext(tabContext)) {
|
|
8278
|
+
clearRunIndicatorActivity();
|
|
8279
|
+
addEvent(error.message, "error");
|
|
8280
|
+
}
|
|
5715
8281
|
} finally {
|
|
5716
|
-
|
|
5717
|
-
|
|
8282
|
+
if (isCurrentTabContext(tabContext)) {
|
|
8283
|
+
elements.compactButton.disabled = !!currentState?.isCompacting;
|
|
8284
|
+
elements.compactButton.textContent = currentState?.isCompacting ? "Compacting…" : "Compact";
|
|
8285
|
+
}
|
|
5718
8286
|
}
|
|
5719
8287
|
});
|
|
5720
8288
|
elements.setModelButton.addEventListener("click", async () => {
|
|
5721
8289
|
if (!elements.modelSelect.value) return;
|
|
8290
|
+
const tabContext = activeTabContext();
|
|
5722
8291
|
try {
|
|
5723
8292
|
const selected = JSON.parse(elements.modelSelect.value);
|
|
5724
|
-
await api("/api/model", { method: "POST", body: selected });
|
|
5725
|
-
await refreshState();
|
|
8293
|
+
await api("/api/model", { method: "POST", body: selected, tabId: tabContext.tabId });
|
|
8294
|
+
if (isCurrentTabContext(tabContext)) await refreshState(tabContext);
|
|
5726
8295
|
} catch (error) {
|
|
5727
|
-
addEvent(error.message, "error");
|
|
8296
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5728
8297
|
}
|
|
5729
8298
|
});
|
|
5730
8299
|
elements.setThinkingButton.addEventListener("click", async () => {
|
|
8300
|
+
const tabContext = activeTabContext();
|
|
5731
8301
|
try {
|
|
5732
|
-
await api("/api/thinking", { method: "POST", body: { level: elements.thinkingSelect.value } });
|
|
5733
|
-
await refreshState();
|
|
8302
|
+
await api("/api/thinking", { method: "POST", body: { level: elements.thinkingSelect.value }, tabId: tabContext.tabId });
|
|
8303
|
+
if (isCurrentTabContext(tabContext)) await refreshState(tabContext);
|
|
5734
8304
|
} catch (error) {
|
|
5735
|
-
addEvent(error.message, "error");
|
|
8305
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5736
8306
|
}
|
|
5737
8307
|
});
|
|
5738
|
-
elements.themeSelect.addEventListener("change", () =>
|
|
8308
|
+
elements.themeSelect.addEventListener("change", () => {
|
|
8309
|
+
setThemeByName(elements.themeSelect.value, { persist: true, announce: true }).catch((error) => addEvent(error.message || String(error), "error"));
|
|
8310
|
+
});
|
|
8311
|
+
if (elements.backgroundChooseButton && elements.backgroundInput) {
|
|
8312
|
+
elements.backgroundChooseButton.addEventListener("click", () => elements.backgroundInput.click());
|
|
8313
|
+
elements.backgroundInput.addEventListener("change", () => {
|
|
8314
|
+
const [file] = Array.from(elements.backgroundInput.files || []);
|
|
8315
|
+
elements.backgroundInput.value = "";
|
|
8316
|
+
setCustomBackgroundFromFile(file).catch((error) => addEvent(error.message || String(error), "error"));
|
|
8317
|
+
});
|
|
8318
|
+
}
|
|
8319
|
+
if (elements.backgroundClearButton) {
|
|
8320
|
+
elements.backgroundClearButton.addEventListener("click", () => clearCustomBackground().catch((error) => addEvent(error.message || String(error), "error")));
|
|
8321
|
+
}
|
|
5739
8322
|
elements.openNetworkButton.addEventListener("click", openToNetwork);
|
|
5740
8323
|
elements.agentDoneNotificationsToggle.addEventListener("change", () => {
|
|
5741
8324
|
setAgentDoneNotificationsEnabled(elements.agentDoneNotificationsToggle.checked, {
|
|
@@ -5746,6 +8329,11 @@ elements.agentDoneNotificationsToggle.addEventListener("change", () => {
|
|
|
5746
8329
|
renderAgentDoneNotificationsToggle();
|
|
5747
8330
|
});
|
|
5748
8331
|
});
|
|
8332
|
+
if (elements.thinkingVisibilityToggle) {
|
|
8333
|
+
elements.thinkingVisibilityToggle.addEventListener("change", () => {
|
|
8334
|
+
setThinkingOutputVisible(elements.thinkingVisibilityToggle.checked, { announce: true });
|
|
8335
|
+
});
|
|
8336
|
+
}
|
|
5749
8337
|
elements.toggleSidePanelButton.addEventListener("click", () => {
|
|
5750
8338
|
setSidePanelCollapsed(true);
|
|
5751
8339
|
});
|
|
@@ -5792,6 +8380,7 @@ document.addEventListener("pointermove", (event) => {
|
|
|
5792
8380
|
}, { passive: true });
|
|
5793
8381
|
window.addEventListener("keydown", (event) => {
|
|
5794
8382
|
if (event.key !== "Escape") return;
|
|
8383
|
+
if (elements.dialog?.open || elements.pathPickerDialog?.open) return;
|
|
5795
8384
|
if (publishMenuOpen) {
|
|
5796
8385
|
setPublishMenuOpen(false);
|
|
5797
8386
|
return;
|
|
@@ -5808,8 +8397,17 @@ window.addEventListener("keydown", (event) => {
|
|
|
5808
8397
|
setFooterModelPickerOpen(false);
|
|
5809
8398
|
return;
|
|
5810
8399
|
}
|
|
8400
|
+
if (!elements.commandSuggest.hidden) {
|
|
8401
|
+
hideCommandSuggestions();
|
|
8402
|
+
return;
|
|
8403
|
+
}
|
|
5811
8404
|
if (isMobileView() && !document.body.classList.contains("side-panel-collapsed")) {
|
|
5812
8405
|
setSidePanelCollapsed(true);
|
|
8406
|
+
return;
|
|
8407
|
+
}
|
|
8408
|
+
if (isAbortAvailable()) {
|
|
8409
|
+
event.preventDefault();
|
|
8410
|
+
abortActiveRun({ source: "escape" });
|
|
5813
8411
|
}
|
|
5814
8412
|
});
|
|
5815
8413
|
|
|
@@ -5824,6 +8422,18 @@ elements.pathPickerDialog.addEventListener("close", () => {
|
|
|
5824
8422
|
if (pathPickerState) closePathPicker(null);
|
|
5825
8423
|
});
|
|
5826
8424
|
|
|
8425
|
+
if (elements.attachButton && elements.attachmentInput) {
|
|
8426
|
+
elements.attachButton.addEventListener("click", () => elements.attachmentInput.click());
|
|
8427
|
+
elements.attachmentInput.addEventListener("change", () => {
|
|
8428
|
+
addAttachmentFiles(elements.attachmentInput.files, "picker");
|
|
8429
|
+
elements.attachmentInput.value = "";
|
|
8430
|
+
});
|
|
8431
|
+
}
|
|
8432
|
+
elements.promptInput.addEventListener("paste", handleAttachmentPaste);
|
|
8433
|
+
elements.composer.addEventListener("dragover", handleComposerDragOver);
|
|
8434
|
+
elements.composer.addEventListener("dragleave", handleComposerDragLeave);
|
|
8435
|
+
elements.composer.addEventListener("drop", handleComposerDrop);
|
|
8436
|
+
|
|
5827
8437
|
elements.promptInput.addEventListener("keydown", (event) => {
|
|
5828
8438
|
if (shouldSendPromptFromEnter(event)) {
|
|
5829
8439
|
event.preventDefault();
|
|
@@ -5885,10 +8495,19 @@ updateComposerModeButtons();
|
|
|
5885
8495
|
updateOptionalFeatureAvailability();
|
|
5886
8496
|
loadLastUserPromptCache();
|
|
5887
8497
|
installViewportHandlers();
|
|
5888
|
-
|
|
8498
|
+
currentThemeName = storedThemeName();
|
|
8499
|
+
renderBackgroundControl();
|
|
8500
|
+
initializeThemes().catch((error) => {
|
|
8501
|
+
addEvent(`failed to load themes: ${error.message}`, "warn");
|
|
8502
|
+
initializeCustomBackground().catch((backgroundError) => addEvent(`failed to initialize background: ${backgroundError.message}`, "warn"));
|
|
8503
|
+
});
|
|
5889
8504
|
initializeFastPicks().catch((error) => addEvent(`failed to initialize path fast picks: ${error.message}`, "error"));
|
|
5890
8505
|
restoreAgentDoneNotificationsSetting();
|
|
8506
|
+
restoreThinkingVisibilitySetting();
|
|
8507
|
+
restoreSidePanelSectionState();
|
|
8508
|
+
bindSidePanelSectionToggles();
|
|
5891
8509
|
restoreSidePanelState();
|
|
5892
8510
|
bindMobileViewChanges();
|
|
5893
8511
|
registerPwaServiceWorker();
|
|
8512
|
+
renderServerOfflinePanel();
|
|
5894
8513
|
initializeTabs().catch((error) => addEvent(error.message, "error"));
|