@firstpick/pi-package-webui 0.1.9 → 0.2.1
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 +77 -118
- package/WEBUI_TUI_NATIVE_PARITY.json +666 -0
- package/bin/pi-webui.mjs +375 -28
- package/package.json +6 -3
- package/public/app.js +802 -94
- package/public/index.html +25 -21
- package/public/styles.css +209 -82
- package/start-webui.ps1 +368 -0
- package/start-webui.sh +510 -0
- package/tests/mobile-static.test.mjs +118 -12
- package/tests/native-parity.test.mjs +148 -0
package/public/app.js
CHANGED
|
@@ -60,6 +60,7 @@ const elements = {
|
|
|
60
60
|
backgroundStatus: $("#backgroundStatus"),
|
|
61
61
|
networkStatus: $("#networkStatus"),
|
|
62
62
|
openNetworkButton: $("#openNetworkButton"),
|
|
63
|
+
stopServerButton: $("#stopServerButton"),
|
|
63
64
|
agentDoneNotificationsToggle: $("#agentDoneNotificationsToggle"),
|
|
64
65
|
agentDoneNotificationsStatus: $("#agentDoneNotificationsStatus"),
|
|
65
66
|
optionalFeaturesBox: $("#optionalFeaturesBox"),
|
|
@@ -127,6 +128,7 @@ let refreshMessagesTimer = null;
|
|
|
127
128
|
let refreshStateTimer = null;
|
|
128
129
|
let refreshFooterTimer = null;
|
|
129
130
|
let refreshTabsTimer = null;
|
|
131
|
+
let foregroundReconcileTimer = null;
|
|
130
132
|
let eventSource = null;
|
|
131
133
|
let activeDialog = null;
|
|
132
134
|
let nativeCommandTabId = null;
|
|
@@ -156,6 +158,8 @@ let codexUsageRenderTimer = null;
|
|
|
156
158
|
let backendOffline = false;
|
|
157
159
|
let backendOfflineNoticeShown = false;
|
|
158
160
|
let latestMessages = [];
|
|
161
|
+
let promptHistoryByTab = new Map();
|
|
162
|
+
let promptHistoryNavigation = null;
|
|
159
163
|
let transientMessages = [];
|
|
160
164
|
let actionEntrySeenKeysByTab = new Map();
|
|
161
165
|
let actionEntryAnimationPrimedTabs = new Set();
|
|
@@ -167,6 +171,7 @@ let blockedTabNotificationPermissionRequested = false;
|
|
|
167
171
|
let blockedTabNotificationFallbackNoted = false;
|
|
168
172
|
let agentDoneNotificationsEnabled = false;
|
|
169
173
|
let thinkingOutputVisible = true;
|
|
174
|
+
let toolOutputGloballyExpanded = false;
|
|
170
175
|
let agentDoneNotificationPermissionRequested = false;
|
|
171
176
|
let agentDoneNotificationFallbackNoted = false;
|
|
172
177
|
let agentDoneNotificationKeys = new Set();
|
|
@@ -192,6 +197,9 @@ let currentRunStartedAt = null;
|
|
|
192
197
|
let currentRunStreamChars = 0;
|
|
193
198
|
let latestTokPerSecond = null;
|
|
194
199
|
let abortRequestInFlight = false;
|
|
200
|
+
let userBashByTab = new Map();
|
|
201
|
+
let userBashQueuesByTab = new Map();
|
|
202
|
+
let latestQueuedMessagesByTab = new Map();
|
|
195
203
|
let abortLongPressTimer = null;
|
|
196
204
|
let abortLongPressHandled = false;
|
|
197
205
|
const dialogQueue = [];
|
|
@@ -201,6 +209,7 @@ const TAB_STORAGE_KEY = "pi-webui-active-tab";
|
|
|
201
209
|
const PATH_FAST_PICKS_STORAGE_KEY = "pi-webui-path-fast-picks";
|
|
202
210
|
const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications";
|
|
203
211
|
const THINKING_VISIBILITY_STORAGE_KEY = "pi-webui-thinking-visible";
|
|
212
|
+
const TOOL_OUTPUT_EXPANDED_STORAGE_KEY = "pi-webui-tool-output-expanded";
|
|
204
213
|
const THEME_STORAGE_KEY = "pi-webui-theme";
|
|
205
214
|
const CUSTOM_BACKGROUND_STORAGE_KEY = "pi-webui-custom-background";
|
|
206
215
|
const CUSTOM_BACKGROUNDS_STORAGE_KEY = "pi-webui-custom-backgrounds";
|
|
@@ -212,6 +221,8 @@ const DEFAULT_WEBUI_PORT = "31415";
|
|
|
212
221
|
const CUSTOM_BACKGROUND_MAX_FILE_BYTES = 24 * 1024 * 1024;
|
|
213
222
|
const OPTIONAL_FEATURES_STORAGE_KEY = "pi-webui-optional-features-disabled";
|
|
214
223
|
const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
|
|
224
|
+
const PROMPT_HISTORY_STORAGE_KEY = "pi-webui-prompt-history";
|
|
225
|
+
const PROMPT_HISTORY_LIMIT_PER_TAB = 50;
|
|
215
226
|
const ATTACHMENT_MAX_FILES = 12;
|
|
216
227
|
const ATTACHMENT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
217
228
|
const ATTACHMENT_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
@@ -237,10 +248,12 @@ const STREAM_OUTPUT_HIDE_DELAY_MS = 300;
|
|
|
237
248
|
const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220;
|
|
238
249
|
const STREAM_OUTPUT_MIN_VISIBLE_MS = 900;
|
|
239
250
|
const TOOL_LIVE_UPDATE_THROTTLE_MS = 80;
|
|
251
|
+
const UNEXPOSED_THINKING_TEXT = "No thinking content was exposed by the provider.";
|
|
240
252
|
const TODO_PROGRESS_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)\]\s+.+$/;
|
|
241
253
|
const TODO_PROGRESS_PARTIAL_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)?\]?\s*.*$/;
|
|
242
254
|
const CHAT_SCROLL_KEYS = new Set(["ArrowDown", "ArrowUp", "End", "Home", "PageDown", "PageUp", " "]);
|
|
243
255
|
const TAB_ACTIVITY_IDLE_RECONCILE_GRACE_MS = 1200;
|
|
256
|
+
const FOREGROUND_RECONCILE_DELAY_MS = 120;
|
|
244
257
|
const TAB_GROUP_STATUS_PRIORITY = ["blocked", "done", "idle", "working"];
|
|
245
258
|
const EXTENSION_UI_BLOCKING_METHODS = new Set(["select", "confirm", "input", "editor"]);
|
|
246
259
|
const BLOCKED_TAB_NOTIFICATION_TAG_PREFIX = "pi-webui-blocked-tab";
|
|
@@ -249,6 +262,7 @@ const BLOCKED_TAB_NOTIFICATION_ICON = "/icon-192.png";
|
|
|
249
262
|
const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
|
|
250
263
|
const statusEntries = new Map();
|
|
251
264
|
const widgets = new Map();
|
|
265
|
+
const todoProgressWidgetExpandedByTab = new Map();
|
|
252
266
|
const liveToolRuns = new Map();
|
|
253
267
|
const liveToolCards = new Map();
|
|
254
268
|
const liveToolRenderQueue = new Map();
|
|
@@ -360,6 +374,10 @@ function bindGitWorkflowToActiveTab() {
|
|
|
360
374
|
return gitWorkflow;
|
|
361
375
|
}
|
|
362
376
|
|
|
377
|
+
function gitWorkflowActionTabId() {
|
|
378
|
+
return activeTabId;
|
|
379
|
+
}
|
|
380
|
+
|
|
363
381
|
function resetGitWorkflowForTab(tabId = activeTabId) {
|
|
364
382
|
if (!tabId) return;
|
|
365
383
|
gitWorkflowsByTab.set(tabId, createGitWorkflowState());
|
|
@@ -434,10 +452,12 @@ function sidePanelSectionRecords() {
|
|
|
434
452
|
|
|
435
453
|
function readStoredSidePanelSectionCollapsedIds() {
|
|
436
454
|
try {
|
|
437
|
-
const
|
|
455
|
+
const stored = localStorage.getItem(SIDE_PANEL_SECTION_STORAGE_KEY);
|
|
456
|
+
if (stored === null) return null;
|
|
457
|
+
const parsed = JSON.parse(stored);
|
|
438
458
|
return new Set(Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : []);
|
|
439
459
|
} catch {
|
|
440
|
-
return
|
|
460
|
+
return null;
|
|
441
461
|
}
|
|
442
462
|
}
|
|
443
463
|
|
|
@@ -462,17 +482,32 @@ function setSidePanelSectionCollapsed(record, collapsed, { persist = true } = {}
|
|
|
462
482
|
if (persist) persistSidePanelSectionState();
|
|
463
483
|
}
|
|
464
484
|
|
|
485
|
+
function setOnlySidePanelSectionExpanded(targetRecord, { persist = true } = {}) {
|
|
486
|
+
const targetId = targetRecord?.id || null;
|
|
487
|
+
for (const record of sidePanelSectionRecords()) {
|
|
488
|
+
setSidePanelSectionCollapsed(record, record.id !== targetId, { persist: false });
|
|
489
|
+
}
|
|
490
|
+
if (persist) persistSidePanelSectionState();
|
|
491
|
+
}
|
|
492
|
+
|
|
465
493
|
function restoreSidePanelSectionState() {
|
|
494
|
+
const records = sidePanelSectionRecords();
|
|
466
495
|
const collapsedIds = readStoredSidePanelSectionCollapsedIds();
|
|
467
|
-
|
|
468
|
-
|
|
496
|
+
const expandedRecords = collapsedIds ? records.filter(({ id }) => !collapsedIds.has(id)) : [];
|
|
497
|
+
const expandedId = expandedRecords.length === 1 ? expandedRecords[0].id : null;
|
|
498
|
+
for (const record of records) {
|
|
499
|
+
setSidePanelSectionCollapsed(record, record.id !== expandedId, { persist: false });
|
|
469
500
|
}
|
|
470
501
|
}
|
|
471
502
|
|
|
472
503
|
function bindSidePanelSectionToggles() {
|
|
473
504
|
for (const record of sidePanelSectionRecords()) {
|
|
474
505
|
record.button.addEventListener("click", () => {
|
|
475
|
-
|
|
506
|
+
if (record.section.classList.contains("collapsed")) {
|
|
507
|
+
setOnlySidePanelSectionExpanded(record);
|
|
508
|
+
} else {
|
|
509
|
+
setSidePanelSectionCollapsed(record, true);
|
|
510
|
+
}
|
|
476
511
|
});
|
|
477
512
|
}
|
|
478
513
|
}
|
|
@@ -560,6 +595,22 @@ function persistThinkingOutputVisible(visible) {
|
|
|
560
595
|
}
|
|
561
596
|
}
|
|
562
597
|
|
|
598
|
+
function readStoredToolOutputExpanded() {
|
|
599
|
+
try {
|
|
600
|
+
return localStorage.getItem(TOOL_OUTPUT_EXPANDED_STORAGE_KEY) === "1";
|
|
601
|
+
} catch {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function persistToolOutputExpanded(expanded) {
|
|
607
|
+
try {
|
|
608
|
+
localStorage.setItem(TOOL_OUTPUT_EXPANDED_STORAGE_KEY, expanded ? "1" : "0");
|
|
609
|
+
} catch {
|
|
610
|
+
// Ignore storage failures; this can remain a page-local preference.
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
563
614
|
function thinkingVisibilityStatusText() {
|
|
564
615
|
return thinkingOutputVisible ? "Visible" : "Hidden from transcript";
|
|
565
616
|
}
|
|
@@ -587,6 +638,24 @@ function setThinkingOutputVisible(visible, { announce = false } = {}) {
|
|
|
587
638
|
if (announce) addEvent(thinkingOutputVisible ? "thinking output shown" : "thinking output hidden", thinkingOutputVisible ? "info" : "warn");
|
|
588
639
|
}
|
|
589
640
|
|
|
641
|
+
function applyToolOutputExpansionToDom(expanded = toolOutputGloballyExpanded) {
|
|
642
|
+
for (const details of elements.chat.querySelectorAll(".tool-output-details, .tool-raw-details, .message.toolResult .message-collapse, .message.toolExecution details, .message.bashExecution .message-collapse")) {
|
|
643
|
+
details.open = !!expanded;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function setToolOutputGloballyExpanded(expanded, { announce = false, rerender = false } = {}) {
|
|
648
|
+
toolOutputGloballyExpanded = !!expanded;
|
|
649
|
+
persistToolOutputExpanded(toolOutputGloballyExpanded);
|
|
650
|
+
if (rerender) renderAllMessages({ preserveScroll: true });
|
|
651
|
+
else applyToolOutputExpansionToDom();
|
|
652
|
+
if (announce) addEvent(toolOutputGloballyExpanded ? "tool and bash output expanded" : "tool and bash output collapsed", "info");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function restoreToolOutputExpansionSetting() {
|
|
656
|
+
toolOutputGloballyExpanded = readStoredToolOutputExpanded();
|
|
657
|
+
}
|
|
658
|
+
|
|
590
659
|
function restoreThinkingVisibilitySetting() {
|
|
591
660
|
thinkingOutputVisible = readStoredThinkingOutputVisible();
|
|
592
661
|
renderThinkingVisibilityToggle();
|
|
@@ -599,12 +668,34 @@ function setComposerActionsOpen(open) {
|
|
|
599
668
|
if (!shouldOpen) setPublishMenuOpen(false);
|
|
600
669
|
}
|
|
601
670
|
|
|
671
|
+
function isUserBashActive(tabId = activeTabId) {
|
|
672
|
+
return !!tabId && userBashByTab.has(tabId);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function userBashQueueForTab(tabId) {
|
|
676
|
+
if (!tabId) return [];
|
|
677
|
+
let queue = userBashQueuesByTab.get(tabId);
|
|
678
|
+
if (!queue) {
|
|
679
|
+
queue = [];
|
|
680
|
+
userBashQueuesByTab.set(tabId, queue);
|
|
681
|
+
}
|
|
682
|
+
return queue;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function queuedUserBashCount(tabId = activeTabId) {
|
|
686
|
+
return tabId ? userBashQueueForTab(tabId).length : 0;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function isUserBashRunningOrQueued(tabId = activeTabId) {
|
|
690
|
+
return isUserBashActive(tabId) || queuedUserBashCount(tabId) > 0;
|
|
691
|
+
}
|
|
692
|
+
|
|
602
693
|
function isRunActive() {
|
|
603
|
-
return !!currentState?.isStreaming || (runIndicatorLocallyActive && !currentState?.isCompacting);
|
|
694
|
+
return !!currentState?.isStreaming || isUserBashRunningOrQueued() || (runIndicatorLocallyActive && !currentState?.isCompacting);
|
|
604
695
|
}
|
|
605
696
|
|
|
606
697
|
function isAbortAvailable() {
|
|
607
|
-
return runIndicatorIsActive();
|
|
698
|
+
return runIndicatorIsActive() || isUserBashActive();
|
|
608
699
|
}
|
|
609
700
|
|
|
610
701
|
function resizePromptInput() {
|
|
@@ -841,6 +932,20 @@ async function copyText(text) {
|
|
|
841
932
|
if (!copied) throw new Error("Clipboard copy failed");
|
|
842
933
|
}
|
|
843
934
|
|
|
935
|
+
function triggerNativeDownload(download) {
|
|
936
|
+
const url = String(download?.url || "").trim();
|
|
937
|
+
if (!url) return false;
|
|
938
|
+
const anchor = document.createElement("a");
|
|
939
|
+
anchor.href = new URL(url, window.location.href).href;
|
|
940
|
+
anchor.download = String(download.fileName || "");
|
|
941
|
+
anchor.rel = "noopener";
|
|
942
|
+
anchor.hidden = true;
|
|
943
|
+
document.body.append(anchor);
|
|
944
|
+
anchor.click();
|
|
945
|
+
anchor.remove();
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
|
|
844
949
|
async function copyServerStartCommand() {
|
|
845
950
|
const command = serverStartCommandText();
|
|
846
951
|
try {
|
|
@@ -2164,6 +2269,7 @@ function saveActiveDraft() {
|
|
|
2164
2269
|
}
|
|
2165
2270
|
|
|
2166
2271
|
function restoreActiveDraft() {
|
|
2272
|
+
resetPromptHistoryNavigation();
|
|
2167
2273
|
elements.promptInput.value = activeTabId ? tabDrafts.get(activeTabId) || "" : "";
|
|
2168
2274
|
resizePromptInput();
|
|
2169
2275
|
renderCommandSuggestions();
|
|
@@ -2243,8 +2349,12 @@ function resetActiveTabUi() {
|
|
|
2243
2349
|
resetChatOutput();
|
|
2244
2350
|
elements.stateDetails.replaceChildren();
|
|
2245
2351
|
elements.eventLog.replaceChildren();
|
|
2246
|
-
|
|
2247
|
-
|
|
2352
|
+
const queuedSnapshot = activeTabId ? latestQueuedMessagesByTab.get(activeTabId) : null;
|
|
2353
|
+
if (queuedSnapshot) renderQueue({ tabId: activeTabId, ...queuedSnapshot });
|
|
2354
|
+
else {
|
|
2355
|
+
elements.queueBox.textContent = "No queued messages.";
|
|
2356
|
+
elements.queueBox.classList.add("muted");
|
|
2357
|
+
}
|
|
2248
2358
|
elements.commandsBox.textContent = "Loading…";
|
|
2249
2359
|
elements.commandsBox.classList.add("muted");
|
|
2250
2360
|
elements.sessionLine.textContent = activeTab() ? "Connecting…" : "No terminal tabs.";
|
|
@@ -3960,12 +4070,19 @@ function renderTodoProgressWidget(_key, lines) {
|
|
|
3960
4070
|
const todo = parseTodoProgressWidget(lines);
|
|
3961
4071
|
if (!todo) return null;
|
|
3962
4072
|
|
|
3963
|
-
const
|
|
4073
|
+
const tabId = activeTabId || "default";
|
|
4074
|
+
const node = make("details", "widget todo-widget");
|
|
4075
|
+
node.open = todoProgressWidgetExpandedByTab.get(tabId) === true;
|
|
3964
4076
|
node.setAttribute("aria-label", "Todo progress");
|
|
4077
|
+
node.addEventListener("toggle", () => {
|
|
4078
|
+
todoProgressWidgetExpandedByTab.set(tabId, node.open);
|
|
4079
|
+
});
|
|
3965
4080
|
|
|
3966
4081
|
const percent = todo.total > 0 ? Math.max(0, Math.min(100, (todo.done / todo.total) * 100)) : 0;
|
|
4082
|
+
const summary = make("summary", "todo-widget-summary");
|
|
3967
4083
|
const header = make("div", "todo-widget-header");
|
|
3968
4084
|
header.append(
|
|
4085
|
+
make("span", "todo-widget-toggle", "›"),
|
|
3969
4086
|
make("span", "todo-widget-title", "Todo progress"),
|
|
3970
4087
|
make("span", "todo-widget-count", `${todo.done}/${todo.total}`),
|
|
3971
4088
|
make("span", "todo-widget-meta", todo.partial ? `${todo.partial} partial` : "active"),
|
|
@@ -3975,7 +4092,9 @@ function renderTodoProgressWidget(_key, lines) {
|
|
|
3975
4092
|
const fill = make("span", "todo-widget-progress-fill");
|
|
3976
4093
|
fill.style.width = `${percent}%`;
|
|
3977
4094
|
progress.append(fill);
|
|
4095
|
+
summary.append(header, progress);
|
|
3978
4096
|
|
|
4097
|
+
const body = make("div", "todo-widget-body");
|
|
3979
4098
|
const list = make("ol", "todo-widget-list");
|
|
3980
4099
|
for (const item of todo.items) {
|
|
3981
4100
|
const row = make("li", `todo-widget-item ${item.status}`);
|
|
@@ -3985,9 +4104,11 @@ function renderTodoProgressWidget(_key, lines) {
|
|
|
3985
4104
|
);
|
|
3986
4105
|
list.append(row);
|
|
3987
4106
|
}
|
|
4107
|
+
if (todo.items.length) body.append(list);
|
|
4108
|
+
if (todo.footer) body.append(make("div", "todo-widget-footer", todo.footer));
|
|
3988
4109
|
|
|
3989
|
-
node.append(
|
|
3990
|
-
if (
|
|
4110
|
+
node.append(summary);
|
|
4111
|
+
if (body.children.length) node.append(body);
|
|
3991
4112
|
return node;
|
|
3992
4113
|
}
|
|
3993
4114
|
|
|
@@ -4049,6 +4170,17 @@ function releaseNpmActionButton(label, command, className = "") {
|
|
|
4049
4170
|
return button;
|
|
4050
4171
|
}
|
|
4051
4172
|
|
|
4173
|
+
function releaseNpmStreamHeader(label, lineCount, { live = false } = {}) {
|
|
4174
|
+
const header = make("div", "release-npm-stream-header");
|
|
4175
|
+
const safeLineCount = Math.max(0, Number(lineCount) || 0);
|
|
4176
|
+
header.append(
|
|
4177
|
+
make("span", `release-npm-stream-dot${live ? " live" : ""}`),
|
|
4178
|
+
make("span", "release-npm-stream-title", label),
|
|
4179
|
+
make("span", "release-npm-stream-count", `${safeLineCount} line${safeLineCount === 1 ? "" : "s"}`),
|
|
4180
|
+
);
|
|
4181
|
+
return header;
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4052
4184
|
function renderReleaseNpmOutputWidget() {
|
|
4053
4185
|
if (!isOptionalFeatureEnabled("releaseNpm")) return null;
|
|
4054
4186
|
const outputLines = getWidgetLines("release-npm:output");
|
|
@@ -4074,15 +4206,17 @@ function renderReleaseNpmOutputWidget() {
|
|
|
4074
4206
|
);
|
|
4075
4207
|
header.append(titleWrap, meta, actions);
|
|
4076
4208
|
|
|
4209
|
+
const streamLines = outputLines.length ? outputLines : ["Waiting for release output..."];
|
|
4210
|
+
const streamHeader = releaseNpmStreamHeader("Live output stream", outputLines.length, { live: true });
|
|
4077
4211
|
const terminal = make("div", "release-npm-terminal");
|
|
4078
4212
|
terminal.setAttribute("role", "log");
|
|
4079
4213
|
terminal.setAttribute("aria-live", "polite");
|
|
4080
|
-
for (const line of
|
|
4214
|
+
for (const line of streamLines) {
|
|
4081
4215
|
appendReleaseNpmTerminalLine(terminal, line);
|
|
4082
4216
|
}
|
|
4083
4217
|
|
|
4084
4218
|
const controls = make("div", "release-npm-controls", details.controls || "Controls: /release-toggle expands/collapses · /release-abort stops subprocess");
|
|
4085
|
-
node.append(header, terminal, controls);
|
|
4219
|
+
node.append(header, streamHeader, terminal, controls);
|
|
4086
4220
|
requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
4087
4221
|
return node;
|
|
4088
4222
|
}
|
|
@@ -4106,11 +4240,13 @@ function renderReleaseNpmLogWidget() {
|
|
|
4106
4240
|
actions.append(releaseNpmActionButton("Close log", "/release-npm-logs close"));
|
|
4107
4241
|
header.append(titleWrap, meta, actions);
|
|
4108
4242
|
|
|
4243
|
+
const logLines = lines.slice(2).filter((line, index) => index > 0 || stripAnsi(line).trim());
|
|
4244
|
+
const streamHeader = releaseNpmStreamHeader("Saved output stream", logLines.length);
|
|
4109
4245
|
const terminal = make("div", "release-npm-terminal");
|
|
4110
|
-
for (const line of
|
|
4246
|
+
for (const line of logLines) {
|
|
4111
4247
|
appendReleaseNpmTerminalLine(terminal, line);
|
|
4112
4248
|
}
|
|
4113
|
-
node.append(header, terminal);
|
|
4249
|
+
node.append(header, streamHeader, terminal);
|
|
4114
4250
|
requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
4115
4251
|
return node;
|
|
4116
4252
|
}
|
|
@@ -4140,15 +4276,17 @@ function renderReleaseAurOutputWidget() {
|
|
|
4140
4276
|
);
|
|
4141
4277
|
header.append(titleWrap, meta, actions);
|
|
4142
4278
|
|
|
4279
|
+
const streamLines = outputLines.length ? outputLines : ["Waiting for release-aur output..."];
|
|
4280
|
+
const streamHeader = releaseNpmStreamHeader("Live AUR output stream", outputLines.length, { live: true });
|
|
4143
4281
|
const terminal = make("div", "release-npm-terminal");
|
|
4144
4282
|
terminal.setAttribute("role", "log");
|
|
4145
4283
|
terminal.setAttribute("aria-live", "polite");
|
|
4146
|
-
for (const line of
|
|
4284
|
+
for (const line of streamLines) {
|
|
4147
4285
|
appendReleaseNpmTerminalLine(terminal, line);
|
|
4148
4286
|
}
|
|
4149
4287
|
|
|
4150
4288
|
const controls = make("div", "release-npm-controls", details.controls || "Controls: /release-aur toggle expands/collapses · /release-aur abort stops subprocess");
|
|
4151
|
-
node.append(header, terminal, controls);
|
|
4289
|
+
node.append(header, streamHeader, terminal, controls);
|
|
4152
4290
|
requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
4153
4291
|
return node;
|
|
4154
4292
|
}
|
|
@@ -4172,11 +4310,13 @@ function renderReleaseAurLogWidget() {
|
|
|
4172
4310
|
actions.append(releaseNpmActionButton("Close log", "/release-aur logs close"));
|
|
4173
4311
|
header.append(titleWrap, meta, actions);
|
|
4174
4312
|
|
|
4313
|
+
const logLines = lines.slice(2).filter((line, index) => index > 0 || stripAnsi(line).trim());
|
|
4314
|
+
const streamHeader = releaseNpmStreamHeader("Saved AUR output stream", logLines.length);
|
|
4175
4315
|
const terminal = make("div", "release-npm-terminal");
|
|
4176
|
-
for (const line of
|
|
4316
|
+
for (const line of logLines) {
|
|
4177
4317
|
appendReleaseNpmTerminalLine(terminal, line);
|
|
4178
4318
|
}
|
|
4179
|
-
node.append(header, terminal);
|
|
4319
|
+
node.append(header, streamHeader, terminal);
|
|
4180
4320
|
requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
4181
4321
|
return node;
|
|
4182
4322
|
}
|
|
@@ -4312,24 +4452,24 @@ function renderGitWorkflow() {
|
|
|
4312
4452
|
elements.gitWorkflowCancelButton.disabled = false;
|
|
4313
4453
|
|
|
4314
4454
|
if (gitWorkflow.step === "add") {
|
|
4315
|
-
addGitWorkflowAction("Run git add .", runGitAdd, "primary", false);
|
|
4455
|
+
addGitWorkflowAction("Run git add .", () => runGitAdd(), "primary", false);
|
|
4316
4456
|
} else if (gitWorkflow.step === "generate") {
|
|
4317
|
-
addGitWorkflowAction("Run /git-staged-msg", runGitMessagePrompt, "primary", false);
|
|
4457
|
+
addGitWorkflowAction("Run /git-staged-msg", () => runGitMessagePrompt(), "primary", false);
|
|
4318
4458
|
addGitWorkflowAction("Preview current message files", () => loadGitWorkflowMessage({ requireFresh: false }), "", false);
|
|
4319
4459
|
} else if (gitWorkflow.step === "generating") {
|
|
4320
4460
|
addGitWorkflowAction("Refresh message preview", () => loadGitWorkflowMessage({ requireFresh: true }), "", false);
|
|
4321
4461
|
} else if (gitWorkflow.step === "message") {
|
|
4322
4462
|
addGitWorkflowAction("Commit short", () => commitGitWorkflow("short"), "primary", false);
|
|
4323
4463
|
addGitWorkflowAction("Commit long", () => commitGitWorkflow("long"), "primary", false);
|
|
4324
|
-
addGitWorkflowAction("Regenerate", runGitMessagePrompt, "", false);
|
|
4464
|
+
addGitWorkflowAction("Regenerate", () => runGitMessagePrompt(), "", false);
|
|
4325
4465
|
} else if (gitWorkflow.step === "push") {
|
|
4326
|
-
addGitWorkflowAction("Run git push", pushGitWorkflow, "primary", false);
|
|
4466
|
+
addGitWorkflowAction("Run git push", () => pushGitWorkflow(), "primary", false);
|
|
4327
4467
|
} else if (gitWorkflow.step === "done") {
|
|
4328
4468
|
addGitWorkflowAction("Close", () => setGitWorkflow({ active: false }), "primary", false);
|
|
4329
|
-
addGitWorkflowAction("Start another", startGitWorkflow, "", false);
|
|
4469
|
+
addGitWorkflowAction("Start another", () => startGitWorkflow(), "", false);
|
|
4330
4470
|
} else if (["cancelled", "error"].includes(gitWorkflow.step)) {
|
|
4331
4471
|
addGitWorkflowAction("Close", () => setGitWorkflow({ active: false }), "primary", false);
|
|
4332
|
-
addGitWorkflowAction("Restart", startGitWorkflow, "", false);
|
|
4472
|
+
addGitWorkflowAction("Restart", () => startGitWorkflow(), "", false);
|
|
4333
4473
|
}
|
|
4334
4474
|
}
|
|
4335
4475
|
|
|
@@ -4357,8 +4497,7 @@ function failGitWorkflow(error, step, { tabId = activeTabId } = {}) {
|
|
|
4357
4497
|
}, { tabId });
|
|
4358
4498
|
}
|
|
4359
4499
|
|
|
4360
|
-
function startGitWorkflow() {
|
|
4361
|
-
const tabId = activeTabId;
|
|
4500
|
+
function startGitWorkflow(tabId = activeTabId) {
|
|
4362
4501
|
if (!tabId) return;
|
|
4363
4502
|
if (!isOptionalFeatureEnabled("gitWorkflow")) {
|
|
4364
4503
|
const tabContext = activeTabContext(tabId);
|
|
@@ -4382,8 +4521,7 @@ function startGitWorkflow() {
|
|
|
4382
4521
|
}, { tabId });
|
|
4383
4522
|
}
|
|
4384
4523
|
|
|
4385
|
-
async function cancelGitWorkflow() {
|
|
4386
|
-
const tabId = activeTabId;
|
|
4524
|
+
async function cancelGitWorkflow(tabId = gitWorkflowActionTabId()) {
|
|
4387
4525
|
const tabContext = activeTabContext(tabId);
|
|
4388
4526
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4389
4527
|
if (!workflow?.active) return;
|
|
@@ -4398,8 +4536,7 @@ async function cancelGitWorkflow() {
|
|
|
4398
4536
|
if (shouldAbortPi && isCurrentTabContext(tabContext)) scheduleAbortStateChecks();
|
|
4399
4537
|
}
|
|
4400
4538
|
|
|
4401
|
-
async function runGitAdd() {
|
|
4402
|
-
const tabId = activeTabId;
|
|
4539
|
+
async function runGitAdd(tabId = gitWorkflowActionTabId()) {
|
|
4403
4540
|
const tabContext = activeTabContext(tabId);
|
|
4404
4541
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4405
4542
|
if (!workflow) return;
|
|
@@ -4415,10 +4552,11 @@ async function runGitAdd() {
|
|
|
4415
4552
|
}
|
|
4416
4553
|
}
|
|
4417
4554
|
|
|
4418
|
-
async function runGitMessagePrompt() {
|
|
4419
|
-
const tabId = activeTabId;
|
|
4555
|
+
async function runGitMessagePrompt(tabId = gitWorkflowActionTabId()) {
|
|
4420
4556
|
const tabContext = activeTabContext(tabId);
|
|
4421
|
-
|
|
4557
|
+
const targetTab = tabs.find((tab) => tab.id === tabId);
|
|
4558
|
+
const targetBusy = tabId === activeTabId ? !!currentState?.isStreaming : activityForTab(targetTab).isWorking;
|
|
4559
|
+
if (targetBusy) {
|
|
4422
4560
|
failGitWorkflow(new Error("Pi is currently running. Wait for it to finish or abort before generating a staged commit message."), "generate", { tabId });
|
|
4423
4561
|
return;
|
|
4424
4562
|
}
|
|
@@ -4441,7 +4579,8 @@ async function runGitMessagePrompt() {
|
|
|
4441
4579
|
if (isCurrentTabContext(tabContext)) scheduleRefreshState(120, tabContext);
|
|
4442
4580
|
setTimeout(() => {
|
|
4443
4581
|
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
4444
|
-
|
|
4582
|
+
const targetStillBusy = tabId === activeTabId && currentState?.isStreaming;
|
|
4583
|
+
if (isCurrentGitWorkflowRun(runId, tabId) && currentWorkflow?.step === "generating" && !targetStillBusy) {
|
|
4445
4584
|
loadGitWorkflowMessage({ requireFresh: true, retries: 1, runId, tabId });
|
|
4446
4585
|
}
|
|
4447
4586
|
}, 2500);
|
|
@@ -4483,8 +4622,7 @@ async function loadGitWorkflowMessage({ requireFresh = false, retries = 0, runId
|
|
|
4483
4622
|
}
|
|
4484
4623
|
}
|
|
4485
4624
|
|
|
4486
|
-
async function commitGitWorkflow(variant) {
|
|
4487
|
-
const tabId = activeTabId;
|
|
4625
|
+
async function commitGitWorkflow(variant, tabId = gitWorkflowActionTabId()) {
|
|
4488
4626
|
const tabContext = activeTabContext(tabId);
|
|
4489
4627
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4490
4628
|
if (!workflow) return;
|
|
@@ -4500,8 +4638,7 @@ async function commitGitWorkflow(variant) {
|
|
|
4500
4638
|
}
|
|
4501
4639
|
}
|
|
4502
4640
|
|
|
4503
|
-
async function pushGitWorkflow() {
|
|
4504
|
-
const tabId = activeTabId;
|
|
4641
|
+
async function pushGitWorkflow(tabId = gitWorkflowActionTabId()) {
|
|
4505
4642
|
const tabContext = activeTabContext(tabId);
|
|
4506
4643
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4507
4644
|
if (!workflow) return;
|
|
@@ -4521,19 +4658,31 @@ function resumeGitWorkflowForActiveTab(tabContext = activeTabContext()) {
|
|
|
4521
4658
|
if (!isCurrentTabContext(tabContext)) return;
|
|
4522
4659
|
bindGitWorkflowToActiveTab();
|
|
4523
4660
|
renderGitWorkflow();
|
|
4524
|
-
|
|
4661
|
+
const workflowTabId = gitWorkflowActionTabId();
|
|
4662
|
+
if (workflowTabId === tabContext.tabId && gitWorkflow.active && gitWorkflow.step === "generating" && !currentState?.isStreaming) {
|
|
4525
4663
|
const retryDelayMs = Math.max(0, 2500 - (Date.now() - (gitWorkflow.messageRequestedAt || 0)));
|
|
4526
4664
|
if (retryDelayMs > 0) {
|
|
4527
4665
|
setTimeout(() => resumeGitWorkflowForActiveTab(tabContext), retryDelayMs);
|
|
4528
4666
|
return;
|
|
4529
4667
|
}
|
|
4530
|
-
loadGitWorkflowMessage({ requireFresh: true, retries: 3, runId: gitWorkflow.runId, tabId:
|
|
4668
|
+
loadGitWorkflowMessage({ requireFresh: true, retries: 3, runId: gitWorkflow.runId, tabId: workflowTabId });
|
|
4531
4669
|
}
|
|
4532
4670
|
}
|
|
4533
4671
|
|
|
4672
|
+
function normalizeQueuedMessages(event) {
|
|
4673
|
+
const normalize = (items) => (Array.isArray(items) ? items.map((item) => String(item || "")).filter((item) => item.trim()) : []);
|
|
4674
|
+
return {
|
|
4675
|
+
steering: normalize(event?.steering),
|
|
4676
|
+
followUp: normalize(event?.followUp),
|
|
4677
|
+
};
|
|
4678
|
+
}
|
|
4679
|
+
|
|
4534
4680
|
function renderQueue(event) {
|
|
4535
|
-
const
|
|
4536
|
-
const
|
|
4681
|
+
const snapshot = normalizeQueuedMessages(event);
|
|
4682
|
+
const tabId = event?.tabId || activeTabId;
|
|
4683
|
+
if (tabId) latestQueuedMessagesByTab.set(tabId, snapshot);
|
|
4684
|
+
const steering = snapshot.steering;
|
|
4685
|
+
const followUp = snapshot.followUp;
|
|
4537
4686
|
if (steering.length === 0 && followUp.length === 0) {
|
|
4538
4687
|
elements.queueBox.textContent = "No queued messages.";
|
|
4539
4688
|
elements.queueBox.classList.add("muted");
|
|
@@ -4543,9 +4692,32 @@ function renderQueue(event) {
|
|
|
4543
4692
|
const lines = [];
|
|
4544
4693
|
if (steering.length) lines.push(`Steering (${steering.length}):`, ...steering.map((item) => `• ${item}`));
|
|
4545
4694
|
if (followUp.length) lines.push(`Follow-up (${followUp.length}):`, ...followUp.map((item) => `• ${item}`));
|
|
4695
|
+
lines.push("↳ Alt+Up restores the latest observed queue snapshot to the composer (RPC queue clearing is pending upstream support).");
|
|
4546
4696
|
elements.queueBox.textContent = lines.join("\n");
|
|
4547
4697
|
}
|
|
4548
4698
|
|
|
4699
|
+
function queuedMessagesForComposer(tabId = activeTabId) {
|
|
4700
|
+
const snapshot = latestQueuedMessagesByTab.get(tabId) || { steering: [], followUp: [] };
|
|
4701
|
+
return [...(snapshot.steering || []), ...(snapshot.followUp || [])].map((item) => String(item || "").trim()).filter(Boolean);
|
|
4702
|
+
}
|
|
4703
|
+
|
|
4704
|
+
function restoreQueuedMessagesToComposerFromShortcut() {
|
|
4705
|
+
const queued = queuedMessagesForComposer();
|
|
4706
|
+
if (queued.length === 0) {
|
|
4707
|
+
addEvent("no queued messages to restore", "warn");
|
|
4708
|
+
return false;
|
|
4709
|
+
}
|
|
4710
|
+
const queuedText = queued.join("\n\n");
|
|
4711
|
+
const currentText = elements.promptInput.value || "";
|
|
4712
|
+
elements.promptInput.value = [queuedText, currentText].filter((item) => item.trim()).join("\n\n");
|
|
4713
|
+
resizePromptInput();
|
|
4714
|
+
renderCommandSuggestions();
|
|
4715
|
+
saveActiveDraft();
|
|
4716
|
+
focusPromptInput({ defer: true });
|
|
4717
|
+
addEvent(`restored ${queued.length} queued message${queued.length === 1 ? "" : "s"} to composer; Pi's RPC queue is still pending upstream clear support`, "warn");
|
|
4718
|
+
return true;
|
|
4719
|
+
}
|
|
4720
|
+
|
|
4549
4721
|
function appendText(parent, text, className = "text-block") {
|
|
4550
4722
|
const block = make("pre", className);
|
|
4551
4723
|
block.textContent = text || "";
|
|
@@ -5107,11 +5279,12 @@ function renderContent(parent, content, { markdown = false } = {}) {
|
|
|
5107
5279
|
if (markdown) appendMarkdown(parent, stripTodoProgressLines(text));
|
|
5108
5280
|
else appendText(parent, text);
|
|
5109
5281
|
} else if (part.type === "thinking") {
|
|
5110
|
-
|
|
5282
|
+
const thinking = visibleThinkingText(assistantThinkingText(part));
|
|
5283
|
+
if (!thinkingOutputVisible || !thinking) continue;
|
|
5111
5284
|
const details = make("details", "thinking-block");
|
|
5112
5285
|
details.open = true;
|
|
5113
5286
|
details.append(make("summary", undefined, "thinking"));
|
|
5114
|
-
appendText(details,
|
|
5287
|
+
appendText(details, thinking, "thinking-text");
|
|
5115
5288
|
parent.append(details);
|
|
5116
5289
|
} else if (part.type === "toolCall") {
|
|
5117
5290
|
const details = make("details");
|
|
@@ -5147,6 +5320,13 @@ function assistantThinkingText(part) {
|
|
|
5147
5320
|
return typeof part.content === "string" ? part.content : "";
|
|
5148
5321
|
}
|
|
5149
5322
|
|
|
5323
|
+
function visibleThinkingText(text) {
|
|
5324
|
+
const value = String(text || "");
|
|
5325
|
+
const trimmed = value.trim();
|
|
5326
|
+
if (!trimmed || trimmed === UNEXPOSED_THINKING_TEXT) return "";
|
|
5327
|
+
return value;
|
|
5328
|
+
}
|
|
5329
|
+
|
|
5150
5330
|
function isAssistantToolCallPart(part) {
|
|
5151
5331
|
return !!(part && typeof part === "object" && (part.type === "toolCall" || part.toolCall));
|
|
5152
5332
|
}
|
|
@@ -5208,8 +5388,8 @@ function assistantDisplayMessages(message) {
|
|
|
5208
5388
|
const part = content[index];
|
|
5209
5389
|
const isThinkingPart = part && typeof part === "object" && (part.type === "thinking" || typeof part.thinking === "string");
|
|
5210
5390
|
if (isThinkingPart) {
|
|
5211
|
-
const thinking = assistantThinkingText(part)
|
|
5212
|
-
displayMessages.push({ ...base, role: "thinking", title: "thinking", content: thinking, thinking });
|
|
5391
|
+
const thinking = visibleThinkingText(assistantThinkingText(part));
|
|
5392
|
+
if (thinking) displayMessages.push({ ...base, role: "thinking", title: "thinking", content: thinking, thinking });
|
|
5213
5393
|
continue;
|
|
5214
5394
|
}
|
|
5215
5395
|
if (isAssistantToolCallPart(part)) {
|
|
@@ -5251,6 +5431,136 @@ function stickyUserPromptPreview(message) {
|
|
|
5251
5431
|
return stickyUserPromptPreviewText(messageUserPromptText(message));
|
|
5252
5432
|
}
|
|
5253
5433
|
|
|
5434
|
+
function promptHistoryText(value) {
|
|
5435
|
+
return stripAnsi(String(value ?? "")).replace(/\r\n?/g, "\n").trim();
|
|
5436
|
+
}
|
|
5437
|
+
|
|
5438
|
+
function promptHistoryMessageText(message) {
|
|
5439
|
+
if (message?.role !== "user") return "";
|
|
5440
|
+
const text = promptHistoryText(textFromContent(message.content));
|
|
5441
|
+
return text.startsWith("/") ? "" : text;
|
|
5442
|
+
}
|
|
5443
|
+
|
|
5444
|
+
function promptHistoryForTab(tabId = activeTabId) {
|
|
5445
|
+
if (!tabId) return [];
|
|
5446
|
+
return promptHistoryByTab.get(tabId) || [];
|
|
5447
|
+
}
|
|
5448
|
+
|
|
5449
|
+
function promptHistoryWithEntry(history, text) {
|
|
5450
|
+
const prompt = promptHistoryText(text);
|
|
5451
|
+
if (!prompt) return history || [];
|
|
5452
|
+
return [...(history || []).filter((entry) => entry !== prompt), prompt].slice(-PROMPT_HISTORY_LIMIT_PER_TAB);
|
|
5453
|
+
}
|
|
5454
|
+
|
|
5455
|
+
function promptHistoryEqual(left = [], right = []) {
|
|
5456
|
+
return left.length === right.length && left.every((entry, index) => entry === right[index]);
|
|
5457
|
+
}
|
|
5458
|
+
|
|
5459
|
+
function setPromptHistoryForTab(tabId, history, { persist = true } = {}) {
|
|
5460
|
+
if (!tabId) return;
|
|
5461
|
+
const entries = (history || []).map(promptHistoryText).filter(Boolean).slice(-PROMPT_HISTORY_LIMIT_PER_TAB);
|
|
5462
|
+
if (entries.length) promptHistoryByTab.set(tabId, entries);
|
|
5463
|
+
else promptHistoryByTab.delete(tabId);
|
|
5464
|
+
if (persist) persistPromptHistoryCache();
|
|
5465
|
+
}
|
|
5466
|
+
|
|
5467
|
+
function loadPromptHistoryCache() {
|
|
5468
|
+
try {
|
|
5469
|
+
const raw = JSON.parse(localStorage.getItem(PROMPT_HISTORY_STORAGE_KEY) || "{}");
|
|
5470
|
+
promptHistoryByTab = new Map(Object.entries(raw)
|
|
5471
|
+
.map(([tabId, entries]) => [tabId, Array.isArray(entries) ? entries.map(promptHistoryText).filter(Boolean).slice(-PROMPT_HISTORY_LIMIT_PER_TAB) : []])
|
|
5472
|
+
.filter(([, entries]) => entries.length));
|
|
5473
|
+
} catch {
|
|
5474
|
+
promptHistoryByTab = new Map();
|
|
5475
|
+
}
|
|
5476
|
+
}
|
|
5477
|
+
|
|
5478
|
+
function persistPromptHistoryCache() {
|
|
5479
|
+
try {
|
|
5480
|
+
const entries = [...promptHistoryByTab.entries()]
|
|
5481
|
+
.filter(([tabId, history]) => tabId && Array.isArray(history) && history.length)
|
|
5482
|
+
.slice(-24)
|
|
5483
|
+
.map(([tabId, history]) => [tabId, history.slice(-PROMPT_HISTORY_LIMIT_PER_TAB)]);
|
|
5484
|
+
localStorage.setItem(PROMPT_HISTORY_STORAGE_KEY, JSON.stringify(Object.fromEntries(entries)));
|
|
5485
|
+
} catch {
|
|
5486
|
+
// Ignore storage failures; in-memory prompt history still works for this page load.
|
|
5487
|
+
}
|
|
5488
|
+
}
|
|
5489
|
+
|
|
5490
|
+
function rememberPromptHistory(text, { tabId = activeTabId } = {}) {
|
|
5491
|
+
if (!tabId) return;
|
|
5492
|
+
setPromptHistoryForTab(tabId, promptHistoryWithEntry(promptHistoryForTab(tabId), text));
|
|
5493
|
+
}
|
|
5494
|
+
|
|
5495
|
+
function syncPromptHistoryFromMessages(messages = latestMessages) {
|
|
5496
|
+
if (!activeTabId) return;
|
|
5497
|
+
const prompts = (messages || []).map(promptHistoryMessageText).filter(Boolean);
|
|
5498
|
+
if (!prompts.length) return;
|
|
5499
|
+
const currentHistory = promptHistoryForTab(activeTabId);
|
|
5500
|
+
let nextHistory = currentHistory;
|
|
5501
|
+
for (const prompt of prompts) nextHistory = promptHistoryWithEntry(nextHistory, prompt);
|
|
5502
|
+
if (!promptHistoryEqual(currentHistory, nextHistory)) setPromptHistoryForTab(activeTabId, nextHistory);
|
|
5503
|
+
}
|
|
5504
|
+
|
|
5505
|
+
function resetPromptHistoryNavigation() {
|
|
5506
|
+
promptHistoryNavigation = null;
|
|
5507
|
+
}
|
|
5508
|
+
|
|
5509
|
+
function activePromptHistoryNavigation(history = promptHistoryForTab()) {
|
|
5510
|
+
if (!promptHistoryNavigation || promptHistoryNavigation.tabId !== activeTabId) return null;
|
|
5511
|
+
const index = promptHistoryNavigation.index;
|
|
5512
|
+
if (!Number.isInteger(index) || index < 0 || index >= history.length || elements.promptInput.value !== history[index]) {
|
|
5513
|
+
resetPromptHistoryNavigation();
|
|
5514
|
+
return null;
|
|
5515
|
+
}
|
|
5516
|
+
return promptHistoryNavigation;
|
|
5517
|
+
}
|
|
5518
|
+
|
|
5519
|
+
function applyPromptHistoryValue(value) {
|
|
5520
|
+
const input = elements.promptInput;
|
|
5521
|
+
input.value = value || "";
|
|
5522
|
+
resizePromptInput();
|
|
5523
|
+
try {
|
|
5524
|
+
input.setSelectionRange(input.value.length, input.value.length);
|
|
5525
|
+
} catch {
|
|
5526
|
+
// Some input implementations can reject selection updates; history recall still worked.
|
|
5527
|
+
}
|
|
5528
|
+
hideCommandSuggestions();
|
|
5529
|
+
}
|
|
5530
|
+
|
|
5531
|
+
function recallPreviousPromptFromHistory() {
|
|
5532
|
+
if (!activeTabId) return false;
|
|
5533
|
+
const history = promptHistoryForTab(activeTabId);
|
|
5534
|
+
if (!history.length) return false;
|
|
5535
|
+
const navigation = activePromptHistoryNavigation(history);
|
|
5536
|
+
if (!navigation && elements.promptInput.value.trim()) return false;
|
|
5537
|
+
const index = navigation ? Math.max(0, navigation.index - 1) : history.length - 1;
|
|
5538
|
+
promptHistoryNavigation = {
|
|
5539
|
+
tabId: activeTabId,
|
|
5540
|
+
index,
|
|
5541
|
+
draft: navigation ? navigation.draft : elements.promptInput.value || "",
|
|
5542
|
+
};
|
|
5543
|
+
applyPromptHistoryValue(history[index]);
|
|
5544
|
+
return true;
|
|
5545
|
+
}
|
|
5546
|
+
|
|
5547
|
+
function recallNextPromptFromHistory() {
|
|
5548
|
+
if (!activeTabId) return false;
|
|
5549
|
+
const history = promptHistoryForTab(activeTabId);
|
|
5550
|
+
const navigation = activePromptHistoryNavigation(history);
|
|
5551
|
+
if (!navigation) return false;
|
|
5552
|
+
if (navigation.index >= history.length - 1) {
|
|
5553
|
+
const draft = navigation.draft || "";
|
|
5554
|
+
resetPromptHistoryNavigation();
|
|
5555
|
+
applyPromptHistoryValue(draft);
|
|
5556
|
+
return true;
|
|
5557
|
+
}
|
|
5558
|
+
const index = navigation.index + 1;
|
|
5559
|
+
promptHistoryNavigation = { ...navigation, index };
|
|
5560
|
+
applyPromptHistoryValue(history[index]);
|
|
5561
|
+
return true;
|
|
5562
|
+
}
|
|
5563
|
+
|
|
5254
5564
|
function loadLastUserPromptCache() {
|
|
5255
5565
|
try {
|
|
5256
5566
|
const raw = JSON.parse(localStorage.getItem(LAST_USER_PROMPT_STORAGE_KEY) || "{}");
|
|
@@ -5326,8 +5636,18 @@ function stickyUserPromptViewportGap() {
|
|
|
5326
5636
|
|
|
5327
5637
|
function resetChatOutput() {
|
|
5328
5638
|
liveToolCards.clear();
|
|
5329
|
-
|
|
5330
|
-
if (elements.stickyUserPromptButton)
|
|
5639
|
+
const preservedNodes = [];
|
|
5640
|
+
if (elements.stickyUserPromptButton) preservedNodes.push(elements.stickyUserPromptButton);
|
|
5641
|
+
if (runIndicatorBubble?.parentElement === elements.chat) preservedNodes.push(runIndicatorBubble);
|
|
5642
|
+
elements.chat.replaceChildren(...preservedNodes);
|
|
5643
|
+
}
|
|
5644
|
+
|
|
5645
|
+
function appendChatMessageBubble(bubble) {
|
|
5646
|
+
if (runIndicatorBubble?.parentElement === elements.chat && bubble !== runIndicatorBubble) {
|
|
5647
|
+
elements.chat.insertBefore(bubble, runIndicatorBubble);
|
|
5648
|
+
} else {
|
|
5649
|
+
elements.chat.append(bubble);
|
|
5650
|
+
}
|
|
5331
5651
|
}
|
|
5332
5652
|
|
|
5333
5653
|
function userPromptTargets() {
|
|
@@ -5539,7 +5859,7 @@ function appendToolOutput(parent, text, { label = "output", previewLines = 10, p
|
|
|
5539
5859
|
const lines = clean.split(/\r?\n/);
|
|
5540
5860
|
if (lines.length > previewLines) {
|
|
5541
5861
|
const details = make("details", "tool-output-details");
|
|
5542
|
-
details.open = open;
|
|
5862
|
+
details.open = open || toolOutputGloballyExpanded;
|
|
5543
5863
|
details.append(make("summary", "tool-output-summary", `${label} (${lines.length} lines; expand)`));
|
|
5544
5864
|
appendText(details, clean, "code-block tool-output-code");
|
|
5545
5865
|
parent.append(details);
|
|
@@ -5649,6 +5969,37 @@ function appendToolRawDetails(parent, tool) {
|
|
|
5649
5969
|
parent.append(details);
|
|
5650
5970
|
}
|
|
5651
5971
|
|
|
5972
|
+
function toolRenderSignatureReplacer() {
|
|
5973
|
+
const seen = new WeakSet();
|
|
5974
|
+
return (key, value) => {
|
|
5975
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
5976
|
+
if (typeof value === "string" && value.length > 8000) return `${value.slice(0, 4000)}…${value.slice(-4000)} (${value.length} chars)`;
|
|
5977
|
+
if (value && typeof value === "object") {
|
|
5978
|
+
if (seen.has(value)) return "[Circular]";
|
|
5979
|
+
seen.add(value);
|
|
5980
|
+
}
|
|
5981
|
+
return toolRawDetailsReplacer(key, value);
|
|
5982
|
+
};
|
|
5983
|
+
}
|
|
5984
|
+
|
|
5985
|
+
function toolExecutionRenderSignature(message) {
|
|
5986
|
+
const tool = normalizeToolExecution(message);
|
|
5987
|
+
try {
|
|
5988
|
+
return JSON.stringify({
|
|
5989
|
+
name: tool.name,
|
|
5990
|
+
args: tool.args,
|
|
5991
|
+
result: tool.result,
|
|
5992
|
+
details: tool.details,
|
|
5993
|
+
isPartial: tool.isPartial,
|
|
5994
|
+
isError: tool.isError,
|
|
5995
|
+
startedAt: tool.startedAt,
|
|
5996
|
+
endedAt: tool.endedAt,
|
|
5997
|
+
}, toolRenderSignatureReplacer());
|
|
5998
|
+
} catch {
|
|
5999
|
+
return `${message?.toolName || message?.name || "tool"}|${message?.toolCallId || ""}|${message?.isPartial ? "partial" : "final"}|${message?.isError ? "error" : "ok"}`;
|
|
6000
|
+
}
|
|
6001
|
+
}
|
|
6002
|
+
|
|
5652
6003
|
function renderBashToolExecution(parent, tool) {
|
|
5653
6004
|
const command = toolArgText(tool.args, "command", "");
|
|
5654
6005
|
const timeout = toolArgValue(tool.args, "timeout");
|
|
@@ -5750,9 +6101,15 @@ function liveToolRunMessage(run) {
|
|
|
5750
6101
|
|
|
5751
6102
|
function applyToolExecutionBubbleState(bubble, message) {
|
|
5752
6103
|
const status = toolExecutionStatus(message);
|
|
5753
|
-
|
|
5754
|
-
bubble.classList.
|
|
5755
|
-
|
|
6104
|
+
const nextClass = `tool-${status}`;
|
|
6105
|
+
if (bubble.dataset.toolStatus !== status || !bubble.classList.contains(nextClass)) {
|
|
6106
|
+
for (const className of ["tool-pending", "tool-running", "tool-success", "tool-error"]) {
|
|
6107
|
+
if (className !== nextClass) bubble.classList.remove(className);
|
|
6108
|
+
}
|
|
6109
|
+
bubble.classList.add(nextClass);
|
|
6110
|
+
bubble.dataset.toolStatus = status;
|
|
6111
|
+
}
|
|
6112
|
+
bubble.classList.toggle("error", !!(message.isError || status === "error"));
|
|
5756
6113
|
if (message.toolCallId) {
|
|
5757
6114
|
const id = String(message.toolCallId);
|
|
5758
6115
|
bubble.dataset.toolCallId = id;
|
|
@@ -5815,7 +6172,7 @@ function reuseToolExecutionBubble(reusableToolCards, message, { streaming = fals
|
|
|
5815
6172
|
bubble.removeAttribute("data-user-prompt");
|
|
5816
6173
|
}
|
|
5817
6174
|
if (!streaming && !transient) renderActionFeedbackControls(bubble, message, messageIndex);
|
|
5818
|
-
|
|
6175
|
+
appendChatMessageBubble(bubble);
|
|
5819
6176
|
return { bubble, body };
|
|
5820
6177
|
}
|
|
5821
6178
|
|
|
@@ -5829,10 +6186,13 @@ function updateLiveToolCard(bubble, message) {
|
|
|
5829
6186
|
if (role) role.textContent = messageTitle(message);
|
|
5830
6187
|
const timestamp = header?.querySelector(".muted");
|
|
5831
6188
|
if (timestamp) timestamp.textContent = formatDate(message.timestamp);
|
|
6189
|
+
const nextRenderSignature = toolExecutionRenderSignature(message);
|
|
6190
|
+
if (bubble._toolRenderSignature === nextRenderSignature && body.childElementCount > 0) return true;
|
|
5832
6191
|
const detailsOpenState = captureToolDetailsOpenState(body);
|
|
5833
6192
|
body.replaceChildren();
|
|
5834
6193
|
renderToolExecution(body, message);
|
|
5835
6194
|
restoreToolDetailsOpenState(body, detailsOpenState);
|
|
6195
|
+
bubble._toolRenderSignature = nextRenderSignature;
|
|
5836
6196
|
return true;
|
|
5837
6197
|
}
|
|
5838
6198
|
|
|
@@ -5979,9 +6339,10 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
5979
6339
|
if (message.isError) bubble.classList.add("error");
|
|
5980
6340
|
} else if (message.role === "toolExecution") {
|
|
5981
6341
|
renderToolExecution(body, message);
|
|
6342
|
+
bubble._toolRenderSignature = toolExecutionRenderSignature(message);
|
|
5982
6343
|
} else if (message.role === "thinking") {
|
|
5983
|
-
const thinkingText = message.thinking || textFromContent(message.content);
|
|
5984
|
-
if (thinkingOutputVisible &&
|
|
6344
|
+
const thinkingText = visibleThinkingText(message.thinking || textFromContent(message.content));
|
|
6345
|
+
if (thinkingOutputVisible && thinkingText) appendText(body, thinkingText, "thinking-text");
|
|
5985
6346
|
} else if (message.role === "toolCall") {
|
|
5986
6347
|
appendText(body, JSON.stringify(message.arguments ?? message.content ?? {}, null, 2), "code-block");
|
|
5987
6348
|
} else if (message.role === "assistantEvent") {
|
|
@@ -5992,7 +6353,7 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
5992
6353
|
|
|
5993
6354
|
if (isCollapsibleOutput) {
|
|
5994
6355
|
const details = make("details", "message-collapse");
|
|
5995
|
-
if (message.isError) details.open = true;
|
|
6356
|
+
if (message.isError || toolOutputGloballyExpanded) details.open = true;
|
|
5996
6357
|
details.append(header, body);
|
|
5997
6358
|
bubble.append(details);
|
|
5998
6359
|
if (message.role === "toolResult" && !message.isError) {
|
|
@@ -6006,7 +6367,7 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
6006
6367
|
bubble.append(header, body);
|
|
6007
6368
|
}
|
|
6008
6369
|
if (!streaming && !transient) renderActionFeedbackControls(bubble, message, messageIndex);
|
|
6009
|
-
|
|
6370
|
+
appendChatMessageBubble(bubble);
|
|
6010
6371
|
return { bubble, body };
|
|
6011
6372
|
}
|
|
6012
6373
|
|
|
@@ -6049,11 +6410,11 @@ function appendTranscriptMessage(message, { streaming = false, messageIndex = -1
|
|
|
6049
6410
|
}
|
|
6050
6411
|
|
|
6051
6412
|
function stateHasRunIndicatorActivity(state = currentState) {
|
|
6052
|
-
return !!state?.isStreaming || !!state?.isCompacting;
|
|
6413
|
+
return !!state?.isStreaming || !!state?.isCompacting || isUserBashActive();
|
|
6053
6414
|
}
|
|
6054
6415
|
|
|
6055
6416
|
function runIndicatorIsActive() {
|
|
6056
|
-
return runIndicatorLocallyActive || stateHasRunIndicatorActivity(currentState);
|
|
6417
|
+
return runIndicatorLocallyActive || stateHasRunIndicatorActivity(currentState) || isUserBashActive();
|
|
6057
6418
|
}
|
|
6058
6419
|
|
|
6059
6420
|
function clearRunIndicatorGraceCheck() {
|
|
@@ -6126,22 +6487,24 @@ function stopRunIndicatorTicker() {
|
|
|
6126
6487
|
runIndicatorTimer = null;
|
|
6127
6488
|
}
|
|
6128
6489
|
|
|
6490
|
+
function createRunIndicatorBubble() {
|
|
6491
|
+
runIndicatorBubble = make("article", "message runIndicator run-indicator-message streaming");
|
|
6492
|
+
runIndicatorBubble.setAttribute("aria-live", "polite");
|
|
6493
|
+
runIndicatorBubble.setAttribute("aria-label", "Agent is running:");
|
|
6494
|
+
|
|
6495
|
+
const body = make("div", "message-body");
|
|
6496
|
+
const row = make("div", "run-indicator-row");
|
|
6497
|
+
const pulse = make("span", "run-indicator-pulse");
|
|
6498
|
+
pulse.setAttribute("aria-hidden", "true");
|
|
6499
|
+
runIndicatorText = make("span", "run-indicator-text");
|
|
6500
|
+
runIndicatorMeta = make("span", "run-indicator-meta");
|
|
6501
|
+
row.append(pulse, runIndicatorText, runIndicatorMeta);
|
|
6502
|
+
body.append(row);
|
|
6503
|
+
runIndicatorBubble.append(body);
|
|
6504
|
+
}
|
|
6505
|
+
|
|
6129
6506
|
function ensureRunIndicatorBubble() {
|
|
6130
|
-
if (runIndicatorBubble
|
|
6131
|
-
runIndicatorBubble = make("article", "message runIndicator run-indicator-message streaming");
|
|
6132
|
-
runIndicatorBubble.setAttribute("aria-live", "polite");
|
|
6133
|
-
runIndicatorBubble.setAttribute("aria-label", "Agent is running:");
|
|
6134
|
-
|
|
6135
|
-
const body = make("div", "message-body");
|
|
6136
|
-
const row = make("div", "run-indicator-row");
|
|
6137
|
-
const pulse = make("span", "run-indicator-pulse");
|
|
6138
|
-
pulse.setAttribute("aria-hidden", "true");
|
|
6139
|
-
runIndicatorText = make("span", "run-indicator-text");
|
|
6140
|
-
runIndicatorMeta = make("span", "run-indicator-meta");
|
|
6141
|
-
row.append(pulse, runIndicatorText, runIndicatorMeta);
|
|
6142
|
-
body.append(row);
|
|
6143
|
-
runIndicatorBubble.append(body);
|
|
6144
|
-
}
|
|
6507
|
+
if (!runIndicatorBubble || !runIndicatorText || !runIndicatorMeta) createRunIndicatorBubble();
|
|
6145
6508
|
if (elements.chat.lastElementChild !== runIndicatorBubble) elements.chat.append(runIndicatorBubble);
|
|
6146
6509
|
}
|
|
6147
6510
|
|
|
@@ -6149,9 +6512,11 @@ function updateRunIndicatorBubble() {
|
|
|
6149
6512
|
if (!runIndicatorIsActive()) return;
|
|
6150
6513
|
if (!runIndicatorStartedAt) runIndicatorStartedAt = performance.now();
|
|
6151
6514
|
ensureRunIndicatorBubble();
|
|
6152
|
-
|
|
6515
|
+
const headline = runIndicatorHeadline();
|
|
6516
|
+
if (runIndicatorText.textContent !== headline) runIndicatorText.textContent = headline;
|
|
6153
6517
|
const detail = runIndicatorDetail();
|
|
6154
|
-
|
|
6518
|
+
const meta = runIndicatorShowsElapsed() ? `${detail} · run time ${formatRunIndicatorElapsed()}` : detail;
|
|
6519
|
+
if (runIndicatorMeta.textContent !== meta) runIndicatorMeta.textContent = meta;
|
|
6155
6520
|
}
|
|
6156
6521
|
|
|
6157
6522
|
function removeRunIndicatorBubble() {
|
|
@@ -6324,6 +6689,7 @@ function renderAllMessages({ preserveScroll = false } = {}) {
|
|
|
6324
6689
|
});
|
|
6325
6690
|
}
|
|
6326
6691
|
rememberActionEntries(transcriptItems);
|
|
6692
|
+
applyToolOutputExpansionToDom();
|
|
6327
6693
|
renderRunIndicator({ scroll: false });
|
|
6328
6694
|
updateStickyUserPromptButton();
|
|
6329
6695
|
if (shouldFollow) scrollChatToBottom({ force: true });
|
|
@@ -6335,12 +6701,13 @@ function renderAllMessages({ preserveScroll = false } = {}) {
|
|
|
6335
6701
|
updateStickyUserPromptButton();
|
|
6336
6702
|
}
|
|
6337
6703
|
|
|
6338
|
-
function addTransientMessage({ role = "notice", title, content, level = "info" }) {
|
|
6704
|
+
function addTransientMessage({ role = "notice", title, content, level = "info", ...details }) {
|
|
6339
6705
|
transientMessages.push({
|
|
6340
6706
|
role,
|
|
6341
6707
|
title,
|
|
6342
6708
|
level,
|
|
6343
6709
|
content,
|
|
6710
|
+
...details,
|
|
6344
6711
|
timestamp: Date.now(),
|
|
6345
6712
|
});
|
|
6346
6713
|
if (transientMessages.length > 80) transientMessages.splice(0, transientMessages.length - 80);
|
|
@@ -7154,6 +7521,7 @@ function renderMessages(messages) {
|
|
|
7154
7521
|
latestMessages = messages || [];
|
|
7155
7522
|
cleanupLiveToolRunsForMessages(latestMessages);
|
|
7156
7523
|
syncLastUserPromptFromMessages(latestMessages);
|
|
7524
|
+
syncPromptHistoryFromMessages(latestMessages);
|
|
7157
7525
|
renderAllMessages();
|
|
7158
7526
|
renderFooter();
|
|
7159
7527
|
renderFeedbackTray();
|
|
@@ -7237,9 +7605,9 @@ function ensureStreamingThinkingBubble() {
|
|
|
7237
7605
|
return true;
|
|
7238
7606
|
}
|
|
7239
7607
|
|
|
7240
|
-
function showStreamingThinking(
|
|
7608
|
+
function showStreamingThinking(initialText = "") {
|
|
7241
7609
|
if (!ensureStreamingThinkingBubble()) return;
|
|
7242
|
-
if (!streamThinking.textContent) streamThinking.textContent =
|
|
7610
|
+
if (initialText && !streamThinking.textContent) streamThinking.textContent = initialText;
|
|
7243
7611
|
}
|
|
7244
7612
|
|
|
7245
7613
|
function resetStreamBubble() {
|
|
@@ -7255,7 +7623,7 @@ function resetStreamBubble() {
|
|
|
7255
7623
|
}
|
|
7256
7624
|
|
|
7257
7625
|
function thinkingDeltaText(update) {
|
|
7258
|
-
return update.delta || update.thinking || update.content || "";
|
|
7626
|
+
return visibleThinkingText(update.delta || update.thinking || update.content || "");
|
|
7259
7627
|
}
|
|
7260
7628
|
|
|
7261
7629
|
function assistantStreamingMessage(event) {
|
|
@@ -7282,37 +7650,38 @@ function assistantThinkingTextFromMessage(message) {
|
|
|
7282
7650
|
if (!Array.isArray(content)) return null;
|
|
7283
7651
|
const parts = content
|
|
7284
7652
|
.filter((part) => part && typeof part === "object" && (part.type === "thinking" || typeof part.thinking === "string"))
|
|
7285
|
-
.map((part) => assistantThinkingText(part))
|
|
7653
|
+
.map((part) => visibleThinkingText(assistantThinkingText(part)))
|
|
7286
7654
|
.filter((text) => text.trim());
|
|
7287
7655
|
return parts.length ? parts.join("\n\n") : "";
|
|
7288
7656
|
}
|
|
7289
7657
|
|
|
7290
7658
|
function setStreamingThinkingText(text) {
|
|
7291
|
-
|
|
7659
|
+
const thinking = visibleThinkingText(text);
|
|
7660
|
+
if (!thinkingOutputVisible || !thinking) return false;
|
|
7292
7661
|
showStreamingThinking("");
|
|
7293
|
-
if (streamThinking) streamThinking.textContent =
|
|
7662
|
+
if (streamThinking) streamThinking.textContent = thinking;
|
|
7663
|
+
return true;
|
|
7294
7664
|
}
|
|
7295
7665
|
|
|
7296
7666
|
function syncStreamingThinkingFromMessage(event, { placeholder = "" } = {}) {
|
|
7297
7667
|
if (!thinkingOutputVisible) return true;
|
|
7298
7668
|
const text = assistantThinkingTextFromMessage(assistantStreamingMessage(event));
|
|
7299
7669
|
if (text === null) return false;
|
|
7300
|
-
|
|
7301
|
-
return true;
|
|
7670
|
+
return setStreamingThinkingText(text || placeholder);
|
|
7302
7671
|
}
|
|
7303
7672
|
|
|
7304
7673
|
function handleMessageUpdate(event) {
|
|
7305
7674
|
const update = event.assistantMessageEvent || {};
|
|
7306
7675
|
if (update.type === "thinking_start") {
|
|
7307
7676
|
setRunIndicatorActivity("Thinking…", { scroll: false });
|
|
7308
|
-
syncStreamingThinkingFromMessage(event
|
|
7677
|
+
syncStreamingThinkingFromMessage(event);
|
|
7309
7678
|
scrollChatToBottom();
|
|
7310
7679
|
} else if (update.type === "thinking_delta") {
|
|
7311
7680
|
const delta = thinkingDeltaText(update);
|
|
7312
7681
|
currentRunStreamChars += delta.length;
|
|
7313
7682
|
setRunIndicatorActivity("Thinking…", { scroll: false });
|
|
7314
7683
|
const synced = syncStreamingThinkingFromMessage(event);
|
|
7315
|
-
if (thinkingOutputVisible && (!synced ||
|
|
7684
|
+
if (thinkingOutputVisible && delta && (!synced || !streamThinking?.textContent)) {
|
|
7316
7685
|
showStreamingThinking("");
|
|
7317
7686
|
if (streamThinking?.textContent === "Thinking…") streamThinking.textContent = "";
|
|
7318
7687
|
if (streamThinking) streamThinking.textContent += delta;
|
|
@@ -7932,6 +8301,35 @@ async function refreshAll(tabContext = activeTabContext()) {
|
|
|
7932
8301
|
resumeGitWorkflowForActiveTab(tabContext);
|
|
7933
8302
|
}
|
|
7934
8303
|
|
|
8304
|
+
function ensureActiveEventStream(tabContext = activeTabContext()) {
|
|
8305
|
+
if (!tabContext.tabId || !isCurrentTabContext(tabContext)) return;
|
|
8306
|
+
if (!eventSource || eventSource.readyState === EventSource.CLOSED) connectEvents(tabContext);
|
|
8307
|
+
}
|
|
8308
|
+
|
|
8309
|
+
async function reconcileForegroundState(reason = "resume") {
|
|
8310
|
+
if (document.visibilityState === "hidden") return;
|
|
8311
|
+
|
|
8312
|
+
const tabResult = await Promise.allSettled([refreshTabs()]);
|
|
8313
|
+
const tabContext = activeTabContext();
|
|
8314
|
+
ensureActiveEventStream(tabContext);
|
|
8315
|
+
|
|
8316
|
+
const results = [...tabResult];
|
|
8317
|
+
if (tabContext.tabId) results.push(...(await Promise.allSettled([refreshAll(tabContext)])));
|
|
8318
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
8319
|
+
|
|
8320
|
+
for (const result of results) {
|
|
8321
|
+
if (result.status === "rejected") addEvent(`foreground refresh failed after ${reason}: ${result.reason?.message || String(result.reason)}`, "error");
|
|
8322
|
+
}
|
|
8323
|
+
}
|
|
8324
|
+
|
|
8325
|
+
function scheduleForegroundReconcile(reason = "resume", delay = FOREGROUND_RECONCILE_DELAY_MS) {
|
|
8326
|
+
clearTimeout(foregroundReconcileTimer);
|
|
8327
|
+
foregroundReconcileTimer = setTimeout(() => {
|
|
8328
|
+
foregroundReconcileTimer = null;
|
|
8329
|
+
reconcileForegroundState(reason).catch((error) => addEvent(`foreground refresh failed after ${reason}: ${error.message || String(error)}`, "error"));
|
|
8330
|
+
}, delay);
|
|
8331
|
+
}
|
|
8332
|
+
|
|
7935
8333
|
async function openToNetwork() {
|
|
7936
8334
|
if (latestNetwork?.open) {
|
|
7937
8335
|
await closeNetworkAccess();
|
|
@@ -8006,6 +8404,216 @@ async function closeNetworkAccess() {
|
|
|
8006
8404
|
}
|
|
8007
8405
|
}
|
|
8008
8406
|
|
|
8407
|
+
async function stopServer() {
|
|
8408
|
+
if (!confirm("Stop the Pi Web UI server?\n\nThis disconnects all browser clients and stops the Pi tabs managed by this Web UI.")) return;
|
|
8409
|
+
|
|
8410
|
+
const button = elements.stopServerButton;
|
|
8411
|
+
button.disabled = true;
|
|
8412
|
+
button.textContent = "Stopping…";
|
|
8413
|
+
try {
|
|
8414
|
+
await api("/api/shutdown", { method: "POST", scoped: false });
|
|
8415
|
+
addEvent("Pi Web UI server stop requested", "warn");
|
|
8416
|
+
setBackendOffline(true, new Error("stop requested from side panel"));
|
|
8417
|
+
} catch (error) {
|
|
8418
|
+
if (error?.backendOffline) {
|
|
8419
|
+
addEvent("Pi Web UI server appears to be offline after stop request", "warn");
|
|
8420
|
+
setBackendOffline(true, error);
|
|
8421
|
+
return;
|
|
8422
|
+
}
|
|
8423
|
+
addEvent(error.message || String(error), "error");
|
|
8424
|
+
button.disabled = false;
|
|
8425
|
+
button.textContent = "Stop Server";
|
|
8426
|
+
}
|
|
8427
|
+
}
|
|
8428
|
+
|
|
8429
|
+
function appShortcutModelLabel(model) {
|
|
8430
|
+
return model ? `${model.provider}/${model.id}` : "unknown model";
|
|
8431
|
+
}
|
|
8432
|
+
|
|
8433
|
+
async function cycleModelFromShortcut(direction = "forward") {
|
|
8434
|
+
const tabContext = activeTabContext();
|
|
8435
|
+
if (!tabContext.tabId) return;
|
|
8436
|
+
try {
|
|
8437
|
+
const response = await api("/api/model-cycle", { method: "POST", body: { direction }, tabId: tabContext.tabId });
|
|
8438
|
+
applyResponseTab(response);
|
|
8439
|
+
const model = response.data?.model;
|
|
8440
|
+
const scope = response.data?.scoped ? `scoped (${response.data.scopeSource})` : "all models";
|
|
8441
|
+
if (isCurrentTabContext(tabContext)) {
|
|
8442
|
+
addTransientMessage({ role: "native", title: "model cycle", content: `Model set to ${appShortcutModelLabel(model)} via ${direction} cycle over ${scope}.`, level: "info" });
|
|
8443
|
+
await Promise.allSettled([refreshState(tabContext), refreshModels(tabContext), refreshStats(tabContext)]);
|
|
8444
|
+
}
|
|
8445
|
+
} catch (error) {
|
|
8446
|
+
if (isCurrentTabContext(tabContext)) {
|
|
8447
|
+
addEvent(error.message, "error");
|
|
8448
|
+
addTransientMessage({ role: "error", title: "model cycle", content: error.message, level: "error" });
|
|
8449
|
+
}
|
|
8450
|
+
}
|
|
8451
|
+
}
|
|
8452
|
+
|
|
8453
|
+
async function cycleThinkingFromShortcut() {
|
|
8454
|
+
const tabContext = activeTabContext();
|
|
8455
|
+
if (!tabContext.tabId) return;
|
|
8456
|
+
try {
|
|
8457
|
+
const response = await api("/api/thinking-cycle", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
8458
|
+
if (response.data?.level && currentState) currentState = { ...currentState, thinkingLevel: response.data.level };
|
|
8459
|
+
if (isCurrentTabContext(tabContext)) {
|
|
8460
|
+
addTransientMessage({ role: "native", title: "thinking", content: response.data?.level ? `Thinking level: ${response.data.level}` : "Thinking level did not change.", level: "info" });
|
|
8461
|
+
await Promise.allSettled([refreshState(tabContext), refreshStats(tabContext)]);
|
|
8462
|
+
}
|
|
8463
|
+
} catch (error) {
|
|
8464
|
+
if (isCurrentTabContext(tabContext)) {
|
|
8465
|
+
addEvent(error.message, "error");
|
|
8466
|
+
addTransientMessage({ role: "error", title: "thinking", content: error.message, level: "error" });
|
|
8467
|
+
}
|
|
8468
|
+
}
|
|
8469
|
+
}
|
|
8470
|
+
|
|
8471
|
+
function clearPromptFromShortcut() {
|
|
8472
|
+
const input = elements.promptInput;
|
|
8473
|
+
if (document.activeElement !== input) return false;
|
|
8474
|
+
if (input.selectionStart !== input.selectionEnd) return false;
|
|
8475
|
+
if (!input.value) return false;
|
|
8476
|
+
input.value = "";
|
|
8477
|
+
resizePromptInput();
|
|
8478
|
+
renderCommandSuggestions();
|
|
8479
|
+
addEvent("prompt cleared", "info");
|
|
8480
|
+
return true;
|
|
8481
|
+
}
|
|
8482
|
+
|
|
8483
|
+
function parseUserBashInput(message) {
|
|
8484
|
+
const text = String(message || "").trim();
|
|
8485
|
+
if (!text.startsWith("!") || text === "!" || text === "!!") return null;
|
|
8486
|
+
const excludeFromContext = text.startsWith("!!");
|
|
8487
|
+
const command = text.slice(excludeFromContext ? 2 : 1).trim();
|
|
8488
|
+
if (!command) return null;
|
|
8489
|
+
return { command, excludeFromContext };
|
|
8490
|
+
}
|
|
8491
|
+
|
|
8492
|
+
function userBashOutputSummary(result = {}, excludeFromContext = false) {
|
|
8493
|
+
const output = String(result.output || "").trimEnd();
|
|
8494
|
+
const status = result.cancelled ? "cancelled" : result.exitCode === 0 ? "exit 0" : result.exitCode === undefined || result.exitCode === null ? "finished" : `exit ${result.exitCode}`;
|
|
8495
|
+
const context = excludeFromContext ? "excluded from LLM context" : "included in the next LLM context";
|
|
8496
|
+
const lines = [`# ${status}; ${context}`];
|
|
8497
|
+
if (output) lines.push("", output);
|
|
8498
|
+
if (result.truncated && result.fullOutputPath) lines.push("", `Full output: ${result.fullOutputPath}`);
|
|
8499
|
+
return lines.join("\n");
|
|
8500
|
+
}
|
|
8501
|
+
|
|
8502
|
+
function clearComposerAfterUserBash({ usesPromptInput, targetTabId, tabContext }) {
|
|
8503
|
+
if (!usesPromptInput) return;
|
|
8504
|
+
clearAttachments(targetTabId);
|
|
8505
|
+
if (isCurrentTabContext(tabContext)) {
|
|
8506
|
+
elements.promptInput.value = "";
|
|
8507
|
+
resizePromptInput();
|
|
8508
|
+
} else {
|
|
8509
|
+
tabDrafts.set(targetTabId, "");
|
|
8510
|
+
}
|
|
8511
|
+
}
|
|
8512
|
+
|
|
8513
|
+
function enqueueUserBashCommand(parsed, { usesPromptInput = false, targetTabId = activeTabId } = {}) {
|
|
8514
|
+
if (!targetTabId || !parsed?.command) return;
|
|
8515
|
+
const tabContext = activeTabContext(targetTabId);
|
|
8516
|
+
clearComposerAfterUserBash({ usesPromptInput, targetTabId, tabContext });
|
|
8517
|
+
const queue = userBashQueueForTab(targetTabId);
|
|
8518
|
+
queue.push({ command: parsed.command, excludeFromContext: parsed.excludeFromContext === true, enqueuedAt: Date.now() });
|
|
8519
|
+
const waiting = queue.length;
|
|
8520
|
+
if (isCurrentTabContext(tabContext)) {
|
|
8521
|
+
addTransientMessage({
|
|
8522
|
+
role: "bashExecution",
|
|
8523
|
+
title: parsed.excludeFromContext ? "bash (!! queued)" : "bash (! queued)",
|
|
8524
|
+
command: parsed.command,
|
|
8525
|
+
output: `Queued behind the active bash command. Position: ${waiting}.\n\nOutput will be ${parsed.excludeFromContext ? "excluded from" : "included in the next"} LLM context when it runs.`,
|
|
8526
|
+
excludeFromContext: parsed.excludeFromContext === true,
|
|
8527
|
+
level: "info",
|
|
8528
|
+
});
|
|
8529
|
+
addEvent(`bash queued (${waiting} waiting): ${parsed.command}`, "info");
|
|
8530
|
+
setRunIndicatorActivity(`Bash queued (${waiting} waiting)…`);
|
|
8531
|
+
updateComposerModeButtons();
|
|
8532
|
+
}
|
|
8533
|
+
}
|
|
8534
|
+
|
|
8535
|
+
function dequeueNextUserBashCommand(targetTabId) {
|
|
8536
|
+
return userBashQueueForTab(targetTabId).shift() || null;
|
|
8537
|
+
}
|
|
8538
|
+
|
|
8539
|
+
async function runUserBashCommand(parsed, { usesPromptInput = false, targetTabId = activeTabId, queued = false } = {}) {
|
|
8540
|
+
if (!targetTabId || !parsed?.command) return;
|
|
8541
|
+
const tabContext = activeTabContext(targetTabId);
|
|
8542
|
+
const { command, excludeFromContext } = parsed;
|
|
8543
|
+
autoFollowChat = true;
|
|
8544
|
+
setComposerActionsOpen(false);
|
|
8545
|
+
hideCommandSuggestions();
|
|
8546
|
+
userBashByTab.set(targetTabId, { command, excludeFromContext, startedAt: Date.now() });
|
|
8547
|
+
markTabWorkingLocally(targetTabId);
|
|
8548
|
+
if (isCurrentTabContext(tabContext)) {
|
|
8549
|
+
const waiting = queuedUserBashCount(targetTabId);
|
|
8550
|
+
setRunIndicatorActivity(`Running bash: ${command}${waiting ? ` (${waiting} queued)` : ""}`);
|
|
8551
|
+
addTransientMessage({
|
|
8552
|
+
role: "bashExecution",
|
|
8553
|
+
title: excludeFromContext ? "bash (!!)" : "bash (!)" ,
|
|
8554
|
+
command,
|
|
8555
|
+
output: `${queued ? "Dequeued and running.\n\n" : ""}${excludeFromContext ? "Output will be excluded from LLM context." : "Output will be included in the next LLM context."}\n\nRunning…`,
|
|
8556
|
+
excludeFromContext,
|
|
8557
|
+
level: "info",
|
|
8558
|
+
});
|
|
8559
|
+
}
|
|
8560
|
+
clearComposerAfterUserBash({ usesPromptInput, targetTabId, tabContext });
|
|
8561
|
+
|
|
8562
|
+
try {
|
|
8563
|
+
const response = await api("/api/bash", { method: "POST", body: { command, excludeFromContext }, tabId: targetTabId });
|
|
8564
|
+
const result = response.data || {};
|
|
8565
|
+
applyResponseTab(response);
|
|
8566
|
+
if (isCurrentTabContext(tabContext)) {
|
|
8567
|
+
addTransientMessage({
|
|
8568
|
+
role: "bashExecution",
|
|
8569
|
+
title: excludeFromContext ? "bash (!! complete)" : "bash (! complete)",
|
|
8570
|
+
command,
|
|
8571
|
+
output: userBashOutputSummary(result, excludeFromContext),
|
|
8572
|
+
exitCode: result.exitCode,
|
|
8573
|
+
cancelled: result.cancelled === true,
|
|
8574
|
+
truncated: result.truncated === true,
|
|
8575
|
+
fullOutputPath: result.fullOutputPath,
|
|
8576
|
+
excludeFromContext,
|
|
8577
|
+
level: result.cancelled ? "warn" : result.exitCode ? "error" : "info",
|
|
8578
|
+
});
|
|
8579
|
+
addEvent(`bash ${result.cancelled ? "cancelled" : "finished"}: ${command}`, result.cancelled || result.exitCode ? "warn" : "info");
|
|
8580
|
+
scheduleRefreshMessages(250, tabContext);
|
|
8581
|
+
scheduleRefreshState(250, tabContext);
|
|
8582
|
+
} else {
|
|
8583
|
+
scheduleRefreshTabs(300);
|
|
8584
|
+
}
|
|
8585
|
+
} catch (error) {
|
|
8586
|
+
if (isCurrentTabContext(tabContext)) {
|
|
8587
|
+
addEvent(error.message, "error");
|
|
8588
|
+
addTransientMessage({ role: "error", title: excludeFromContext ? "!! bash failed" : "! bash failed", content: error.message, level: "error" });
|
|
8589
|
+
}
|
|
8590
|
+
} finally {
|
|
8591
|
+
userBashByTab.delete(targetTabId);
|
|
8592
|
+
const nextQueued = dequeueNextUserBashCommand(targetTabId);
|
|
8593
|
+
if (isCurrentTabContext(tabContext)) {
|
|
8594
|
+
if (nextQueued) {
|
|
8595
|
+
setRunIndicatorActivity(`Starting queued bash (${queuedUserBashCount(targetTabId)} waiting)…`);
|
|
8596
|
+
} else if (!currentState?.isStreaming && !currentState?.isCompacting) {
|
|
8597
|
+
markTabIdleLocally(targetTabId);
|
|
8598
|
+
clearRunIndicatorActivity();
|
|
8599
|
+
} else {
|
|
8600
|
+
syncRunIndicatorFromState(currentState);
|
|
8601
|
+
}
|
|
8602
|
+
updateComposerModeButtons();
|
|
8603
|
+
}
|
|
8604
|
+
if (nextQueued) void runUserBashCommand(nextQueued, { usesPromptInput: false, targetTabId, queued: true });
|
|
8605
|
+
}
|
|
8606
|
+
}
|
|
8607
|
+
|
|
8608
|
+
async function sendUserBashCommand(parsed, { usesPromptInput = false, targetTabId = activeTabId } = {}) {
|
|
8609
|
+
if (!targetTabId || !parsed?.command) return;
|
|
8610
|
+
if (isUserBashActive(targetTabId) || queuedUserBashCount(targetTabId) > 0) {
|
|
8611
|
+
enqueueUserBashCommand(parsed, { usesPromptInput, targetTabId });
|
|
8612
|
+
return;
|
|
8613
|
+
}
|
|
8614
|
+
await runUserBashCommand(parsed, { usesPromptInput, targetTabId });
|
|
8615
|
+
}
|
|
8616
|
+
|
|
8009
8617
|
async function sendPrompt(kind = "prompt", explicitMessage) {
|
|
8010
8618
|
const usesPromptInput = explicitMessage === undefined;
|
|
8011
8619
|
const rawMessage = usesPromptInput ? elements.promptInput.value : explicitMessage;
|
|
@@ -8016,6 +8624,11 @@ async function sendPrompt(kind = "prompt", explicitMessage) {
|
|
|
8016
8624
|
const attachments = usesPromptInput ? [...attachmentsForTab(targetTabId)] : [];
|
|
8017
8625
|
if (!originalMessage && attachments.length === 0) return;
|
|
8018
8626
|
if (kind === "prompt" && attachments.length === 0 && await handleNativeSlashSelectorCommand(originalMessage, { usesPromptInput })) return;
|
|
8627
|
+
const userBash = kind === "prompt" && attachments.length === 0 ? parseUserBashInput(originalMessage) : null;
|
|
8628
|
+
if (userBash) {
|
|
8629
|
+
await sendUserBashCommand(userBash, { usesPromptInput, targetTabId });
|
|
8630
|
+
return;
|
|
8631
|
+
}
|
|
8019
8632
|
|
|
8020
8633
|
const targetWasStreaming = !!currentState?.isStreaming;
|
|
8021
8634
|
const busyBehavior = elements.busyBehavior.value || "followUp";
|
|
@@ -8034,7 +8647,10 @@ async function sendPrompt(kind = "prompt", explicitMessage) {
|
|
|
8034
8647
|
message = composeMessageWithAttachments(originalMessage, prepared.uploadedFiles, prepared.inlineImageIds);
|
|
8035
8648
|
const bodyBase = { message };
|
|
8036
8649
|
if (prepared.images.length) bodyBase.images = prepared.images;
|
|
8037
|
-
if (
|
|
8650
|
+
if (!message.startsWith("/")) {
|
|
8651
|
+
rememberPromptHistory(message, { tabId: targetTabId });
|
|
8652
|
+
if (kind === "prompt") rememberLastUserPrompt(message, { tabId: targetTabId });
|
|
8653
|
+
}
|
|
8038
8654
|
if (startsRun && isCurrentTabContext(tabContext)) setRunIndicatorActivity("Sending prompt to Pi…");
|
|
8039
8655
|
|
|
8040
8656
|
let response;
|
|
@@ -8060,12 +8676,15 @@ async function sendPrompt(kind = "prompt", explicitMessage) {
|
|
|
8060
8676
|
}
|
|
8061
8677
|
if (targetStillActive && response?.command === "native_slash_command" && response.data?.copyText) {
|
|
8062
8678
|
try {
|
|
8063
|
-
await
|
|
8679
|
+
await copyText(response.data.copyText);
|
|
8064
8680
|
} catch (error) {
|
|
8065
8681
|
response.data.message = `${response.data.message || "Copy requested, but clipboard access failed."}\n\nClipboard access failed: ${error.message}\n\n${response.data.copyText}`;
|
|
8066
8682
|
response.data.level = "warn";
|
|
8067
8683
|
}
|
|
8068
8684
|
}
|
|
8685
|
+
if (targetStillActive && response?.command === "native_slash_command" && response.data?.download) {
|
|
8686
|
+
if (triggerNativeDownload(response.data.download)) addEvent(`download started: ${response.data.download.fileName || response.data.download.url}`, "info");
|
|
8687
|
+
}
|
|
8069
8688
|
if (targetStillActive && response?.command === "native_slash_command" && response.data?.message) {
|
|
8070
8689
|
addTransientMessage({ role: "native", title: message.split(/\s+/, 1)[0], content: response.data.message, level: response.data.level || "info" });
|
|
8071
8690
|
}
|
|
@@ -8271,7 +8890,7 @@ function handleEvent(event) {
|
|
|
8271
8890
|
switch (event.type) {
|
|
8272
8891
|
case "webui_connected":
|
|
8273
8892
|
addEvent(`connected to ${event.tabTitle || "terminal"} for ${event.cwd}`);
|
|
8274
|
-
|
|
8893
|
+
scheduleForegroundReconcile("event stream reconnect", 0);
|
|
8275
8894
|
break;
|
|
8276
8895
|
case "webui_tab_renamed":
|
|
8277
8896
|
applyTabMetadata(event.tab || { id: event.tabId, title: event.tabTitle, activity: event.tabActivity });
|
|
@@ -8459,7 +9078,7 @@ function handleEvent(event) {
|
|
|
8459
9078
|
syncActiveTabActivityFromState(currentState);
|
|
8460
9079
|
syncRunIndicatorFromState(currentState);
|
|
8461
9080
|
renderStatus();
|
|
8462
|
-
} else if (["set_model", "set_thinking_level", "new_session", "compact"].includes(event.command)) {
|
|
9081
|
+
} else if (["set_model", "cycle_model", "set_thinking_level", "cycle_thinking_level", "new_session", "compact"].includes(event.command)) {
|
|
8463
9082
|
if (event.command === "new_session") {
|
|
8464
9083
|
const tabId = event.tabId || activeTabId;
|
|
8465
9084
|
forgetLastUserPrompt(tabId);
|
|
@@ -8531,7 +9150,7 @@ publishMenuContainer?.addEventListener("focusout", () => {
|
|
|
8531
9150
|
});
|
|
8532
9151
|
elements.releaseNpmButton.addEventListener("click", () => runPublishWorkflow("/release-npm"));
|
|
8533
9152
|
elements.releaseAurButton.addEventListener("click", () => runPublishWorkflow("/release-aur"));
|
|
8534
|
-
elements.gitWorkflowCancelButton.addEventListener("click", cancelGitWorkflow);
|
|
9153
|
+
elements.gitWorkflowCancelButton.addEventListener("click", () => cancelGitWorkflow());
|
|
8535
9154
|
elements.nativeCommandDialog.addEventListener("close", () => {
|
|
8536
9155
|
elements.nativeCommandSearch.oninput = null;
|
|
8537
9156
|
nativeCommandTabId = null;
|
|
@@ -8550,8 +9169,17 @@ async function abortActiveRun({ source = "button" } = {}) {
|
|
|
8550
9169
|
abortRequestInFlight = true;
|
|
8551
9170
|
resetAbortLongPressAffordance();
|
|
8552
9171
|
updateComposerModeButtons();
|
|
9172
|
+
const hadActiveBash = isUserBashActive(tabContext.tabId);
|
|
8553
9173
|
const hadActiveRun = runIndicatorIsActive();
|
|
8554
9174
|
try {
|
|
9175
|
+
if (hadActiveBash) {
|
|
9176
|
+
const command = userBashByTab.get(tabContext.tabId)?.command || "bash";
|
|
9177
|
+
setRunIndicatorActivity(`Abort requested${source === "escape" ? " from Esc" : source === "long-press" ? " from long-press" : ""}; stopping bash…`);
|
|
9178
|
+
await api("/api/abort-bash", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
9179
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
9180
|
+
addTransientMessage({ role: "native", title: "bash aborted", content: `⛔ Abort requested for bash command:\n${command}`, level: "warn" });
|
|
9181
|
+
return;
|
|
9182
|
+
}
|
|
8555
9183
|
if (hadActiveRun) setRunIndicatorActivity(`Abort requested${source === "escape" ? " from Esc" : source === "long-press" ? " from long-press" : ""}; checking whether Pi stopped…`);
|
|
8556
9184
|
await api("/api/abort", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
8557
9185
|
if (!isCurrentTabContext(tabContext)) return;
|
|
@@ -8671,6 +9299,7 @@ if (elements.backgroundClearButton) {
|
|
|
8671
9299
|
elements.backgroundClearButton.addEventListener("click", () => clearCustomBackground().catch((error) => addEvent(error.message || String(error), "error")));
|
|
8672
9300
|
}
|
|
8673
9301
|
elements.openNetworkButton.addEventListener("click", openToNetwork);
|
|
9302
|
+
elements.stopServerButton.addEventListener("click", stopServer);
|
|
8674
9303
|
elements.agentDoneNotificationsToggle.addEventListener("change", () => {
|
|
8675
9304
|
setAgentDoneNotificationsEnabled(elements.agentDoneNotificationsToggle.checked, {
|
|
8676
9305
|
requestPermission: elements.agentDoneNotificationsToggle.checked,
|
|
@@ -8729,6 +9358,72 @@ document.addEventListener("pointermove", (event) => {
|
|
|
8729
9358
|
}
|
|
8730
9359
|
rememberPointerPosition(event);
|
|
8731
9360
|
}, { passive: true });
|
|
9361
|
+
|
|
9362
|
+
function isTextEntryTarget(target) {
|
|
9363
|
+
if (!target) return false;
|
|
9364
|
+
const tag = String(target.tagName || "").toLowerCase();
|
|
9365
|
+
return target.isContentEditable || tag === "textarea" || tag === "input" || tag === "select";
|
|
9366
|
+
}
|
|
9367
|
+
|
|
9368
|
+
function shouldHandleNativeAppShortcut(event) {
|
|
9369
|
+
if (event.defaultPrevented) return false;
|
|
9370
|
+
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.nativeCommandDialog?.open) return false;
|
|
9371
|
+
return event.target === elements.promptInput || !isTextEntryTarget(event.target);
|
|
9372
|
+
}
|
|
9373
|
+
|
|
9374
|
+
function handleNativeAppShortcut(event) {
|
|
9375
|
+
if (!shouldHandleNativeAppShortcut(event)) return;
|
|
9376
|
+
const key = event.key;
|
|
9377
|
+
const lowerKey = String(key || "").toLowerCase();
|
|
9378
|
+
const ctrlOrMeta = event.ctrlKey || event.metaKey;
|
|
9379
|
+
|
|
9380
|
+
if (ctrlOrMeta && !event.altKey && lowerKey === "l") {
|
|
9381
|
+
event.preventDefault();
|
|
9382
|
+
openNativeModelSelector();
|
|
9383
|
+
return;
|
|
9384
|
+
}
|
|
9385
|
+
if (ctrlOrMeta && !event.altKey && lowerKey === "p") {
|
|
9386
|
+
event.preventDefault();
|
|
9387
|
+
cycleModelFromShortcut(event.shiftKey ? "backward" : "forward");
|
|
9388
|
+
return;
|
|
9389
|
+
}
|
|
9390
|
+
if (ctrlOrMeta && !event.altKey && !event.shiftKey && lowerKey === "t") {
|
|
9391
|
+
event.preventDefault();
|
|
9392
|
+
setThinkingOutputVisible(!thinkingOutputVisible, { announce: true });
|
|
9393
|
+
return;
|
|
9394
|
+
}
|
|
9395
|
+
if (ctrlOrMeta && !event.altKey && !event.shiftKey && lowerKey === "o") {
|
|
9396
|
+
event.preventDefault();
|
|
9397
|
+
setToolOutputGloballyExpanded(!toolOutputGloballyExpanded, { announce: true });
|
|
9398
|
+
return;
|
|
9399
|
+
}
|
|
9400
|
+
if (ctrlOrMeta && !event.altKey && !event.shiftKey && lowerKey === "c") {
|
|
9401
|
+
if (clearPromptFromShortcut()) event.preventDefault();
|
|
9402
|
+
return;
|
|
9403
|
+
}
|
|
9404
|
+
if (!event.ctrlKey && !event.metaKey && !event.altKey && event.shiftKey && key === "Tab") {
|
|
9405
|
+
event.preventDefault();
|
|
9406
|
+
cycleThinkingFromShortcut();
|
|
9407
|
+
return;
|
|
9408
|
+
}
|
|
9409
|
+
if (!event.ctrlKey && !event.metaKey && event.altKey && key === "Enter") {
|
|
9410
|
+
event.preventDefault();
|
|
9411
|
+
if (hasComposerPayload()) sendPrompt("follow-up");
|
|
9412
|
+
return;
|
|
9413
|
+
}
|
|
9414
|
+
if (!event.ctrlKey && !event.metaKey && event.altKey && key === "ArrowUp") {
|
|
9415
|
+
event.preventDefault();
|
|
9416
|
+
restoreQueuedMessagesToComposerFromShortcut();
|
|
9417
|
+
}
|
|
9418
|
+
}
|
|
9419
|
+
|
|
9420
|
+
window.addEventListener("keydown", handleNativeAppShortcut, { capture: true });
|
|
9421
|
+
document.addEventListener("visibilitychange", () => {
|
|
9422
|
+
if (document.visibilityState === "visible") scheduleForegroundReconcile("visibility resume", 0);
|
|
9423
|
+
});
|
|
9424
|
+
window.addEventListener("pageshow", () => scheduleForegroundReconcile("page show", 0));
|
|
9425
|
+
window.addEventListener("focus", () => scheduleForegroundReconcile("window focus"));
|
|
9426
|
+
window.addEventListener("online", () => scheduleForegroundReconcile("network online", 0));
|
|
8732
9427
|
window.addEventListener("keydown", (event) => {
|
|
8733
9428
|
if (event.key !== "Escape") return;
|
|
8734
9429
|
if (elements.dialog?.open || elements.pathPickerDialog?.open) return;
|
|
@@ -8789,6 +9484,7 @@ elements.composer.addEventListener("dragleave", handleComposerDragLeave);
|
|
|
8789
9484
|
elements.composer.addEventListener("drop", handleComposerDrop);
|
|
8790
9485
|
|
|
8791
9486
|
elements.promptInput.addEventListener("keydown", (event) => {
|
|
9487
|
+
if (event.defaultPrevented) return;
|
|
8792
9488
|
if (shouldSendPromptFromEnter(event)) {
|
|
8793
9489
|
event.preventDefault();
|
|
8794
9490
|
hideCommandSuggestions();
|
|
@@ -8817,9 +9513,18 @@ elements.promptInput.addEventListener("keydown", (event) => {
|
|
|
8817
9513
|
hideCommandSuggestions();
|
|
8818
9514
|
}
|
|
8819
9515
|
}
|
|
9516
|
+
|
|
9517
|
+
if (!event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey && event.key === "ArrowUp" && recallPreviousPromptFromHistory()) {
|
|
9518
|
+
event.preventDefault();
|
|
9519
|
+
return;
|
|
9520
|
+
}
|
|
9521
|
+
if (!event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey && event.key === "ArrowDown" && recallNextPromptFromHistory()) {
|
|
9522
|
+
event.preventDefault();
|
|
9523
|
+
}
|
|
8820
9524
|
});
|
|
8821
9525
|
|
|
8822
9526
|
elements.promptInput.addEventListener("input", () => {
|
|
9527
|
+
resetPromptHistoryNavigation();
|
|
8823
9528
|
resizePromptInput();
|
|
8824
9529
|
renderCommandSuggestions();
|
|
8825
9530
|
});
|
|
@@ -8828,6 +9533,7 @@ elements.promptInput.addEventListener("focus", () => {
|
|
|
8828
9533
|
setTimeout(updateVisualViewportVars, 0);
|
|
8829
9534
|
});
|
|
8830
9535
|
elements.promptInput.addEventListener("click", () => {
|
|
9536
|
+
resetPromptHistoryNavigation();
|
|
8831
9537
|
updateVisualViewportVars();
|
|
8832
9538
|
syncMobileChatToBottomForInput();
|
|
8833
9539
|
renderCommandSuggestions();
|
|
@@ -8848,6 +9554,7 @@ focusPromptInput({ defer: true });
|
|
|
8848
9554
|
updateComposerModeButtons();
|
|
8849
9555
|
updateOptionalFeatureAvailability();
|
|
8850
9556
|
loadLastUserPromptCache();
|
|
9557
|
+
loadPromptHistoryCache();
|
|
8851
9558
|
installViewportHandlers();
|
|
8852
9559
|
currentThemeName = storedThemeName();
|
|
8853
9560
|
renderBackgroundControl();
|
|
@@ -8858,6 +9565,7 @@ initializeThemes().catch((error) => {
|
|
|
8858
9565
|
initializeFastPicks().catch((error) => addEvent(`failed to initialize path fast picks: ${error.message}`, "error"));
|
|
8859
9566
|
restoreAgentDoneNotificationsSetting();
|
|
8860
9567
|
restoreThinkingVisibilitySetting();
|
|
9568
|
+
restoreToolOutputExpansionSetting();
|
|
8861
9569
|
restoreSidePanelSectionState();
|
|
8862
9570
|
bindSidePanelSectionToggles();
|
|
8863
9571
|
restoreSidePanelState();
|