@firstpick/pi-package-webui 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/bin/pi-webui.mjs +425 -13
- package/index.ts +82 -10
- package/package.json +1 -1
- package/public/app.js +2623 -241
- package/public/catppuccin-mocha-background.png +0 -0
- package/public/index.html +151 -54
- package/public/matrix-background.webp +0 -0
- package/public/service-worker.js +3 -1
- package/public/styles.css +628 -16
- package/tests/mobile-static.test.mjs +144 -30
package/public/app.js
CHANGED
|
@@ -21,6 +21,9 @@ const elements = {
|
|
|
21
21
|
promptInput: $("#promptInput"),
|
|
22
22
|
sendButton: $("#sendButton"),
|
|
23
23
|
commandSuggest: $("#commandSuggest"),
|
|
24
|
+
attachmentTray: $("#attachmentTray"),
|
|
25
|
+
attachButton: $("#attachButton"),
|
|
26
|
+
attachmentInput: $("#attachmentInput"),
|
|
24
27
|
busyBehavior: $("#busyBehavior"),
|
|
25
28
|
steerButton: $("#steerButton"),
|
|
26
29
|
followUpButton: $("#followUpButton"),
|
|
@@ -43,7 +46,13 @@ const elements = {
|
|
|
43
46
|
setModelButton: $("#setModelButton"),
|
|
44
47
|
thinkingSelect: $("#thinkingSelect"),
|
|
45
48
|
setThinkingButton: $("#setThinkingButton"),
|
|
49
|
+
thinkingVisibilityToggle: $("#thinkingVisibilityToggle"),
|
|
50
|
+
thinkingVisibilityStatus: $("#thinkingVisibilityStatus"),
|
|
46
51
|
themeSelect: $("#themeSelect"),
|
|
52
|
+
backgroundInput: $("#backgroundInput"),
|
|
53
|
+
backgroundChooseButton: $("#backgroundChooseButton"),
|
|
54
|
+
backgroundClearButton: $("#backgroundClearButton"),
|
|
55
|
+
backgroundStatus: $("#backgroundStatus"),
|
|
47
56
|
networkStatus: $("#networkStatus"),
|
|
48
57
|
openNetworkButton: $("#openNetworkButton"),
|
|
49
58
|
agentDoneNotificationsToggle: $("#agentDoneNotificationsToggle"),
|
|
@@ -72,12 +81,21 @@ const elements = {
|
|
|
72
81
|
pathPickerError: $("#pathPickerError"),
|
|
73
82
|
pathPickerCancelButton: $("#pathPickerCancelButton"),
|
|
74
83
|
pathPickerChooseButton: $("#pathPickerChooseButton"),
|
|
84
|
+
nativeCommandDialog: $("#nativeCommandDialog"),
|
|
85
|
+
nativeCommandTitle: $("#nativeCommandTitle"),
|
|
86
|
+
nativeCommandMessage: $("#nativeCommandMessage"),
|
|
87
|
+
nativeCommandSearch: $("#nativeCommandSearch"),
|
|
88
|
+
nativeCommandBody: $("#nativeCommandBody"),
|
|
89
|
+
nativeCommandError: $("#nativeCommandError"),
|
|
90
|
+
nativeCommandActions: $("#nativeCommandActions"),
|
|
75
91
|
};
|
|
76
92
|
|
|
77
93
|
let currentState = null;
|
|
78
94
|
let tabs = [];
|
|
79
95
|
let activeTabId = null;
|
|
96
|
+
let activeTabGeneration = 0;
|
|
80
97
|
let tabDrafts = new Map();
|
|
98
|
+
let tabAttachments = new Map();
|
|
81
99
|
let tabActivities = new Map();
|
|
82
100
|
let tabSeenCompletionSerials = new Map();
|
|
83
101
|
let streamBubble = null;
|
|
@@ -104,6 +122,7 @@ let refreshFooterTimer = null;
|
|
|
104
122
|
let refreshTabsTimer = null;
|
|
105
123
|
let eventSource = null;
|
|
106
124
|
let activeDialog = null;
|
|
125
|
+
let nativeCommandTabId = null;
|
|
107
126
|
let pathPickerState = null;
|
|
108
127
|
let pathFastPicks = [];
|
|
109
128
|
let pathFastPicksReady = false;
|
|
@@ -133,12 +152,16 @@ let blockedTabNotificationKeys = new Set();
|
|
|
133
152
|
let blockedTabNotificationPermissionRequested = false;
|
|
134
153
|
let blockedTabNotificationFallbackNoted = false;
|
|
135
154
|
let agentDoneNotificationsEnabled = false;
|
|
155
|
+
let thinkingOutputVisible = true;
|
|
136
156
|
let agentDoneNotificationPermissionRequested = false;
|
|
137
157
|
let agentDoneNotificationFallbackNoted = false;
|
|
138
158
|
let agentDoneNotificationKeys = new Set();
|
|
139
159
|
let availableModels = [];
|
|
140
160
|
let availableThemes = [];
|
|
141
161
|
let currentThemeName = "catppuccin-mocha";
|
|
162
|
+
let customBackground = null;
|
|
163
|
+
let customBackgroundObjectUrl = null;
|
|
164
|
+
let customBackgroundLoading = false;
|
|
142
165
|
let footerScopedModels = [];
|
|
143
166
|
let footerScopedModelPatterns = [];
|
|
144
167
|
let footerScopedModelSource = "none";
|
|
@@ -154,14 +177,32 @@ let maxVisualViewportHeight = 0;
|
|
|
154
177
|
let currentRunStartedAt = null;
|
|
155
178
|
let currentRunStreamChars = 0;
|
|
156
179
|
let latestTokPerSecond = null;
|
|
180
|
+
let abortRequestInFlight = false;
|
|
181
|
+
let abortLongPressTimer = null;
|
|
182
|
+
let abortLongPressHandled = false;
|
|
157
183
|
const dialogQueue = [];
|
|
158
184
|
const SIDE_PANEL_STORAGE_KEY = "pi-webui-side-panel-collapsed";
|
|
185
|
+
const SIDE_PANEL_SECTION_STORAGE_KEY = "pi-webui-side-panel-sections-collapsed";
|
|
159
186
|
const TAB_STORAGE_KEY = "pi-webui-active-tab";
|
|
160
187
|
const PATH_FAST_PICKS_STORAGE_KEY = "pi-webui-path-fast-picks";
|
|
161
188
|
const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications";
|
|
189
|
+
const THINKING_VISIBILITY_STORAGE_KEY = "pi-webui-thinking-visible";
|
|
162
190
|
const THEME_STORAGE_KEY = "pi-webui-theme";
|
|
191
|
+
const CUSTOM_BACKGROUND_STORAGE_KEY = "pi-webui-custom-background";
|
|
192
|
+
const CUSTOM_BACKGROUNDS_STORAGE_KEY = "pi-webui-custom-backgrounds";
|
|
193
|
+
const CUSTOM_BACKGROUND_IDB_NAME = "pi-webui-custom-background";
|
|
194
|
+
const CUSTOM_BACKGROUND_IDB_STORE = "backgrounds";
|
|
195
|
+
const CUSTOM_BACKGROUND_LEGACY_ID = "active";
|
|
196
|
+
const CUSTOM_BACKGROUND_MAX_FILE_BYTES = 24 * 1024 * 1024;
|
|
163
197
|
const OPTIONAL_FEATURES_STORAGE_KEY = "pi-webui-optional-features-disabled";
|
|
164
198
|
const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
|
|
199
|
+
const ATTACHMENT_MAX_FILES = 12;
|
|
200
|
+
const ATTACHMENT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
201
|
+
const ATTACHMENT_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
202
|
+
const ATTACHMENT_INLINE_IMAGE_MAX_BYTES = 8 * 1024 * 1024;
|
|
203
|
+
const ATTACHMENT_INLINE_IMAGE_TOTAL_MAX_BYTES = 16 * 1024 * 1024;
|
|
204
|
+
const INLINE_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
205
|
+
const BACKGROUND_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
165
206
|
const DEFAULT_THEME_NAME = "catppuccin-mocha";
|
|
166
207
|
const MOBILE_VIEW_QUERY = "(max-width: 720px), (max-device-width: 720px), (pointer: coarse) and (hover: none)";
|
|
167
208
|
const CHAT_BOTTOM_THRESHOLD_PX = 96;
|
|
@@ -173,6 +214,7 @@ const CHAT_USER_SCROLL_INTENT_MS = 700;
|
|
|
173
214
|
const RUN_INDICATOR_TICK_MS = 1000;
|
|
174
215
|
const RUN_INDICATOR_START_GRACE_MS = 2500;
|
|
175
216
|
const RUN_INDICATOR_STATE_RECHECK_MS = 5000;
|
|
217
|
+
const ABORT_LONG_PRESS_MS = 700;
|
|
176
218
|
const STREAM_OUTPUT_HIDE_DELAY_MS = 300;
|
|
177
219
|
const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220;
|
|
178
220
|
const STREAM_OUTPUT_MIN_VISIBLE_MS = 900;
|
|
@@ -188,6 +230,8 @@ const BLOCKED_TAB_NOTIFICATION_ICON = "/icon-192.png";
|
|
|
188
230
|
const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
|
|
189
231
|
const statusEntries = new Map();
|
|
190
232
|
const widgets = new Map();
|
|
233
|
+
const liveToolRuns = new Map();
|
|
234
|
+
const liveToolCards = new Map();
|
|
191
235
|
// Optional feature detection intentionally checks loaded Pi capabilities (RPC-visible
|
|
192
236
|
// commands and live widget events), not npm package folders. This keeps local dev
|
|
193
237
|
// symlinks and independently installed packages working.
|
|
@@ -260,6 +304,8 @@ const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
|
260
304
|
["git-footer-refresh", "gitFooterStatus"],
|
|
261
305
|
["todo-progress-status", "todoProgressWidget"],
|
|
262
306
|
]);
|
|
307
|
+
const HIDDEN_COMMAND_NAMES = new Set(["webui-tree-navigate"]);
|
|
308
|
+
const NATIVE_SELECTOR_COMMANDS = new Set(["model", "settings", "theme", "fork", "clone", "resume", "tree", "login", "logout", "scoped-models"]);
|
|
263
309
|
const optionalFeatureInstallInProgress = new Set();
|
|
264
310
|
const gitWorkflow = {
|
|
265
311
|
active: false,
|
|
@@ -313,6 +359,63 @@ function readStoredSidePanelCollapsed() {
|
|
|
313
359
|
}
|
|
314
360
|
}
|
|
315
361
|
|
|
362
|
+
function sidePanelSectionRecords() {
|
|
363
|
+
return Array.from(elements.sidePanel.querySelectorAll("[data-side-panel-section]"))
|
|
364
|
+
.map((section) => {
|
|
365
|
+
const id = section.dataset.sidePanelSection || "";
|
|
366
|
+
const button = section.querySelector("[data-side-panel-section-toggle]");
|
|
367
|
+
const contentId = button?.getAttribute("aria-controls") || "";
|
|
368
|
+
const content = contentId ? document.getElementById(contentId) : null;
|
|
369
|
+
return { id, section, button, content };
|
|
370
|
+
})
|
|
371
|
+
.filter((record) => record.id && record.button && record.content);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function readStoredSidePanelSectionCollapsedIds() {
|
|
375
|
+
try {
|
|
376
|
+
const parsed = JSON.parse(localStorage.getItem(SIDE_PANEL_SECTION_STORAGE_KEY) || "[]");
|
|
377
|
+
return new Set(Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : []);
|
|
378
|
+
} catch {
|
|
379
|
+
return new Set();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function persistSidePanelSectionState() {
|
|
384
|
+
try {
|
|
385
|
+
const collapsed = sidePanelSectionRecords()
|
|
386
|
+
.filter(({ section }) => section.classList.contains("collapsed"))
|
|
387
|
+
.map(({ id }) => id);
|
|
388
|
+
localStorage.setItem(SIDE_PANEL_SECTION_STORAGE_KEY, JSON.stringify(collapsed));
|
|
389
|
+
} catch {
|
|
390
|
+
// Ignore storage failures; section toggles should still work for this page load.
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function setSidePanelSectionCollapsed(record, collapsed, { persist = true } = {}) {
|
|
395
|
+
const label = record.button.querySelector(".side-panel-section-label")?.textContent?.trim() || "side panel";
|
|
396
|
+
record.section.classList.toggle("collapsed", collapsed);
|
|
397
|
+
record.content.hidden = collapsed;
|
|
398
|
+
record.button.setAttribute("aria-expanded", collapsed ? "false" : "true");
|
|
399
|
+
record.button.setAttribute("aria-label", `${collapsed ? "Expand" : "Collapse"} ${label} section`);
|
|
400
|
+
record.button.setAttribute("title", `${collapsed ? "Expand" : "Collapse"} ${label} section`);
|
|
401
|
+
if (persist) persistSidePanelSectionState();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function restoreSidePanelSectionState() {
|
|
405
|
+
const collapsedIds = readStoredSidePanelSectionCollapsedIds();
|
|
406
|
+
for (const record of sidePanelSectionRecords()) {
|
|
407
|
+
setSidePanelSectionCollapsed(record, collapsedIds.has(record.id), { persist: false });
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function bindSidePanelSectionToggles() {
|
|
412
|
+
for (const record of sidePanelSectionRecords()) {
|
|
413
|
+
record.button.addEventListener("click", () => {
|
|
414
|
+
setSidePanelSectionCollapsed(record, !record.section.classList.contains("collapsed"));
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
316
419
|
function readStoredAgentDoneNotificationsEnabled() {
|
|
317
420
|
try {
|
|
318
421
|
return localStorage.getItem(AGENT_DONE_NOTIFICATIONS_STORAGE_KEY) === "1";
|
|
@@ -379,6 +482,55 @@ function restoreAgentDoneNotificationsSetting() {
|
|
|
379
482
|
renderAgentDoneNotificationsToggle();
|
|
380
483
|
}
|
|
381
484
|
|
|
485
|
+
function readStoredThinkingOutputVisible() {
|
|
486
|
+
try {
|
|
487
|
+
const stored = localStorage.getItem(THINKING_VISIBILITY_STORAGE_KEY);
|
|
488
|
+
return stored === null ? true : stored === "1";
|
|
489
|
+
} catch {
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function persistThinkingOutputVisible(visible) {
|
|
495
|
+
try {
|
|
496
|
+
localStorage.setItem(THINKING_VISIBILITY_STORAGE_KEY, visible ? "1" : "0");
|
|
497
|
+
} catch {
|
|
498
|
+
// Ignore storage failures; the toggle should still work for this page load.
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function thinkingVisibilityStatusText() {
|
|
503
|
+
return thinkingOutputVisible ? "Visible" : "Hidden from transcript";
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function renderThinkingVisibilityToggle() {
|
|
507
|
+
if (!elements.thinkingVisibilityToggle) return;
|
|
508
|
+
elements.thinkingVisibilityToggle.checked = thinkingOutputVisible;
|
|
509
|
+
elements.thinkingVisibilityToggle.setAttribute("aria-describedby", "thinkingVisibilityStatus");
|
|
510
|
+
if (elements.thinkingVisibilityStatus) elements.thinkingVisibilityStatus.textContent = thinkingVisibilityStatusText();
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function removeStreamingThinkingBubble() {
|
|
514
|
+
streamThinkingBubble?.remove();
|
|
515
|
+
streamThinkingBubble = null;
|
|
516
|
+
streamThinking = null;
|
|
517
|
+
renderRunIndicator({ scroll: false });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function setThinkingOutputVisible(visible, { announce = false } = {}) {
|
|
521
|
+
thinkingOutputVisible = !!visible;
|
|
522
|
+
persistThinkingOutputVisible(thinkingOutputVisible);
|
|
523
|
+
renderThinkingVisibilityToggle();
|
|
524
|
+
if (!thinkingOutputVisible) removeStreamingThinkingBubble();
|
|
525
|
+
renderAllMessages({ preserveScroll: true });
|
|
526
|
+
if (announce) addEvent(thinkingOutputVisible ? "thinking output shown" : "thinking output hidden", thinkingOutputVisible ? "info" : "warn");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function restoreThinkingVisibilitySetting() {
|
|
530
|
+
thinkingOutputVisible = readStoredThinkingOutputVisible();
|
|
531
|
+
renderThinkingVisibilityToggle();
|
|
532
|
+
}
|
|
533
|
+
|
|
382
534
|
function setComposerActionsOpen(open) {
|
|
383
535
|
const shouldOpen = open && isMobileView();
|
|
384
536
|
document.body.classList.toggle("composer-actions-open", shouldOpen);
|
|
@@ -387,7 +539,11 @@ function setComposerActionsOpen(open) {
|
|
|
387
539
|
}
|
|
388
540
|
|
|
389
541
|
function isRunActive() {
|
|
390
|
-
return !!currentState?.isStreaming;
|
|
542
|
+
return !!currentState?.isStreaming || (runIndicatorLocallyActive && !currentState?.isCompacting);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function isAbortAvailable() {
|
|
546
|
+
return runIndicatorIsActive();
|
|
391
547
|
}
|
|
392
548
|
|
|
393
549
|
function resizePromptInput() {
|
|
@@ -401,12 +557,20 @@ function resizePromptInput() {
|
|
|
401
557
|
|
|
402
558
|
function updateComposerModeButtons() {
|
|
403
559
|
const runActive = isRunActive();
|
|
560
|
+
const abortAvailable = isAbortAvailable();
|
|
404
561
|
const target = runActive ? elements.composerRow : elements.composerActionsPanel;
|
|
405
|
-
const before = runActive ? elements.
|
|
562
|
+
const before = runActive ? elements.abortButton : null;
|
|
406
563
|
for (const button of [elements.steerButton, elements.followUpButton]) {
|
|
407
564
|
if (button.parentElement !== target) target.insertBefore(button, before);
|
|
565
|
+
button.hidden = !runActive;
|
|
566
|
+
button.disabled = !runActive;
|
|
408
567
|
}
|
|
409
|
-
|
|
568
|
+
elements.abortButton.hidden = !abortAvailable;
|
|
569
|
+
elements.abortButton.disabled = !abortAvailable || abortRequestInFlight;
|
|
570
|
+
elements.abortButton.textContent = abortRequestInFlight ? "Aborting…" : "Abort";
|
|
571
|
+
elements.abortButton.title = abortAvailable ? "Abort the active Pi run (Esc or hold)" : "Abort is available while Pi is running";
|
|
572
|
+
elements.abortButton.setAttribute("aria-label", elements.abortButton.title);
|
|
573
|
+
document.body.classList.toggle("pi-run-active", runActive || abortAvailable);
|
|
410
574
|
}
|
|
411
575
|
|
|
412
576
|
function updateFooterModelPickerPosition() {
|
|
@@ -562,6 +726,633 @@ async function api(path, { method = "GET", body, tabId = activeTabId, scoped = t
|
|
|
562
726
|
return data;
|
|
563
727
|
}
|
|
564
728
|
|
|
729
|
+
function formatBytes(bytes) {
|
|
730
|
+
const value = Number(bytes) || 0;
|
|
731
|
+
if (value < 1024) return `${value} B`;
|
|
732
|
+
const units = ["KB", "MB", "GB"];
|
|
733
|
+
let scaled = value / 1024;
|
|
734
|
+
for (const unit of units) {
|
|
735
|
+
if (scaled < 1024 || unit === units[units.length - 1]) return `${scaled.toFixed(scaled >= 10 ? 1 : 2)} ${unit}`;
|
|
736
|
+
scaled /= 1024;
|
|
737
|
+
}
|
|
738
|
+
return `${value} B`;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function inferMimeTypeFromName(name = "") {
|
|
742
|
+
const ext = String(name).split(".").pop()?.toLowerCase() || "";
|
|
743
|
+
const map = {
|
|
744
|
+
md: "text/markdown",
|
|
745
|
+
markdown: "text/markdown",
|
|
746
|
+
txt: "text/plain",
|
|
747
|
+
log: "text/plain",
|
|
748
|
+
csv: "text/csv",
|
|
749
|
+
json: "application/json",
|
|
750
|
+
xml: "application/xml",
|
|
751
|
+
yaml: "application/x-yaml",
|
|
752
|
+
yml: "application/x-yaml",
|
|
753
|
+
toml: "application/toml",
|
|
754
|
+
pdf: "application/pdf",
|
|
755
|
+
doc: "application/msword",
|
|
756
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
757
|
+
mp3: "audio/mpeg",
|
|
758
|
+
wav: "audio/wav",
|
|
759
|
+
m4a: "audio/mp4",
|
|
760
|
+
mp4: "video/mp4",
|
|
761
|
+
mov: "video/quicktime",
|
|
762
|
+
webm: "video/webm",
|
|
763
|
+
};
|
|
764
|
+
return map[ext] || "application/octet-stream";
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function attachmentKind(mimeType = "", name = "") {
|
|
768
|
+
const type = String(mimeType || inferMimeTypeFromName(name));
|
|
769
|
+
if (type.startsWith("image/")) return "image";
|
|
770
|
+
if (type.startsWith("video/")) return "video";
|
|
771
|
+
if (type.startsWith("audio/")) return "audio";
|
|
772
|
+
if (type.startsWith("text/") || /(?:json|xml|pdf|word|excel|powerpoint|document|spreadsheet|presentation|markdown|csv)/i.test(type)) return "doc";
|
|
773
|
+
return "file";
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function attachmentIcon(kind) {
|
|
777
|
+
return kind === "image" ? "🖼️" : kind === "video" ? "🎞️" : kind === "audio" ? "🎵" : kind === "doc" ? "📄" : "📎";
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function attachmentsForTab(tabId = activeTabId) {
|
|
781
|
+
return tabId ? tabAttachments.get(tabId) || [] : [];
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function ensureAttachmentsForTab(tabId = activeTabId) {
|
|
785
|
+
if (!tabId) return [];
|
|
786
|
+
if (!tabAttachments.has(tabId)) tabAttachments.set(tabId, []);
|
|
787
|
+
return tabAttachments.get(tabId);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function hasComposerPayload() {
|
|
791
|
+
return !!elements.promptInput.value.trim() || attachmentsForTab().length > 0;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function renderAttachmentTray() {
|
|
795
|
+
const tray = elements.attachmentTray;
|
|
796
|
+
if (!tray) return;
|
|
797
|
+
const attachments = attachmentsForTab();
|
|
798
|
+
tray.innerHTML = "";
|
|
799
|
+
tray.hidden = attachments.length === 0;
|
|
800
|
+
if (attachments.length === 0) return;
|
|
801
|
+
|
|
802
|
+
for (const attachment of attachments) {
|
|
803
|
+
const pill = make("span", "attachment-pill");
|
|
804
|
+
pill.title = `${attachment.name}\n${attachment.mimeType}\n${formatBytes(attachment.size)}`;
|
|
805
|
+
const icon = make("span", "attachment-pill-icon", attachmentIcon(attachment.kind));
|
|
806
|
+
const name = make("span", "attachment-pill-name", attachment.name);
|
|
807
|
+
const meta = make("span", "attachment-pill-meta", `${attachment.kind} · ${formatBytes(attachment.size)}`);
|
|
808
|
+
const remove = make("button", "attachment-remove-button", "×");
|
|
809
|
+
remove.type = "button";
|
|
810
|
+
remove.setAttribute("aria-label", `Remove ${attachment.name}`);
|
|
811
|
+
remove.addEventListener("click", () => removeAttachment(attachment.id));
|
|
812
|
+
pill.append(icon, name, meta, remove);
|
|
813
|
+
tray.append(pill);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function removeAttachment(id, tabId = activeTabId) {
|
|
818
|
+
const attachments = attachmentsForTab(tabId);
|
|
819
|
+
const index = attachments.findIndex((attachment) => attachment.id === id);
|
|
820
|
+
if (index === -1) return;
|
|
821
|
+
const [removed] = attachments.splice(index, 1);
|
|
822
|
+
if (removed?.previewUrl) URL.revokeObjectURL(removed.previewUrl);
|
|
823
|
+
if (attachments.length === 0) tabAttachments.delete(tabId);
|
|
824
|
+
if (tabId === activeTabId) renderAttachmentTray();
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function clearAttachments(tabId = activeTabId) {
|
|
828
|
+
const attachments = attachmentsForTab(tabId);
|
|
829
|
+
for (const attachment of attachments) {
|
|
830
|
+
if (attachment.previewUrl) URL.revokeObjectURL(attachment.previewUrl);
|
|
831
|
+
}
|
|
832
|
+
if (tabId) tabAttachments.delete(tabId);
|
|
833
|
+
if (tabId === activeTabId) renderAttachmentTray();
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function addAttachmentFiles(fileList, source = "picker") {
|
|
837
|
+
const files = Array.from(fileList || []).filter(Boolean);
|
|
838
|
+
if (!files.length) return;
|
|
839
|
+
const attachments = ensureAttachmentsForTab();
|
|
840
|
+
if (!attachments.length && !activeTabId) return;
|
|
841
|
+
let totalBytes = attachments.reduce((sum, attachment) => sum + attachment.size, 0);
|
|
842
|
+
let added = 0;
|
|
843
|
+
const skipped = [];
|
|
844
|
+
|
|
845
|
+
for (const file of files) {
|
|
846
|
+
const name = file.name || `${source}-attachment`;
|
|
847
|
+
if (attachments.length >= ATTACHMENT_MAX_FILES) {
|
|
848
|
+
skipped.push(`${name}: attachment limit is ${ATTACHMENT_MAX_FILES}`);
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (file.size > ATTACHMENT_MAX_FILE_BYTES) {
|
|
852
|
+
skipped.push(`${name}: larger than ${formatBytes(ATTACHMENT_MAX_FILE_BYTES)}`);
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
if (totalBytes + file.size > ATTACHMENT_MAX_TOTAL_BYTES) {
|
|
856
|
+
skipped.push(`${name}: total attachment limit is ${formatBytes(ATTACHMENT_MAX_TOTAL_BYTES)}`);
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
const mimeType = file.type || inferMimeTypeFromName(name);
|
|
860
|
+
const kind = attachmentKind(mimeType, name);
|
|
861
|
+
attachments.push({
|
|
862
|
+
id: `att-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
863
|
+
file,
|
|
864
|
+
name,
|
|
865
|
+
mimeType,
|
|
866
|
+
size: file.size || 0,
|
|
867
|
+
source,
|
|
868
|
+
kind,
|
|
869
|
+
previewUrl: kind === "image" ? URL.createObjectURL(file) : undefined,
|
|
870
|
+
});
|
|
871
|
+
totalBytes += file.size || 0;
|
|
872
|
+
added++;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
renderAttachmentTray();
|
|
876
|
+
if (added) addEvent(`attached ${added} ${added === 1 ? "file" : "files"} from ${source}`, "info");
|
|
877
|
+
if (skipped.length) addEvent(`skipped attachments: ${skipped.join("; ")}`, "warn");
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function clipboardFiles(dataTransfer) {
|
|
881
|
+
const files = [];
|
|
882
|
+
const seen = new Set();
|
|
883
|
+
for (const file of Array.from(dataTransfer?.files || [])) {
|
|
884
|
+
const key = `${file.name}:${file.size}:${file.type}`;
|
|
885
|
+
if (!seen.has(key)) {
|
|
886
|
+
seen.add(key);
|
|
887
|
+
files.push(file);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
for (const item of Array.from(dataTransfer?.items || [])) {
|
|
891
|
+
if (item.kind !== "file") continue;
|
|
892
|
+
const file = item.getAsFile?.();
|
|
893
|
+
if (!file) continue;
|
|
894
|
+
const key = `${file.name}:${file.size}:${file.type}`;
|
|
895
|
+
if (!seen.has(key)) {
|
|
896
|
+
seen.add(key);
|
|
897
|
+
files.push(file);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return files;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function handleAttachmentPaste(event) {
|
|
904
|
+
const files = clipboardFiles(event.clipboardData);
|
|
905
|
+
if (!files.length) return;
|
|
906
|
+
event.preventDefault();
|
|
907
|
+
addAttachmentFiles(files, "clipboard");
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function isFileDrag(event) {
|
|
911
|
+
return Array.from(event.dataTransfer?.types || []).includes("Files");
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function handleComposerDragOver(event) {
|
|
915
|
+
if (!isFileDrag(event)) return;
|
|
916
|
+
event.preventDefault();
|
|
917
|
+
elements.composer.classList.add("drag-over");
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function handleComposerDragLeave(event) {
|
|
921
|
+
if (!elements.composer.contains(event.relatedTarget)) elements.composer.classList.remove("drag-over");
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function handleComposerDrop(event) {
|
|
925
|
+
if (!isFileDrag(event)) return;
|
|
926
|
+
event.preventDefault();
|
|
927
|
+
elements.composer.classList.remove("drag-over");
|
|
928
|
+
addAttachmentFiles(event.dataTransfer?.files, "drop");
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function readFileAsBase64(file) {
|
|
932
|
+
return new Promise((resolve, reject) => {
|
|
933
|
+
const reader = new FileReader();
|
|
934
|
+
reader.onerror = () => reject(reader.error || new Error("Failed to read attachment"));
|
|
935
|
+
reader.onload = () => {
|
|
936
|
+
const result = String(reader.result || "");
|
|
937
|
+
const comma = result.indexOf(",");
|
|
938
|
+
resolve(comma === -1 ? result : result.slice(comma + 1));
|
|
939
|
+
};
|
|
940
|
+
reader.readAsDataURL(file);
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function readFileAsDataUrl(file) {
|
|
945
|
+
return new Promise((resolve, reject) => {
|
|
946
|
+
const reader = new FileReader();
|
|
947
|
+
reader.onerror = () => reject(reader.error || new Error("Failed to read background image"));
|
|
948
|
+
reader.onload = () => resolve(String(reader.result || ""));
|
|
949
|
+
reader.readAsDataURL(file);
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function sanitizeBackgroundName(name) {
|
|
954
|
+
const safe = String(name || "custom background").replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 120);
|
|
955
|
+
return safe || "custom background";
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function backgroundMimeType(file) {
|
|
959
|
+
const declared = String(file?.type || "").split(";", 1)[0].trim().toLowerCase();
|
|
960
|
+
if (BACKGROUND_IMAGE_MIME_TYPES.has(declared)) return declared;
|
|
961
|
+
const ext = String(file?.name || "").split(".").pop()?.toLowerCase() || "";
|
|
962
|
+
const byExt = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp", gif: "image/gif" };
|
|
963
|
+
return byExt[ext] || declared || "application/octet-stream";
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function normalizeCustomBackgroundRecord(value) {
|
|
967
|
+
if (!value || typeof value !== "object") return null;
|
|
968
|
+
const dataUrl = String(value.dataUrl || "");
|
|
969
|
+
const match = dataUrl.match(/^data:(image\/(?:png|jpeg|webp|gif));base64,[A-Za-z0-9+/]+={0,2}$/i);
|
|
970
|
+
if (!match) return null;
|
|
971
|
+
return {
|
|
972
|
+
name: sanitizeBackgroundName(value.name),
|
|
973
|
+
mimeType: match[1].toLowerCase(),
|
|
974
|
+
size: Math.max(0, Number(value.size) || 0),
|
|
975
|
+
dataUrl,
|
|
976
|
+
updatedAt: Number(value.updatedAt) || Date.now(),
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function dataUrlToBlob(dataUrl) {
|
|
981
|
+
const match = String(dataUrl || "").match(/^data:(image\/(?:png|jpeg|webp|gif));base64,([A-Za-z0-9+/]+={0,2})$/i);
|
|
982
|
+
if (!match) throw new Error("Invalid background data URL");
|
|
983
|
+
const binary = atob(match[2]);
|
|
984
|
+
const bytes = new Uint8Array(binary.length);
|
|
985
|
+
for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
|
|
986
|
+
return new Blob([bytes], { type: match[1].toLowerCase() });
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function revokeCustomBackgroundObjectUrl() {
|
|
990
|
+
if (!customBackgroundObjectUrl) return;
|
|
991
|
+
URL.revokeObjectURL(customBackgroundObjectUrl);
|
|
992
|
+
customBackgroundObjectUrl = null;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function setCustomBackgroundRecord(background, { objectUrl = null } = {}) {
|
|
996
|
+
const record = normalizeCustomBackgroundRecord(background);
|
|
997
|
+
revokeCustomBackgroundObjectUrl();
|
|
998
|
+
customBackground = record;
|
|
999
|
+
if (!record) return null;
|
|
1000
|
+
if (objectUrl) customBackgroundObjectUrl = objectUrl;
|
|
1001
|
+
else {
|
|
1002
|
+
try {
|
|
1003
|
+
customBackgroundObjectUrl = URL.createObjectURL(dataUrlToBlob(record.dataUrl));
|
|
1004
|
+
} catch {
|
|
1005
|
+
customBackgroundObjectUrl = null;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return record;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function idbRequest(request) {
|
|
1012
|
+
return new Promise((resolve, reject) => {
|
|
1013
|
+
request.onsuccess = () => resolve(request.result);
|
|
1014
|
+
request.onerror = () => reject(request.error || new Error("IndexedDB request failed"));
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function idbTransactionDone(transaction) {
|
|
1019
|
+
return new Promise((resolve, reject) => {
|
|
1020
|
+
transaction.oncomplete = () => resolve();
|
|
1021
|
+
transaction.onerror = () => reject(transaction.error || new Error("IndexedDB transaction failed"));
|
|
1022
|
+
transaction.onabort = () => reject(transaction.error || new Error("IndexedDB transaction aborted"));
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function openCustomBackgroundDb() {
|
|
1027
|
+
return new Promise((resolve, reject) => {
|
|
1028
|
+
const indexedDb = window.indexedDB;
|
|
1029
|
+
if (!indexedDb) {
|
|
1030
|
+
reject(new Error("IndexedDB unavailable"));
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const request = indexedDb.open(CUSTOM_BACKGROUND_IDB_NAME, 1);
|
|
1034
|
+
request.onupgradeneeded = () => {
|
|
1035
|
+
const db = request.result;
|
|
1036
|
+
if (!db.objectStoreNames.contains(CUSTOM_BACKGROUND_IDB_STORE)) db.createObjectStore(CUSTOM_BACKGROUND_IDB_STORE);
|
|
1037
|
+
};
|
|
1038
|
+
request.onsuccess = () => resolve(request.result);
|
|
1039
|
+
request.onerror = () => reject(request.error || new Error("Failed to open background storage"));
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function customBackgroundThemeKey(themeName = currentThemeName) {
|
|
1044
|
+
return String(themeName || DEFAULT_THEME_NAME).trim() || DEFAULT_THEME_NAME;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
async function readCustomBackgroundFromIndexedDb(themeName = currentThemeName) {
|
|
1048
|
+
const db = await openCustomBackgroundDb();
|
|
1049
|
+
try {
|
|
1050
|
+
return await idbRequest(db.transaction(CUSTOM_BACKGROUND_IDB_STORE, "readonly").objectStore(CUSTOM_BACKGROUND_IDB_STORE).get(customBackgroundThemeKey(themeName)));
|
|
1051
|
+
} finally {
|
|
1052
|
+
db.close();
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
async function readLegacyCustomBackgroundFromIndexedDb() {
|
|
1057
|
+
const db = await openCustomBackgroundDb();
|
|
1058
|
+
try {
|
|
1059
|
+
return await idbRequest(db.transaction(CUSTOM_BACKGROUND_IDB_STORE, "readonly").objectStore(CUSTOM_BACKGROUND_IDB_STORE).get(CUSTOM_BACKGROUND_LEGACY_ID));
|
|
1060
|
+
} finally {
|
|
1061
|
+
db.close();
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
async function writeCustomBackgroundToIndexedDb(background, themeName = currentThemeName) {
|
|
1066
|
+
const db = await openCustomBackgroundDb();
|
|
1067
|
+
try {
|
|
1068
|
+
const transaction = db.transaction(CUSTOM_BACKGROUND_IDB_STORE, "readwrite");
|
|
1069
|
+
transaction.objectStore(CUSTOM_BACKGROUND_IDB_STORE).put(background, customBackgroundThemeKey(themeName));
|
|
1070
|
+
await idbTransactionDone(transaction);
|
|
1071
|
+
} finally {
|
|
1072
|
+
db.close();
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
async function deleteCustomBackgroundFromIndexedDb(themeName = currentThemeName) {
|
|
1077
|
+
const db = await openCustomBackgroundDb();
|
|
1078
|
+
try {
|
|
1079
|
+
const transaction = db.transaction(CUSTOM_BACKGROUND_IDB_STORE, "readwrite");
|
|
1080
|
+
transaction.objectStore(CUSTOM_BACKGROUND_IDB_STORE).delete(customBackgroundThemeKey(themeName));
|
|
1081
|
+
await idbTransactionDone(transaction);
|
|
1082
|
+
} finally {
|
|
1083
|
+
db.close();
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async function deleteLegacyCustomBackgroundFromIndexedDb() {
|
|
1088
|
+
const db = await openCustomBackgroundDb();
|
|
1089
|
+
try {
|
|
1090
|
+
const transaction = db.transaction(CUSTOM_BACKGROUND_IDB_STORE, "readwrite");
|
|
1091
|
+
transaction.objectStore(CUSTOM_BACKGROUND_IDB_STORE).delete(CUSTOM_BACKGROUND_LEGACY_ID);
|
|
1092
|
+
await idbTransactionDone(transaction);
|
|
1093
|
+
} finally {
|
|
1094
|
+
db.close();
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function readCustomBackgroundFromLocalStorage(themeName = currentThemeName, { includeLegacy = false } = {}) {
|
|
1099
|
+
try {
|
|
1100
|
+
const parsed = JSON.parse(localStorage.getItem(CUSTOM_BACKGROUNDS_STORAGE_KEY) || "{}");
|
|
1101
|
+
const record = parsed && typeof parsed === "object" ? normalizeCustomBackgroundRecord(parsed[customBackgroundThemeKey(themeName)]) : null;
|
|
1102
|
+
if (record) return record;
|
|
1103
|
+
} catch {
|
|
1104
|
+
// Fall through to legacy storage below.
|
|
1105
|
+
}
|
|
1106
|
+
if (!includeLegacy) return null;
|
|
1107
|
+
try {
|
|
1108
|
+
return normalizeCustomBackgroundRecord(JSON.parse(localStorage.getItem(CUSTOM_BACKGROUND_STORAGE_KEY) || "null"));
|
|
1109
|
+
} catch {
|
|
1110
|
+
return null;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function writeCustomBackgroundToLocalStorage(background, themeName = currentThemeName) {
|
|
1115
|
+
const record = normalizeCustomBackgroundRecord(background);
|
|
1116
|
+
if (!record) throw new Error("Invalid background image data");
|
|
1117
|
+
const key = customBackgroundThemeKey(themeName);
|
|
1118
|
+
const parsed = JSON.parse(localStorage.getItem(CUSTOM_BACKGROUNDS_STORAGE_KEY) || "{}");
|
|
1119
|
+
const backgrounds = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1120
|
+
backgrounds[key] = record;
|
|
1121
|
+
localStorage.setItem(CUSTOM_BACKGROUNDS_STORAGE_KEY, JSON.stringify(backgrounds));
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function removeCustomBackgroundFromLocalStorage(themeName = currentThemeName, { includeLegacy = false } = {}) {
|
|
1125
|
+
const key = customBackgroundThemeKey(themeName);
|
|
1126
|
+
try {
|
|
1127
|
+
const parsed = JSON.parse(localStorage.getItem(CUSTOM_BACKGROUNDS_STORAGE_KEY) || "{}");
|
|
1128
|
+
const backgrounds = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1129
|
+
delete backgrounds[key];
|
|
1130
|
+
localStorage.setItem(CUSTOM_BACKGROUNDS_STORAGE_KEY, JSON.stringify(backgrounds));
|
|
1131
|
+
} catch {
|
|
1132
|
+
// Ignore fallback cleanup failures.
|
|
1133
|
+
}
|
|
1134
|
+
if (includeLegacy) {
|
|
1135
|
+
try {
|
|
1136
|
+
localStorage.removeItem(CUSTOM_BACKGROUND_STORAGE_KEY);
|
|
1137
|
+
} catch {
|
|
1138
|
+
// Ignore legacy cleanup failures.
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
async function readStoredCustomBackground(themeName = currentThemeName, { includeLegacy = false } = {}) {
|
|
1144
|
+
try {
|
|
1145
|
+
const stored = normalizeCustomBackgroundRecord(await readCustomBackgroundFromIndexedDb(themeName));
|
|
1146
|
+
if (stored) return stored;
|
|
1147
|
+
if (includeLegacy) {
|
|
1148
|
+
const legacy = normalizeCustomBackgroundRecord(await readLegacyCustomBackgroundFromIndexedDb());
|
|
1149
|
+
if (legacy) return legacy;
|
|
1150
|
+
}
|
|
1151
|
+
} catch {
|
|
1152
|
+
// Fall back to localStorage for older browsers or private browsing modes.
|
|
1153
|
+
}
|
|
1154
|
+
return readCustomBackgroundFromLocalStorage(themeName, { includeLegacy });
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
async function persistCustomBackground(background, themeName = currentThemeName) {
|
|
1158
|
+
const record = normalizeCustomBackgroundRecord(background);
|
|
1159
|
+
if (!record) throw new Error("Invalid background image data");
|
|
1160
|
+
try {
|
|
1161
|
+
await writeCustomBackgroundToIndexedDb(record, themeName);
|
|
1162
|
+
removeCustomBackgroundFromLocalStorage(themeName);
|
|
1163
|
+
return;
|
|
1164
|
+
} catch {
|
|
1165
|
+
// Fall back to localStorage when IndexedDB is unavailable.
|
|
1166
|
+
}
|
|
1167
|
+
writeCustomBackgroundToLocalStorage(record, themeName);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
async function clearStoredCustomBackground(themeName = currentThemeName, { includeLegacy = false } = {}) {
|
|
1171
|
+
await Promise.allSettled([
|
|
1172
|
+
deleteCustomBackgroundFromIndexedDb(themeName),
|
|
1173
|
+
includeLegacy ? deleteLegacyCustomBackgroundFromIndexedDb() : Promise.resolve(),
|
|
1174
|
+
Promise.resolve().then(() => removeCustomBackgroundFromLocalStorage(themeName, { includeLegacy })),
|
|
1175
|
+
]);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function customBackgroundCssImage(background = customBackground) {
|
|
1179
|
+
if (!background?.dataUrl) return null;
|
|
1180
|
+
return `url("${customBackgroundObjectUrl || background.dataUrl}")`;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function renderBackgroundControl() {
|
|
1184
|
+
if (!elements.backgroundStatus) return;
|
|
1185
|
+
const active = !!customBackground?.dataUrl;
|
|
1186
|
+
const themeLabel = displayThemeName(currentThemeName) || currentThemeName || "theme";
|
|
1187
|
+
elements.backgroundStatus.textContent = customBackgroundLoading
|
|
1188
|
+
? `Loading ${themeLabel} background…`
|
|
1189
|
+
: active
|
|
1190
|
+
? `${themeLabel}: ${customBackground.name || "background"}`
|
|
1191
|
+
: `${themeLabel}: theme default`;
|
|
1192
|
+
if (elements.backgroundChooseButton) {
|
|
1193
|
+
elements.backgroundChooseButton.disabled = customBackgroundLoading;
|
|
1194
|
+
elements.backgroundChooseButton.textContent = active ? "Change background" : "Add background";
|
|
1195
|
+
}
|
|
1196
|
+
if (elements.backgroundInput) elements.backgroundInput.disabled = customBackgroundLoading;
|
|
1197
|
+
if (elements.backgroundClearButton) {
|
|
1198
|
+
elements.backgroundClearButton.hidden = !active;
|
|
1199
|
+
elements.backgroundClearButton.disabled = customBackgroundLoading;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function applyCustomBackgroundOverride({ render = true } = {}) {
|
|
1204
|
+
const activeImage = customBackgroundCssImage();
|
|
1205
|
+
document.body.classList.toggle("custom-background-active", !!activeImage);
|
|
1206
|
+
if (activeImage) document.documentElement.style.setProperty("--theme-background-image", activeImage);
|
|
1207
|
+
if (render) renderBackgroundControl();
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function reapplyCurrentThemeBackground() {
|
|
1211
|
+
const theme = availableThemes.find((item) => item.name === currentThemeName);
|
|
1212
|
+
if (theme && isOptionalFeatureEnabled("themeBundle")) applyTheme(theme, { persist: false });
|
|
1213
|
+
else {
|
|
1214
|
+
document.documentElement.style.setProperty("--theme-background-image", "none");
|
|
1215
|
+
applyCustomBackgroundOverride();
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
async function loadCustomBackgroundForTheme(themeName = currentThemeName, { includeLegacy = false } = {}) {
|
|
1220
|
+
const themeKey = customBackgroundThemeKey(themeName);
|
|
1221
|
+
customBackgroundLoading = true;
|
|
1222
|
+
renderBackgroundControl();
|
|
1223
|
+
try {
|
|
1224
|
+
const background = await readStoredCustomBackground(themeKey, { includeLegacy });
|
|
1225
|
+
if (customBackgroundThemeKey(currentThemeName) !== themeKey) return;
|
|
1226
|
+
setCustomBackgroundRecord(background);
|
|
1227
|
+
if (background && includeLegacy) {
|
|
1228
|
+
persistCustomBackground(background, themeKey).catch(() => {});
|
|
1229
|
+
}
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
if (customBackgroundThemeKey(currentThemeName) === themeKey) {
|
|
1232
|
+
addEvent(`failed to load ${displayThemeName(themeKey) || themeKey} background: ${error.message || String(error)}`, "warn");
|
|
1233
|
+
setCustomBackgroundRecord(null);
|
|
1234
|
+
}
|
|
1235
|
+
} finally {
|
|
1236
|
+
if (customBackgroundThemeKey(currentThemeName) === themeKey) {
|
|
1237
|
+
customBackgroundLoading = false;
|
|
1238
|
+
applyCustomBackgroundOverride();
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
async function setCustomBackgroundFromFile(file) {
|
|
1244
|
+
if (!file) return;
|
|
1245
|
+
const mimeType = backgroundMimeType(file);
|
|
1246
|
+
if (!BACKGROUND_IMAGE_MIME_TYPES.has(mimeType)) {
|
|
1247
|
+
addEvent("background must be a PNG, JPEG, WebP, or GIF image", "error");
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if ((file.size || 0) > CUSTOM_BACKGROUND_MAX_FILE_BYTES) {
|
|
1251
|
+
addEvent(`background image is larger than ${formatBytes(CUSTOM_BACKGROUND_MAX_FILE_BYTES)}`, "error");
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const themeName = customBackgroundThemeKey(currentThemeName);
|
|
1256
|
+
customBackgroundLoading = true;
|
|
1257
|
+
renderBackgroundControl();
|
|
1258
|
+
try {
|
|
1259
|
+
const rawDataUrl = await readFileAsDataUrl(file);
|
|
1260
|
+
const dataUrl = rawDataUrl.replace(/^data:;base64,/i, `data:${mimeType};base64,`);
|
|
1261
|
+
const background = normalizeCustomBackgroundRecord({
|
|
1262
|
+
name: file.name,
|
|
1263
|
+
mimeType,
|
|
1264
|
+
size: file.size || 0,
|
|
1265
|
+
dataUrl,
|
|
1266
|
+
updatedAt: Date.now(),
|
|
1267
|
+
});
|
|
1268
|
+
if (!background) throw new Error("Unsupported or invalid background image data");
|
|
1269
|
+
let objectUrl = null;
|
|
1270
|
+
try {
|
|
1271
|
+
objectUrl = URL.createObjectURL(file);
|
|
1272
|
+
} catch {
|
|
1273
|
+
objectUrl = null;
|
|
1274
|
+
}
|
|
1275
|
+
const targetStillActive = customBackgroundThemeKey(currentThemeName) === themeName;
|
|
1276
|
+
if (targetStillActive) {
|
|
1277
|
+
setCustomBackgroundRecord(background, { objectUrl });
|
|
1278
|
+
applyCustomBackgroundOverride({ render: false });
|
|
1279
|
+
} else if (objectUrl) {
|
|
1280
|
+
URL.revokeObjectURL(objectUrl);
|
|
1281
|
+
}
|
|
1282
|
+
try {
|
|
1283
|
+
await persistCustomBackground(background, themeName);
|
|
1284
|
+
addEvent(`custom background saved for ${displayThemeName(themeName) || themeName}: ${background.name}`);
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
addEvent(`background changed for this page, but persistent save failed: ${error.message || String(error)}`, "warn");
|
|
1287
|
+
}
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
addEvent(`failed to set background: ${error.message || String(error)}`, "error");
|
|
1290
|
+
} finally {
|
|
1291
|
+
if (customBackgroundThemeKey(currentThemeName) === themeName) {
|
|
1292
|
+
customBackgroundLoading = false;
|
|
1293
|
+
renderBackgroundControl();
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
async function clearCustomBackground() {
|
|
1299
|
+
const themeName = customBackgroundThemeKey(currentThemeName);
|
|
1300
|
+
const hadBackground = !!customBackground?.dataUrl;
|
|
1301
|
+
setCustomBackgroundRecord(null);
|
|
1302
|
+
customBackgroundLoading = true;
|
|
1303
|
+
renderBackgroundControl();
|
|
1304
|
+
await clearStoredCustomBackground(themeName, { includeLegacy: true });
|
|
1305
|
+
customBackgroundLoading = false;
|
|
1306
|
+
reapplyCurrentThemeBackground();
|
|
1307
|
+
renderBackgroundControl();
|
|
1308
|
+
if (hadBackground) addEvent(`custom background removed for ${displayThemeName(themeName) || themeName}`);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
async function initializeCustomBackground() {
|
|
1312
|
+
await loadCustomBackgroundForTheme(currentThemeName, { includeLegacy: true });
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
async function prepareAttachmentsForPrompt(attachments, tabId) {
|
|
1316
|
+
if (!attachments.length) return { images: [], uploadedFiles: [], inlineImageIds: new Set() };
|
|
1317
|
+
const files = [];
|
|
1318
|
+
const images = [];
|
|
1319
|
+
const inlineImageIds = new Set();
|
|
1320
|
+
let inlineImageBytes = 0;
|
|
1321
|
+
|
|
1322
|
+
for (const attachment of attachments) {
|
|
1323
|
+
const data = await readFileAsBase64(attachment.file);
|
|
1324
|
+
files.push({
|
|
1325
|
+
id: attachment.id,
|
|
1326
|
+
name: attachment.name,
|
|
1327
|
+
mimeType: attachment.mimeType,
|
|
1328
|
+
size: attachment.size,
|
|
1329
|
+
data,
|
|
1330
|
+
});
|
|
1331
|
+
if (
|
|
1332
|
+
INLINE_IMAGE_MIME_TYPES.has(attachment.mimeType) &&
|
|
1333
|
+
attachment.size <= ATTACHMENT_INLINE_IMAGE_MAX_BYTES &&
|
|
1334
|
+
inlineImageBytes + attachment.size <= ATTACHMENT_INLINE_IMAGE_TOTAL_MAX_BYTES
|
|
1335
|
+
) {
|
|
1336
|
+
images.push({ type: "image", data, mimeType: attachment.mimeType });
|
|
1337
|
+
inlineImageIds.add(attachment.id);
|
|
1338
|
+
inlineImageBytes += attachment.size;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const response = await api("/api/attachments", { method: "POST", body: { files }, tabId });
|
|
1343
|
+
return { images, uploadedFiles: response.data?.files || [], inlineImageIds };
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function composeMessageWithAttachments(message, uploadedFiles, inlineImageIds) {
|
|
1347
|
+
if (!uploadedFiles.length) return message;
|
|
1348
|
+
const baseMessage = message || "Please inspect the attached file(s).";
|
|
1349
|
+
const lines = uploadedFiles.map((file, index) => {
|
|
1350
|
+
const inlineNote = inlineImageIds.has(file.id) ? "sent inline and saved at" : "saved at";
|
|
1351
|
+
return `- ${index + 1}. ${file.name || "attachment"} (${file.mimeType || "application/octet-stream"}, ${formatBytes(file.size)}): ${inlineNote} ${file.path}`;
|
|
1352
|
+
});
|
|
1353
|
+
return `${baseMessage}\n\nAttached files:\n${lines.join("\n")}`;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
565
1356
|
function storedThemeName() {
|
|
566
1357
|
try {
|
|
567
1358
|
return localStorage.getItem(THEME_STORAGE_KEY) || DEFAULT_THEME_NAME;
|
|
@@ -609,16 +1400,32 @@ function isOptionalFeatureEnabled(featureId) {
|
|
|
609
1400
|
return isOptionalFeatureDetected(featureId) && !isOptionalFeatureDisabled(featureId);
|
|
610
1401
|
}
|
|
611
1402
|
|
|
1403
|
+
function renderOptionalFeatureDependentDisplays() {
|
|
1404
|
+
renderOptionalFeatureControls();
|
|
1405
|
+
renderThemeSelect();
|
|
1406
|
+
renderWidgets();
|
|
1407
|
+
renderStatus();
|
|
1408
|
+
renderCommands();
|
|
1409
|
+
cancelStreamingAssistantTextRender();
|
|
1410
|
+
cancelStreamBubbleHide();
|
|
1411
|
+
streamBubble?.remove();
|
|
1412
|
+
streamBubble = null;
|
|
1413
|
+
streamText = null;
|
|
1414
|
+
streamBubbleVisibleSince = 0;
|
|
1415
|
+
renderAllMessages({ preserveScroll: true });
|
|
1416
|
+
if (streamRawText) renderStreamingAssistantText();
|
|
1417
|
+
}
|
|
1418
|
+
|
|
612
1419
|
function setOptionalFeatureDisabled(featureId, disabled) {
|
|
613
1420
|
if (!OPTIONAL_FEATURE_BY_ID.has(featureId)) return;
|
|
614
1421
|
if (disabled) disabledOptionalFeatures.add(featureId);
|
|
615
1422
|
else disabledOptionalFeatures.delete(featureId);
|
|
616
1423
|
storeDisabledOptionalFeatures();
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
1424
|
+
renderOptionalFeatureDependentDisplays();
|
|
1425
|
+
const tabContext = activeTabContext();
|
|
1426
|
+
refreshCommands(tabContext).catch((error) => {
|
|
1427
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
1428
|
+
});
|
|
622
1429
|
}
|
|
623
1430
|
|
|
624
1431
|
function displayThemeName(name) {
|
|
@@ -646,6 +1453,16 @@ function themeExportColor(theme, key, fallback) {
|
|
|
646
1453
|
return resolveThemeValue(theme, theme?.export?.[key], fallback);
|
|
647
1454
|
}
|
|
648
1455
|
|
|
1456
|
+
const LOCAL_BACKGROUND_IMAGE_PATTERN = /^(?:none|url\(["']?\/(?!\/)[A-Za-z0-9._~!$&'()*+,=:@%\/-]+["']?\))$/i;
|
|
1457
|
+
const BACKGROUND_OVERLAY_PATTERN = /^(?:none|linear-gradient\([^;\r\n{}<>]+\))$/i;
|
|
1458
|
+
const SAFE_BACKGROUND_TOKEN_PATTERN = /^[A-Za-z0-9%._ -]+$/;
|
|
1459
|
+
|
|
1460
|
+
function themeExportCssValue(theme, key, fallback, pattern = /^[^;\r\n{}<>]+$/) {
|
|
1461
|
+
const raw = String(theme?.export?.[key] ?? "").trim();
|
|
1462
|
+
if (!raw) return fallback;
|
|
1463
|
+
return pattern.test(raw) ? raw : fallback;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
649
1466
|
function hexToRgb(color) {
|
|
650
1467
|
const raw = String(color || "").trim();
|
|
651
1468
|
const match = raw.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
|
@@ -793,9 +1610,15 @@ function applyTheme(theme, { persist = false, announce = false } = {}) {
|
|
|
793
1610
|
"--background-glow-pink": colorWithAlpha(pink, isLight ? 0.16 : 0.34, pink),
|
|
794
1611
|
"--background-glow-blue": colorWithAlpha(accent, isLight ? 0.15 : 0.32, accent),
|
|
795
1612
|
"--background-glow-teal": colorWithAlpha(accent2, isLight ? 0.12 : 0.20, accent2),
|
|
1613
|
+
"--theme-background-image": themeExportCssValue(theme, "backgroundImage", "none", LOCAL_BACKGROUND_IMAGE_PATTERN),
|
|
1614
|
+
"--theme-background-overlay": themeExportCssValue(theme, "backgroundOverlay", "linear-gradient(180deg, rgba(17, 17, 27, 0), rgba(17, 17, 27, 0))", BACKGROUND_OVERLAY_PATTERN),
|
|
1615
|
+
"--theme-background-size": themeExportCssValue(theme, "backgroundSize", "cover", SAFE_BACKGROUND_TOKEN_PATTERN),
|
|
1616
|
+
"--theme-background-position": themeExportCssValue(theme, "backgroundPosition", "center", SAFE_BACKGROUND_TOKEN_PATTERN),
|
|
1617
|
+
"--theme-background-repeat": themeExportCssValue(theme, "backgroundRepeat", "no-repeat", SAFE_BACKGROUND_TOKEN_PATTERN),
|
|
796
1618
|
};
|
|
797
1619
|
|
|
798
1620
|
for (const [name, value] of Object.entries(vars)) root.style.setProperty(name, value);
|
|
1621
|
+
applyCustomBackgroundOverride({ render: false });
|
|
799
1622
|
root.style.colorScheme = isLight ? "light" : "dark";
|
|
800
1623
|
document.body.classList.toggle("theme-light", isLight);
|
|
801
1624
|
document.body.classList.toggle("theme-dark", !isLight);
|
|
@@ -832,11 +1655,17 @@ function renderThemeSelect({ unavailableLabel = "Theme bundle unavailable" } = {
|
|
|
832
1655
|
elements.themeSelect.value = currentThemeName;
|
|
833
1656
|
}
|
|
834
1657
|
|
|
835
|
-
function setThemeByName(name, options = {}) {
|
|
1658
|
+
async function setThemeByName(name, options = {}) {
|
|
836
1659
|
if (!isOptionalFeatureEnabled("themeBundle")) return;
|
|
837
1660
|
const theme = availableThemes.find((item) => item.name === name);
|
|
838
1661
|
if (!theme) return;
|
|
1662
|
+
currentThemeName = theme.name;
|
|
1663
|
+
if (elements.themeSelect && elements.themeSelect.value !== theme.name) elements.themeSelect.value = theme.name;
|
|
1664
|
+
setCustomBackgroundRecord(null);
|
|
1665
|
+
customBackgroundLoading = true;
|
|
839
1666
|
applyTheme(theme, options);
|
|
1667
|
+
renderBackgroundControl();
|
|
1668
|
+
await loadCustomBackgroundForTheme(theme.name, { includeLegacy: !!options.includeLegacy });
|
|
840
1669
|
}
|
|
841
1670
|
|
|
842
1671
|
async function initializeThemes() {
|
|
@@ -857,8 +1686,8 @@ async function initializeThemes() {
|
|
|
857
1686
|
const stored = storedThemeName();
|
|
858
1687
|
currentThemeName = availableThemes.some((theme) => theme.name === stored) ? stored : DEFAULT_THEME_NAME;
|
|
859
1688
|
renderThemeSelect();
|
|
860
|
-
setThemeByName(currentThemeName, { persist: false });
|
|
861
|
-
if (isOptionalFeatureEnabled("themeBundle") && !availableThemes.some((theme) => theme.name === currentThemeName) && availableThemes[0])
|
|
1689
|
+
await setThemeByName(currentThemeName, { persist: false, includeLegacy: true });
|
|
1690
|
+
if (isOptionalFeatureEnabled("themeBundle") && !availableThemes.some((theme) => theme.name === currentThemeName) && availableThemes[0]) await setThemeByName(availableThemes[0].name, { persist: false });
|
|
862
1691
|
if (!availableThemes.length) addEvent("theme bundle unavailable; using built-in default theme", "warn");
|
|
863
1692
|
}
|
|
864
1693
|
|
|
@@ -866,6 +1695,26 @@ function activeTab() {
|
|
|
866
1695
|
return tabs.find((tab) => tab.id === activeTabId) || null;
|
|
867
1696
|
}
|
|
868
1697
|
|
|
1698
|
+
function activeTabContext(tabId = activeTabId) {
|
|
1699
|
+
return { tabId: tabId || null, generation: activeTabGeneration };
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function setActiveTabId(tabId, { remember = false } = {}) {
|
|
1703
|
+
const nextTabId = tabId || null;
|
|
1704
|
+
if (nextTabId !== activeTabId) activeTabGeneration += 1;
|
|
1705
|
+
activeTabId = nextTabId;
|
|
1706
|
+
if (remember) rememberActiveTab();
|
|
1707
|
+
return activeTabContext(nextTabId);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function isCurrentTabContext(context) {
|
|
1711
|
+
return !!context && context.tabId === activeTabId && context.generation === activeTabGeneration;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
function eventTargetsActiveTab(event) {
|
|
1715
|
+
return !event?.tabId || event.tabId === activeTabId;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
869
1718
|
function normalizeTabActivity(activity = {}) {
|
|
870
1719
|
const status = activity.status === "working" || activity.isWorking ? "working" : activity.status === "done" ? "done" : "idle";
|
|
871
1720
|
const completionSerial = Number(activity.completionSerial);
|
|
@@ -1123,11 +1972,12 @@ function restoreActiveDraft() {
|
|
|
1123
1972
|
elements.promptInput.value = activeTabId ? tabDrafts.get(activeTabId) || "" : "";
|
|
1124
1973
|
resizePromptInput();
|
|
1125
1974
|
renderCommandSuggestions();
|
|
1975
|
+
renderAttachmentTray();
|
|
1126
1976
|
}
|
|
1127
1977
|
|
|
1128
1978
|
function focusPromptInput({ defer = false } = {}) {
|
|
1129
1979
|
const focus = () => {
|
|
1130
|
-
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || document.visibilityState === "hidden") return;
|
|
1980
|
+
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || elements.nativeCommandDialog.open || document.visibilityState === "hidden") return;
|
|
1131
1981
|
try {
|
|
1132
1982
|
elements.promptInput.focus({ preventScroll: true });
|
|
1133
1983
|
} catch {
|
|
@@ -1179,6 +2029,8 @@ function resetActiveTabUi() {
|
|
|
1179
2029
|
statusEntries.clear();
|
|
1180
2030
|
widgets.clear();
|
|
1181
2031
|
transientMessages = [];
|
|
2032
|
+
liveToolRuns.clear();
|
|
2033
|
+
liveToolCards.clear();
|
|
1182
2034
|
availableCommands = [];
|
|
1183
2035
|
resetOptionalFeatureAvailability();
|
|
1184
2036
|
commandSuggestions = [];
|
|
@@ -1189,6 +2041,8 @@ function resetActiveTabUi() {
|
|
|
1189
2041
|
removeRunIndicatorBubble();
|
|
1190
2042
|
hideCommandSuggestions();
|
|
1191
2043
|
cancelPendingDialogs();
|
|
2044
|
+
if (elements.nativeCommandDialog.open) closeNativeCommandDialog();
|
|
2045
|
+
if (pathPickerState) closePathPicker(null);
|
|
1192
2046
|
Object.assign(gitWorkflow, {
|
|
1193
2047
|
active: false,
|
|
1194
2048
|
step: "idle",
|
|
@@ -1450,8 +2304,7 @@ async function refreshTabs({ selectStored = false } = {}) {
|
|
|
1450
2304
|
syncAgentDoneNotificationsFromTabs(tabs, previousTabs);
|
|
1451
2305
|
const stored = selectStored ? restoreStoredTabId() : null;
|
|
1452
2306
|
if (!activeTabId || !tabs.some((tab) => tab.id === activeTabId)) {
|
|
1453
|
-
|
|
1454
|
-
rememberActiveTab();
|
|
2307
|
+
setActiveTabId((stored && tabs.some((tab) => tab.id === stored) ? stored : tabs[0]?.id) || null, { remember: true });
|
|
1455
2308
|
}
|
|
1456
2309
|
renderTabs();
|
|
1457
2310
|
return tabs;
|
|
@@ -1463,15 +2316,14 @@ async function switchTab(tabId) {
|
|
|
1463
2316
|
setMobileTabsExpanded(false);
|
|
1464
2317
|
footerModelPickerOpen = false;
|
|
1465
2318
|
saveActiveDraft();
|
|
1466
|
-
|
|
1467
|
-
rememberActiveTab();
|
|
2319
|
+
const tabContext = setActiveTabId(tabId, { remember: true });
|
|
1468
2320
|
resetActiveTabUi();
|
|
1469
2321
|
renderTabs();
|
|
1470
2322
|
restoreActiveDraft();
|
|
1471
2323
|
focusPromptInput({ defer: true });
|
|
1472
|
-
connectEvents();
|
|
1473
|
-
await refreshAll();
|
|
1474
|
-
markTabOutputSeen();
|
|
2324
|
+
connectEvents(tabContext);
|
|
2325
|
+
await refreshAll(tabContext);
|
|
2326
|
+
if (isCurrentTabContext(tabContext)) markTabOutputSeen();
|
|
1475
2327
|
}
|
|
1476
2328
|
|
|
1477
2329
|
async function createTerminalTab(cwd = activeTab()?.cwd, { triggerButton = elements.newTabButton } = {}) {
|
|
@@ -1540,22 +2392,25 @@ async function closeTerminalTabs(tabIds, { label = "selected terminal tabs" } =
|
|
|
1540
2392
|
const closedIds = response.data?.closedIds || targetIds;
|
|
1541
2393
|
tabs = response.data?.tabs || tabs.filter((item) => !closedIds.includes(item.id));
|
|
1542
2394
|
syncTabMetadata(tabs);
|
|
1543
|
-
for (const id of closedIds)
|
|
2395
|
+
for (const id of closedIds) {
|
|
2396
|
+
tabDrafts.delete(id);
|
|
2397
|
+
clearAttachments(id);
|
|
2398
|
+
}
|
|
1544
2399
|
clearOpenTerminalTabGroup(null, { force: true });
|
|
1545
2400
|
|
|
1546
|
-
|
|
1547
|
-
|
|
2401
|
+
const activeTabNeedsFallback = closedIds.includes(activeTabId) || !tabs.some((item) => item.id === activeTabId);
|
|
2402
|
+
if (activeTabNeedsFallback) {
|
|
2403
|
+
const tabContext = setActiveTabId((response.data?.activeTabId && tabs.some((item) => item.id === response.data.activeTabId)
|
|
1548
2404
|
? response.data.activeTabId
|
|
1549
|
-
: (fallbackTabId && tabs.some((item) => item.id === fallbackTabId) ? fallbackTabId : tabs[0]?.id)) || null;
|
|
1550
|
-
rememberActiveTab();
|
|
2405
|
+
: (fallbackTabId && tabs.some((item) => item.id === fallbackTabId) ? fallbackTabId : tabs[0]?.id)) || null, { remember: true });
|
|
1551
2406
|
resetActiveTabUi();
|
|
1552
2407
|
renderTabs();
|
|
1553
2408
|
restoreActiveDraft();
|
|
1554
2409
|
focusPromptInput({ defer: true });
|
|
1555
|
-
connectEvents();
|
|
2410
|
+
connectEvents(tabContext);
|
|
1556
2411
|
if (activeTabId) {
|
|
1557
|
-
await refreshAll();
|
|
1558
|
-
markTabOutputSeen();
|
|
2412
|
+
await refreshAll(tabContext);
|
|
2413
|
+
if (isCurrentTabContext(tabContext)) markTabOutputSeen();
|
|
1559
2414
|
}
|
|
1560
2415
|
} else {
|
|
1561
2416
|
renderTabs();
|
|
@@ -1587,10 +2442,11 @@ async function initializeTabs() {
|
|
|
1587
2442
|
renderTabs();
|
|
1588
2443
|
restoreActiveDraft();
|
|
1589
2444
|
focusPromptInput({ defer: true });
|
|
1590
|
-
|
|
2445
|
+
const tabContext = activeTabContext();
|
|
2446
|
+
connectEvents(tabContext);
|
|
1591
2447
|
if (activeTabId) {
|
|
1592
|
-
await refreshAll();
|
|
1593
|
-
markTabOutputSeen();
|
|
2448
|
+
await refreshAll(tabContext);
|
|
2449
|
+
if (isCurrentTabContext(tabContext)) markTabOutputSeen();
|
|
1594
2450
|
}
|
|
1595
2451
|
}
|
|
1596
2452
|
|
|
@@ -2143,16 +2999,20 @@ function setFooterModelPickerOpen(open) {
|
|
|
2143
2999
|
|
|
2144
3000
|
async function applyFooterModel(model) {
|
|
2145
3001
|
if (!model?.provider || !model?.id) return;
|
|
3002
|
+
const tabContext = activeTabContext();
|
|
2146
3003
|
try {
|
|
2147
3004
|
footerModelPickerOpen = false;
|
|
2148
|
-
await api("/api/model", { method: "POST", body: { provider: model.provider, modelId: model.id } });
|
|
2149
|
-
|
|
2150
|
-
await
|
|
3005
|
+
await api("/api/model", { method: "POST", body: { provider: model.provider, modelId: model.id }, tabId: tabContext.tabId });
|
|
3006
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
3007
|
+
await refreshState(tabContext);
|
|
3008
|
+
await refreshModels(tabContext);
|
|
2151
3009
|
} catch (error) {
|
|
2152
|
-
addEvent(error.message, "error");
|
|
3010
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
2153
3011
|
} finally {
|
|
2154
|
-
|
|
2155
|
-
|
|
3012
|
+
if (isCurrentTabContext(tabContext)) {
|
|
3013
|
+
document.body.classList.toggle("footer-model-picker-open", footerModelPickerOpen);
|
|
3014
|
+
renderFooter();
|
|
3015
|
+
}
|
|
2156
3016
|
}
|
|
2157
3017
|
}
|
|
2158
3018
|
|
|
@@ -2422,10 +3282,11 @@ function pickCwd(tab, initialCwd) {
|
|
|
2422
3282
|
async function changeActiveTabCwd() {
|
|
2423
3283
|
const tab = activeTab();
|
|
2424
3284
|
if (!tab) return;
|
|
3285
|
+
const tabContext = activeTabContext(tab.id);
|
|
2425
3286
|
|
|
2426
3287
|
const currentCwd = latestWorkspace?.cwd || tab.cwd || "";
|
|
2427
3288
|
const cwd = await pickCwd(tab, currentCwd);
|
|
2428
|
-
if (!cwd || cwd === currentCwd) return;
|
|
3289
|
+
if (!isCurrentTabContext(tabContext) || !cwd || cwd === currentCwd) return;
|
|
2429
3290
|
if (!window.confirm(`Restart ${tab.title} in:\n${cwd}\n\nCurrent in-flight work in this tab will be stopped.`)) return;
|
|
2430
3291
|
|
|
2431
3292
|
saveActiveDraft();
|
|
@@ -2433,16 +3294,21 @@ async function changeActiveTabCwd() {
|
|
|
2433
3294
|
const response = await api(`/api/tabs/${encodeURIComponent(tab.id)}`, { method: "PATCH", body: { cwd }, scoped: false });
|
|
2434
3295
|
tabs = response.data?.tabs || tabs;
|
|
2435
3296
|
syncTabMetadata(tabs);
|
|
2436
|
-
|
|
3297
|
+
if (!isCurrentTabContext(tabContext)) {
|
|
3298
|
+
renderTabs();
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
const nextContext = setActiveTabId(response.data?.tab?.id || activeTabId);
|
|
2437
3302
|
resetActiveTabUi();
|
|
2438
3303
|
renderTabs();
|
|
2439
3304
|
restoreActiveDraft();
|
|
2440
|
-
connectEvents();
|
|
2441
|
-
await refreshAll();
|
|
3305
|
+
connectEvents(nextContext);
|
|
3306
|
+
await refreshAll(nextContext);
|
|
3307
|
+
if (!isCurrentTabContext(nextContext)) return;
|
|
2442
3308
|
const changedCwd = response.data?.tab?.cwd || cwd;
|
|
2443
3309
|
addEvent(response.data?.changed === false ? `cwd unchanged: ${changedCwd}` : `changed ${tab.title} cwd to ${changedCwd}`, "info");
|
|
2444
3310
|
} catch (error) {
|
|
2445
|
-
addEvent(error.message, "error");
|
|
3311
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
2446
3312
|
}
|
|
2447
3313
|
}
|
|
2448
3314
|
|
|
@@ -2505,19 +3371,34 @@ function renderFooter() {
|
|
|
2505
3371
|
updateFooterModelPickerPosition();
|
|
2506
3372
|
}
|
|
2507
3373
|
|
|
2508
|
-
function scheduleRefreshMessages(delay = 120) {
|
|
3374
|
+
function scheduleRefreshMessages(delay = 120, tabContext = activeTabContext()) {
|
|
2509
3375
|
clearTimeout(refreshMessagesTimer);
|
|
2510
|
-
refreshMessagesTimer = setTimeout(() =>
|
|
3376
|
+
refreshMessagesTimer = setTimeout(() => {
|
|
3377
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
3378
|
+
refreshMessages(tabContext).catch((error) => {
|
|
3379
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
3380
|
+
});
|
|
3381
|
+
}, delay);
|
|
2511
3382
|
}
|
|
2512
3383
|
|
|
2513
|
-
function scheduleRefreshState(delay = 120) {
|
|
3384
|
+
function scheduleRefreshState(delay = 120, tabContext = activeTabContext()) {
|
|
2514
3385
|
clearTimeout(refreshStateTimer);
|
|
2515
|
-
refreshStateTimer = setTimeout(() =>
|
|
3386
|
+
refreshStateTimer = setTimeout(() => {
|
|
3387
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
3388
|
+
refreshState(tabContext).catch((error) => {
|
|
3389
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
3390
|
+
});
|
|
3391
|
+
}, delay);
|
|
2516
3392
|
}
|
|
2517
3393
|
|
|
2518
|
-
function scheduleRefreshFooter(delay = 300) {
|
|
3394
|
+
function scheduleRefreshFooter(delay = 300, tabContext = activeTabContext()) {
|
|
2519
3395
|
clearTimeout(refreshFooterTimer);
|
|
2520
|
-
refreshFooterTimer = setTimeout(() =>
|
|
3396
|
+
refreshFooterTimer = setTimeout(() => {
|
|
3397
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
3398
|
+
refreshFooterData(tabContext).catch((error) => {
|
|
3399
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
3400
|
+
});
|
|
3401
|
+
}, delay);
|
|
2521
3402
|
}
|
|
2522
3403
|
|
|
2523
3404
|
function renderStatus() {
|
|
@@ -2608,6 +3489,7 @@ function releaseDialogPromptParts(prompt) {
|
|
|
2608
3489
|
title: question,
|
|
2609
3490
|
message,
|
|
2610
3491
|
plainMessage: stripAnsi(message),
|
|
3492
|
+
featureId: isAurReleasePrompt ? "releaseAur" : "releaseNpm",
|
|
2611
3493
|
};
|
|
2612
3494
|
}
|
|
2613
3495
|
|
|
@@ -2763,13 +3645,17 @@ function appendReleaseNpmTerminalLine(parent, line) {
|
|
|
2763
3645
|
}
|
|
2764
3646
|
|
|
2765
3647
|
async function sendReleaseNpmCommand(command) {
|
|
3648
|
+
const tabContext = activeTabContext();
|
|
2766
3649
|
try {
|
|
2767
|
-
await api("/api/prompt", { method: "POST", body: { message: command }, tabId:
|
|
3650
|
+
await api("/api/prompt", { method: "POST", body: { message: command }, tabId: tabContext.tabId });
|
|
3651
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
2768
3652
|
addEvent(`${command} sent`, "info");
|
|
2769
|
-
scheduleRefreshState();
|
|
3653
|
+
scheduleRefreshState(120, tabContext);
|
|
2770
3654
|
} catch (error) {
|
|
2771
|
-
|
|
2772
|
-
|
|
3655
|
+
if (isCurrentTabContext(tabContext)) {
|
|
3656
|
+
addEvent(error.message, "error");
|
|
3657
|
+
addTransientMessage({ role: "error", title: command, content: error.message, level: "error" });
|
|
3658
|
+
}
|
|
2773
3659
|
}
|
|
2774
3660
|
}
|
|
2775
3661
|
|
|
@@ -3077,8 +3963,11 @@ function failGitWorkflow(error, step = gitWorkflow.step) {
|
|
|
3077
3963
|
|
|
3078
3964
|
function startGitWorkflow() {
|
|
3079
3965
|
if (!isOptionalFeatureEnabled("gitWorkflow")) {
|
|
3966
|
+
const tabContext = activeTabContext();
|
|
3080
3967
|
addEvent(commandUnavailableMessage("git-staged-msg"), "warn");
|
|
3081
|
-
refreshCommands().catch((error) =>
|
|
3968
|
+
refreshCommands(tabContext).catch((error) => {
|
|
3969
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
3970
|
+
});
|
|
3082
3971
|
return;
|
|
3083
3972
|
}
|
|
3084
3973
|
if (gitWorkflow.active && !["done", "cancelled", "error"].includes(gitWorkflow.step) && !confirm("Restart the active git workflow?")) return;
|
|
@@ -3225,6 +4114,287 @@ function appendText(parent, text, className = "text-block") {
|
|
|
3225
4114
|
return block;
|
|
3226
4115
|
}
|
|
3227
4116
|
|
|
4117
|
+
function safeMarkdownLinkHref(url) {
|
|
4118
|
+
const href = String(url || "").trim();
|
|
4119
|
+
if (!href || /[\u0000-\u001f\u007f]/.test(href)) return "";
|
|
4120
|
+
if (/^(?:https?:|mailto:)/i.test(href)) return href;
|
|
4121
|
+
if (/^(?:#|\/(?!\/)|\.\/|\.\.\/)/.test(href)) return href;
|
|
4122
|
+
return "";
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
function appendInlineMarkdown(parent, text, depth = 0) {
|
|
4126
|
+
const value = String(text || "");
|
|
4127
|
+
if (!value) return;
|
|
4128
|
+
if (depth > 6) {
|
|
4129
|
+
parent.append(document.createTextNode(value));
|
|
4130
|
+
return;
|
|
4131
|
+
}
|
|
4132
|
+
let index = 0;
|
|
4133
|
+
const appendPlain = (end) => {
|
|
4134
|
+
if (end > index) parent.append(document.createTextNode(value.slice(index, end)));
|
|
4135
|
+
index = end;
|
|
4136
|
+
};
|
|
4137
|
+
while (index < value.length) {
|
|
4138
|
+
if (value[index] === "`") {
|
|
4139
|
+
const end = value.indexOf("`", index + 1);
|
|
4140
|
+
if (end > index + 1) {
|
|
4141
|
+
const code = make("code", "markdown-inline-code", value.slice(index + 1, end));
|
|
4142
|
+
parent.append(code);
|
|
4143
|
+
index = end + 1;
|
|
4144
|
+
continue;
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
if (value[index] === "[") {
|
|
4148
|
+
const labelEnd = value.indexOf("](", index + 1);
|
|
4149
|
+
const linkEnd = labelEnd === -1 ? -1 : value.indexOf(")", labelEnd + 2);
|
|
4150
|
+
if (labelEnd !== -1 && linkEnd !== -1) {
|
|
4151
|
+
const label = value.slice(index + 1, labelEnd);
|
|
4152
|
+
const href = safeMarkdownLinkHref(value.slice(labelEnd + 2, linkEnd));
|
|
4153
|
+
if (href) {
|
|
4154
|
+
const link = make("a");
|
|
4155
|
+
link.href = href;
|
|
4156
|
+
if (/^https?:/i.test(href)) {
|
|
4157
|
+
link.target = "_blank";
|
|
4158
|
+
link.rel = "noopener noreferrer";
|
|
4159
|
+
}
|
|
4160
|
+
appendInlineMarkdown(link, label, depth + 1);
|
|
4161
|
+
parent.append(link);
|
|
4162
|
+
} else {
|
|
4163
|
+
parent.append(document.createTextNode(value.slice(index, linkEnd + 1)));
|
|
4164
|
+
}
|
|
4165
|
+
index = linkEnd + 1;
|
|
4166
|
+
continue;
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
const strongMarker = value.startsWith("**", index) ? "**" : value.startsWith("__", index) ? "__" : "";
|
|
4170
|
+
if (strongMarker) {
|
|
4171
|
+
const end = value.indexOf(strongMarker, index + 2);
|
|
4172
|
+
if (end > index + 2) {
|
|
4173
|
+
const strong = make("strong");
|
|
4174
|
+
appendInlineMarkdown(strong, value.slice(index + 2, end), depth + 1);
|
|
4175
|
+
parent.append(strong);
|
|
4176
|
+
index = end + 2;
|
|
4177
|
+
continue;
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
if (value.startsWith("~~", index)) {
|
|
4181
|
+
const end = value.indexOf("~~", index + 2);
|
|
4182
|
+
if (end > index + 2) {
|
|
4183
|
+
const del = make("del");
|
|
4184
|
+
appendInlineMarkdown(del, value.slice(index + 2, end), depth + 1);
|
|
4185
|
+
parent.append(del);
|
|
4186
|
+
index = end + 2;
|
|
4187
|
+
continue;
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
const emphasisMarker = value[index] === "*" || value[index] === "_" ? value[index] : "";
|
|
4191
|
+
if (emphasisMarker && value[index + 1] !== emphasisMarker) {
|
|
4192
|
+
const end = value.indexOf(emphasisMarker, index + 1);
|
|
4193
|
+
if (end > index + 1) {
|
|
4194
|
+
const em = make("em");
|
|
4195
|
+
appendInlineMarkdown(em, value.slice(index + 1, end), depth + 1);
|
|
4196
|
+
parent.append(em);
|
|
4197
|
+
index = end + 1;
|
|
4198
|
+
continue;
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
const nextSpecials = ["`", "[", "**", "__", "~~", "*", "_"]
|
|
4202
|
+
.map((marker) => value.indexOf(marker, index + 1))
|
|
4203
|
+
.filter((pos) => pos !== -1);
|
|
4204
|
+
appendPlain(nextSpecials.length ? Math.min(...nextSpecials) : value.length);
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
|
|
4208
|
+
function appendMarkdownParagraph(parent, lines) {
|
|
4209
|
+
const paragraph = make("p");
|
|
4210
|
+
lines.forEach((line, index) => {
|
|
4211
|
+
if (index > 0) paragraph.append(make("br"));
|
|
4212
|
+
appendInlineMarkdown(paragraph, line);
|
|
4213
|
+
});
|
|
4214
|
+
parent.append(paragraph);
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
function appendMarkdownCodeBlock(parent, code, language = "") {
|
|
4218
|
+
const wrapper = make("div", "markdown-code-block");
|
|
4219
|
+
if (language) wrapper.append(make("div", "markdown-code-language", language));
|
|
4220
|
+
const pre = make("pre", "code-block markdown-code");
|
|
4221
|
+
const codeNode = make("code", language ? `language-${language.replace(/[^a-z0-9_-]/gi, "")}` : "");
|
|
4222
|
+
codeNode.textContent = code.replace(/\n+$/g, "");
|
|
4223
|
+
pre.append(codeNode);
|
|
4224
|
+
wrapper.append(pre);
|
|
4225
|
+
parent.append(wrapper);
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
function markdownTableSeparator(line) {
|
|
4229
|
+
return /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line || "");
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
function splitMarkdownTableRow(line) {
|
|
4233
|
+
let row = String(line || "").trim();
|
|
4234
|
+
if (row.startsWith("|")) row = row.slice(1);
|
|
4235
|
+
if (row.endsWith("|")) row = row.slice(0, -1);
|
|
4236
|
+
return row.split(/(?<!\\)\|/).map((cell) => cell.replace(/\\\|/g, "|").trim());
|
|
4237
|
+
}
|
|
4238
|
+
|
|
4239
|
+
function appendMarkdownTable(parent, rows) {
|
|
4240
|
+
const wrapper = make("div", "markdown-table-wrapper");
|
|
4241
|
+
const table = make("table", "markdown-table");
|
|
4242
|
+
const thead = make("thead");
|
|
4243
|
+
const tbody = make("tbody");
|
|
4244
|
+
const headerRow = make("tr");
|
|
4245
|
+
for (const cell of rows[0] || []) {
|
|
4246
|
+
const th = make("th");
|
|
4247
|
+
appendInlineMarkdown(th, cell);
|
|
4248
|
+
headerRow.append(th);
|
|
4249
|
+
}
|
|
4250
|
+
thead.append(headerRow);
|
|
4251
|
+
for (const row of rows.slice(1)) {
|
|
4252
|
+
const tr = make("tr");
|
|
4253
|
+
for (const cell of row) {
|
|
4254
|
+
const td = make("td");
|
|
4255
|
+
appendInlineMarkdown(td, cell);
|
|
4256
|
+
tr.append(td);
|
|
4257
|
+
}
|
|
4258
|
+
tbody.append(tr);
|
|
4259
|
+
}
|
|
4260
|
+
table.append(thead, tbody);
|
|
4261
|
+
wrapper.append(table);
|
|
4262
|
+
parent.append(wrapper);
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
function markdownListMatch(line) {
|
|
4266
|
+
const unordered = line.match(/^\s{0,3}[-*+]\s+(.+)$/);
|
|
4267
|
+
if (unordered) return { ordered: false, text: unordered[1] };
|
|
4268
|
+
const ordered = line.match(/^\s{0,3}(\d+)[.)]\s+(.+)$/);
|
|
4269
|
+
if (ordered) return { ordered: true, start: Number(ordered[1]), text: ordered[2] };
|
|
4270
|
+
return null;
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
function appendMarkdownList(parent, items, ordered = false, start = null) {
|
|
4274
|
+
const list = make(ordered ? "ol" : "ul", "markdown-list");
|
|
4275
|
+
if (ordered && Number.isFinite(start) && start > 1) list.start = start;
|
|
4276
|
+
for (const itemText of items) {
|
|
4277
|
+
const li = make("li");
|
|
4278
|
+
const task = String(itemText).match(/^\[( |x|X|-)\]\s+(.+)$/);
|
|
4279
|
+
if (task) {
|
|
4280
|
+
li.classList.add("markdown-task-item");
|
|
4281
|
+
const checkbox = make("input", "markdown-task-checkbox");
|
|
4282
|
+
checkbox.type = "checkbox";
|
|
4283
|
+
checkbox.disabled = true;
|
|
4284
|
+
checkbox.checked = task[1].toLowerCase() === "x";
|
|
4285
|
+
li.append(checkbox);
|
|
4286
|
+
appendInlineMarkdown(li, task[2]);
|
|
4287
|
+
} else {
|
|
4288
|
+
appendInlineMarkdown(li, itemText);
|
|
4289
|
+
}
|
|
4290
|
+
list.append(li);
|
|
4291
|
+
}
|
|
4292
|
+
parent.append(list);
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4295
|
+
function renderMarkdownInto(parent, text) {
|
|
4296
|
+
const raw = String(text || "").replace(/\r\n?/g, "\n");
|
|
4297
|
+
const lines = raw.split("\n");
|
|
4298
|
+
let index = 0;
|
|
4299
|
+
let paragraph = [];
|
|
4300
|
+
const flushParagraph = () => {
|
|
4301
|
+
if (paragraph.length) appendMarkdownParagraph(parent, paragraph);
|
|
4302
|
+
paragraph = [];
|
|
4303
|
+
};
|
|
4304
|
+
|
|
4305
|
+
while (index < lines.length) {
|
|
4306
|
+
const line = lines[index];
|
|
4307
|
+
if (!line.trim()) {
|
|
4308
|
+
flushParagraph();
|
|
4309
|
+
index += 1;
|
|
4310
|
+
continue;
|
|
4311
|
+
}
|
|
4312
|
+
const fence = line.match(/^\s*```\s*([\w.+-]*)\s*$/);
|
|
4313
|
+
if (fence) {
|
|
4314
|
+
flushParagraph();
|
|
4315
|
+
const language = fence[1] || "";
|
|
4316
|
+
const codeLines = [];
|
|
4317
|
+
index += 1;
|
|
4318
|
+
while (index < lines.length && !/^\s*```\s*$/.test(lines[index])) {
|
|
4319
|
+
codeLines.push(lines[index]);
|
|
4320
|
+
index += 1;
|
|
4321
|
+
}
|
|
4322
|
+
if (index < lines.length) index += 1;
|
|
4323
|
+
appendMarkdownCodeBlock(parent, codeLines.join("\n"), language);
|
|
4324
|
+
continue;
|
|
4325
|
+
}
|
|
4326
|
+
if (markdownTableSeparator(lines[index + 1]) && line.includes("|")) {
|
|
4327
|
+
flushParagraph();
|
|
4328
|
+
const rows = [splitMarkdownTableRow(line)];
|
|
4329
|
+
index += 2;
|
|
4330
|
+
while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
|
|
4331
|
+
rows.push(splitMarkdownTableRow(lines[index]));
|
|
4332
|
+
index += 1;
|
|
4333
|
+
}
|
|
4334
|
+
appendMarkdownTable(parent, rows);
|
|
4335
|
+
continue;
|
|
4336
|
+
}
|
|
4337
|
+
const heading = line.match(/^\s{0,3}(#{1,6})\s+(.+?)\s*#*\s*$/);
|
|
4338
|
+
if (heading) {
|
|
4339
|
+
flushParagraph();
|
|
4340
|
+
const level = Math.min(6, heading[1].length);
|
|
4341
|
+
const node = make(`h${level}`, `markdown-heading markdown-heading-${level}`);
|
|
4342
|
+
appendInlineMarkdown(node, heading[2]);
|
|
4343
|
+
parent.append(node);
|
|
4344
|
+
index += 1;
|
|
4345
|
+
continue;
|
|
4346
|
+
}
|
|
4347
|
+
if (/^\s{0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/.test(line)) {
|
|
4348
|
+
flushParagraph();
|
|
4349
|
+
parent.append(make("hr", "markdown-rule"));
|
|
4350
|
+
index += 1;
|
|
4351
|
+
continue;
|
|
4352
|
+
}
|
|
4353
|
+
if (/^\s{0,3}>\s?/.test(line)) {
|
|
4354
|
+
flushParagraph();
|
|
4355
|
+
const quoteLines = [];
|
|
4356
|
+
while (index < lines.length && /^\s{0,3}>\s?/.test(lines[index])) {
|
|
4357
|
+
quoteLines.push(lines[index].replace(/^\s{0,3}>\s?/, ""));
|
|
4358
|
+
index += 1;
|
|
4359
|
+
}
|
|
4360
|
+
const quote = make("blockquote", "markdown-blockquote");
|
|
4361
|
+
renderMarkdownInto(quote, quoteLines.join("\n"));
|
|
4362
|
+
parent.append(quote);
|
|
4363
|
+
continue;
|
|
4364
|
+
}
|
|
4365
|
+
const listMatch = markdownListMatch(line);
|
|
4366
|
+
if (listMatch) {
|
|
4367
|
+
flushParagraph();
|
|
4368
|
+
const ordered = listMatch.ordered;
|
|
4369
|
+
const start = listMatch.start || null;
|
|
4370
|
+
const items = [];
|
|
4371
|
+
while (index < lines.length) {
|
|
4372
|
+
const item = markdownListMatch(lines[index]);
|
|
4373
|
+
if (!item || item.ordered !== ordered) break;
|
|
4374
|
+
items.push(item.text);
|
|
4375
|
+
index += 1;
|
|
4376
|
+
}
|
|
4377
|
+
appendMarkdownList(parent, items, ordered, start);
|
|
4378
|
+
continue;
|
|
4379
|
+
}
|
|
4380
|
+
paragraph.push(line);
|
|
4381
|
+
index += 1;
|
|
4382
|
+
}
|
|
4383
|
+
flushParagraph();
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
function appendMarkdown(parent, text) {
|
|
4387
|
+
const block = make("div", "markdown-body");
|
|
4388
|
+
renderMarkdownInto(block, text);
|
|
4389
|
+
parent.append(block);
|
|
4390
|
+
return block;
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
function renderMarkdown(block, text) {
|
|
4394
|
+
block.replaceChildren();
|
|
4395
|
+
renderMarkdownInto(block, text);
|
|
4396
|
+
}
|
|
4397
|
+
|
|
3228
4398
|
function appendImage(parent, part) {
|
|
3229
4399
|
const wrapper = make("div", "image-block");
|
|
3230
4400
|
const img = document.createElement("img");
|
|
@@ -3238,7 +4408,7 @@ function appendImage(parent, part) {
|
|
|
3238
4408
|
}
|
|
3239
4409
|
|
|
3240
4410
|
function isActionFeedbackMessage(message) {
|
|
3241
|
-
return message?.role === "assistant" || message?.role === "toolResult" || message?.role === "bashExecution";
|
|
4411
|
+
return message?.role === "assistant" || message?.role === "toolExecution" || message?.role === "toolResult" || message?.role === "bashExecution";
|
|
3242
4412
|
}
|
|
3243
4413
|
|
|
3244
4414
|
function truncateActionFeedbackText(text, limit = ACTION_FEEDBACK_SNIPPET_LIMIT) {
|
|
@@ -3253,6 +4423,7 @@ function actionFeedbackKey(message, messageIndex) {
|
|
|
3253
4423
|
messageIndex,
|
|
3254
4424
|
message?.role || "message",
|
|
3255
4425
|
message?.toolName || "",
|
|
4426
|
+
message?.toolCallId || "",
|
|
3256
4427
|
message?.command || "",
|
|
3257
4428
|
message?.timestamp || "",
|
|
3258
4429
|
].join("|");
|
|
@@ -3270,6 +4441,12 @@ function actionFeedbackSummary(message) {
|
|
|
3270
4441
|
snippet: truncateActionFeedbackText(`$ ${message.command || ""}\n\n${message.output || ""}`),
|
|
3271
4442
|
};
|
|
3272
4443
|
}
|
|
4444
|
+
if (message?.role === "toolExecution") {
|
|
4445
|
+
const result = toolExecutionResult(message);
|
|
4446
|
+
const args = message.arguments === undefined ? "" : JSON.stringify(message.arguments, null, 2);
|
|
4447
|
+
const output = toolResultText(result);
|
|
4448
|
+
return { kind: "action", title, snippet: truncateActionFeedbackText([args, output].filter(Boolean).join("\n\n")) };
|
|
4449
|
+
}
|
|
3273
4450
|
return { kind: "action", title, snippet: truncateActionFeedbackText(textFromContent(message?.content)) };
|
|
3274
4451
|
}
|
|
3275
4452
|
|
|
@@ -3298,9 +4475,10 @@ function actionFeedbackSteerMessage(item) {
|
|
|
3298
4475
|
}
|
|
3299
4476
|
|
|
3300
4477
|
async function sendLiveActionFeedback(item) {
|
|
4478
|
+
const tabContext = activeTabContext(item.tabId);
|
|
3301
4479
|
if (!isRunActive()) return;
|
|
3302
4480
|
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`);
|
|
4481
|
+
if (isCurrentTabContext(tabContext)) addEvent(`sent ${ACTION_FEEDBACK_REACTIONS[item.reaction]?.icon || "feedback"} action feedback as live steering`);
|
|
3304
4482
|
}
|
|
3305
4483
|
|
|
3306
4484
|
function setActionFeedback(message, messageIndex, reaction) {
|
|
@@ -3382,13 +4560,13 @@ function isMissingActionFeedbackEndpoint(error) {
|
|
|
3382
4560
|
return error?.statusCode === 404 || /not found/i.test(error?.message || "");
|
|
3383
4561
|
}
|
|
3384
4562
|
|
|
3385
|
-
async function postQueuedFeedback(tabId, items) {
|
|
4563
|
+
async function postQueuedFeedback(tabId, items, tabContext = activeTabContext(tabId)) {
|
|
3386
4564
|
const feedback = items.map(serializeActionFeedback);
|
|
3387
4565
|
try {
|
|
3388
4566
|
await api("/api/action-feedback", { method: "POST", body: { feedback }, tabId });
|
|
3389
4567
|
} catch (error) {
|
|
3390
4568
|
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");
|
|
4569
|
+
if (isCurrentTabContext(tabContext)) addEvent("/api/action-feedback not found; falling back to a normal prompt. Restart Web UI to use the dedicated endpoint.", "warn");
|
|
3392
4570
|
await api("/api/prompt", { method: "POST", body: { message: formatActionFeedbackLearningPrompt(feedback) }, tabId });
|
|
3393
4571
|
}
|
|
3394
4572
|
}
|
|
@@ -3433,6 +4611,7 @@ function renderFeedbackTray() {
|
|
|
3433
4611
|
|
|
3434
4612
|
async function submitQueuedActionFeedback() {
|
|
3435
4613
|
const tabId = activeTabId;
|
|
4614
|
+
const tabContext = activeTabContext(tabId);
|
|
3436
4615
|
const items = queuedActionFeedback(tabId);
|
|
3437
4616
|
if (!tabId || items.length === 0 || actionFeedbackSendBusy) return;
|
|
3438
4617
|
if (isRunActive()) {
|
|
@@ -3446,28 +4625,32 @@ async function submitQueuedActionFeedback() {
|
|
|
3446
4625
|
setRunIndicatorActivity("Sending action feedback to Pi…");
|
|
3447
4626
|
renderFeedbackTray();
|
|
3448
4627
|
try {
|
|
3449
|
-
await postQueuedFeedback(tabId, items);
|
|
4628
|
+
await postQueuedFeedback(tabId, items, tabContext);
|
|
3450
4629
|
actionFeedbackByTab.get(tabId)?.clear();
|
|
4630
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
3451
4631
|
renderAllMessages({ preserveScroll: true });
|
|
3452
4632
|
addEvent("feedback sent; Pi will create a LEARNING");
|
|
3453
|
-
scheduleRefreshState();
|
|
3454
|
-
scheduleRefreshMessages();
|
|
3455
|
-
scheduleRefreshFooter();
|
|
4633
|
+
scheduleRefreshState(120, tabContext);
|
|
4634
|
+
scheduleRefreshMessages(120, tabContext);
|
|
4635
|
+
scheduleRefreshFooter(300, tabContext);
|
|
3456
4636
|
} catch (error) {
|
|
3457
4637
|
markTabIdleLocally(tabId);
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
4638
|
+
if (isCurrentTabContext(tabContext)) {
|
|
4639
|
+
clearRunIndicatorActivity();
|
|
4640
|
+
addEvent(error.message, "error");
|
|
4641
|
+
addTransientMessage({ role: "error", title: "feedback", content: error.message, level: "error" });
|
|
4642
|
+
}
|
|
3461
4643
|
} finally {
|
|
3462
4644
|
actionFeedbackSendBusy = false;
|
|
3463
4645
|
renderFeedbackTray();
|
|
3464
4646
|
}
|
|
3465
4647
|
}
|
|
3466
4648
|
|
|
3467
|
-
function renderContent(parent, content) {
|
|
4649
|
+
function renderContent(parent, content, { markdown = false } = {}) {
|
|
3468
4650
|
if (content === undefined || content === null) return;
|
|
3469
4651
|
if (typeof content === "string") {
|
|
3470
|
-
|
|
4652
|
+
if (markdown) appendMarkdown(parent, stripTodoProgressLines(content));
|
|
4653
|
+
else appendText(parent, content);
|
|
3471
4654
|
return;
|
|
3472
4655
|
}
|
|
3473
4656
|
if (!Array.isArray(content)) {
|
|
@@ -3481,8 +4664,11 @@ function renderContent(parent, content) {
|
|
|
3481
4664
|
continue;
|
|
3482
4665
|
}
|
|
3483
4666
|
if (part.type === "text") {
|
|
3484
|
-
|
|
4667
|
+
const text = assistantTextPartText(part);
|
|
4668
|
+
if (markdown) appendMarkdown(parent, stripTodoProgressLines(text));
|
|
4669
|
+
else appendText(parent, text);
|
|
3485
4670
|
} else if (part.type === "thinking") {
|
|
4671
|
+
if (!thinkingOutputVisible) continue;
|
|
3486
4672
|
const details = make("details", "thinking-block");
|
|
3487
4673
|
details.open = true;
|
|
3488
4674
|
details.append(make("summary", undefined, "thinking"));
|
|
@@ -3503,10 +4689,11 @@ function renderContent(parent, content) {
|
|
|
3503
4689
|
}
|
|
3504
4690
|
|
|
3505
4691
|
function messageTitle(message) {
|
|
3506
|
-
if (message.role === "assistant") return "
|
|
4692
|
+
if (message.role === "assistant") return message.title || "final output";
|
|
3507
4693
|
if (message.title) return message.title;
|
|
3508
4694
|
if (message.role === "thinking") return "thinking";
|
|
3509
4695
|
if (message.role === "toolCall") return `tool call: ${message.toolName || "unknown"}`;
|
|
4696
|
+
if (message.role === "toolExecution") return toolExecutionTitle(message);
|
|
3510
4697
|
if (message.role === "assistantEvent") return "assistant event";
|
|
3511
4698
|
if (message.role === "toolResult") return `tool result: ${message.toolName || "unknown"}`;
|
|
3512
4699
|
if (message.role === "bashExecution") return `bash: ${message.command || ""}`;
|
|
@@ -3537,13 +4724,26 @@ function assistantToolCallArguments(part) {
|
|
|
3537
4724
|
return part?.arguments || part?.args || part?.input || part?.toolCall?.arguments || {};
|
|
3538
4725
|
}
|
|
3539
4726
|
|
|
4727
|
+
function assistantTextPartText(part) {
|
|
4728
|
+
if (!part || typeof part !== "object" || part.type !== "text") return "";
|
|
4729
|
+
if (typeof part.text === "string") return part.text;
|
|
4730
|
+
return typeof part.content === "string" ? part.content : "";
|
|
4731
|
+
}
|
|
4732
|
+
|
|
4733
|
+
function isEmptyAssistantTextPart(part) {
|
|
4734
|
+
return !!(part && typeof part === "object" && part.type === "text" && !assistantTextPartText(part).trim());
|
|
4735
|
+
}
|
|
4736
|
+
|
|
3540
4737
|
function assistantFinalOutputPart(part) {
|
|
3541
4738
|
if (part === undefined || part === null) return null;
|
|
3542
4739
|
if (typeof part !== "object") {
|
|
3543
4740
|
const text = String(part);
|
|
3544
4741
|
return text.trim() ? { type: "text", text } : null;
|
|
3545
4742
|
}
|
|
3546
|
-
if (part.type === "text")
|
|
4743
|
+
if (part.type === "text") {
|
|
4744
|
+
const text = assistantTextPartText(part);
|
|
4745
|
+
return text.trim() ? { ...part, type: "text", text } : null;
|
|
4746
|
+
}
|
|
3547
4747
|
if (typeof part.text === "string") return part.text.trim() ? { ...part, type: "text", text: part.text } : null;
|
|
3548
4748
|
if (part.type === "image") return part;
|
|
3549
4749
|
if (typeof part.content === "string" && part.type !== "thinking" && part.type !== "toolCall" && typeof part.thinking !== "string") {
|
|
@@ -3557,10 +4757,10 @@ function assistantDisplayMessages(message) {
|
|
|
3557
4757
|
const base = { timestamp: message.timestamp };
|
|
3558
4758
|
const content = message.content;
|
|
3559
4759
|
if (typeof content === "string") {
|
|
3560
|
-
return content.trim() ? [{ ...message, title: "
|
|
4760
|
+
return content.trim() ? [{ ...message, title: "final output" }] : [];
|
|
3561
4761
|
}
|
|
3562
4762
|
if (!Array.isArray(content)) {
|
|
3563
|
-
return content === undefined || content === null ? [] : [{ ...message, title: "
|
|
4763
|
+
return content === undefined || content === null ? [] : [{ ...message, title: "final output" }];
|
|
3564
4764
|
}
|
|
3565
4765
|
|
|
3566
4766
|
const displayMessages = [];
|
|
@@ -3576,7 +4776,8 @@ function assistantDisplayMessages(message) {
|
|
|
3576
4776
|
if (isAssistantToolCallPart(part)) {
|
|
3577
4777
|
const toolName = assistantToolCallName(part);
|
|
3578
4778
|
const args = assistantToolCallArguments(part);
|
|
3579
|
-
|
|
4779
|
+
const toolCallId = assistantToolCallId(part);
|
|
4780
|
+
displayMessages.push({ ...base, role: "toolCall", title: `tool call: ${toolName}`, toolName, toolCallId, arguments: args, content: args });
|
|
3580
4781
|
continue;
|
|
3581
4782
|
}
|
|
3582
4783
|
const finalPart = assistantFinalOutputPart(part);
|
|
@@ -3584,13 +4785,14 @@ function assistantDisplayMessages(message) {
|
|
|
3584
4785
|
if (!assistantHasToolCallAfter(content, index)) finalParts.push(finalPart);
|
|
3585
4786
|
continue;
|
|
3586
4787
|
}
|
|
4788
|
+
if (isEmptyAssistantTextPart(part)) continue;
|
|
3587
4789
|
if (part !== undefined && part !== null) {
|
|
3588
4790
|
displayMessages.push({ ...base, role: "assistantEvent", title: part?.type ? `assistant ${part.type}` : "assistant event", content: part });
|
|
3589
4791
|
}
|
|
3590
4792
|
}
|
|
3591
4793
|
|
|
3592
4794
|
if (finalParts.length > 0) {
|
|
3593
|
-
displayMessages.push({ ...message, title: "
|
|
4795
|
+
displayMessages.push({ ...message, title: "final output", content: finalParts });
|
|
3594
4796
|
}
|
|
3595
4797
|
return displayMessages;
|
|
3596
4798
|
}
|
|
@@ -3684,64 +4886,472 @@ function stickyUserPromptViewportGap() {
|
|
|
3684
4886
|
}
|
|
3685
4887
|
|
|
3686
4888
|
function resetChatOutput() {
|
|
4889
|
+
liveToolCards.clear();
|
|
3687
4890
|
elements.chat.replaceChildren();
|
|
3688
4891
|
if (elements.stickyUserPromptButton) elements.chat.append(elements.stickyUserPromptButton);
|
|
3689
4892
|
}
|
|
3690
4893
|
|
|
3691
|
-
function userPromptTargets() {
|
|
3692
|
-
return [...elements.chat.querySelectorAll('.message[data-user-prompt="true"][data-message-index]')]
|
|
3693
|
-
.map((node) => {
|
|
3694
|
-
const index = Number(node.dataset.messageIndex);
|
|
3695
|
-
if (!Number.isInteger(index)) return null;
|
|
3696
|
-
const message = latestMessages[index];
|
|
3697
|
-
if (!message) return null;
|
|
3698
|
-
return { index, message, node, top: chatScrollTopForNode(node), preview: stickyUserPromptPreview(message) };
|
|
3699
|
-
})
|
|
3700
|
-
.filter(Boolean)
|
|
3701
|
-
.sort((a, b) => a.index - b.index);
|
|
4894
|
+
function userPromptTargets() {
|
|
4895
|
+
return [...elements.chat.querySelectorAll('.message[data-user-prompt="true"][data-message-index]')]
|
|
4896
|
+
.map((node) => {
|
|
4897
|
+
const index = Number(node.dataset.messageIndex);
|
|
4898
|
+
if (!Number.isInteger(index)) return null;
|
|
4899
|
+
const message = latestMessages[index];
|
|
4900
|
+
if (!message) return null;
|
|
4901
|
+
return { index, message, node, top: chatScrollTopForNode(node), preview: stickyUserPromptPreview(message) };
|
|
4902
|
+
})
|
|
4903
|
+
.filter(Boolean)
|
|
4904
|
+
.sort((a, b) => a.index - b.index);
|
|
4905
|
+
}
|
|
4906
|
+
|
|
4907
|
+
function findStickyUserPromptTarget(targets = userPromptTargets()) {
|
|
4908
|
+
if (targets.length === 0) return cachedLastUserPromptTarget();
|
|
4909
|
+
const viewportTop = elements.chat.scrollTop + stickyUserPromptViewportGap();
|
|
4910
|
+
const previousPrompt = targets.filter((target) => target.top < viewportTop - STICKY_USER_PROMPT_TOP_GAP_PX).at(-1);
|
|
4911
|
+
if (previousPrompt) return previousPrompt;
|
|
4912
|
+
|
|
4913
|
+
const latestPrompt = targets.at(-1);
|
|
4914
|
+
const latestTopInView = latestPrompt.top - elements.chat.scrollTop;
|
|
4915
|
+
const latestVisibleNearTop = latestTopInView >= 0 && latestTopInView <= Math.min(elements.chat.clientHeight * 0.55, 180);
|
|
4916
|
+
if (targets.length === 1 && latestVisibleNearTop) return null;
|
|
4917
|
+
return latestPrompt;
|
|
4918
|
+
}
|
|
4919
|
+
|
|
4920
|
+
function updateStickyUserPromptButton() {
|
|
4921
|
+
const button = elements.stickyUserPromptButton;
|
|
4922
|
+
if (!button) return;
|
|
4923
|
+
const targets = userPromptTargets();
|
|
4924
|
+
const target = findStickyUserPromptTarget(targets);
|
|
4925
|
+
if (!target) {
|
|
4926
|
+
button.hidden = true;
|
|
4927
|
+
button.removeAttribute("data-message-index");
|
|
4928
|
+
button.removeAttribute("data-compacted");
|
|
4929
|
+
button.replaceChildren();
|
|
4930
|
+
return;
|
|
4931
|
+
}
|
|
4932
|
+
|
|
4933
|
+
const ordinal = target.compacted ? 1 : targets.findIndex((item) => item.index === target.index) + 1;
|
|
4934
|
+
const isLatest = target.compacted || ordinal === targets.length;
|
|
4935
|
+
const label = target.compacted ? "Last user prompt (compacted)" : isLatest ? "Last user prompt" : "Previous user prompt";
|
|
4936
|
+
const meta = target.compacted ? "summary ↑" : `${ordinal}/${targets.length} ↑`;
|
|
4937
|
+
button.hidden = false;
|
|
4938
|
+
button.dataset.compacted = target.compacted ? "true" : "false";
|
|
4939
|
+
if (Number.isInteger(target.index) && target.index >= 0) button.dataset.messageIndex = String(target.index);
|
|
4940
|
+
else button.removeAttribute("data-message-index");
|
|
4941
|
+
button.title = target.compacted ? `Prompt was compacted; jump to compaction summary: ${target.preview}` : `Jump to ${label.toLowerCase()}: ${target.preview}`;
|
|
4942
|
+
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}`);
|
|
4943
|
+
button.replaceChildren(
|
|
4944
|
+
make("span", "sticky-user-prompt-label", label),
|
|
4945
|
+
make("span", "sticky-user-prompt-text", target.preview),
|
|
4946
|
+
make("span", "sticky-user-prompt-meta", meta),
|
|
4947
|
+
);
|
|
4948
|
+
}
|
|
4949
|
+
|
|
4950
|
+
function assistantToolCallId(part) {
|
|
4951
|
+
const id = part?.id || part?.toolCallId || part?.tool_call_id || part?.toolCall?.id || part?.toolCall?.toolCallId || part?.toolCall?.tool_call_id;
|
|
4952
|
+
return id === undefined || id === null ? "" : String(id);
|
|
4953
|
+
}
|
|
4954
|
+
|
|
4955
|
+
function toolResultCallId(message) {
|
|
4956
|
+
const id = message?.toolCallId || message?.tool_call_id;
|
|
4957
|
+
return id === undefined || id === null ? "" : String(id);
|
|
4958
|
+
}
|
|
4959
|
+
|
|
4960
|
+
function buildToolResultMap(messages = latestMessages) {
|
|
4961
|
+
const results = new Map();
|
|
4962
|
+
for (const message of messages || []) {
|
|
4963
|
+
if (message?.role !== "toolResult") continue;
|
|
4964
|
+
const id = toolResultCallId(message);
|
|
4965
|
+
if (id && !results.has(id)) results.set(id, message);
|
|
4966
|
+
}
|
|
4967
|
+
return results;
|
|
4968
|
+
}
|
|
4969
|
+
|
|
4970
|
+
function buildAssistantToolCallIdSet(messages = latestMessages) {
|
|
4971
|
+
const ids = new Set();
|
|
4972
|
+
for (const message of messages || []) {
|
|
4973
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
4974
|
+
for (const part of message.content) {
|
|
4975
|
+
if (!isAssistantToolCallPart(part)) continue;
|
|
4976
|
+
const id = assistantToolCallId(part);
|
|
4977
|
+
if (id) ids.add(id);
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
return ids;
|
|
4981
|
+
}
|
|
4982
|
+
|
|
4983
|
+
function toolResultForCallId(toolCallId, messages = latestMessages) {
|
|
4984
|
+
const id = String(toolCallId || "");
|
|
4985
|
+
if (!id) return null;
|
|
4986
|
+
for (const message of messages || []) {
|
|
4987
|
+
if (message?.role === "toolResult" && toolResultCallId(message) === id) return message;
|
|
4988
|
+
}
|
|
4989
|
+
return null;
|
|
4990
|
+
}
|
|
4991
|
+
|
|
4992
|
+
function cleanupLiveToolRunsForMessages(messages = latestMessages) {
|
|
4993
|
+
const results = buildToolResultMap(messages);
|
|
4994
|
+
for (const id of liveToolRuns.keys()) {
|
|
4995
|
+
if (results.has(id)) liveToolRuns.delete(id);
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4998
|
+
|
|
4999
|
+
function shortenToolPath(value, fallback = ".") {
|
|
5000
|
+
const path = normalizeDisplayPath(value || fallback);
|
|
5001
|
+
if (path.length <= 96) return path;
|
|
5002
|
+
return `…${path.slice(-95)}`;
|
|
5003
|
+
}
|
|
5004
|
+
|
|
5005
|
+
function toolArgValue(args, keys) {
|
|
5006
|
+
const keyList = Array.isArray(keys) ? keys : [keys];
|
|
5007
|
+
for (const key of keyList) {
|
|
5008
|
+
if (args && Object.prototype.hasOwnProperty.call(args, key)) return args[key];
|
|
5009
|
+
}
|
|
5010
|
+
return undefined;
|
|
5011
|
+
}
|
|
5012
|
+
|
|
5013
|
+
function toolArgText(args, keys, fallback = "") {
|
|
5014
|
+
const value = toolArgValue(args, keys);
|
|
5015
|
+
if (value === undefined || value === null) return fallback;
|
|
5016
|
+
if (typeof value === "string") return value;
|
|
5017
|
+
return String(value);
|
|
5018
|
+
}
|
|
5019
|
+
|
|
5020
|
+
function toolExecutionResult(message) {
|
|
5021
|
+
if (message?.result) return message.result;
|
|
5022
|
+
if (message?.partialResult) return { ...message.partialResult, isError: false };
|
|
5023
|
+
if (message?.role === "toolResult") return message;
|
|
5024
|
+
return null;
|
|
5025
|
+
}
|
|
5026
|
+
|
|
5027
|
+
function toolResultText(result) {
|
|
5028
|
+
if (!result) return "";
|
|
5029
|
+
return stripAnsi(textFromContent(result.content)).replace(/\s+$/g, "");
|
|
5030
|
+
}
|
|
5031
|
+
|
|
5032
|
+
function toolExecutionStatus(message) {
|
|
5033
|
+
const result = toolExecutionResult(message);
|
|
5034
|
+
if (message?.isPartial) return "running";
|
|
5035
|
+
if (!result) return "pending";
|
|
5036
|
+
return message?.isError || result?.isError ? "error" : "success";
|
|
5037
|
+
}
|
|
5038
|
+
|
|
5039
|
+
function toolExecutionTitle(message) {
|
|
5040
|
+
const name = runIndicatorToolName(message?.toolName || message?.name || "tool");
|
|
5041
|
+
const status = toolExecutionStatus(message);
|
|
5042
|
+
if (status === "running") return `tool: ${name} (running)`;
|
|
5043
|
+
if (status === "pending") return `tool: ${name} (pending)`;
|
|
5044
|
+
if (status === "error") return `tool: ${name} (failed)`;
|
|
5045
|
+
return `tool: ${name}`;
|
|
5046
|
+
}
|
|
5047
|
+
|
|
5048
|
+
function toolLineRange(args) {
|
|
5049
|
+
const offset = toolArgValue(args, "offset");
|
|
5050
|
+
const limit = toolArgValue(args, "limit");
|
|
5051
|
+
const start = Number.isFinite(Number(offset)) ? Number(offset) : null;
|
|
5052
|
+
const count = Number.isFinite(Number(limit)) ? Number(limit) : null;
|
|
5053
|
+
if (start === null && count === null) return "";
|
|
5054
|
+
const first = start ?? 1;
|
|
5055
|
+
const last = count === null ? "" : first + count - 1;
|
|
5056
|
+
return `:${first}${last ? `-${last}` : ""}`;
|
|
5057
|
+
}
|
|
5058
|
+
|
|
5059
|
+
function appendToolTitle(parent, name, subject = "", meta = []) {
|
|
5060
|
+
const line = make("div", "tool-title-line");
|
|
5061
|
+
line.append(make("span", "tool-name", name));
|
|
5062
|
+
if (subject) line.append(make("span", "tool-subject", subject));
|
|
5063
|
+
parent.append(line);
|
|
5064
|
+
const items = meta.filter(Boolean);
|
|
5065
|
+
if (items.length > 0) {
|
|
5066
|
+
const metaLine = make("div", "tool-meta-line");
|
|
5067
|
+
for (const item of items) metaLine.append(make("span", "tool-meta-pill", item));
|
|
5068
|
+
parent.append(metaLine);
|
|
5069
|
+
}
|
|
5070
|
+
}
|
|
5071
|
+
|
|
5072
|
+
function appendToolCommand(parent, command, meta = []) {
|
|
5073
|
+
const line = make("pre", "tool-command-line");
|
|
5074
|
+
line.textContent = `$ ${command || "..."}`;
|
|
5075
|
+
parent.append(line);
|
|
5076
|
+
const items = meta.filter(Boolean);
|
|
5077
|
+
if (items.length > 0) {
|
|
5078
|
+
const metaLine = make("div", "tool-meta-line");
|
|
5079
|
+
for (const item of items) metaLine.append(make("span", "tool-meta-pill", item));
|
|
5080
|
+
parent.append(metaLine);
|
|
5081
|
+
}
|
|
5082
|
+
}
|
|
5083
|
+
|
|
5084
|
+
function appendToolImages(parent, result) {
|
|
5085
|
+
if (!Array.isArray(result?.content)) return;
|
|
5086
|
+
for (const part of result.content) {
|
|
5087
|
+
if (part?.type === "image") appendImage(parent, part);
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
|
|
5091
|
+
function appendToolOutput(parent, text, { label = "output", previewLines = 10, previewFromEnd = false, open = false, emptyText = "" } = {}) {
|
|
5092
|
+
const clean = stripAnsi(text).replace(/\s+$/g, "");
|
|
5093
|
+
if (!clean) {
|
|
5094
|
+
if (emptyText) appendText(parent, emptyText, "code-block tool-output-code muted-output");
|
|
5095
|
+
return;
|
|
5096
|
+
}
|
|
5097
|
+
const lines = clean.split(/\r?\n/);
|
|
5098
|
+
if (lines.length > previewLines) {
|
|
5099
|
+
const details = make("details", "tool-output-details");
|
|
5100
|
+
details.open = open;
|
|
5101
|
+
details.append(make("summary", "tool-output-summary", `${label} (${lines.length} lines; expand)`));
|
|
5102
|
+
appendText(details, clean, "code-block tool-output-code");
|
|
5103
|
+
parent.append(details);
|
|
5104
|
+
|
|
5105
|
+
const preview = make("div", "tool-output-preview");
|
|
5106
|
+
const visibleLines = previewFromEnd ? lines.slice(-previewLines) : lines.slice(0, previewLines);
|
|
5107
|
+
const omitted = lines.length - visibleLines.length;
|
|
5108
|
+
const hint = previewFromEnd
|
|
5109
|
+
? `… ${omitted} earlier line${omitted === 1 ? "" : "s"}; expand for full output`
|
|
5110
|
+
: `… ${omitted} more line${omitted === 1 ? "" : "s"}; expand for full output`;
|
|
5111
|
+
appendText(preview, `${visibleLines.join("\n")}\n${hint}`, "code-block tool-output-code tool-output-preview-text");
|
|
5112
|
+
parent.append(preview);
|
|
5113
|
+
return;
|
|
5114
|
+
}
|
|
5115
|
+
appendText(parent, clean, "code-block tool-output-code");
|
|
5116
|
+
}
|
|
5117
|
+
|
|
5118
|
+
function appendToolWarnings(parent, details = {}) {
|
|
5119
|
+
const warnings = [];
|
|
5120
|
+
if (details.fullOutputPath) warnings.push(`Full output: ${details.fullOutputPath}`);
|
|
5121
|
+
const truncation = details.truncation;
|
|
5122
|
+
if (truncation?.truncated) {
|
|
5123
|
+
if (truncation.truncatedBy === "lines") warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
|
|
5124
|
+
else if (truncation.outputLines) warnings.push(`Truncated: ${truncation.outputLines} lines shown`);
|
|
5125
|
+
else warnings.push("Output truncated");
|
|
5126
|
+
}
|
|
5127
|
+
if (details.matchLimitReached) warnings.push(`Match limit reached: ${details.matchLimitReached}`);
|
|
5128
|
+
if (details.resultLimitReached) warnings.push(`Result limit reached: ${details.resultLimitReached}`);
|
|
5129
|
+
if (details.entryLimitReached) warnings.push(`Entry limit reached: ${details.entryLimitReached}`);
|
|
5130
|
+
if (warnings.length === 0) return;
|
|
5131
|
+
const box = make("div", "tool-warnings");
|
|
5132
|
+
for (const warning of warnings) box.append(make("div", "tool-warning", warning));
|
|
5133
|
+
parent.append(box);
|
|
5134
|
+
}
|
|
5135
|
+
|
|
5136
|
+
function appendToolDiff(parent, diff) {
|
|
5137
|
+
const value = String(diff || "").replace(/\s+$/g, "");
|
|
5138
|
+
if (!value) return false;
|
|
5139
|
+
const block = make("div", "tool-diff");
|
|
5140
|
+
for (const line of value.split(/\r?\n/)) {
|
|
5141
|
+
const cls = /^@@/.test(line)
|
|
5142
|
+
? "diff-hunk"
|
|
5143
|
+
: /^\+/.test(line) && !/^\+\+\+/.test(line)
|
|
5144
|
+
? "diff-added"
|
|
5145
|
+
: /^-/.test(line) && !/^---/.test(line)
|
|
5146
|
+
? "diff-removed"
|
|
5147
|
+
: /^(?:\+\+\+|---)/.test(line)
|
|
5148
|
+
? "diff-file"
|
|
5149
|
+
: "diff-context";
|
|
5150
|
+
block.append(make("div", cls, line || " "));
|
|
5151
|
+
}
|
|
5152
|
+
parent.append(block);
|
|
5153
|
+
return true;
|
|
5154
|
+
}
|
|
5155
|
+
|
|
5156
|
+
function normalizeToolExecution(message) {
|
|
5157
|
+
const result = toolExecutionResult(message);
|
|
5158
|
+
const args = message?.arguments ?? message?.args ?? {};
|
|
5159
|
+
const name = runIndicatorToolName(message?.toolName || message?.name || "tool");
|
|
5160
|
+
return {
|
|
5161
|
+
name,
|
|
5162
|
+
args,
|
|
5163
|
+
result,
|
|
5164
|
+
text: toolResultText(result),
|
|
5165
|
+
details: result?.details || message?.details || {},
|
|
5166
|
+
isPartial: !!message?.isPartial,
|
|
5167
|
+
isError: !!(message?.isError || result?.isError),
|
|
5168
|
+
startedAt: message?.startedAt || null,
|
|
5169
|
+
endedAt: message?.endedAt || null,
|
|
5170
|
+
};
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
function toolElapsedLabel(tool) {
|
|
5174
|
+
if (!tool.startedAt) return "";
|
|
5175
|
+
const end = tool.endedAt || Date.now();
|
|
5176
|
+
return `${tool.isPartial ? "elapsed" : "took"} ${formatDuration(end - tool.startedAt)}`;
|
|
5177
|
+
}
|
|
5178
|
+
|
|
5179
|
+
function toolStatusLabel(tool) {
|
|
5180
|
+
if (tool.isPartial) return "live";
|
|
5181
|
+
if (tool.isError) return "failed";
|
|
5182
|
+
if (tool.result) return "done";
|
|
5183
|
+
return "pending";
|
|
5184
|
+
}
|
|
5185
|
+
|
|
5186
|
+
function toolStateMeta(tool) {
|
|
5187
|
+
return [toolElapsedLabel(tool), toolStatusLabel(tool)];
|
|
5188
|
+
}
|
|
5189
|
+
|
|
5190
|
+
function toolLineCountLabel(text, label = "line") {
|
|
5191
|
+
const value = String(text || "").replace(/\s+$/g, "");
|
|
5192
|
+
if (!value) return "";
|
|
5193
|
+
const count = value.split(/\r?\n/).length;
|
|
5194
|
+
return `${count} ${label}${count === 1 ? "" : "s"}`;
|
|
5195
|
+
}
|
|
5196
|
+
|
|
5197
|
+
function toolRawDetailsReplacer(key, value) {
|
|
5198
|
+
if (typeof value === "string" && value.length > 4000) return `${value.slice(0, 4000)}… (${value.length - 4000} chars omitted)`;
|
|
5199
|
+
return value;
|
|
5200
|
+
}
|
|
5201
|
+
|
|
5202
|
+
function appendToolRawDetails(parent, tool) {
|
|
5203
|
+
const raw = JSON.stringify({ arguments: tool.args ?? {}, result: tool.result ?? null, details: tool.details ?? {} }, toolRawDetailsReplacer, 2);
|
|
5204
|
+
const details = make("details", "tool-raw-details");
|
|
5205
|
+
details.append(make("summary", "tool-raw-summary", "raw tool data"));
|
|
5206
|
+
appendText(details, raw, "code-block tool-raw-code");
|
|
5207
|
+
parent.append(details);
|
|
5208
|
+
}
|
|
5209
|
+
|
|
5210
|
+
function renderBashToolExecution(parent, tool) {
|
|
5211
|
+
const command = toolArgText(tool.args, "command", "");
|
|
5212
|
+
const timeout = toolArgValue(tool.args, "timeout");
|
|
5213
|
+
const meta = [timeout ? `timeout ${timeout}s` : "", ...toolStateMeta(tool)];
|
|
5214
|
+
appendToolCommand(parent, command, meta);
|
|
5215
|
+
appendToolOutput(parent, tool.text, { label: tool.isPartial ? "live output" : "output", previewLines: 5, previewFromEnd: true, open: tool.isError, emptyText: tool.isPartial ? "(no output yet)" : "" });
|
|
5216
|
+
appendToolWarnings(parent, tool.details);
|
|
5217
|
+
}
|
|
5218
|
+
|
|
5219
|
+
function renderReadToolExecution(parent, tool) {
|
|
5220
|
+
const path = toolArgText(tool.args, ["file_path", "path"], "");
|
|
5221
|
+
appendToolTitle(parent, "read", `${shortenToolPath(path)}${toolLineRange(tool.args)}`, [toolLineCountLabel(tool.text), ...toolStateMeta(tool)]);
|
|
5222
|
+
appendToolImages(parent, tool.result);
|
|
5223
|
+
appendToolOutput(parent, tool.text, { label: "file output", previewLines: 10, open: tool.isError });
|
|
5224
|
+
appendToolWarnings(parent, tool.details);
|
|
5225
|
+
}
|
|
5226
|
+
|
|
5227
|
+
function renderWriteToolExecution(parent, tool) {
|
|
5228
|
+
const path = toolArgText(tool.args, ["file_path", "path"], "");
|
|
5229
|
+
const content = toolArgText(tool.args, "content", "");
|
|
5230
|
+
const lineCount = content ? content.split(/\r?\n/).length : 0;
|
|
5231
|
+
appendToolTitle(parent, "write", shortenToolPath(path), [lineCount > 0 ? `${lineCount} line${lineCount === 1 ? "" : "s"}` : "", ...toolStateMeta(tool)]);
|
|
5232
|
+
appendToolOutput(parent, content, { label: "content", previewLines: 10 });
|
|
5233
|
+
appendToolOutput(parent, tool.text, { label: "result", previewLines: 6, open: tool.isError });
|
|
5234
|
+
}
|
|
5235
|
+
|
|
5236
|
+
function renderEditToolExecution(parent, tool) {
|
|
5237
|
+
const path = toolArgText(tool.args, ["file_path", "path"], "");
|
|
5238
|
+
const edits = Array.isArray(tool.args?.edits) ? tool.args.edits.length : 0;
|
|
5239
|
+
appendToolTitle(parent, "edit", shortenToolPath(path), [edits ? `${edits} replacement${edits === 1 ? "" : "s"}` : "", ...toolStateMeta(tool)]);
|
|
5240
|
+
const hasDiff = appendToolDiff(parent, tool.details?.diff || tool.details?.patch);
|
|
5241
|
+
appendToolOutput(parent, tool.text, { label: "result", previewLines: hasDiff ? 4 : 10, open: tool.isError });
|
|
5242
|
+
}
|
|
5243
|
+
|
|
5244
|
+
function renderGrepToolExecution(parent, tool) {
|
|
5245
|
+
const pattern = toolArgText(tool.args, "pattern", "");
|
|
5246
|
+
const path = toolArgText(tool.args, "path", ".");
|
|
5247
|
+
appendToolTitle(parent, "grep", `/${pattern || "…"}/ in ${shortenToolPath(path)}`, [tool.args?.glob ? `glob ${tool.args.glob}` : "", tool.args?.ignoreCase ? "ignore-case" : "", tool.args?.literal ? "literal" : "", toolLineCountLabel(tool.text, "match line"), ...toolStateMeta(tool)]);
|
|
5248
|
+
appendToolOutput(parent, tool.text, { label: "matches", previewLines: 10, open: tool.isError });
|
|
5249
|
+
appendToolWarnings(parent, tool.details);
|
|
5250
|
+
}
|
|
5251
|
+
|
|
5252
|
+
function renderFindToolExecution(parent, tool) {
|
|
5253
|
+
const pattern = toolArgText(tool.args, "pattern", "");
|
|
5254
|
+
const path = toolArgText(tool.args, "path", ".");
|
|
5255
|
+
appendToolTitle(parent, "find", `${pattern || "…"} in ${shortenToolPath(path)}`, [tool.args?.limit ? `limit ${tool.args.limit}` : "", toolLineCountLabel(tool.text, "result"), ...toolStateMeta(tool)]);
|
|
5256
|
+
appendToolOutput(parent, tool.text, { label: "results", previewLines: 10, open: tool.isError });
|
|
5257
|
+
appendToolWarnings(parent, tool.details);
|
|
5258
|
+
}
|
|
5259
|
+
|
|
5260
|
+
function renderLsToolExecution(parent, tool) {
|
|
5261
|
+
const path = toolArgText(tool.args, "path", ".");
|
|
5262
|
+
appendToolTitle(parent, "ls", shortenToolPath(path), [tool.args?.limit ? `limit ${tool.args.limit}` : "", toolLineCountLabel(tool.text, "entry"), ...toolStateMeta(tool)]);
|
|
5263
|
+
appendToolOutput(parent, tool.text, { label: "entries", previewLines: 20, open: tool.isError });
|
|
5264
|
+
appendToolWarnings(parent, tool.details);
|
|
5265
|
+
}
|
|
5266
|
+
|
|
5267
|
+
function renderGenericToolExecution(parent, tool) {
|
|
5268
|
+
appendToolTitle(parent, tool.name, "", toolStateMeta(tool));
|
|
5269
|
+
appendToolOutput(parent, JSON.stringify(tool.args ?? {}, null, 2), { label: "arguments", previewLines: 12 });
|
|
5270
|
+
appendToolImages(parent, tool.result);
|
|
5271
|
+
appendToolOutput(parent, tool.text, { label: "result", previewLines: 10, open: tool.isError });
|
|
5272
|
+
appendToolWarnings(parent, tool.details);
|
|
5273
|
+
}
|
|
5274
|
+
|
|
5275
|
+
const WEBUI_TOOL_RENDERERS = {
|
|
5276
|
+
bash: renderBashToolExecution,
|
|
5277
|
+
read: renderReadToolExecution,
|
|
5278
|
+
write: renderWriteToolExecution,
|
|
5279
|
+
edit: renderEditToolExecution,
|
|
5280
|
+
grep: renderGrepToolExecution,
|
|
5281
|
+
find: renderFindToolExecution,
|
|
5282
|
+
ls: renderLsToolExecution,
|
|
5283
|
+
};
|
|
5284
|
+
|
|
5285
|
+
function renderToolExecution(parent, message) {
|
|
5286
|
+
const tool = normalizeToolExecution(message);
|
|
5287
|
+
const renderer = WEBUI_TOOL_RENDERERS[tool.name] || renderGenericToolExecution;
|
|
5288
|
+
renderer(parent, tool);
|
|
5289
|
+
appendToolRawDetails(parent, tool);
|
|
5290
|
+
}
|
|
5291
|
+
|
|
5292
|
+
function liveToolRunMessage(run) {
|
|
5293
|
+
return {
|
|
5294
|
+
role: "toolExecution",
|
|
5295
|
+
title: toolExecutionTitle(run),
|
|
5296
|
+
toolName: run.toolName,
|
|
5297
|
+
toolCallId: run.toolCallId,
|
|
5298
|
+
arguments: run.arguments,
|
|
5299
|
+
result: run.result,
|
|
5300
|
+
isPartial: run.isPartial,
|
|
5301
|
+
isError: run.isError,
|
|
5302
|
+
startedAt: run.startedAt,
|
|
5303
|
+
endedAt: run.endedAt,
|
|
5304
|
+
timestamp: run.timestamp,
|
|
5305
|
+
live: true,
|
|
5306
|
+
};
|
|
5307
|
+
}
|
|
5308
|
+
|
|
5309
|
+
function renderLiveToolRun(run, { scroll = true } = {}) {
|
|
5310
|
+
if (!run?.toolCallId) return;
|
|
5311
|
+
const existing = liveToolCards.get(run.toolCallId);
|
|
5312
|
+
const shouldFollow = scroll && (autoFollowChat || isChatNearBottom());
|
|
5313
|
+
const created = appendMessage(liveToolRunMessage(run), { transient: true, animateEntry: !existing });
|
|
5314
|
+
if (existing?.isConnected && existing !== created.bubble) existing.replaceWith(created.bubble);
|
|
5315
|
+
renderRunIndicator({ scroll: false });
|
|
5316
|
+
if (shouldFollow) scrollChatToBottom();
|
|
3702
5317
|
}
|
|
3703
5318
|
|
|
3704
|
-
function
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
const
|
|
3708
|
-
|
|
5319
|
+
function upsertLiveToolRun(event, patch = {}) {
|
|
5320
|
+
const id = String(event.toolCallId || "");
|
|
5321
|
+
if (!id) return null;
|
|
5322
|
+
const existing = liveToolRuns.get(id) || {};
|
|
5323
|
+
const now = Date.now();
|
|
5324
|
+
const run = {
|
|
5325
|
+
...existing,
|
|
5326
|
+
role: "toolExecution",
|
|
5327
|
+
live: true,
|
|
5328
|
+
toolCallId: id,
|
|
5329
|
+
toolName: event.toolName || existing.toolName || "tool",
|
|
5330
|
+
arguments: event.args ?? existing.arguments ?? {},
|
|
5331
|
+
timestamp: existing.timestamp || now,
|
|
5332
|
+
startedAt: existing.startedAt || now,
|
|
5333
|
+
updatedAt: now,
|
|
5334
|
+
...patch,
|
|
5335
|
+
};
|
|
5336
|
+
liveToolRuns.set(id, run);
|
|
5337
|
+
return run;
|
|
5338
|
+
}
|
|
3709
5339
|
|
|
3710
|
-
|
|
3711
|
-
const
|
|
3712
|
-
|
|
3713
|
-
if (targets.length === 1 && latestVisibleNearTop) return null;
|
|
3714
|
-
return latestPrompt;
|
|
5340
|
+
function handleToolExecutionStart(event) {
|
|
5341
|
+
const run = upsertLiveToolRun(event, { isPartial: true, isError: false });
|
|
5342
|
+
if (run) renderLiveToolRun(run);
|
|
3715
5343
|
}
|
|
3716
5344
|
|
|
3717
|
-
function
|
|
3718
|
-
const
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
if (!target) {
|
|
3723
|
-
button.hidden = true;
|
|
3724
|
-
button.removeAttribute("data-message-index");
|
|
3725
|
-
button.removeAttribute("data-compacted");
|
|
3726
|
-
button.replaceChildren();
|
|
3727
|
-
return;
|
|
3728
|
-
}
|
|
5345
|
+
function handleToolExecutionUpdate(event) {
|
|
5346
|
+
const result = { ...(event.partialResult || {}), isError: false };
|
|
5347
|
+
const run = upsertLiveToolRun(event, { result, isPartial: true, isError: false });
|
|
5348
|
+
if (run) renderLiveToolRun(run, { scroll: false });
|
|
5349
|
+
}
|
|
3729
5350
|
|
|
3730
|
-
|
|
3731
|
-
const
|
|
3732
|
-
const
|
|
3733
|
-
|
|
3734
|
-
button.hidden = false;
|
|
3735
|
-
button.dataset.compacted = target.compacted ? "true" : "false";
|
|
3736
|
-
if (Number.isInteger(target.index) && target.index >= 0) button.dataset.messageIndex = String(target.index);
|
|
3737
|
-
else button.removeAttribute("data-message-index");
|
|
3738
|
-
button.title = target.compacted ? `Prompt was compacted; jump to compaction summary: ${target.preview}` : `Jump to ${label.toLowerCase()}: ${target.preview}`;
|
|
3739
|
-
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}`);
|
|
3740
|
-
button.replaceChildren(
|
|
3741
|
-
make("span", "sticky-user-prompt-label", label),
|
|
3742
|
-
make("span", "sticky-user-prompt-text", target.preview),
|
|
3743
|
-
make("span", "sticky-user-prompt-meta", meta),
|
|
3744
|
-
);
|
|
5351
|
+
function handleToolExecutionEnd(event) {
|
|
5352
|
+
const result = { ...(event.result || {}), isError: !!event.isError };
|
|
5353
|
+
const run = upsertLiveToolRun(event, { result, isPartial: false, isError: !!event.isError, endedAt: Date.now() });
|
|
5354
|
+
if (run) renderLiveToolRun(run);
|
|
3745
5355
|
}
|
|
3746
5356
|
|
|
3747
5357
|
function toolResultPreviewText(message, lineLimit = 10) {
|
|
@@ -3771,12 +5381,23 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
3771
5381
|
const role = String(message.role || "message");
|
|
3772
5382
|
const safeRole = role.replace(/[^a-z0-9_-]/gi, "");
|
|
3773
5383
|
const bubble = make("article", `message ${safeRole}${message.level ? ` ${message.level}` : ""}${streaming ? " streaming" : ""}${animateEntry ? " action-enter" : ""}`);
|
|
5384
|
+
if (message.role === "toolExecution") {
|
|
5385
|
+
const status = toolExecutionStatus(message);
|
|
5386
|
+
bubble.classList.add(`tool-${status}`);
|
|
5387
|
+
if (message.isError || status === "error") bubble.classList.add("error");
|
|
5388
|
+
if (message.toolCallId) {
|
|
5389
|
+
bubble.dataset.toolCallId = String(message.toolCallId);
|
|
5390
|
+
if (message.live) liveToolCards.set(String(message.toolCallId), bubble);
|
|
5391
|
+
}
|
|
5392
|
+
}
|
|
3774
5393
|
if (!transient && messageIndex >= 0) {
|
|
3775
5394
|
bubble.dataset.messageIndex = String(messageIndex);
|
|
3776
5395
|
if (role === "user") bubble.dataset.userPrompt = "true";
|
|
3777
5396
|
}
|
|
3778
5397
|
const isCollapsibleOutput = !streaming && (message.role === "toolResult" || message.role === "bashExecution" || message.role === "compactionSummary");
|
|
3779
5398
|
|
|
5399
|
+
const hideMessageHeader = message.role === "assistant" && !isCollapsibleOutput;
|
|
5400
|
+
if (hideMessageHeader) bubble.setAttribute("aria-label", messageTitle(message));
|
|
3780
5401
|
const header = make(isCollapsibleOutput ? "summary" : "div", "message-header");
|
|
3781
5402
|
header.append(make("span", "message-role", messageTitle(message)));
|
|
3782
5403
|
header.append(make("span", "muted", formatDate(message.timestamp)));
|
|
@@ -3789,15 +5410,17 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
3789
5410
|
} else if (message.role === "toolResult") {
|
|
3790
5411
|
renderContent(body, message.content);
|
|
3791
5412
|
if (message.isError) bubble.classList.add("error");
|
|
5413
|
+
} else if (message.role === "toolExecution") {
|
|
5414
|
+
renderToolExecution(body, message);
|
|
3792
5415
|
} else if (message.role === "thinking") {
|
|
3793
5416
|
const thinkingText = message.thinking || textFromContent(message.content);
|
|
3794
|
-
if (thinkingText || !streaming) appendText(body, thinkingText || "No thinking content was exposed by the provider.", "thinking-text");
|
|
5417
|
+
if (thinkingOutputVisible && (thinkingText || !streaming)) appendText(body, thinkingText || "No thinking content was exposed by the provider.", "thinking-text");
|
|
3795
5418
|
} else if (message.role === "toolCall") {
|
|
3796
5419
|
appendText(body, JSON.stringify(message.arguments ?? message.content ?? {}, null, 2), "code-block");
|
|
3797
5420
|
} else if (message.role === "assistantEvent") {
|
|
3798
5421
|
appendText(body, typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? {}, null, 2), "code-block");
|
|
3799
5422
|
} else {
|
|
3800
|
-
renderContent(body, message.content);
|
|
5423
|
+
renderContent(body, message.content, { markdown: message.role === "assistant" });
|
|
3801
5424
|
}
|
|
3802
5425
|
|
|
3803
5426
|
if (isCollapsibleOutput) {
|
|
@@ -3810,6 +5433,8 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
3810
5433
|
appendText(preview, toolResultPreviewText(message, 10), "code-block tool-result-preview-text");
|
|
3811
5434
|
bubble.append(preview);
|
|
3812
5435
|
}
|
|
5436
|
+
} else if (hideMessageHeader) {
|
|
5437
|
+
bubble.append(body);
|
|
3813
5438
|
} else {
|
|
3814
5439
|
bubble.append(header, body);
|
|
3815
5440
|
}
|
|
@@ -3826,13 +5451,31 @@ function appendTranscriptMessage(message, { streaming = false, messageIndex = -1
|
|
|
3826
5451
|
let finalOutput = null;
|
|
3827
5452
|
const displayMessages = assistantDisplayMessages(message);
|
|
3828
5453
|
displayMessages.forEach((displayMessage) => {
|
|
3829
|
-
|
|
5454
|
+
let transcriptMessage = displayMessage;
|
|
5455
|
+
if (displayMessage.role === "toolCall" && displayMessage.toolCallId) {
|
|
5456
|
+
const result = toolResultForCallId(displayMessage.toolCallId);
|
|
5457
|
+
const liveRun = liveToolRuns.get(displayMessage.toolCallId);
|
|
5458
|
+
transcriptMessage = {
|
|
5459
|
+
...displayMessage,
|
|
5460
|
+
role: "toolExecution",
|
|
5461
|
+
title: `tool: ${displayMessage.toolName || "unknown"}`,
|
|
5462
|
+
arguments: liveRun?.arguments ?? displayMessage.arguments,
|
|
5463
|
+
result: result || liveRun?.result || null,
|
|
5464
|
+
isPartial: !result && !!liveRun?.isPartial,
|
|
5465
|
+
isError: !!(result?.isError || liveRun?.isError),
|
|
5466
|
+
startedAt: liveRun?.startedAt || null,
|
|
5467
|
+
endedAt: liveRun?.endedAt || null,
|
|
5468
|
+
live: !!liveRun && !result,
|
|
5469
|
+
};
|
|
5470
|
+
}
|
|
5471
|
+
if (transcriptMessage.role === "thinking" && !thinkingOutputVisible) return;
|
|
5472
|
+
const created = appendMessage(transcriptMessage, {
|
|
3830
5473
|
streaming: false,
|
|
3831
|
-
messageIndex:
|
|
5474
|
+
messageIndex: ["assistant", "toolExecution"].includes(transcriptMessage.role) ? messageIndex : -1,
|
|
3832
5475
|
transient: false,
|
|
3833
|
-
animateEntry: animateEntry && isActionTranscriptMessage(
|
|
5476
|
+
animateEntry: animateEntry && isActionTranscriptMessage(transcriptMessage),
|
|
3834
5477
|
});
|
|
3835
|
-
if (
|
|
5478
|
+
if (transcriptMessage.role === "assistant") finalOutput = created;
|
|
3836
5479
|
});
|
|
3837
5480
|
return finalOutput;
|
|
3838
5481
|
}
|
|
@@ -3850,25 +5493,29 @@ function clearRunIndicatorGraceCheck() {
|
|
|
3850
5493
|
runIndicatorGraceCheckTimer = null;
|
|
3851
5494
|
}
|
|
3852
5495
|
|
|
3853
|
-
function scheduleRunIndicatorGraceCheck() {
|
|
5496
|
+
function scheduleRunIndicatorGraceCheck(tabContext = activeTabContext()) {
|
|
3854
5497
|
if (!runIndicatorLocallyActive || stateHasRunIndicatorActivity(currentState) || !runIndicatorStartedAt) return;
|
|
3855
5498
|
const elapsedMs = performance.now() - runIndicatorStartedAt;
|
|
3856
5499
|
const delayMs = Math.max(120, RUN_INDICATOR_START_GRACE_MS - elapsedMs + 120);
|
|
3857
5500
|
clearRunIndicatorGraceCheck();
|
|
3858
5501
|
runIndicatorGraceCheckTimer = setTimeout(() => {
|
|
3859
5502
|
runIndicatorGraceCheckTimer = null;
|
|
3860
|
-
if (!runIndicatorLocallyActive || stateHasRunIndicatorActivity(currentState)) return;
|
|
5503
|
+
if (!isCurrentTabContext(tabContext) || !runIndicatorLocallyActive || stateHasRunIndicatorActivity(currentState)) return;
|
|
3861
5504
|
runIndicatorLastStateCheckAt = performance.now();
|
|
3862
|
-
refreshState().catch((error) =>
|
|
5505
|
+
refreshState(tabContext).catch((error) => {
|
|
5506
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5507
|
+
});
|
|
3863
5508
|
}, delayMs);
|
|
3864
5509
|
}
|
|
3865
5510
|
|
|
3866
|
-
function maybeRefreshRunIndicatorState() {
|
|
5511
|
+
function maybeRefreshRunIndicatorState(tabContext = activeTabContext()) {
|
|
3867
5512
|
if (!runIndicatorIsActive()) return;
|
|
3868
5513
|
const now = performance.now();
|
|
3869
5514
|
if (now - runIndicatorLastStateCheckAt < RUN_INDICATOR_STATE_RECHECK_MS) return;
|
|
3870
5515
|
runIndicatorLastStateCheckAt = now;
|
|
3871
|
-
refreshState().catch((error) =>
|
|
5516
|
+
refreshState(tabContext).catch((error) => {
|
|
5517
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5518
|
+
});
|
|
3872
5519
|
}
|
|
3873
5520
|
|
|
3874
5521
|
function formatRunIndicatorElapsed() {
|
|
@@ -3965,6 +5612,7 @@ function setRunIndicatorActivity(activity, { active = true, scroll = true } = {}
|
|
|
3965
5612
|
}
|
|
3966
5613
|
runIndicatorActivity = activity || runIndicatorActivity || "Waiting for output or action…";
|
|
3967
5614
|
renderRunIndicator({ scroll });
|
|
5615
|
+
updateComposerModeButtons();
|
|
3968
5616
|
if (active) scheduleRunIndicatorGraceCheck();
|
|
3969
5617
|
}
|
|
3970
5618
|
|
|
@@ -3975,6 +5623,7 @@ function clearRunIndicatorActivity({ render = true } = {}) {
|
|
|
3975
5623
|
runIndicatorStartedAt = null;
|
|
3976
5624
|
runIndicatorActivity = "Waiting for output or action…";
|
|
3977
5625
|
if (render) renderRunIndicator();
|
|
5626
|
+
updateComposerModeButtons();
|
|
3978
5627
|
}
|
|
3979
5628
|
|
|
3980
5629
|
function syncRunIndicatorFromState(state = currentState) {
|
|
@@ -3994,15 +5643,21 @@ function syncRunIndicatorFromState(state = currentState) {
|
|
|
3994
5643
|
} else {
|
|
3995
5644
|
renderRunIndicator();
|
|
3996
5645
|
}
|
|
5646
|
+
updateComposerModeButtons();
|
|
3997
5647
|
}
|
|
3998
5648
|
|
|
3999
5649
|
function runIndicatorToolName(name) {
|
|
4000
5650
|
return cleanStatusText(name || "tool") || "tool";
|
|
4001
5651
|
}
|
|
4002
5652
|
|
|
4003
|
-
function scheduleAbortStateChecks() {
|
|
5653
|
+
function scheduleAbortStateChecks(tabContext = activeTabContext()) {
|
|
4004
5654
|
for (const delay of [250, 900, 1800, 3600]) {
|
|
4005
|
-
setTimeout(() =>
|
|
5655
|
+
setTimeout(() => {
|
|
5656
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5657
|
+
refreshState(tabContext).catch((error) => {
|
|
5658
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5659
|
+
});
|
|
5660
|
+
}, delay);
|
|
4006
5661
|
}
|
|
4007
5662
|
}
|
|
4008
5663
|
|
|
@@ -4014,7 +5669,7 @@ function messageTimestampMs(message) {
|
|
|
4014
5669
|
}
|
|
4015
5670
|
|
|
4016
5671
|
function isActionTranscriptMessage(message) {
|
|
4017
|
-
return ["assistantEvent", "bashExecution", "toolCall", "toolResult"].includes(message?.role);
|
|
5672
|
+
return ["assistantEvent", "bashExecution", "toolCall", "toolExecution", "toolResult"].includes(message?.role);
|
|
4018
5673
|
}
|
|
4019
5674
|
|
|
4020
5675
|
function assistantMessageHasActionContent(message) {
|
|
@@ -4042,6 +5697,7 @@ function actionEntryKey(item) {
|
|
|
4042
5697
|
item?.messageIndex ?? -1,
|
|
4043
5698
|
message.role || "message",
|
|
4044
5699
|
message.toolName || "",
|
|
5700
|
+
message.toolCallId || "",
|
|
4045
5701
|
message.command || "",
|
|
4046
5702
|
message.title || "",
|
|
4047
5703
|
message.timestamp || "",
|
|
@@ -4065,12 +5721,22 @@ function rememberActionEntries(items) {
|
|
|
4065
5721
|
|
|
4066
5722
|
function orderedTranscriptItems() {
|
|
4067
5723
|
const items = [];
|
|
5724
|
+
const assistantToolCallIds = buildAssistantToolCallIdSet(latestMessages);
|
|
5725
|
+
const toolResults = buildToolResultMap(latestMessages);
|
|
4068
5726
|
latestMessages.forEach((message, index) => {
|
|
5727
|
+
const resultId = message?.role === "toolResult" ? toolResultCallId(message) : "";
|
|
5728
|
+
if (resultId && assistantToolCallIds.has(resultId)) return;
|
|
4069
5729
|
items.push({ message, messageIndex: index, transient: false, timestampMs: messageTimestampMs(message), order: index });
|
|
4070
5730
|
});
|
|
4071
5731
|
transientMessages.forEach((message, index) => {
|
|
4072
5732
|
items.push({ message, messageIndex: index, transient: true, timestampMs: messageTimestampMs(message), order: latestMessages.length + index });
|
|
4073
5733
|
});
|
|
5734
|
+
let liveOrder = latestMessages.length + transientMessages.length;
|
|
5735
|
+
for (const [toolCallId, run] of liveToolRuns.entries()) {
|
|
5736
|
+
if (assistantToolCallIds.has(toolCallId) || toolResults.has(toolCallId)) continue;
|
|
5737
|
+
const message = liveToolRunMessage(run);
|
|
5738
|
+
items.push({ message, messageIndex: -1, transient: true, timestampMs: messageTimestampMs(message), order: liveOrder++ });
|
|
5739
|
+
}
|
|
4074
5740
|
return items.sort((a, b) => a.timestampMs - b.timestampMs || a.order - b.order);
|
|
4075
5741
|
}
|
|
4076
5742
|
|
|
@@ -4222,7 +5888,7 @@ function showComposerButtonTooltip(button) {
|
|
|
4222
5888
|
}
|
|
4223
5889
|
|
|
4224
5890
|
function sendPromptFromModeButton(kind, button) {
|
|
4225
|
-
if (!
|
|
5891
|
+
if (!hasComposerPayload()) {
|
|
4226
5892
|
showComposerButtonTooltip(button);
|
|
4227
5893
|
return;
|
|
4228
5894
|
}
|
|
@@ -4245,6 +5911,7 @@ function optionalFeatureIdForCommand(name) {
|
|
|
4245
5911
|
}
|
|
4246
5912
|
|
|
4247
5913
|
function isCommandVisible(command) {
|
|
5914
|
+
if (HIDDEN_COMMAND_NAMES.has(command.name)) return false;
|
|
4248
5915
|
const featureId = optionalFeatureIdForCommand(command.name);
|
|
4249
5916
|
return !featureId || isOptionalFeatureEnabled(featureId);
|
|
4250
5917
|
}
|
|
@@ -4264,13 +5931,31 @@ function optionalFeatureUnavailableMessage(featureId) {
|
|
|
4264
5931
|
return `${feature.label} unavailable: ${feature.capabilityLabel} is not loaded. Install or enable ${feature.packageName}.`;
|
|
4265
5932
|
}
|
|
4266
5933
|
|
|
5934
|
+
function rememberOptionalControlDefault(button, key, value) {
|
|
5935
|
+
if (!(key in button.dataset)) button.dataset[key] = value || "";
|
|
5936
|
+
}
|
|
5937
|
+
|
|
4267
5938
|
function setOptionalControlState(button, available, unavailableTitle) {
|
|
4268
5939
|
if (!button) return;
|
|
4269
|
-
|
|
5940
|
+
rememberOptionalControlDefault(button, "defaultTitle", button.getAttribute("title"));
|
|
5941
|
+
rememberOptionalControlDefault(button, "defaultAriaLabel", button.getAttribute("aria-label"));
|
|
5942
|
+
if (button.hasAttribute("data-tooltip")) rememberOptionalControlDefault(button, "defaultTooltip", button.getAttribute("data-tooltip"));
|
|
5943
|
+
|
|
5944
|
+
const nextTitle = available ? button.dataset.defaultTitle : unavailableTitle;
|
|
5945
|
+
const nextAriaLabel = available ? button.dataset.defaultAriaLabel : unavailableTitle;
|
|
5946
|
+
const nextTooltip = available ? button.dataset.defaultTooltip : unavailableTitle;
|
|
5947
|
+
|
|
4270
5948
|
button.disabled = !available;
|
|
4271
5949
|
button.setAttribute("aria-disabled", available ? "false" : "true");
|
|
4272
5950
|
button.classList.toggle("feature-unavailable", !available);
|
|
4273
|
-
button.setAttribute("title",
|
|
5951
|
+
if (nextTitle) button.setAttribute("title", nextTitle);
|
|
5952
|
+
else button.removeAttribute("title");
|
|
5953
|
+
if (nextAriaLabel) button.setAttribute("aria-label", nextAriaLabel);
|
|
5954
|
+
else button.removeAttribute("aria-label");
|
|
5955
|
+
if (button.dataset.defaultTooltip !== undefined) {
|
|
5956
|
+
if (nextTooltip) button.setAttribute("data-tooltip", nextTooltip);
|
|
5957
|
+
else button.removeAttribute("data-tooltip");
|
|
5958
|
+
}
|
|
4274
5959
|
}
|
|
4275
5960
|
|
|
4276
5961
|
function resetOptionalFeatureAvailability() {
|
|
@@ -4398,8 +6083,9 @@ async function installOptionalFeature(featureId) {
|
|
|
4398
6083
|
if (confirm(`${feature.label} install finished. Reload the active Pi tab now to enable newly loaded resources?`)) {
|
|
4399
6084
|
sendPrompt("prompt", "/reload");
|
|
4400
6085
|
} else {
|
|
4401
|
-
|
|
4402
|
-
|
|
6086
|
+
const tabContext = activeTabContext();
|
|
6087
|
+
await Promise.allSettled([refreshCommands(tabContext), initializeThemes()]);
|
|
6088
|
+
if (isCurrentTabContext(tabContext)) renderOptionalFeatureControls();
|
|
4403
6089
|
}
|
|
4404
6090
|
} catch (error) {
|
|
4405
6091
|
addEvent(error.message || String(error), "error");
|
|
@@ -4415,13 +6101,478 @@ function runPublishWorkflow(command) {
|
|
|
4415
6101
|
const commandName = String(command || "").replace(/^\//, "").split(/\s+/)[0];
|
|
4416
6102
|
const featureId = OPTIONAL_COMMAND_FEATURES.get(commandName);
|
|
4417
6103
|
if ((featureId && !isOptionalFeatureEnabled(featureId)) || !hasAvailableCommand(commandName)) {
|
|
6104
|
+
const tabContext = activeTabContext();
|
|
4418
6105
|
addEvent(commandUnavailableMessage(commandName), "warn");
|
|
4419
|
-
refreshCommands().catch((error) =>
|
|
6106
|
+
refreshCommands(tabContext).catch((error) => {
|
|
6107
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
6108
|
+
});
|
|
4420
6109
|
return;
|
|
4421
6110
|
}
|
|
4422
6111
|
sendPrompt("prompt", command);
|
|
4423
6112
|
}
|
|
4424
6113
|
|
|
6114
|
+
function slashCommandName(message) {
|
|
6115
|
+
const match = String(message || "").trim().match(/^\/([^\s]+)$/);
|
|
6116
|
+
return match ? match[1] : "";
|
|
6117
|
+
}
|
|
6118
|
+
|
|
6119
|
+
function openNativeCommandDialog({ title, message = "", searchPlaceholder = "" } = {}) {
|
|
6120
|
+
nativeCommandTabId ||= activeTabId;
|
|
6121
|
+
elements.nativeCommandTitle.textContent = title || "Pi command";
|
|
6122
|
+
elements.nativeCommandMessage.textContent = message;
|
|
6123
|
+
elements.nativeCommandMessage.hidden = !message;
|
|
6124
|
+
elements.nativeCommandSearch.value = "";
|
|
6125
|
+
elements.nativeCommandSearch.placeholder = searchPlaceholder || "Filter choices…";
|
|
6126
|
+
elements.nativeCommandSearch.hidden = !searchPlaceholder;
|
|
6127
|
+
elements.nativeCommandSearch.oninput = null;
|
|
6128
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6129
|
+
elements.nativeCommandError.hidden = true;
|
|
6130
|
+
elements.nativeCommandError.textContent = "";
|
|
6131
|
+
elements.nativeCommandActions.replaceChildren();
|
|
6132
|
+
addNativeCommandAction("Cancel", closeNativeCommandDialog);
|
|
6133
|
+
if (!elements.nativeCommandDialog.open) elements.nativeCommandDialog.showModal();
|
|
6134
|
+
if (searchPlaceholder) queueMicrotask(() => elements.nativeCommandSearch.focus());
|
|
6135
|
+
}
|
|
6136
|
+
|
|
6137
|
+
function closeNativeCommandDialog() {
|
|
6138
|
+
if (elements.nativeCommandDialog.open) elements.nativeCommandDialog.close();
|
|
6139
|
+
elements.nativeCommandSearch.oninput = null;
|
|
6140
|
+
nativeCommandTabId = null;
|
|
6141
|
+
}
|
|
6142
|
+
|
|
6143
|
+
function nativeCommandApi(path, options = {}) {
|
|
6144
|
+
return api(path, { ...options, tabId: options.tabId || nativeCommandTabId || activeTabId });
|
|
6145
|
+
}
|
|
6146
|
+
|
|
6147
|
+
function setNativeCommandError(message) {
|
|
6148
|
+
elements.nativeCommandError.textContent = message || "";
|
|
6149
|
+
elements.nativeCommandError.hidden = !message;
|
|
6150
|
+
}
|
|
6151
|
+
|
|
6152
|
+
function addNativeCommandAction(label, handler, className) {
|
|
6153
|
+
const button = make("button", className, label);
|
|
6154
|
+
button.type = "button";
|
|
6155
|
+
button.addEventListener("click", handler);
|
|
6156
|
+
elements.nativeCommandActions.append(button);
|
|
6157
|
+
return button;
|
|
6158
|
+
}
|
|
6159
|
+
|
|
6160
|
+
function renderNativeLoading(label = "Loading…") {
|
|
6161
|
+
elements.nativeCommandBody.replaceChildren(make("div", "native-command-empty muted", label));
|
|
6162
|
+
}
|
|
6163
|
+
|
|
6164
|
+
function nativeSelectorMatches(item, query) {
|
|
6165
|
+
if (!query) return true;
|
|
6166
|
+
const needle = query.toLowerCase();
|
|
6167
|
+
return [item.label, item.description, item.meta, item.badge]
|
|
6168
|
+
.filter(Boolean)
|
|
6169
|
+
.some((value) => String(value).toLowerCase().includes(needle));
|
|
6170
|
+
}
|
|
6171
|
+
|
|
6172
|
+
function renderNativeSelectorItems(items, { emptyText = "No choices.", onSelect, activeId } = {}) {
|
|
6173
|
+
const query = elements.nativeCommandSearch.value.trim();
|
|
6174
|
+
const filtered = items.filter((item) => nativeSelectorMatches(item, query));
|
|
6175
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6176
|
+
if (!filtered.length) {
|
|
6177
|
+
elements.nativeCommandBody.append(make("div", "native-command-empty muted", emptyText));
|
|
6178
|
+
return;
|
|
6179
|
+
}
|
|
6180
|
+
const list = make("div", "native-selector-list");
|
|
6181
|
+
for (const item of filtered) {
|
|
6182
|
+
const button = make("button", `native-selector-item${item.id === activeId ? " active" : ""}`);
|
|
6183
|
+
button.type = "button";
|
|
6184
|
+
if (item.depth !== undefined) button.style.setProperty("--tree-depth", String(item.depth));
|
|
6185
|
+
button.disabled = item.disabled === true;
|
|
6186
|
+
button.addEventListener("click", () => onSelect?.(item));
|
|
6187
|
+
const title = make("span", "native-selector-title");
|
|
6188
|
+
title.append(make("strong", undefined, item.label || item.id || "choice"));
|
|
6189
|
+
if (item.badge) title.append(make("span", "native-selector-badge", item.badge));
|
|
6190
|
+
const detail = make("span", "native-selector-detail", item.description || "");
|
|
6191
|
+
const meta = make("span", "native-selector-meta", item.meta || "");
|
|
6192
|
+
button.append(title);
|
|
6193
|
+
if (item.description) button.append(detail);
|
|
6194
|
+
if (item.meta) button.append(meta);
|
|
6195
|
+
list.append(button);
|
|
6196
|
+
}
|
|
6197
|
+
elements.nativeCommandBody.append(list);
|
|
6198
|
+
}
|
|
6199
|
+
|
|
6200
|
+
function setNativeActionBusy(button, busy, label = "Working…") {
|
|
6201
|
+
if (!button) return;
|
|
6202
|
+
if (!button.dataset.defaultLabel) button.dataset.defaultLabel = button.textContent || "";
|
|
6203
|
+
button.disabled = busy;
|
|
6204
|
+
button.textContent = busy ? label : button.dataset.defaultLabel;
|
|
6205
|
+
}
|
|
6206
|
+
|
|
6207
|
+
function modelOptionLabel(model) {
|
|
6208
|
+
return `${model.provider}/${model.id}`;
|
|
6209
|
+
}
|
|
6210
|
+
|
|
6211
|
+
async function openNativeModelSelector() {
|
|
6212
|
+
openNativeCommandDialog({ title: "/model", message: "Select the active model for this Pi tab.", searchPlaceholder: "Filter models…" });
|
|
6213
|
+
renderNativeLoading("Loading models…");
|
|
6214
|
+
try {
|
|
6215
|
+
const response = await nativeCommandApi("/api/models");
|
|
6216
|
+
const models = Array.isArray(response.data?.models) ? response.data.models : [];
|
|
6217
|
+
const activeId = currentState?.model ? `${currentState.model.provider}/${currentState.model.id}` : "";
|
|
6218
|
+
const items = models.map((model) => ({
|
|
6219
|
+
id: modelOptionLabel(model),
|
|
6220
|
+
label: modelOptionLabel(model),
|
|
6221
|
+
description: model.name || model.description || "",
|
|
6222
|
+
meta: model.contextWindow ? `context ${model.contextWindow}` : model.provider,
|
|
6223
|
+
model,
|
|
6224
|
+
badge: modelOptionLabel(model) === activeId ? "current" : "",
|
|
6225
|
+
}));
|
|
6226
|
+
const render = () => renderNativeSelectorItems(items, {
|
|
6227
|
+
emptyText: "No models match this filter.",
|
|
6228
|
+
activeId,
|
|
6229
|
+
onSelect: async (item) => {
|
|
6230
|
+
setNativeCommandError("");
|
|
6231
|
+
try {
|
|
6232
|
+
await nativeCommandApi("/api/model", { method: "POST", body: { provider: item.model.provider, modelId: item.model.id } });
|
|
6233
|
+
addTransientMessage({ role: "native", title: "/model", content: `Model set to ${item.label}.`, level: "info" });
|
|
6234
|
+
closeNativeCommandDialog();
|
|
6235
|
+
await refreshState();
|
|
6236
|
+
} catch (error) {
|
|
6237
|
+
setNativeCommandError(error.message || String(error));
|
|
6238
|
+
}
|
|
6239
|
+
},
|
|
6240
|
+
});
|
|
6241
|
+
elements.nativeCommandSearch.oninput = render;
|
|
6242
|
+
render();
|
|
6243
|
+
} catch (error) {
|
|
6244
|
+
setNativeCommandError(error.message || String(error));
|
|
6245
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6246
|
+
}
|
|
6247
|
+
}
|
|
6248
|
+
|
|
6249
|
+
function openNativeThemeSelector() {
|
|
6250
|
+
openNativeCommandDialog({ title: "/theme", message: "Select the browser Web UI theme. Pi terminal theme changes remain native-TUI only.", searchPlaceholder: "Filter themes…" });
|
|
6251
|
+
const load = async () => {
|
|
6252
|
+
if (!availableThemes.length) await initializeThemes();
|
|
6253
|
+
const items = availableThemes.map((theme) => ({
|
|
6254
|
+
id: theme.name,
|
|
6255
|
+
label: theme.label || displayThemeName(theme.name) || theme.name,
|
|
6256
|
+
description: theme.name,
|
|
6257
|
+
meta: theme.author ? `by ${theme.author}` : "browser theme",
|
|
6258
|
+
theme,
|
|
6259
|
+
badge: theme.name === currentThemeName ? "current" : "",
|
|
6260
|
+
}));
|
|
6261
|
+
const render = () => renderNativeSelectorItems(items, {
|
|
6262
|
+
emptyText: "No themes match this filter.",
|
|
6263
|
+
activeId: currentThemeName,
|
|
6264
|
+
onSelect: async (item) => {
|
|
6265
|
+
try {
|
|
6266
|
+
await setThemeByName(item.theme.name, { persist: true, announce: true });
|
|
6267
|
+
addTransientMessage({ role: "native", title: "/theme", content: `Theme set to ${item.label}.`, level: "info" });
|
|
6268
|
+
closeNativeCommandDialog();
|
|
6269
|
+
} catch (error) {
|
|
6270
|
+
setNativeCommandError(error.message || String(error));
|
|
6271
|
+
}
|
|
6272
|
+
},
|
|
6273
|
+
});
|
|
6274
|
+
elements.nativeCommandSearch.oninput = render;
|
|
6275
|
+
render();
|
|
6276
|
+
};
|
|
6277
|
+
renderNativeLoading("Loading themes…");
|
|
6278
|
+
load().catch((error) => {
|
|
6279
|
+
setNativeCommandError(error.message || String(error));
|
|
6280
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6281
|
+
});
|
|
6282
|
+
}
|
|
6283
|
+
|
|
6284
|
+
function nativeSettingSelect(label, value, options) {
|
|
6285
|
+
const field = make("label", "native-settings-field");
|
|
6286
|
+
field.append(make("span", "native-settings-label", label));
|
|
6287
|
+
const select = make("select");
|
|
6288
|
+
for (const option of options) {
|
|
6289
|
+
const element = make("option", undefined, option.label || option.value);
|
|
6290
|
+
element.value = option.value;
|
|
6291
|
+
select.append(element);
|
|
6292
|
+
}
|
|
6293
|
+
select.value = value;
|
|
6294
|
+
field.append(select);
|
|
6295
|
+
return { field, select };
|
|
6296
|
+
}
|
|
6297
|
+
|
|
6298
|
+
function nativeSettingToggle(label, checked, hint) {
|
|
6299
|
+
const field = make("label", "native-settings-toggle");
|
|
6300
|
+
const input = make("input");
|
|
6301
|
+
input.type = "checkbox";
|
|
6302
|
+
input.checked = !!checked;
|
|
6303
|
+
const text = make("span");
|
|
6304
|
+
text.append(make("strong", undefined, label));
|
|
6305
|
+
if (hint) text.append(make("span", "native-settings-hint", hint));
|
|
6306
|
+
field.append(input, text);
|
|
6307
|
+
return { field, input };
|
|
6308
|
+
}
|
|
6309
|
+
|
|
6310
|
+
function openNativeSettingsDialog() {
|
|
6311
|
+
openNativeCommandDialog({ title: "/settings", message: "Quick Web UI settings for the active Pi tab." });
|
|
6312
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6313
|
+
const state = currentState || {};
|
|
6314
|
+
const body = make("div", "native-settings-grid");
|
|
6315
|
+
const thinking = nativeSettingSelect("Thinking level", state.thinkingLevel || "off", ["off", "minimal", "low", "medium", "high", "xhigh"].map((value) => ({ value })));
|
|
6316
|
+
const steering = nativeSettingSelect("Steering queue", state.steeringMode || "one-at-a-time", [
|
|
6317
|
+
{ value: "one-at-a-time", label: "one at a time" },
|
|
6318
|
+
{ value: "all", label: "all queued" },
|
|
6319
|
+
]);
|
|
6320
|
+
const followUp = nativeSettingSelect("Follow-up queue", state.followUpMode || "one-at-a-time", [
|
|
6321
|
+
{ value: "one-at-a-time", label: "one at a time" },
|
|
6322
|
+
{ value: "all", label: "all queued" },
|
|
6323
|
+
]);
|
|
6324
|
+
const autoCompact = nativeSettingToggle("Auto compaction", state.autoCompactionEnabled !== false, "Let Pi compact when context is nearly full.");
|
|
6325
|
+
const thinkingOutput = nativeSettingToggle("Show thinking output", thinkingOutputVisible, "Local browser transcript visibility.");
|
|
6326
|
+
const doneNotifications = nativeSettingToggle("Agent done notifications", agentDoneNotificationsEnabled, "Browser notification after background tab work completes.");
|
|
6327
|
+
const busyBehavior = nativeSettingSelect("Busy prompt behavior", elements.busyBehavior.value || "followUp", [
|
|
6328
|
+
{ value: "followUp", label: "follow-up" },
|
|
6329
|
+
{ value: "steer", label: "steer" },
|
|
6330
|
+
]);
|
|
6331
|
+
body.append(thinking.field, steering.field, followUp.field, busyBehavior.field, autoCompact.field, thinkingOutput.field, doneNotifications.field);
|
|
6332
|
+
elements.nativeCommandBody.append(body);
|
|
6333
|
+
elements.nativeCommandActions.replaceChildren();
|
|
6334
|
+
addNativeCommandAction("Model…", () => openNativeModelSelector());
|
|
6335
|
+
addNativeCommandAction("Theme…", () => openNativeThemeSelector());
|
|
6336
|
+
addNativeCommandAction("Cancel", closeNativeCommandDialog);
|
|
6337
|
+
const save = addNativeCommandAction("Apply", async () => {
|
|
6338
|
+
setNativeActionBusy(save, true, "Applying…");
|
|
6339
|
+
setNativeCommandError("");
|
|
6340
|
+
try {
|
|
6341
|
+
const requests = [];
|
|
6342
|
+
if (thinking.select.value !== state.thinkingLevel) requests.push(nativeCommandApi("/api/thinking", { method: "POST", body: { level: thinking.select.value } }));
|
|
6343
|
+
if (steering.select.value !== state.steeringMode) requests.push(nativeCommandApi("/api/steering-mode", { method: "POST", body: { mode: steering.select.value } }));
|
|
6344
|
+
if (followUp.select.value !== state.followUpMode) requests.push(nativeCommandApi("/api/follow-up-mode", { method: "POST", body: { mode: followUp.select.value } }));
|
|
6345
|
+
if (autoCompact.input.checked !== state.autoCompactionEnabled) requests.push(nativeCommandApi("/api/auto-compaction", { method: "POST", body: { enabled: autoCompact.input.checked } }));
|
|
6346
|
+
elements.busyBehavior.value = busyBehavior.select.value;
|
|
6347
|
+
if (thinkingOutput.input.checked !== thinkingOutputVisible) setThinkingOutputVisible(thinkingOutput.input.checked);
|
|
6348
|
+
if (doneNotifications.input.checked !== agentDoneNotificationsEnabled) await setAgentDoneNotificationsEnabled(doneNotifications.input.checked);
|
|
6349
|
+
await Promise.all(requests);
|
|
6350
|
+
addTransientMessage({ role: "native", title: "/settings", content: "Settings updated.", level: "info" });
|
|
6351
|
+
closeNativeCommandDialog();
|
|
6352
|
+
await refreshState();
|
|
6353
|
+
} catch (error) {
|
|
6354
|
+
setNativeCommandError(error.message || String(error));
|
|
6355
|
+
} finally {
|
|
6356
|
+
setNativeActionBusy(save, false);
|
|
6357
|
+
}
|
|
6358
|
+
}, "primary");
|
|
6359
|
+
}
|
|
6360
|
+
|
|
6361
|
+
async function openNativeForkSelector() {
|
|
6362
|
+
openNativeCommandDialog({ title: "/fork", message: "Choose a previous user message to fork before.", searchPlaceholder: "Filter fork points…" });
|
|
6363
|
+
renderNativeLoading("Loading fork points…");
|
|
6364
|
+
try {
|
|
6365
|
+
const response = await nativeCommandApi("/api/fork-messages");
|
|
6366
|
+
const items = (response.data?.messages || []).map((message, index) => ({
|
|
6367
|
+
id: message.entryId,
|
|
6368
|
+
label: `#${index + 1} user message`,
|
|
6369
|
+
description: message.text || "",
|
|
6370
|
+
meta: message.entryId,
|
|
6371
|
+
message,
|
|
6372
|
+
})).reverse();
|
|
6373
|
+
const render = () => renderNativeSelectorItems(items, {
|
|
6374
|
+
emptyText: "No user messages are available to fork from.",
|
|
6375
|
+
onSelect: async (item) => {
|
|
6376
|
+
setNativeCommandError("");
|
|
6377
|
+
try {
|
|
6378
|
+
const result = await nativeCommandApi("/api/fork", { method: "POST", body: { entryId: item.message.entryId } });
|
|
6379
|
+
applyResponseTab(result);
|
|
6380
|
+
const restoredText = result.data?.text || result.data?.result?.text || "";
|
|
6381
|
+
if (restoredText) {
|
|
6382
|
+
elements.promptInput.value = restoredText;
|
|
6383
|
+
resizePromptInput();
|
|
6384
|
+
focusPromptInput({ defer: true });
|
|
6385
|
+
}
|
|
6386
|
+
addTransientMessage({ role: "native", title: "/fork", content: result.data?.message || "Forked the current session.", level: "info" });
|
|
6387
|
+
closeNativeCommandDialog();
|
|
6388
|
+
await refreshAll();
|
|
6389
|
+
} catch (error) {
|
|
6390
|
+
setNativeCommandError(error.message || String(error));
|
|
6391
|
+
}
|
|
6392
|
+
},
|
|
6393
|
+
});
|
|
6394
|
+
elements.nativeCommandSearch.oninput = render;
|
|
6395
|
+
render();
|
|
6396
|
+
} catch (error) {
|
|
6397
|
+
setNativeCommandError(error.message || String(error));
|
|
6398
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
|
|
6402
|
+
function openNativeCloneDialog() {
|
|
6403
|
+
openNativeCommandDialog({ title: "/clone", message: "Duplicate the current session at the current position." });
|
|
6404
|
+
elements.nativeCommandBody.append(make("p", "native-command-note", "This creates a new forked session and switches this Web UI tab to it."));
|
|
6405
|
+
elements.nativeCommandActions.replaceChildren();
|
|
6406
|
+
addNativeCommandAction("Cancel", closeNativeCommandDialog);
|
|
6407
|
+
const clone = addNativeCommandAction("Clone session", async () => {
|
|
6408
|
+
setNativeActionBusy(clone, true, "Cloning…");
|
|
6409
|
+
try {
|
|
6410
|
+
const result = await nativeCommandApi("/api/clone", { method: "POST", body: {} });
|
|
6411
|
+
applyResponseTab(result);
|
|
6412
|
+
addTransientMessage({ role: "native", title: "/clone", content: result.data?.message || "Cloned the current session.", level: "info" });
|
|
6413
|
+
closeNativeCommandDialog();
|
|
6414
|
+
await refreshAll();
|
|
6415
|
+
} catch (error) {
|
|
6416
|
+
setNativeCommandError(error.message || String(error));
|
|
6417
|
+
} finally {
|
|
6418
|
+
setNativeActionBusy(clone, false);
|
|
6419
|
+
}
|
|
6420
|
+
}, "primary");
|
|
6421
|
+
}
|
|
6422
|
+
|
|
6423
|
+
async function openNativeResumeSelector(scope = "current") {
|
|
6424
|
+
openNativeCommandDialog({ title: "/resume", message: "Resume another persisted Pi session.", searchPlaceholder: "Filter sessions…" });
|
|
6425
|
+
renderNativeLoading("Loading sessions…");
|
|
6426
|
+
const selectedScope = scope === "all" ? "all" : "current";
|
|
6427
|
+
try {
|
|
6428
|
+
const response = await nativeCommandApi(`/api/sessions?scope=${encodeURIComponent(selectedScope)}`);
|
|
6429
|
+
const items = (response.data?.sessions || []).map((session) => ({
|
|
6430
|
+
id: session.path,
|
|
6431
|
+
label: session.name || session.firstMessage || session.id || session.path,
|
|
6432
|
+
description: session.firstMessage || "(no messages)",
|
|
6433
|
+
meta: `${session.cwd || "unknown cwd"} · ${session.messageCount || 0} messages · ${session.modified || "unknown time"}`,
|
|
6434
|
+
badge: session.current ? "current" : "",
|
|
6435
|
+
disabled: session.current,
|
|
6436
|
+
session,
|
|
6437
|
+
}));
|
|
6438
|
+
const render = () => renderNativeSelectorItems(items, {
|
|
6439
|
+
emptyText: selectedScope === "all" ? "No sessions match this filter." : "No sessions for this working directory match this filter.",
|
|
6440
|
+
onSelect: async (item) => {
|
|
6441
|
+
setNativeCommandError("");
|
|
6442
|
+
try {
|
|
6443
|
+
const result = await nativeCommandApi("/api/switch-session", { method: "POST", body: { sessionPath: item.session.path } });
|
|
6444
|
+
applyResponseTab(result);
|
|
6445
|
+
addTransientMessage({ role: "native", title: "/resume", content: result.data?.message || "Resumed selected session.", level: "info" });
|
|
6446
|
+
closeNativeCommandDialog();
|
|
6447
|
+
await refreshAll();
|
|
6448
|
+
} catch (error) {
|
|
6449
|
+
setNativeCommandError(error.message || String(error));
|
|
6450
|
+
}
|
|
6451
|
+
},
|
|
6452
|
+
});
|
|
6453
|
+
elements.nativeCommandSearch.oninput = render;
|
|
6454
|
+
elements.nativeCommandActions.replaceChildren();
|
|
6455
|
+
addNativeCommandAction(selectedScope === "all" ? "Current cwd" : "All sessions", () => openNativeResumeSelector(selectedScope === "all" ? "current" : "all"));
|
|
6456
|
+
addNativeCommandAction("Cancel", closeNativeCommandDialog);
|
|
6457
|
+
render();
|
|
6458
|
+
} catch (error) {
|
|
6459
|
+
setNativeCommandError(error.message || String(error));
|
|
6460
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6461
|
+
}
|
|
6462
|
+
}
|
|
6463
|
+
|
|
6464
|
+
async function openNativeTreeSelector() {
|
|
6465
|
+
openNativeCommandDialog({ title: "/tree", message: "Navigate the current session tree. Choosing a user message restores it into the editor.", searchPlaceholder: "Filter tree…" });
|
|
6466
|
+
renderNativeLoading("Loading session tree…");
|
|
6467
|
+
try {
|
|
6468
|
+
const response = await nativeCommandApi("/api/session-tree");
|
|
6469
|
+
const nodes = response.data?.nodes || [];
|
|
6470
|
+
const summarize = nativeSettingToggle("Summarize abandoned branch", false, "Optional; may call the active model before switching branches.");
|
|
6471
|
+
const labelField = make("label", "native-settings-field");
|
|
6472
|
+
labelField.append(make("span", "native-settings-label", "Optional label"));
|
|
6473
|
+
const labelInput = make("input", "dialog-input");
|
|
6474
|
+
labelInput.placeholder = "checkpoint label";
|
|
6475
|
+
labelField.append(labelInput);
|
|
6476
|
+
const options = make("div", "native-tree-options");
|
|
6477
|
+
options.append(summarize.field, labelField);
|
|
6478
|
+
const items = nodes.map((node) => ({
|
|
6479
|
+
id: node.id,
|
|
6480
|
+
label: `${node.title}${node.label ? ` · ${node.label}` : ""}`,
|
|
6481
|
+
description: node.text || "",
|
|
6482
|
+
meta: `${node.timestamp || ""}${node.childCount ? ` · ${node.childCount} child${node.childCount === 1 ? "" : "ren"}` : ""}`,
|
|
6483
|
+
badge: node.currentLeaf ? "leaf" : "",
|
|
6484
|
+
depth: node.depth || 0,
|
|
6485
|
+
node,
|
|
6486
|
+
}));
|
|
6487
|
+
const navigate = async (item) => {
|
|
6488
|
+
setNativeCommandError("");
|
|
6489
|
+
try {
|
|
6490
|
+
const result = await nativeCommandApi("/api/tree-navigate", {
|
|
6491
|
+
method: "POST",
|
|
6492
|
+
body: {
|
|
6493
|
+
entryId: item.node.id,
|
|
6494
|
+
summarize: summarize.input.checked,
|
|
6495
|
+
label: labelInput.value.trim() || undefined,
|
|
6496
|
+
},
|
|
6497
|
+
});
|
|
6498
|
+
applyResponseTab(result);
|
|
6499
|
+
addTransientMessage({ role: "native", title: "/tree", content: result.data?.message || "Navigated the session tree.", level: "info" });
|
|
6500
|
+
closeNativeCommandDialog();
|
|
6501
|
+
await refreshAll();
|
|
6502
|
+
} catch (error) {
|
|
6503
|
+
setNativeCommandError(error.message || String(error));
|
|
6504
|
+
}
|
|
6505
|
+
};
|
|
6506
|
+
const render = () => {
|
|
6507
|
+
renderNativeSelectorItems(items, { emptyText: "No session tree entries match this filter.", onSelect: navigate });
|
|
6508
|
+
elements.nativeCommandBody.prepend(options);
|
|
6509
|
+
};
|
|
6510
|
+
elements.nativeCommandSearch.oninput = render;
|
|
6511
|
+
render();
|
|
6512
|
+
} catch (error) {
|
|
6513
|
+
setNativeCommandError(error.message || String(error));
|
|
6514
|
+
elements.nativeCommandBody.replaceChildren();
|
|
6515
|
+
}
|
|
6516
|
+
}
|
|
6517
|
+
|
|
6518
|
+
function openNativeScopedModelsInfo() {
|
|
6519
|
+
openNativeCommandDialog({ title: "/scoped-models", message: "Scoped model selection is available in the footer model picker." });
|
|
6520
|
+
elements.nativeCommandBody.append(make("p", "native-command-note", "Use the footer model chip to choose among scoped models. The full native scoped-models editor is still TUI-only."));
|
|
6521
|
+
}
|
|
6522
|
+
|
|
6523
|
+
function openNativeAuthInfo(mode) {
|
|
6524
|
+
const command = mode === "logout" ? "/logout" : "/login";
|
|
6525
|
+
openNativeCommandDialog({ title: command, message: "Provider credential entry is intentionally not implemented in the browser yet." });
|
|
6526
|
+
const note = [
|
|
6527
|
+
"Use native Pi TUI authentication for now, or configure provider credentials through environment variables or models.json.",
|
|
6528
|
+
"This avoids accepting or storing API keys in the Web UI until the credential flow has a dedicated security design.",
|
|
6529
|
+
].join("\n\n");
|
|
6530
|
+
elements.nativeCommandBody.append(make("p", "native-command-note", note));
|
|
6531
|
+
}
|
|
6532
|
+
|
|
6533
|
+
async function handleNativeSlashSelectorCommand(message, { usesPromptInput = false } = {}) {
|
|
6534
|
+
const name = slashCommandName(message);
|
|
6535
|
+
if (!NATIVE_SELECTOR_COMMANDS.has(name)) return false;
|
|
6536
|
+
setComposerActionsOpen(false);
|
|
6537
|
+
hideCommandSuggestions();
|
|
6538
|
+
if (usesPromptInput) {
|
|
6539
|
+
elements.promptInput.value = "";
|
|
6540
|
+
resizePromptInput();
|
|
6541
|
+
}
|
|
6542
|
+
switch (name) {
|
|
6543
|
+
case "model":
|
|
6544
|
+
await openNativeModelSelector();
|
|
6545
|
+
return true;
|
|
6546
|
+
case "settings":
|
|
6547
|
+
openNativeSettingsDialog();
|
|
6548
|
+
return true;
|
|
6549
|
+
case "theme":
|
|
6550
|
+
openNativeThemeSelector();
|
|
6551
|
+
return true;
|
|
6552
|
+
case "fork":
|
|
6553
|
+
await openNativeForkSelector();
|
|
6554
|
+
return true;
|
|
6555
|
+
case "clone":
|
|
6556
|
+
openNativeCloneDialog();
|
|
6557
|
+
return true;
|
|
6558
|
+
case "resume":
|
|
6559
|
+
await openNativeResumeSelector();
|
|
6560
|
+
return true;
|
|
6561
|
+
case "tree":
|
|
6562
|
+
await openNativeTreeSelector();
|
|
6563
|
+
return true;
|
|
6564
|
+
case "scoped-models":
|
|
6565
|
+
openNativeScopedModelsInfo();
|
|
6566
|
+
return true;
|
|
6567
|
+
case "login":
|
|
6568
|
+
case "logout":
|
|
6569
|
+
openNativeAuthInfo(name);
|
|
6570
|
+
return true;
|
|
6571
|
+
default:
|
|
6572
|
+
return false;
|
|
6573
|
+
}
|
|
6574
|
+
}
|
|
6575
|
+
|
|
4425
6576
|
function shouldSendPromptFromEnter(event) {
|
|
4426
6577
|
if (event.key !== "Enter" || event.shiftKey || event.isComposing) return false;
|
|
4427
6578
|
if (event.ctrlKey || event.metaKey) return true;
|
|
@@ -4430,6 +6581,7 @@ function shouldSendPromptFromEnter(event) {
|
|
|
4430
6581
|
|
|
4431
6582
|
function renderMessages(messages) {
|
|
4432
6583
|
latestMessages = messages || [];
|
|
6584
|
+
cleanupLiveToolRunsForMessages(latestMessages);
|
|
4433
6585
|
syncLastUserPromptFromMessages(latestMessages);
|
|
4434
6586
|
renderAllMessages();
|
|
4435
6587
|
renderFooter();
|
|
@@ -4472,7 +6624,7 @@ function renderStreamingAssistantText() {
|
|
|
4472
6624
|
const assistantText = stripTodoProgressLines(streamRawText, { streaming: true });
|
|
4473
6625
|
if (assistantText) {
|
|
4474
6626
|
ensureStreamBubble();
|
|
4475
|
-
streamText
|
|
6627
|
+
renderMarkdown(streamText, assistantText);
|
|
4476
6628
|
} else {
|
|
4477
6629
|
scheduleStreamBubbleHide();
|
|
4478
6630
|
}
|
|
@@ -4493,26 +6645,29 @@ function suppressStreamingAssistantTextBeforeToolCall() {
|
|
|
4493
6645
|
|
|
4494
6646
|
function ensureStreamBubble() {
|
|
4495
6647
|
cancelStreamBubbleHide();
|
|
4496
|
-
if (streamBubble) return;
|
|
4497
|
-
const created = appendMessage({ role: "assistant", title: "
|
|
6648
|
+
if (streamBubble?.parentElement === elements.chat) return;
|
|
6649
|
+
const created = appendMessage({ role: "assistant", title: "final output", timestamp: Date.now(), content: "" }, { streaming: true });
|
|
4498
6650
|
streamBubble = created.bubble;
|
|
4499
|
-
streamText =
|
|
6651
|
+
streamText = make("div", "markdown-body streaming-markdown");
|
|
6652
|
+
created.body.append(streamText);
|
|
4500
6653
|
streamBubbleVisibleSince = performance.now();
|
|
4501
6654
|
renderRunIndicator({ scroll: false });
|
|
4502
6655
|
scrollChatToBottom();
|
|
4503
6656
|
}
|
|
4504
6657
|
|
|
4505
6658
|
function ensureStreamingThinkingBubble() {
|
|
4506
|
-
if (
|
|
6659
|
+
if (!thinkingOutputVisible) return false;
|
|
6660
|
+
if (streamThinkingBubble?.parentElement === elements.chat) return true;
|
|
4507
6661
|
const created = appendMessage({ role: "thinking", title: "thinking", timestamp: Date.now(), content: "" }, { streaming: true });
|
|
4508
6662
|
streamThinkingBubble = created.bubble;
|
|
4509
6663
|
streamThinking = appendText(created.body, "", "thinking-text");
|
|
4510
6664
|
renderRunIndicator({ scroll: false });
|
|
4511
6665
|
scrollChatToBottom();
|
|
6666
|
+
return true;
|
|
4512
6667
|
}
|
|
4513
6668
|
|
|
4514
6669
|
function showStreamingThinking(placeholder = "Thinking…") {
|
|
4515
|
-
ensureStreamingThinkingBubble();
|
|
6670
|
+
if (!ensureStreamingThinkingBubble()) return;
|
|
4516
6671
|
if (!streamThinking.textContent) streamThinking.textContent = placeholder;
|
|
4517
6672
|
}
|
|
4518
6673
|
|
|
@@ -4545,9 +6700,8 @@ function assistantTextFromMessage(message) {
|
|
|
4545
6700
|
const parts = [];
|
|
4546
6701
|
for (let index = 0; index < content.length; index += 1) {
|
|
4547
6702
|
const part = content[index];
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
}
|
|
6703
|
+
const text = assistantTextPartText(part);
|
|
6704
|
+
if (text && !assistantHasToolCallAfter(content, index)) parts.push(text);
|
|
4551
6705
|
}
|
|
4552
6706
|
return parts.length ? parts.join("\n\n") : "";
|
|
4553
6707
|
}
|
|
@@ -4563,11 +6717,13 @@ function assistantThinkingTextFromMessage(message) {
|
|
|
4563
6717
|
}
|
|
4564
6718
|
|
|
4565
6719
|
function setStreamingThinkingText(text) {
|
|
6720
|
+
if (!thinkingOutputVisible) return;
|
|
4566
6721
|
showStreamingThinking("");
|
|
4567
|
-
streamThinking.textContent = text;
|
|
6722
|
+
if (streamThinking) streamThinking.textContent = text;
|
|
4568
6723
|
}
|
|
4569
6724
|
|
|
4570
6725
|
function syncStreamingThinkingFromMessage(event, { placeholder = "" } = {}) {
|
|
6726
|
+
if (!thinkingOutputVisible) return true;
|
|
4571
6727
|
const text = assistantThinkingTextFromMessage(assistantStreamingMessage(event));
|
|
4572
6728
|
if (text === null) return false;
|
|
4573
6729
|
if (text || placeholder || streamThinkingBubble) setStreamingThinkingText(text || placeholder);
|
|
@@ -4585,10 +6741,10 @@ function handleMessageUpdate(event) {
|
|
|
4585
6741
|
currentRunStreamChars += delta.length;
|
|
4586
6742
|
setRunIndicatorActivity("Thinking…", { scroll: false });
|
|
4587
6743
|
const synced = syncStreamingThinkingFromMessage(event);
|
|
4588
|
-
if (!synced || (!streamThinking?.textContent && delta)) {
|
|
6744
|
+
if (thinkingOutputVisible && (!synced || (!streamThinking?.textContent && delta))) {
|
|
4589
6745
|
showStreamingThinking("");
|
|
4590
|
-
if (streamThinking
|
|
4591
|
-
streamThinking.textContent += delta;
|
|
6746
|
+
if (streamThinking?.textContent === "Thinking…") streamThinking.textContent = "";
|
|
6747
|
+
if (streamThinking) streamThinking.textContent += delta;
|
|
4592
6748
|
}
|
|
4593
6749
|
renderFooter();
|
|
4594
6750
|
scrollChatToBottom();
|
|
@@ -4623,29 +6779,36 @@ function handleMessageUpdate(event) {
|
|
|
4623
6779
|
}
|
|
4624
6780
|
}
|
|
4625
6781
|
|
|
4626
|
-
async function refreshState() {
|
|
4627
|
-
|
|
6782
|
+
async function refreshState(tabContext = activeTabContext()) {
|
|
6783
|
+
if (!tabContext.tabId) return;
|
|
6784
|
+
const response = await api("/api/state", { tabId: tabContext.tabId });
|
|
6785
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
4628
6786
|
currentState = response.data || null;
|
|
4629
6787
|
syncActiveTabActivityFromState(currentState);
|
|
4630
6788
|
syncRunIndicatorFromState(currentState);
|
|
4631
6789
|
renderStatus();
|
|
4632
6790
|
}
|
|
4633
6791
|
|
|
4634
|
-
async function refreshStats() {
|
|
4635
|
-
|
|
6792
|
+
async function refreshStats(tabContext = activeTabContext()) {
|
|
6793
|
+
if (!tabContext.tabId) return;
|
|
6794
|
+
const response = await api("/api/stats", { tabId: tabContext.tabId });
|
|
6795
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
4636
6796
|
latestStats = response.data || null;
|
|
4637
6797
|
renderFooter();
|
|
4638
6798
|
}
|
|
4639
6799
|
|
|
4640
|
-
async function refreshWorkspace() {
|
|
6800
|
+
async function refreshWorkspace(tabContext = activeTabContext()) {
|
|
6801
|
+
if (!tabContext.tabId) return;
|
|
6802
|
+
let nextWorkspace = null;
|
|
4641
6803
|
try {
|
|
4642
|
-
const response = await api("/api/workspace");
|
|
4643
|
-
|
|
6804
|
+
const response = await api("/api/workspace", { tabId: tabContext.tabId });
|
|
6805
|
+
nextWorkspace = response.data || null;
|
|
4644
6806
|
} catch (error) {
|
|
6807
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
4645
6808
|
// Older webui server processes do not have /api/workspace. Fall back to /api/health,
|
|
4646
6809
|
// which has exposed cwd from the beginning, so the footer still shows the real path.
|
|
4647
|
-
const health = await api("/api/health");
|
|
4648
|
-
|
|
6810
|
+
const health = await api("/api/health", { tabId: tabContext.tabId });
|
|
6811
|
+
nextWorkspace = health.cwd
|
|
4649
6812
|
? {
|
|
4650
6813
|
cwd: health.cwd,
|
|
4651
6814
|
displayCwd: normalizeDisplayPath(health.cwd),
|
|
@@ -4654,6 +6817,8 @@ async function refreshWorkspace() {
|
|
|
4654
6817
|
}
|
|
4655
6818
|
: null;
|
|
4656
6819
|
}
|
|
6820
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
6821
|
+
latestWorkspace = nextWorkspace;
|
|
4657
6822
|
renderFooter();
|
|
4658
6823
|
}
|
|
4659
6824
|
|
|
@@ -4722,12 +6887,15 @@ async function refreshNetworkStatus() {
|
|
|
4722
6887
|
renderNetworkStatus();
|
|
4723
6888
|
}
|
|
4724
6889
|
|
|
4725
|
-
async function refreshFooterData() {
|
|
4726
|
-
|
|
6890
|
+
async function refreshFooterData(tabContext = activeTabContext()) {
|
|
6891
|
+
if (!tabContext.tabId) return;
|
|
6892
|
+
await Promise.allSettled([refreshStats(tabContext), refreshWorkspace(tabContext)]);
|
|
4727
6893
|
}
|
|
4728
6894
|
|
|
4729
|
-
async function refreshMessages() {
|
|
4730
|
-
|
|
6895
|
+
async function refreshMessages(tabContext = activeTabContext()) {
|
|
6896
|
+
if (!tabContext.tabId) return;
|
|
6897
|
+
const response = await api("/api/messages", { tabId: tabContext.tabId });
|
|
6898
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
4731
6899
|
latestMessages = response.data?.messages || [];
|
|
4732
6900
|
resetStreamBubble();
|
|
4733
6901
|
renderMessages(latestMessages);
|
|
@@ -4735,21 +6903,28 @@ async function refreshMessages() {
|
|
|
4735
6903
|
renderFooter();
|
|
4736
6904
|
}
|
|
4737
6905
|
|
|
4738
|
-
async function refreshModels() {
|
|
4739
|
-
|
|
6906
|
+
async function refreshModels(tabContext = activeTabContext()) {
|
|
6907
|
+
if (!tabContext.tabId) return;
|
|
6908
|
+
const response = await api("/api/models", { tabId: tabContext.tabId });
|
|
4740
6909
|
const models = response.data?.models || [];
|
|
4741
|
-
|
|
6910
|
+
let scopedModels = [];
|
|
6911
|
+
let scopedModelPatterns = [];
|
|
6912
|
+
let scopedModelSource = "none";
|
|
6913
|
+
let scopedModelError = null;
|
|
4742
6914
|
try {
|
|
4743
|
-
const scopedResponse = await api("/api/scoped-models");
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
6915
|
+
const scopedResponse = await api("/api/scoped-models", { tabId: tabContext.tabId });
|
|
6916
|
+
scopedModels = scopedResponse.data?.models || [];
|
|
6917
|
+
scopedModelPatterns = scopedResponse.data?.patterns || [];
|
|
6918
|
+
scopedModelSource = scopedResponse.data?.source || "none";
|
|
4747
6919
|
} catch (error) {
|
|
4748
|
-
|
|
4749
|
-
footerScopedModelPatterns = [];
|
|
4750
|
-
footerScopedModelSource = "none";
|
|
4751
|
-
addEvent(`failed to load scoped models: ${error.message}`, "warn");
|
|
6920
|
+
scopedModelError = error;
|
|
4752
6921
|
}
|
|
6922
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
6923
|
+
availableModels = models;
|
|
6924
|
+
footerScopedModels = scopedModels;
|
|
6925
|
+
footerScopedModelPatterns = scopedModelPatterns;
|
|
6926
|
+
footerScopedModelSource = scopedModelSource;
|
|
6927
|
+
if (scopedModelError) addEvent(`failed to load scoped models: ${scopedModelError.message}`, "warn");
|
|
4753
6928
|
elements.modelSelect.replaceChildren();
|
|
4754
6929
|
for (const model of models) {
|
|
4755
6930
|
const option = document.createElement("option");
|
|
@@ -5127,10 +7302,7 @@ function insertPathSuggestion(index = commandSuggestIndex) {
|
|
|
5127
7302
|
return true;
|
|
5128
7303
|
}
|
|
5129
7304
|
|
|
5130
|
-
|
|
5131
|
-
const response = await api("/api/commands");
|
|
5132
|
-
availableCommands = normalizeCommands(response.data?.commands || []);
|
|
5133
|
-
updateOptionalFeatureAvailability();
|
|
7305
|
+
function renderCommands() {
|
|
5134
7306
|
elements.commandsBox.replaceChildren();
|
|
5135
7307
|
if (!availableCommands.length) {
|
|
5136
7308
|
elements.commandsBox.textContent = "No RPC-visible commands.";
|
|
@@ -5161,8 +7333,27 @@ async function refreshCommands() {
|
|
|
5161
7333
|
renderCommandSuggestions();
|
|
5162
7334
|
}
|
|
5163
7335
|
|
|
5164
|
-
async function
|
|
5165
|
-
|
|
7336
|
+
async function refreshCommands(tabContext = activeTabContext()) {
|
|
7337
|
+
if (!tabContext.tabId) return;
|
|
7338
|
+
const response = await api("/api/commands", { tabId: tabContext.tabId });
|
|
7339
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
7340
|
+
availableCommands = normalizeCommands(response.data?.commands || []);
|
|
7341
|
+
updateOptionalFeatureAvailability();
|
|
7342
|
+
renderCommands();
|
|
7343
|
+
}
|
|
7344
|
+
|
|
7345
|
+
async function refreshAll(tabContext = activeTabContext()) {
|
|
7346
|
+
if (!tabContext.tabId) return;
|
|
7347
|
+
const results = await Promise.allSettled([
|
|
7348
|
+
refreshState(tabContext),
|
|
7349
|
+
refreshMessages(tabContext),
|
|
7350
|
+
refreshModels(tabContext),
|
|
7351
|
+
refreshCommands(tabContext),
|
|
7352
|
+
refreshStats(tabContext),
|
|
7353
|
+
refreshWorkspace(tabContext),
|
|
7354
|
+
refreshNetworkStatus(),
|
|
7355
|
+
]);
|
|
7356
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5166
7357
|
for (const result of results) {
|
|
5167
7358
|
if (result.status === "rejected") addEvent(result.reason.message || String(result.reason), "error");
|
|
5168
7359
|
}
|
|
@@ -5245,42 +7436,56 @@ async function closeNetworkAccess() {
|
|
|
5245
7436
|
async function sendPrompt(kind = "prompt", explicitMessage) {
|
|
5246
7437
|
const usesPromptInput = explicitMessage === undefined;
|
|
5247
7438
|
const rawMessage = usesPromptInput ? elements.promptInput.value : explicitMessage;
|
|
5248
|
-
const
|
|
5249
|
-
if (!message) return;
|
|
5250
|
-
|
|
7439
|
+
const originalMessage = String(rawMessage || "").trim();
|
|
5251
7440
|
const targetTabId = activeTabId;
|
|
5252
|
-
|
|
5253
|
-
|
|
7441
|
+
if (!targetTabId) return;
|
|
7442
|
+
const tabContext = activeTabContext(targetTabId);
|
|
7443
|
+
const attachments = usesPromptInput ? [...attachmentsForTab(targetTabId)] : [];
|
|
7444
|
+
if (!originalMessage && attachments.length === 0) return;
|
|
7445
|
+
if (kind === "prompt" && attachments.length === 0 && await handleNativeSlashSelectorCommand(originalMessage, { usesPromptInput })) return;
|
|
7446
|
+
|
|
7447
|
+
const targetWasStreaming = !!currentState?.isStreaming;
|
|
7448
|
+
const busyBehavior = elements.busyBehavior.value || "followUp";
|
|
7449
|
+
const startsRun = kind === "prompt" && !targetWasStreaming;
|
|
5254
7450
|
autoFollowChat = true;
|
|
5255
7451
|
updateJumpToLatestButton();
|
|
5256
7452
|
setComposerActionsOpen(false);
|
|
5257
7453
|
if (startsRun) {
|
|
5258
7454
|
markTabWorkingLocally(targetTabId);
|
|
5259
|
-
setRunIndicatorActivity("Sending prompt to Pi…");
|
|
7455
|
+
setRunIndicatorActivity(attachments.length ? "Uploading attachments…" : "Sending prompt to Pi…");
|
|
5260
7456
|
}
|
|
5261
7457
|
|
|
7458
|
+
let message = originalMessage;
|
|
5262
7459
|
try {
|
|
7460
|
+
const prepared = attachments.length ? await prepareAttachmentsForPrompt(attachments, targetTabId) : { images: [], uploadedFiles: [], inlineImageIds: new Set() };
|
|
7461
|
+
message = composeMessageWithAttachments(originalMessage, prepared.uploadedFiles, prepared.inlineImageIds);
|
|
7462
|
+
const bodyBase = { message };
|
|
7463
|
+
if (prepared.images.length) bodyBase.images = prepared.images;
|
|
7464
|
+
if (kind === "prompt" && !message.startsWith("/")) rememberLastUserPrompt(message, { tabId: targetTabId });
|
|
7465
|
+
if (startsRun && isCurrentTabContext(tabContext)) setRunIndicatorActivity("Sending prompt to Pi…");
|
|
7466
|
+
|
|
5263
7467
|
let response;
|
|
5264
7468
|
if (kind === "steer") {
|
|
5265
|
-
response = await api("/api/steer", { method: "POST", body:
|
|
7469
|
+
response = await api("/api/steer", { method: "POST", body: bodyBase, tabId: targetTabId });
|
|
5266
7470
|
} else if (kind === "follow-up") {
|
|
5267
|
-
response = await api("/api/follow-up", { method: "POST", body:
|
|
7471
|
+
response = await api("/api/follow-up", { method: "POST", body: bodyBase, tabId: targetTabId });
|
|
5268
7472
|
} else {
|
|
5269
|
-
const body = {
|
|
5270
|
-
if (
|
|
7473
|
+
const body = { ...bodyBase };
|
|
7474
|
+
if (targetWasStreaming) body.streamingBehavior = busyBehavior;
|
|
5271
7475
|
response = await api("/api/prompt", { method: "POST", body, tabId: targetTabId });
|
|
5272
7476
|
}
|
|
5273
7477
|
applyResponseTab(response);
|
|
5274
7478
|
if (response?.command === "native_slash_command" && /^\/new(?:\s|$)/.test(message)) forgetLastUserPrompt(targetTabId);
|
|
7479
|
+
const targetStillActive = isCurrentTabContext(tabContext);
|
|
5275
7480
|
if (startsRun && response?.command === "native_slash_command") {
|
|
5276
7481
|
markTabIdleLocally(targetTabId);
|
|
5277
|
-
clearRunIndicatorActivity();
|
|
5278
|
-
} else if (kind === "steer" && currentState?.isStreaming) {
|
|
7482
|
+
if (targetStillActive) clearRunIndicatorActivity();
|
|
7483
|
+
} else if (targetStillActive && kind === "steer" && currentState?.isStreaming) {
|
|
5279
7484
|
setRunIndicatorActivity("Steering sent; waiting for the next output or action…");
|
|
5280
|
-
} else if (kind === "follow-up" && currentState?.isStreaming) {
|
|
7485
|
+
} else if (targetStillActive && kind === "follow-up" && currentState?.isStreaming) {
|
|
5281
7486
|
setRunIndicatorActivity("Follow-up queued; current agent run is still active…");
|
|
5282
7487
|
}
|
|
5283
|
-
if (response?.command === "native_slash_command" && response.data?.copyText) {
|
|
7488
|
+
if (targetStillActive && response?.command === "native_slash_command" && response.data?.copyText) {
|
|
5284
7489
|
try {
|
|
5285
7490
|
await navigator.clipboard.writeText(response.data.copyText);
|
|
5286
7491
|
} catch (error) {
|
|
@@ -5288,22 +7493,33 @@ async function sendPrompt(kind = "prompt", explicitMessage) {
|
|
|
5288
7493
|
response.data.level = "warn";
|
|
5289
7494
|
}
|
|
5290
7495
|
}
|
|
5291
|
-
if (response?.command === "native_slash_command" && response.data?.message) {
|
|
7496
|
+
if (targetStillActive && response?.command === "native_slash_command" && response.data?.message) {
|
|
5292
7497
|
addTransientMessage({ role: "native", title: message.split(/\s+/, 1)[0], content: response.data.message, level: response.data.level || "info" });
|
|
5293
7498
|
}
|
|
5294
7499
|
if (usesPromptInput) {
|
|
5295
|
-
|
|
5296
|
-
|
|
7500
|
+
clearAttachments(targetTabId);
|
|
7501
|
+
if (targetStillActive) {
|
|
7502
|
+
elements.promptInput.value = "";
|
|
7503
|
+
resizePromptInput();
|
|
7504
|
+
} else {
|
|
7505
|
+
tabDrafts.set(targetTabId, "");
|
|
7506
|
+
}
|
|
7507
|
+
}
|
|
7508
|
+
if (targetStillActive) {
|
|
7509
|
+
hideCommandSuggestions();
|
|
7510
|
+
scheduleRefreshState(120, tabContext);
|
|
7511
|
+
} else {
|
|
7512
|
+
scheduleRefreshTabs(300);
|
|
5297
7513
|
}
|
|
5298
|
-
hideCommandSuggestions();
|
|
5299
|
-
scheduleRefreshState();
|
|
5300
7514
|
} catch (error) {
|
|
5301
7515
|
if (startsRun) {
|
|
5302
7516
|
markTabIdleLocally(targetTabId);
|
|
5303
|
-
clearRunIndicatorActivity();
|
|
7517
|
+
if (isCurrentTabContext(tabContext)) clearRunIndicatorActivity();
|
|
7518
|
+
}
|
|
7519
|
+
if (isCurrentTabContext(tabContext)) {
|
|
7520
|
+
addEvent(error.message, "error");
|
|
7521
|
+
addTransientMessage({ role: "error", title: message.startsWith("/") ? message.split(/\s+/, 1)[0] : "error", content: error.message, level: "error" });
|
|
5304
7522
|
}
|
|
5305
|
-
addEvent(error.message, "error");
|
|
5306
|
-
addTransientMessage({ role: "error", title: message.startsWith("/") ? message.split(/\s+/, 1)[0] : "error", content: error.message, level: "error" });
|
|
5307
7523
|
}
|
|
5308
7524
|
}
|
|
5309
7525
|
|
|
@@ -5379,12 +7595,14 @@ function handleExtensionUiRequest(request) {
|
|
|
5379
7595
|
|
|
5380
7596
|
async function sendDialogResponse(payload) {
|
|
5381
7597
|
const { tabId = activeTabId, ...body } = payload;
|
|
7598
|
+
const tabContext = activeTabContext(tabId);
|
|
5382
7599
|
try {
|
|
5383
7600
|
const response = await api("/api/extension-ui-response", { method: "POST", body, tabId });
|
|
5384
7601
|
if (!applyResponseTab(response) && decrementTabPendingBlockerCount(tabId)) renderTabs();
|
|
5385
7602
|
} catch (error) {
|
|
5386
|
-
addEvent(error.message, "error");
|
|
7603
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5387
7604
|
} finally {
|
|
7605
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5388
7606
|
if (elements.dialog.open) elements.dialog.close();
|
|
5389
7607
|
activeDialog = null;
|
|
5390
7608
|
if (runIndicatorIsActive()) setRunIndicatorActivity("Continuing after your response…");
|
|
@@ -5406,7 +7624,8 @@ function showNextDialog() {
|
|
|
5406
7624
|
const request = activeDialog;
|
|
5407
7625
|
|
|
5408
7626
|
const prompt = normalizeDialogPrompt(request);
|
|
5409
|
-
const
|
|
7627
|
+
const detectedReleasePrompt = request.method === "select" ? releaseDialogPromptParts(prompt) : null;
|
|
7628
|
+
const releasePrompt = detectedReleasePrompt && isOptionalFeatureEnabled(detectedReleasePrompt.featureId) ? detectedReleasePrompt : null;
|
|
5410
7629
|
const displayPrompt = releasePrompt || prompt;
|
|
5411
7630
|
const isGuardrailDialog = isGuardrailDialogPrompt(displayPrompt);
|
|
5412
7631
|
const isReleaseDialog = !!releasePrompt;
|
|
@@ -5460,8 +7679,22 @@ function showNextDialog() {
|
|
|
5460
7679
|
elements.dialog.showModal();
|
|
5461
7680
|
}
|
|
5462
7681
|
|
|
7682
|
+
function handleInactiveTabEvent(event) {
|
|
7683
|
+
if (event.type === "extension_ui_request" && EXTENSION_UI_BLOCKING_METHODS.has(event.method)) {
|
|
7684
|
+
if (!event.replayed) notifyBlockedTab(event.tabId, { request: event, count: event.pendingExtensionUiRequestCount });
|
|
7685
|
+
renderTabs();
|
|
7686
|
+
} else if (event.type === "agent_end") {
|
|
7687
|
+
notifyAgentDone(event.tabId, { activity: event.tabActivity, tabTitle: event.tabTitle });
|
|
7688
|
+
}
|
|
7689
|
+
}
|
|
7690
|
+
|
|
5463
7691
|
function handleEvent(event) {
|
|
5464
7692
|
ingestEventTabActivity(event);
|
|
7693
|
+
if (!eventTargetsActiveTab(event)) {
|
|
7694
|
+
handleInactiveTabEvent(event);
|
|
7695
|
+
return;
|
|
7696
|
+
}
|
|
7697
|
+
const tabContext = activeTabContext(event.tabId || activeTabId);
|
|
5465
7698
|
switch (event.type) {
|
|
5466
7699
|
case "webui_connected":
|
|
5467
7700
|
addEvent(`connected to ${event.tabTitle || "terminal"} for ${event.cwd}`);
|
|
@@ -5491,7 +7724,12 @@ function handleEvent(event) {
|
|
|
5491
7724
|
renderStatus();
|
|
5492
7725
|
renderWidgets();
|
|
5493
7726
|
refreshTabs().catch((error) => addEvent(error.message, "error"));
|
|
5494
|
-
setTimeout(() =>
|
|
7727
|
+
setTimeout(() => {
|
|
7728
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
7729
|
+
refreshAll(tabContext).catch((error) => {
|
|
7730
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
7731
|
+
});
|
|
7732
|
+
}, 500);
|
|
5495
7733
|
break;
|
|
5496
7734
|
case "webui_extension_ui_cancelled":
|
|
5497
7735
|
removeQueuedDialogRequests(event.ids || []);
|
|
@@ -5581,10 +7819,18 @@ function handleEvent(event) {
|
|
|
5581
7819
|
scheduleRefreshFooter();
|
|
5582
7820
|
break;
|
|
5583
7821
|
case "tool_execution_start":
|
|
7822
|
+
streamToolCallSeen = true;
|
|
7823
|
+
suppressStreamingAssistantTextBeforeToolCall();
|
|
7824
|
+
handleToolExecutionStart(event);
|
|
5584
7825
|
setRunIndicatorActivity(`Running tool: ${runIndicatorToolName(event.toolName)}…`);
|
|
5585
7826
|
addEvent(`tool ${event.toolName} started`);
|
|
5586
7827
|
break;
|
|
7828
|
+
case "tool_execution_update":
|
|
7829
|
+
handleToolExecutionUpdate(event);
|
|
7830
|
+
setRunIndicatorActivity(`Running tool: ${runIndicatorToolName(event.toolName)}…`, { scroll: false });
|
|
7831
|
+
break;
|
|
5587
7832
|
case "tool_execution_end":
|
|
7833
|
+
handleToolExecutionEnd(event);
|
|
5588
7834
|
setRunIndicatorActivity(`Tool ${runIndicatorToolName(event.toolName)} ${event.isError ? "failed" : "finished"}; waiting for the agent's next step…`);
|
|
5589
7835
|
addEvent(`tool ${event.toolName} ${event.isError ? "failed" : "finished"}`, event.isError ? "error" : "info");
|
|
5590
7836
|
scheduleRefreshMessages();
|
|
@@ -5602,6 +7848,29 @@ function handleEvent(event) {
|
|
|
5602
7848
|
markTabOutputSeen();
|
|
5603
7849
|
scheduleRefreshMessages();
|
|
5604
7850
|
break;
|
|
7851
|
+
case "auto_retry_start": {
|
|
7852
|
+
const seconds = Math.max(0, Math.ceil(Number(event.delayMs || 0) / 1000));
|
|
7853
|
+
const retryText = `Retrying (${event.attempt || "?"}/${event.maxAttempts || "?"}) in ${seconds}s after: ${event.errorMessage || "model/provider error"}`;
|
|
7854
|
+
setRunIndicatorActivity(retryText);
|
|
7855
|
+
addEvent(retryText, "warn");
|
|
7856
|
+
addTransientMessage({ role: "warn", title: "auto retry", content: retryText, level: "warn" });
|
|
7857
|
+
break;
|
|
7858
|
+
}
|
|
7859
|
+
case "auto_retry_end":
|
|
7860
|
+
if (event.success === false) {
|
|
7861
|
+
const retryError = `Retry failed after ${event.attempt || "?"} attempt(s): ${event.finalError || "Unknown error"}`;
|
|
7862
|
+
addEvent(retryError, "error");
|
|
7863
|
+
addTransientMessage({ role: "error", title: "auto retry failed", content: retryError, level: "error" });
|
|
7864
|
+
} else {
|
|
7865
|
+
addEvent(`retry recovered after ${event.attempt || "?"} attempt(s)`);
|
|
7866
|
+
}
|
|
7867
|
+
break;
|
|
7868
|
+
case "extension_error": {
|
|
7869
|
+
const message = `${event.extensionPath || "extension"}${event.event ? ` during ${event.event}` : ""}: ${event.error || "unknown extension error"}`;
|
|
7870
|
+
addEvent(message, "error");
|
|
7871
|
+
addTransientMessage({ role: "error", title: "extension error", content: message, level: "error" });
|
|
7872
|
+
break;
|
|
7873
|
+
}
|
|
5605
7874
|
case "extension_ui_request":
|
|
5606
7875
|
handleExtensionUiRequest(event);
|
|
5607
7876
|
break;
|
|
@@ -5624,18 +7893,23 @@ function handleEvent(event) {
|
|
|
5624
7893
|
}
|
|
5625
7894
|
}
|
|
5626
7895
|
|
|
5627
|
-
function connectEvents() {
|
|
7896
|
+
function connectEvents(tabContext = activeTabContext()) {
|
|
5628
7897
|
eventSource?.close();
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
7898
|
+
eventSource = null;
|
|
7899
|
+
if (!tabContext.tabId || !isCurrentTabContext(tabContext)) return;
|
|
7900
|
+
const source = new EventSource(`/api/events?tab=${encodeURIComponent(tabContext.tabId)}`);
|
|
7901
|
+
eventSource = source;
|
|
7902
|
+
source.onmessage = (message) => {
|
|
7903
|
+
if (eventSource !== source || !isCurrentTabContext(tabContext)) return;
|
|
5632
7904
|
try {
|
|
5633
7905
|
handleEvent(JSON.parse(message.data));
|
|
5634
7906
|
} catch (error) {
|
|
5635
|
-
addEvent(error.message, "error");
|
|
7907
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5636
7908
|
}
|
|
5637
7909
|
};
|
|
5638
|
-
|
|
7910
|
+
source.onerror = () => {
|
|
7911
|
+
if (eventSource === source && isCurrentTabContext(tabContext)) addEvent("event stream disconnected; browser will retry", "warn");
|
|
7912
|
+
};
|
|
5639
7913
|
}
|
|
5640
7914
|
|
|
5641
7915
|
elements.sendFeedbackButton.addEventListener("click", () => submitQueuedActionFeedback());
|
|
@@ -5672,70 +7946,143 @@ publishMenuContainer?.addEventListener("focusout", () => {
|
|
|
5672
7946
|
elements.releaseNpmButton.addEventListener("click", () => runPublishWorkflow("/release-npm"));
|
|
5673
7947
|
elements.releaseAurButton.addEventListener("click", () => runPublishWorkflow("/release-aur"));
|
|
5674
7948
|
elements.gitWorkflowCancelButton.addEventListener("click", cancelGitWorkflow);
|
|
5675
|
-
elements.
|
|
7949
|
+
elements.nativeCommandDialog.addEventListener("close", () => {
|
|
7950
|
+
elements.nativeCommandSearch.oninput = null;
|
|
7951
|
+
nativeCommandTabId = null;
|
|
7952
|
+
});
|
|
7953
|
+
|
|
7954
|
+
function resetAbortLongPressAffordance() {
|
|
7955
|
+
clearTimeout(abortLongPressTimer);
|
|
7956
|
+
abortLongPressTimer = null;
|
|
7957
|
+
elements.abortButton.classList.remove("long-pressing");
|
|
7958
|
+
if (!abortRequestInFlight) elements.abortButton.textContent = "Abort";
|
|
7959
|
+
}
|
|
7960
|
+
|
|
7961
|
+
async function abortActiveRun({ source = "button" } = {}) {
|
|
7962
|
+
if (abortRequestInFlight || !isAbortAvailable()) return;
|
|
7963
|
+
const tabContext = activeTabContext();
|
|
7964
|
+
abortRequestInFlight = true;
|
|
7965
|
+
resetAbortLongPressAffordance();
|
|
7966
|
+
updateComposerModeButtons();
|
|
5676
7967
|
const hadActiveRun = runIndicatorIsActive();
|
|
5677
7968
|
try {
|
|
5678
|
-
if (hadActiveRun) setRunIndicatorActivity(
|
|
5679
|
-
await api("/api/abort", { method: "POST", body: {} });
|
|
7969
|
+
if (hadActiveRun) setRunIndicatorActivity(`Abort requested${source === "escape" ? " from Esc" : source === "long-press" ? " from long-press" : ""}; checking whether Pi stopped…`);
|
|
7970
|
+
await api("/api/abort", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
7971
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5680
7972
|
addAbortTranscriptNotice({ activeRun: hadActiveRun });
|
|
5681
|
-
scheduleAbortStateChecks();
|
|
7973
|
+
scheduleAbortStateChecks(tabContext);
|
|
5682
7974
|
} catch (error) {
|
|
5683
|
-
|
|
5684
|
-
|
|
7975
|
+
if (isCurrentTabContext(tabContext)) {
|
|
7976
|
+
addEvent(error.message, "error");
|
|
7977
|
+
addAbortTranscriptNotice({ errorMessage: error.message });
|
|
7978
|
+
}
|
|
7979
|
+
} finally {
|
|
7980
|
+
abortRequestInFlight = false;
|
|
7981
|
+
updateComposerModeButtons();
|
|
7982
|
+
}
|
|
7983
|
+
}
|
|
7984
|
+
|
|
7985
|
+
function startAbortLongPress(event) {
|
|
7986
|
+
if (!isAbortAvailable() || abortRequestInFlight) return;
|
|
7987
|
+
if (event.button !== undefined && event.button !== 0) return;
|
|
7988
|
+
resetAbortLongPressAffordance();
|
|
7989
|
+
abortLongPressHandled = false;
|
|
7990
|
+
elements.abortButton.classList.add("long-pressing");
|
|
7991
|
+
elements.abortButton.textContent = "Hold…";
|
|
7992
|
+
abortLongPressTimer = setTimeout(() => {
|
|
7993
|
+
abortLongPressTimer = null;
|
|
7994
|
+
abortLongPressHandled = true;
|
|
7995
|
+
abortActiveRun({ source: "long-press" });
|
|
7996
|
+
}, ABORT_LONG_PRESS_MS);
|
|
7997
|
+
}
|
|
7998
|
+
|
|
7999
|
+
elements.abortButton.addEventListener("pointerdown", startAbortLongPress);
|
|
8000
|
+
for (const eventName of ["pointerup", "pointerleave", "pointercancel", "blur"]) {
|
|
8001
|
+
elements.abortButton.addEventListener(eventName, resetAbortLongPressAffordance);
|
|
8002
|
+
}
|
|
8003
|
+
elements.abortButton.addEventListener("click", (event) => {
|
|
8004
|
+
if (abortLongPressHandled) {
|
|
8005
|
+
event.preventDefault();
|
|
8006
|
+
abortLongPressHandled = false;
|
|
8007
|
+
return;
|
|
5685
8008
|
}
|
|
8009
|
+
abortActiveRun({ source: "button" });
|
|
5686
8010
|
});
|
|
5687
8011
|
elements.newSessionButton.addEventListener("click", async () => {
|
|
5688
8012
|
setComposerActionsOpen(false);
|
|
8013
|
+
const tabContext = activeTabContext();
|
|
5689
8014
|
if (!confirm("Start a new Pi session?")) return;
|
|
5690
8015
|
try {
|
|
5691
|
-
const response = await api("/api/new-session", { method: "POST", body: {} });
|
|
8016
|
+
const response = await api("/api/new-session", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
5692
8017
|
applyResponseTab(response);
|
|
5693
|
-
forgetLastUserPrompt(
|
|
5694
|
-
|
|
5695
|
-
|
|
8018
|
+
forgetLastUserPrompt(tabContext.tabId);
|
|
8019
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
8020
|
+
await refreshAll(tabContext);
|
|
8021
|
+
if (isCurrentTabContext(tabContext)) focusPromptInput({ defer: true });
|
|
5696
8022
|
} catch (error) {
|
|
5697
|
-
addEvent(error.message, "error");
|
|
8023
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5698
8024
|
}
|
|
5699
8025
|
});
|
|
5700
8026
|
elements.compactButton.addEventListener("click", async () => {
|
|
5701
8027
|
setComposerActionsOpen(false);
|
|
8028
|
+
const tabContext = activeTabContext();
|
|
5702
8029
|
try {
|
|
5703
8030
|
elements.compactButton.disabled = true;
|
|
5704
8031
|
elements.compactButton.textContent = "Compacting…";
|
|
5705
8032
|
setRunIndicatorActivity("Requesting context compaction…");
|
|
5706
8033
|
scrollChatToBottom({ force: true });
|
|
5707
8034
|
addEvent("manual compaction requested");
|
|
5708
|
-
await api("/api/compact", { method: "POST", body: {} });
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
8035
|
+
await api("/api/compact", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
8036
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
8037
|
+
scheduleRefreshState(120, tabContext);
|
|
8038
|
+
scheduleRefreshMessages(600, tabContext);
|
|
8039
|
+
scheduleRefreshFooter(600, tabContext);
|
|
5712
8040
|
} catch (error) {
|
|
5713
|
-
|
|
5714
|
-
|
|
8041
|
+
if (isCurrentTabContext(tabContext)) {
|
|
8042
|
+
clearRunIndicatorActivity();
|
|
8043
|
+
addEvent(error.message, "error");
|
|
8044
|
+
}
|
|
5715
8045
|
} finally {
|
|
5716
|
-
|
|
5717
|
-
|
|
8046
|
+
if (isCurrentTabContext(tabContext)) {
|
|
8047
|
+
elements.compactButton.disabled = !!currentState?.isCompacting;
|
|
8048
|
+
elements.compactButton.textContent = currentState?.isCompacting ? "Compacting…" : "Compact";
|
|
8049
|
+
}
|
|
5718
8050
|
}
|
|
5719
8051
|
});
|
|
5720
8052
|
elements.setModelButton.addEventListener("click", async () => {
|
|
5721
8053
|
if (!elements.modelSelect.value) return;
|
|
8054
|
+
const tabContext = activeTabContext();
|
|
5722
8055
|
try {
|
|
5723
8056
|
const selected = JSON.parse(elements.modelSelect.value);
|
|
5724
|
-
await api("/api/model", { method: "POST", body: selected });
|
|
5725
|
-
await refreshState();
|
|
8057
|
+
await api("/api/model", { method: "POST", body: selected, tabId: tabContext.tabId });
|
|
8058
|
+
if (isCurrentTabContext(tabContext)) await refreshState(tabContext);
|
|
5726
8059
|
} catch (error) {
|
|
5727
|
-
addEvent(error.message, "error");
|
|
8060
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5728
8061
|
}
|
|
5729
8062
|
});
|
|
5730
8063
|
elements.setThinkingButton.addEventListener("click", async () => {
|
|
8064
|
+
const tabContext = activeTabContext();
|
|
5731
8065
|
try {
|
|
5732
|
-
await api("/api/thinking", { method: "POST", body: { level: elements.thinkingSelect.value } });
|
|
5733
|
-
await refreshState();
|
|
8066
|
+
await api("/api/thinking", { method: "POST", body: { level: elements.thinkingSelect.value }, tabId: tabContext.tabId });
|
|
8067
|
+
if (isCurrentTabContext(tabContext)) await refreshState(tabContext);
|
|
5734
8068
|
} catch (error) {
|
|
5735
|
-
addEvent(error.message, "error");
|
|
8069
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
5736
8070
|
}
|
|
5737
8071
|
});
|
|
5738
|
-
elements.themeSelect.addEventListener("change", () =>
|
|
8072
|
+
elements.themeSelect.addEventListener("change", () => {
|
|
8073
|
+
setThemeByName(elements.themeSelect.value, { persist: true, announce: true }).catch((error) => addEvent(error.message || String(error), "error"));
|
|
8074
|
+
});
|
|
8075
|
+
if (elements.backgroundChooseButton && elements.backgroundInput) {
|
|
8076
|
+
elements.backgroundChooseButton.addEventListener("click", () => elements.backgroundInput.click());
|
|
8077
|
+
elements.backgroundInput.addEventListener("change", () => {
|
|
8078
|
+
const [file] = Array.from(elements.backgroundInput.files || []);
|
|
8079
|
+
elements.backgroundInput.value = "";
|
|
8080
|
+
setCustomBackgroundFromFile(file).catch((error) => addEvent(error.message || String(error), "error"));
|
|
8081
|
+
});
|
|
8082
|
+
}
|
|
8083
|
+
if (elements.backgroundClearButton) {
|
|
8084
|
+
elements.backgroundClearButton.addEventListener("click", () => clearCustomBackground().catch((error) => addEvent(error.message || String(error), "error")));
|
|
8085
|
+
}
|
|
5739
8086
|
elements.openNetworkButton.addEventListener("click", openToNetwork);
|
|
5740
8087
|
elements.agentDoneNotificationsToggle.addEventListener("change", () => {
|
|
5741
8088
|
setAgentDoneNotificationsEnabled(elements.agentDoneNotificationsToggle.checked, {
|
|
@@ -5746,6 +8093,11 @@ elements.agentDoneNotificationsToggle.addEventListener("change", () => {
|
|
|
5746
8093
|
renderAgentDoneNotificationsToggle();
|
|
5747
8094
|
});
|
|
5748
8095
|
});
|
|
8096
|
+
if (elements.thinkingVisibilityToggle) {
|
|
8097
|
+
elements.thinkingVisibilityToggle.addEventListener("change", () => {
|
|
8098
|
+
setThinkingOutputVisible(elements.thinkingVisibilityToggle.checked, { announce: true });
|
|
8099
|
+
});
|
|
8100
|
+
}
|
|
5749
8101
|
elements.toggleSidePanelButton.addEventListener("click", () => {
|
|
5750
8102
|
setSidePanelCollapsed(true);
|
|
5751
8103
|
});
|
|
@@ -5792,6 +8144,7 @@ document.addEventListener("pointermove", (event) => {
|
|
|
5792
8144
|
}, { passive: true });
|
|
5793
8145
|
window.addEventListener("keydown", (event) => {
|
|
5794
8146
|
if (event.key !== "Escape") return;
|
|
8147
|
+
if (elements.dialog?.open || elements.pathPickerDialog?.open) return;
|
|
5795
8148
|
if (publishMenuOpen) {
|
|
5796
8149
|
setPublishMenuOpen(false);
|
|
5797
8150
|
return;
|
|
@@ -5808,8 +8161,17 @@ window.addEventListener("keydown", (event) => {
|
|
|
5808
8161
|
setFooterModelPickerOpen(false);
|
|
5809
8162
|
return;
|
|
5810
8163
|
}
|
|
8164
|
+
if (!elements.commandSuggest.hidden) {
|
|
8165
|
+
hideCommandSuggestions();
|
|
8166
|
+
return;
|
|
8167
|
+
}
|
|
5811
8168
|
if (isMobileView() && !document.body.classList.contains("side-panel-collapsed")) {
|
|
5812
8169
|
setSidePanelCollapsed(true);
|
|
8170
|
+
return;
|
|
8171
|
+
}
|
|
8172
|
+
if (isAbortAvailable()) {
|
|
8173
|
+
event.preventDefault();
|
|
8174
|
+
abortActiveRun({ source: "escape" });
|
|
5813
8175
|
}
|
|
5814
8176
|
});
|
|
5815
8177
|
|
|
@@ -5824,6 +8186,18 @@ elements.pathPickerDialog.addEventListener("close", () => {
|
|
|
5824
8186
|
if (pathPickerState) closePathPicker(null);
|
|
5825
8187
|
});
|
|
5826
8188
|
|
|
8189
|
+
if (elements.attachButton && elements.attachmentInput) {
|
|
8190
|
+
elements.attachButton.addEventListener("click", () => elements.attachmentInput.click());
|
|
8191
|
+
elements.attachmentInput.addEventListener("change", () => {
|
|
8192
|
+
addAttachmentFiles(elements.attachmentInput.files, "picker");
|
|
8193
|
+
elements.attachmentInput.value = "";
|
|
8194
|
+
});
|
|
8195
|
+
}
|
|
8196
|
+
elements.promptInput.addEventListener("paste", handleAttachmentPaste);
|
|
8197
|
+
elements.composer.addEventListener("dragover", handleComposerDragOver);
|
|
8198
|
+
elements.composer.addEventListener("dragleave", handleComposerDragLeave);
|
|
8199
|
+
elements.composer.addEventListener("drop", handleComposerDrop);
|
|
8200
|
+
|
|
5827
8201
|
elements.promptInput.addEventListener("keydown", (event) => {
|
|
5828
8202
|
if (shouldSendPromptFromEnter(event)) {
|
|
5829
8203
|
event.preventDefault();
|
|
@@ -5885,9 +8259,17 @@ updateComposerModeButtons();
|
|
|
5885
8259
|
updateOptionalFeatureAvailability();
|
|
5886
8260
|
loadLastUserPromptCache();
|
|
5887
8261
|
installViewportHandlers();
|
|
5888
|
-
|
|
8262
|
+
currentThemeName = storedThemeName();
|
|
8263
|
+
renderBackgroundControl();
|
|
8264
|
+
initializeThemes().catch((error) => {
|
|
8265
|
+
addEvent(`failed to load themes: ${error.message}`, "warn");
|
|
8266
|
+
initializeCustomBackground().catch((backgroundError) => addEvent(`failed to initialize background: ${backgroundError.message}`, "warn"));
|
|
8267
|
+
});
|
|
5889
8268
|
initializeFastPicks().catch((error) => addEvent(`failed to initialize path fast picks: ${error.message}`, "error"));
|
|
5890
8269
|
restoreAgentDoneNotificationsSetting();
|
|
8270
|
+
restoreThinkingVisibilitySetting();
|
|
8271
|
+
restoreSidePanelSectionState();
|
|
8272
|
+
bindSidePanelSectionToggles();
|
|
5891
8273
|
restoreSidePanelState();
|
|
5892
8274
|
bindMobileViewChanges();
|
|
5893
8275
|
registerPwaServiceWorker();
|