@firstpick/pi-package-webui 0.1.8 → 0.2.0
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 +27 -24
- package/WEBUI_TUI_NATIVE_PARITY.json +666 -0
- package/bin/pi-webui.mjs +686 -29
- package/package.json +6 -3
- package/public/app.js +1007 -94
- package/public/index.html +36 -18
- package/public/styles.css +286 -82
- package/start-webui.ps1 +323 -0
- package/start-webui.sh +461 -0
- package/tests/mobile-static.test.mjs +126 -12
- package/tests/native-parity.test.mjs +148 -0
package/public/app.js
CHANGED
|
@@ -60,9 +60,12 @@ 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"),
|
|
67
|
+
codexUsageBox: $("#codexUsageBox"),
|
|
68
|
+
refreshCodexUsageButton: $("#refreshCodexUsageButton"),
|
|
66
69
|
toggleSidePanelButton: $("#toggleSidePanelButton"),
|
|
67
70
|
sidePanelExpandButton: $("#sidePanelExpandButton"),
|
|
68
71
|
sidePanelBackdrop: $("#sidePanelBackdrop"),
|
|
@@ -125,6 +128,7 @@ let refreshMessagesTimer = null;
|
|
|
125
128
|
let refreshStateTimer = null;
|
|
126
129
|
let refreshFooterTimer = null;
|
|
127
130
|
let refreshTabsTimer = null;
|
|
131
|
+
let foregroundReconcileTimer = null;
|
|
128
132
|
let eventSource = null;
|
|
129
133
|
let activeDialog = null;
|
|
130
134
|
let nativeCommandTabId = null;
|
|
@@ -146,9 +150,16 @@ let pathSuggestAbortController = null;
|
|
|
146
150
|
let latestStats = null;
|
|
147
151
|
let latestWorkspace = null;
|
|
148
152
|
let latestNetwork = null;
|
|
153
|
+
let latestCodexUsage = null;
|
|
154
|
+
let codexUsageError = null;
|
|
155
|
+
let codexUsageLoading = false;
|
|
156
|
+
let refreshCodexUsageTimer = null;
|
|
157
|
+
let codexUsageRenderTimer = null;
|
|
149
158
|
let backendOffline = false;
|
|
150
159
|
let backendOfflineNoticeShown = false;
|
|
151
160
|
let latestMessages = [];
|
|
161
|
+
let promptHistoryByTab = new Map();
|
|
162
|
+
let promptHistoryNavigation = null;
|
|
152
163
|
let transientMessages = [];
|
|
153
164
|
let actionEntrySeenKeysByTab = new Map();
|
|
154
165
|
let actionEntryAnimationPrimedTabs = new Set();
|
|
@@ -160,6 +171,7 @@ let blockedTabNotificationPermissionRequested = false;
|
|
|
160
171
|
let blockedTabNotificationFallbackNoted = false;
|
|
161
172
|
let agentDoneNotificationsEnabled = false;
|
|
162
173
|
let thinkingOutputVisible = true;
|
|
174
|
+
let toolOutputGloballyExpanded = false;
|
|
163
175
|
let agentDoneNotificationPermissionRequested = false;
|
|
164
176
|
let agentDoneNotificationFallbackNoted = false;
|
|
165
177
|
let agentDoneNotificationKeys = new Set();
|
|
@@ -185,6 +197,9 @@ let currentRunStartedAt = null;
|
|
|
185
197
|
let currentRunStreamChars = 0;
|
|
186
198
|
let latestTokPerSecond = null;
|
|
187
199
|
let abortRequestInFlight = false;
|
|
200
|
+
let userBashByTab = new Map();
|
|
201
|
+
let userBashQueuesByTab = new Map();
|
|
202
|
+
let latestQueuedMessagesByTab = new Map();
|
|
188
203
|
let abortLongPressTimer = null;
|
|
189
204
|
let abortLongPressHandled = false;
|
|
190
205
|
const dialogQueue = [];
|
|
@@ -194,6 +209,7 @@ const TAB_STORAGE_KEY = "pi-webui-active-tab";
|
|
|
194
209
|
const PATH_FAST_PICKS_STORAGE_KEY = "pi-webui-path-fast-picks";
|
|
195
210
|
const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications";
|
|
196
211
|
const THINKING_VISIBILITY_STORAGE_KEY = "pi-webui-thinking-visible";
|
|
212
|
+
const TOOL_OUTPUT_EXPANDED_STORAGE_KEY = "pi-webui-tool-output-expanded";
|
|
197
213
|
const THEME_STORAGE_KEY = "pi-webui-theme";
|
|
198
214
|
const CUSTOM_BACKGROUND_STORAGE_KEY = "pi-webui-custom-background";
|
|
199
215
|
const CUSTOM_BACKGROUNDS_STORAGE_KEY = "pi-webui-custom-backgrounds";
|
|
@@ -205,6 +221,8 @@ const DEFAULT_WEBUI_PORT = "31415";
|
|
|
205
221
|
const CUSTOM_BACKGROUND_MAX_FILE_BYTES = 24 * 1024 * 1024;
|
|
206
222
|
const OPTIONAL_FEATURES_STORAGE_KEY = "pi-webui-optional-features-disabled";
|
|
207
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;
|
|
208
226
|
const ATTACHMENT_MAX_FILES = 12;
|
|
209
227
|
const ATTACHMENT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
210
228
|
const ATTACHMENT_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
@@ -220,6 +238,8 @@ const STICKY_USER_PROMPT_TOP_GAP_PX = 12;
|
|
|
220
238
|
const CHAT_FOLLOW_SETTLE_DELAY_MS = 80;
|
|
221
239
|
const CHAT_PROGRAMMATIC_SCROLL_GRACE_MS = 500;
|
|
222
240
|
const CHAT_USER_SCROLL_INTENT_MS = 700;
|
|
241
|
+
const CODEX_USAGE_REFRESH_MS = 5 * 60 * 1000;
|
|
242
|
+
const CODEX_USAGE_RENDER_TICK_MS = 30 * 1000;
|
|
223
243
|
const RUN_INDICATOR_TICK_MS = 1000;
|
|
224
244
|
const RUN_INDICATOR_START_GRACE_MS = 2500;
|
|
225
245
|
const RUN_INDICATOR_STATE_RECHECK_MS = 5000;
|
|
@@ -228,10 +248,12 @@ const STREAM_OUTPUT_HIDE_DELAY_MS = 300;
|
|
|
228
248
|
const STREAM_OUTPUT_TOOLCALL_GUARD_MS = 220;
|
|
229
249
|
const STREAM_OUTPUT_MIN_VISIBLE_MS = 900;
|
|
230
250
|
const TOOL_LIVE_UPDATE_THROTTLE_MS = 80;
|
|
251
|
+
const UNEXPOSED_THINKING_TEXT = "No thinking content was exposed by the provider.";
|
|
231
252
|
const TODO_PROGRESS_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)\]\s+.+$/;
|
|
232
253
|
const TODO_PROGRESS_PARTIAL_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X|-)?\]?\s*.*$/;
|
|
233
254
|
const CHAT_SCROLL_KEYS = new Set(["ArrowDown", "ArrowUp", "End", "Home", "PageDown", "PageUp", " "]);
|
|
234
255
|
const TAB_ACTIVITY_IDLE_RECONCILE_GRACE_MS = 1200;
|
|
256
|
+
const FOREGROUND_RECONCILE_DELAY_MS = 120;
|
|
235
257
|
const TAB_GROUP_STATUS_PRIORITY = ["blocked", "done", "idle", "working"];
|
|
236
258
|
const EXTENSION_UI_BLOCKING_METHODS = new Set(["select", "confirm", "input", "editor"]);
|
|
237
259
|
const BLOCKED_TAB_NOTIFICATION_TAG_PREFIX = "pi-webui-blocked-tab";
|
|
@@ -240,6 +262,7 @@ const BLOCKED_TAB_NOTIFICATION_ICON = "/icon-192.png";
|
|
|
240
262
|
const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
|
|
241
263
|
const statusEntries = new Map();
|
|
242
264
|
const widgets = new Map();
|
|
265
|
+
const todoProgressWidgetExpandedByTab = new Map();
|
|
243
266
|
const liveToolRuns = new Map();
|
|
244
267
|
const liveToolCards = new Map();
|
|
245
268
|
const liveToolRenderQueue = new Map();
|
|
@@ -351,6 +374,10 @@ function bindGitWorkflowToActiveTab() {
|
|
|
351
374
|
return gitWorkflow;
|
|
352
375
|
}
|
|
353
376
|
|
|
377
|
+
function gitWorkflowActionTabId() {
|
|
378
|
+
return activeTabId;
|
|
379
|
+
}
|
|
380
|
+
|
|
354
381
|
function resetGitWorkflowForTab(tabId = activeTabId) {
|
|
355
382
|
if (!tabId) return;
|
|
356
383
|
gitWorkflowsByTab.set(tabId, createGitWorkflowState());
|
|
@@ -425,10 +452,12 @@ function sidePanelSectionRecords() {
|
|
|
425
452
|
|
|
426
453
|
function readStoredSidePanelSectionCollapsedIds() {
|
|
427
454
|
try {
|
|
428
|
-
const
|
|
455
|
+
const stored = localStorage.getItem(SIDE_PANEL_SECTION_STORAGE_KEY);
|
|
456
|
+
if (stored === null) return null;
|
|
457
|
+
const parsed = JSON.parse(stored);
|
|
429
458
|
return new Set(Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : []);
|
|
430
459
|
} catch {
|
|
431
|
-
return
|
|
460
|
+
return null;
|
|
432
461
|
}
|
|
433
462
|
}
|
|
434
463
|
|
|
@@ -453,17 +482,32 @@ function setSidePanelSectionCollapsed(record, collapsed, { persist = true } = {}
|
|
|
453
482
|
if (persist) persistSidePanelSectionState();
|
|
454
483
|
}
|
|
455
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
|
+
|
|
456
493
|
function restoreSidePanelSectionState() {
|
|
494
|
+
const records = sidePanelSectionRecords();
|
|
457
495
|
const collapsedIds = readStoredSidePanelSectionCollapsedIds();
|
|
458
|
-
|
|
459
|
-
|
|
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 });
|
|
460
500
|
}
|
|
461
501
|
}
|
|
462
502
|
|
|
463
503
|
function bindSidePanelSectionToggles() {
|
|
464
504
|
for (const record of sidePanelSectionRecords()) {
|
|
465
505
|
record.button.addEventListener("click", () => {
|
|
466
|
-
|
|
506
|
+
if (record.section.classList.contains("collapsed")) {
|
|
507
|
+
setOnlySidePanelSectionExpanded(record);
|
|
508
|
+
} else {
|
|
509
|
+
setSidePanelSectionCollapsed(record, true);
|
|
510
|
+
}
|
|
467
511
|
});
|
|
468
512
|
}
|
|
469
513
|
}
|
|
@@ -551,6 +595,22 @@ function persistThinkingOutputVisible(visible) {
|
|
|
551
595
|
}
|
|
552
596
|
}
|
|
553
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
|
+
|
|
554
614
|
function thinkingVisibilityStatusText() {
|
|
555
615
|
return thinkingOutputVisible ? "Visible" : "Hidden from transcript";
|
|
556
616
|
}
|
|
@@ -578,6 +638,24 @@ function setThinkingOutputVisible(visible, { announce = false } = {}) {
|
|
|
578
638
|
if (announce) addEvent(thinkingOutputVisible ? "thinking output shown" : "thinking output hidden", thinkingOutputVisible ? "info" : "warn");
|
|
579
639
|
}
|
|
580
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
|
+
|
|
581
659
|
function restoreThinkingVisibilitySetting() {
|
|
582
660
|
thinkingOutputVisible = readStoredThinkingOutputVisible();
|
|
583
661
|
renderThinkingVisibilityToggle();
|
|
@@ -590,12 +668,34 @@ function setComposerActionsOpen(open) {
|
|
|
590
668
|
if (!shouldOpen) setPublishMenuOpen(false);
|
|
591
669
|
}
|
|
592
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
|
+
|
|
593
693
|
function isRunActive() {
|
|
594
|
-
return !!currentState?.isStreaming || (runIndicatorLocallyActive && !currentState?.isCompacting);
|
|
694
|
+
return !!currentState?.isStreaming || isUserBashRunningOrQueued() || (runIndicatorLocallyActive && !currentState?.isCompacting);
|
|
595
695
|
}
|
|
596
696
|
|
|
597
697
|
function isAbortAvailable() {
|
|
598
|
-
return runIndicatorIsActive();
|
|
698
|
+
return runIndicatorIsActive() || isUserBashActive();
|
|
599
699
|
}
|
|
600
700
|
|
|
601
701
|
function resizePromptInput() {
|
|
@@ -832,6 +932,20 @@ async function copyText(text) {
|
|
|
832
932
|
if (!copied) throw new Error("Clipboard copy failed");
|
|
833
933
|
}
|
|
834
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
|
+
|
|
835
949
|
async function copyServerStartCommand() {
|
|
836
950
|
const command = serverStartCommandText();
|
|
837
951
|
try {
|
|
@@ -2155,6 +2269,7 @@ function saveActiveDraft() {
|
|
|
2155
2269
|
}
|
|
2156
2270
|
|
|
2157
2271
|
function restoreActiveDraft() {
|
|
2272
|
+
resetPromptHistoryNavigation();
|
|
2158
2273
|
elements.promptInput.value = activeTabId ? tabDrafts.get(activeTabId) || "" : "";
|
|
2159
2274
|
resizePromptInput();
|
|
2160
2275
|
renderCommandSuggestions();
|
|
@@ -2234,8 +2349,12 @@ function resetActiveTabUi() {
|
|
|
2234
2349
|
resetChatOutput();
|
|
2235
2350
|
elements.stateDetails.replaceChildren();
|
|
2236
2351
|
elements.eventLog.replaceChildren();
|
|
2237
|
-
|
|
2238
|
-
|
|
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
|
+
}
|
|
2239
2358
|
elements.commandsBox.textContent = "Loading…";
|
|
2240
2359
|
elements.commandsBox.classList.add("muted");
|
|
2241
2360
|
elements.sessionLine.textContent = activeTab() ? "Connecting…" : "No terminal tabs.";
|
|
@@ -3584,6 +3703,197 @@ function scheduleRefreshFooter(delay = 300, tabContext = activeTabContext()) {
|
|
|
3584
3703
|
}, delay);
|
|
3585
3704
|
}
|
|
3586
3705
|
|
|
3706
|
+
function formatCodexPlanType(value) {
|
|
3707
|
+
const text = String(value || "").trim();
|
|
3708
|
+
if (!text) return "unknown plan";
|
|
3709
|
+
return text.replace(/[_-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
function formatCodexPercent(value) {
|
|
3713
|
+
const number = Number(value);
|
|
3714
|
+
return Number.isFinite(number) ? `${Math.max(0, Math.min(100, Math.round(number)))}%` : "—";
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
function codexWindowDurationMinutes(window) {
|
|
3718
|
+
const minutes = Number(window?.windowDurationMins);
|
|
3719
|
+
if (Number.isFinite(minutes) && minutes > 0) return minutes;
|
|
3720
|
+
const seconds = Number(window?.windowDurationSeconds);
|
|
3721
|
+
return Number.isFinite(seconds) && seconds > 0 ? seconds / 60 : null;
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
function formatCodexWindowDuration(window) {
|
|
3725
|
+
const minutes = codexWindowDurationMinutes(window);
|
|
3726
|
+
if (!minutes) return "window";
|
|
3727
|
+
if (minutes >= 280 && minutes <= 320) return "5h window";
|
|
3728
|
+
if (minutes >= 9500 && minutes <= 10550) return "weekly window";
|
|
3729
|
+
if (minutes >= 60 * 24) {
|
|
3730
|
+
const days = minutes / (60 * 24);
|
|
3731
|
+
return `${days >= 10 ? Math.round(days) : Number(days.toFixed(1))}d window`;
|
|
3732
|
+
}
|
|
3733
|
+
if (minutes >= 60) {
|
|
3734
|
+
const hours = minutes / 60;
|
|
3735
|
+
return `${Number.isInteger(hours) ? hours : Number(hours.toFixed(1))}h window`;
|
|
3736
|
+
}
|
|
3737
|
+
return `${Math.round(minutes)}m window`;
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
function formatDurationParts(milliseconds) {
|
|
3741
|
+
if (!Number.isFinite(Number(milliseconds))) return "now";
|
|
3742
|
+
const totalMinutes = Math.max(0, Math.ceil(Number(milliseconds) / 60000));
|
|
3743
|
+
if (totalMinutes <= 1) return "<1m";
|
|
3744
|
+
if (totalMinutes < 60) return `${totalMinutes}m`;
|
|
3745
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
3746
|
+
const minutes = totalMinutes % 60;
|
|
3747
|
+
if (hours < 48) return minutes ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
3748
|
+
const days = Math.floor(hours / 24);
|
|
3749
|
+
const remHours = hours % 24;
|
|
3750
|
+
return remHours ? `${days}d ${remHours}h` : `${days}d`;
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
function codexWindowResetDate(window) {
|
|
3754
|
+
const resetAt = window?.resetsAt ? new Date(window.resetsAt) : null;
|
|
3755
|
+
if (resetAt && Number.isFinite(resetAt.getTime())) return resetAt;
|
|
3756
|
+
const resetAfterSeconds = Number(window?.resetAfterSeconds);
|
|
3757
|
+
if (Number.isFinite(resetAfterSeconds) && resetAfterSeconds >= 0) return new Date(Date.now() + resetAfterSeconds * 1000);
|
|
3758
|
+
return null;
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
function formatCodexReset(window) {
|
|
3762
|
+
const resetDate = codexWindowResetDate(window);
|
|
3763
|
+
if (!resetDate) return "reset unknown";
|
|
3764
|
+
const diff = resetDate.getTime() - Date.now();
|
|
3765
|
+
if (diff <= 0) return "resetting now";
|
|
3766
|
+
return `resets in ${formatDurationParts(diff)}`;
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
function codexSnapshotName(snapshot) {
|
|
3770
|
+
return snapshot?.limitName || snapshot?.limitId || "codex";
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
function codexUsageBuckets(data) {
|
|
3774
|
+
const buckets = [];
|
|
3775
|
+
const selected = data?.selected || data?.rateLimits || null;
|
|
3776
|
+
const snapshots = Array.isArray(data?.snapshots) ? data.snapshots : selected ? [selected] : [];
|
|
3777
|
+
const selectedKey = selected?.limitId || selected?.limitName || "codex";
|
|
3778
|
+
const pushWindow = (snapshot, kind, window, { prefix } = {}) => {
|
|
3779
|
+
if (!window) return;
|
|
3780
|
+
const durationLabel = formatCodexWindowDuration(window);
|
|
3781
|
+
const baseLabel = kind === "secondary" && durationLabel === "window" ? "secondary window" : durationLabel;
|
|
3782
|
+
buckets.push({
|
|
3783
|
+
key: `${snapshot?.limitId || snapshot?.limitName || buckets.length}-${kind}`,
|
|
3784
|
+
label: prefix ? `${prefix} · ${baseLabel}` : baseLabel,
|
|
3785
|
+
window,
|
|
3786
|
+
});
|
|
3787
|
+
};
|
|
3788
|
+
|
|
3789
|
+
if (selected) {
|
|
3790
|
+
pushWindow(selected, "primary", selected.primary);
|
|
3791
|
+
pushWindow(selected, "secondary", selected.secondary);
|
|
3792
|
+
}
|
|
3793
|
+
for (const snapshot of snapshots) {
|
|
3794
|
+
const key = snapshot?.limitId || snapshot?.limitName;
|
|
3795
|
+
if (!snapshot || snapshot === selected || key === selectedKey) continue;
|
|
3796
|
+
const name = codexSnapshotName(snapshot);
|
|
3797
|
+
pushWindow(snapshot, "primary", snapshot.primary, { prefix: name });
|
|
3798
|
+
pushWindow(snapshot, "secondary", snapshot.secondary, { prefix: name });
|
|
3799
|
+
}
|
|
3800
|
+
return buckets.slice(0, 6);
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
function renderCodexUsage() {
|
|
3804
|
+
const box = elements.codexUsageBox;
|
|
3805
|
+
if (!box) return;
|
|
3806
|
+
if (elements.refreshCodexUsageButton) {
|
|
3807
|
+
elements.refreshCodexUsageButton.disabled = codexUsageLoading;
|
|
3808
|
+
elements.refreshCodexUsageButton.textContent = codexUsageLoading ? "Refreshing…" : "Refresh usage";
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
box.replaceChildren();
|
|
3812
|
+
box.classList.toggle("muted", !latestCodexUsage);
|
|
3813
|
+
|
|
3814
|
+
if (!latestCodexUsage && codexUsageLoading) {
|
|
3815
|
+
box.textContent = "Checking Codex usage…";
|
|
3816
|
+
return;
|
|
3817
|
+
}
|
|
3818
|
+
if (!latestCodexUsage && codexUsageError) {
|
|
3819
|
+
const title = make("div", "codex-usage-unavailable", "Usage unavailable");
|
|
3820
|
+
const detail = make("div", "codex-usage-detail", codexUsageError.message || String(codexUsageError));
|
|
3821
|
+
box.append(title, detail);
|
|
3822
|
+
return;
|
|
3823
|
+
}
|
|
3824
|
+
if (!latestCodexUsage) {
|
|
3825
|
+
box.textContent = "Codex usage has not loaded yet.";
|
|
3826
|
+
return;
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
const header = make("div", "codex-usage-summary");
|
|
3830
|
+
header.append(
|
|
3831
|
+
make("span", "codex-usage-plan", formatCodexPlanType(latestCodexUsage.planType)),
|
|
3832
|
+
make("span", "codex-usage-fetched", latestCodexUsage.fetchedAt ? `updated ${formatDurationParts(Date.now() - new Date(latestCodexUsage.fetchedAt).getTime())} ago` : "updated now"),
|
|
3833
|
+
);
|
|
3834
|
+
box.append(header);
|
|
3835
|
+
|
|
3836
|
+
const buckets = codexUsageBuckets(latestCodexUsage);
|
|
3837
|
+
if (buckets.length === 0) {
|
|
3838
|
+
box.append(make("div", "codex-usage-detail", "No Codex rate-limit windows were returned."));
|
|
3839
|
+
} else {
|
|
3840
|
+
for (const bucket of buckets) {
|
|
3841
|
+
const usedPercent = Number(bucket.window?.usedPercent);
|
|
3842
|
+
const fillPercent = Number.isFinite(usedPercent) ? Math.max(0, Math.min(100, usedPercent)) : 0;
|
|
3843
|
+
const item = make("div", "codex-usage-bucket");
|
|
3844
|
+
const row = make("div", "codex-usage-row");
|
|
3845
|
+
row.append(
|
|
3846
|
+
make("span", "codex-usage-label", bucket.label),
|
|
3847
|
+
make("strong", "codex-usage-percent", formatCodexPercent(bucket.window?.usedPercent)),
|
|
3848
|
+
);
|
|
3849
|
+
const meter = make("div", "codex-usage-meter");
|
|
3850
|
+
const fill = make("span", "codex-usage-meter-fill");
|
|
3851
|
+
fill.style.width = `${fillPercent}%`;
|
|
3852
|
+
meter.append(fill);
|
|
3853
|
+
item.append(row, meter, make("div", "codex-usage-reset", formatCodexReset(bucket.window)));
|
|
3854
|
+
box.append(item);
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3858
|
+
if (latestCodexUsage.rateLimitReachedType) {
|
|
3859
|
+
box.append(make("div", "codex-usage-warning", `Limit status: ${latestCodexUsage.rateLimitReachedType}`));
|
|
3860
|
+
}
|
|
3861
|
+
if (codexUsageError) {
|
|
3862
|
+
box.append(make("div", "codex-usage-detail", `Latest refresh failed: ${codexUsageError.message || codexUsageError}`));
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
async function refreshCodexUsage({ forceAuthRefresh = false } = {}) {
|
|
3867
|
+
if (codexUsageLoading) return;
|
|
3868
|
+
codexUsageLoading = true;
|
|
3869
|
+
renderCodexUsage();
|
|
3870
|
+
try {
|
|
3871
|
+
const suffix = forceAuthRefresh ? "?refresh=1" : "";
|
|
3872
|
+
const response = await api(`/api/codex-usage${suffix}`, { scoped: false });
|
|
3873
|
+
latestCodexUsage = response.data || null;
|
|
3874
|
+
codexUsageError = null;
|
|
3875
|
+
} catch (error) {
|
|
3876
|
+
codexUsageError = error;
|
|
3877
|
+
} finally {
|
|
3878
|
+
codexUsageLoading = false;
|
|
3879
|
+
renderCodexUsage();
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
function scheduleRefreshCodexUsage(delay = CODEX_USAGE_REFRESH_MS) {
|
|
3884
|
+
clearTimeout(refreshCodexUsageTimer);
|
|
3885
|
+
refreshCodexUsageTimer = setTimeout(() => {
|
|
3886
|
+
refreshCodexUsage().finally(() => scheduleRefreshCodexUsage());
|
|
3887
|
+
}, delay);
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
function initializeCodexUsage() {
|
|
3891
|
+
renderCodexUsage();
|
|
3892
|
+
refreshCodexUsage().finally(() => scheduleRefreshCodexUsage());
|
|
3893
|
+
clearInterval(codexUsageRenderTimer);
|
|
3894
|
+
codexUsageRenderTimer = setInterval(renderCodexUsage, CODEX_USAGE_RENDER_TICK_MS);
|
|
3895
|
+
}
|
|
3896
|
+
|
|
3587
3897
|
function renderStatus() {
|
|
3588
3898
|
const state = currentState;
|
|
3589
3899
|
updateComposerModeButtons();
|
|
@@ -3760,12 +4070,19 @@ function renderTodoProgressWidget(_key, lines) {
|
|
|
3760
4070
|
const todo = parseTodoProgressWidget(lines);
|
|
3761
4071
|
if (!todo) return null;
|
|
3762
4072
|
|
|
3763
|
-
const
|
|
4073
|
+
const tabId = activeTabId || "default";
|
|
4074
|
+
const node = make("details", "widget todo-widget");
|
|
4075
|
+
node.open = todoProgressWidgetExpandedByTab.get(tabId) === true;
|
|
3764
4076
|
node.setAttribute("aria-label", "Todo progress");
|
|
4077
|
+
node.addEventListener("toggle", () => {
|
|
4078
|
+
todoProgressWidgetExpandedByTab.set(tabId, node.open);
|
|
4079
|
+
});
|
|
3765
4080
|
|
|
3766
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");
|
|
3767
4083
|
const header = make("div", "todo-widget-header");
|
|
3768
4084
|
header.append(
|
|
4085
|
+
make("span", "todo-widget-toggle", "›"),
|
|
3769
4086
|
make("span", "todo-widget-title", "Todo progress"),
|
|
3770
4087
|
make("span", "todo-widget-count", `${todo.done}/${todo.total}`),
|
|
3771
4088
|
make("span", "todo-widget-meta", todo.partial ? `${todo.partial} partial` : "active"),
|
|
@@ -3775,7 +4092,9 @@ function renderTodoProgressWidget(_key, lines) {
|
|
|
3775
4092
|
const fill = make("span", "todo-widget-progress-fill");
|
|
3776
4093
|
fill.style.width = `${percent}%`;
|
|
3777
4094
|
progress.append(fill);
|
|
4095
|
+
summary.append(header, progress);
|
|
3778
4096
|
|
|
4097
|
+
const body = make("div", "todo-widget-body");
|
|
3779
4098
|
const list = make("ol", "todo-widget-list");
|
|
3780
4099
|
for (const item of todo.items) {
|
|
3781
4100
|
const row = make("li", `todo-widget-item ${item.status}`);
|
|
@@ -3785,9 +4104,11 @@ function renderTodoProgressWidget(_key, lines) {
|
|
|
3785
4104
|
);
|
|
3786
4105
|
list.append(row);
|
|
3787
4106
|
}
|
|
4107
|
+
if (todo.items.length) body.append(list);
|
|
4108
|
+
if (todo.footer) body.append(make("div", "todo-widget-footer", todo.footer));
|
|
3788
4109
|
|
|
3789
|
-
node.append(
|
|
3790
|
-
if (
|
|
4110
|
+
node.append(summary);
|
|
4111
|
+
if (body.children.length) node.append(body);
|
|
3791
4112
|
return node;
|
|
3792
4113
|
}
|
|
3793
4114
|
|
|
@@ -3849,6 +4170,17 @@ function releaseNpmActionButton(label, command, className = "") {
|
|
|
3849
4170
|
return button;
|
|
3850
4171
|
}
|
|
3851
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
|
+
|
|
3852
4184
|
function renderReleaseNpmOutputWidget() {
|
|
3853
4185
|
if (!isOptionalFeatureEnabled("releaseNpm")) return null;
|
|
3854
4186
|
const outputLines = getWidgetLines("release-npm:output");
|
|
@@ -3874,15 +4206,17 @@ function renderReleaseNpmOutputWidget() {
|
|
|
3874
4206
|
);
|
|
3875
4207
|
header.append(titleWrap, meta, actions);
|
|
3876
4208
|
|
|
4209
|
+
const streamLines = outputLines.length ? outputLines : ["Waiting for release output..."];
|
|
4210
|
+
const streamHeader = releaseNpmStreamHeader("Live output stream", outputLines.length, { live: true });
|
|
3877
4211
|
const terminal = make("div", "release-npm-terminal");
|
|
3878
4212
|
terminal.setAttribute("role", "log");
|
|
3879
4213
|
terminal.setAttribute("aria-live", "polite");
|
|
3880
|
-
for (const line of
|
|
4214
|
+
for (const line of streamLines) {
|
|
3881
4215
|
appendReleaseNpmTerminalLine(terminal, line);
|
|
3882
4216
|
}
|
|
3883
4217
|
|
|
3884
4218
|
const controls = make("div", "release-npm-controls", details.controls || "Controls: /release-toggle expands/collapses · /release-abort stops subprocess");
|
|
3885
|
-
node.append(header, terminal, controls);
|
|
4219
|
+
node.append(header, streamHeader, terminal, controls);
|
|
3886
4220
|
requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
3887
4221
|
return node;
|
|
3888
4222
|
}
|
|
@@ -3906,11 +4240,13 @@ function renderReleaseNpmLogWidget() {
|
|
|
3906
4240
|
actions.append(releaseNpmActionButton("Close log", "/release-npm-logs close"));
|
|
3907
4241
|
header.append(titleWrap, meta, actions);
|
|
3908
4242
|
|
|
4243
|
+
const logLines = lines.slice(2).filter((line, index) => index > 0 || stripAnsi(line).trim());
|
|
4244
|
+
const streamHeader = releaseNpmStreamHeader("Saved output stream", logLines.length);
|
|
3909
4245
|
const terminal = make("div", "release-npm-terminal");
|
|
3910
|
-
for (const line of
|
|
4246
|
+
for (const line of logLines) {
|
|
3911
4247
|
appendReleaseNpmTerminalLine(terminal, line);
|
|
3912
4248
|
}
|
|
3913
|
-
node.append(header, terminal);
|
|
4249
|
+
node.append(header, streamHeader, terminal);
|
|
3914
4250
|
requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
3915
4251
|
return node;
|
|
3916
4252
|
}
|
|
@@ -3940,15 +4276,17 @@ function renderReleaseAurOutputWidget() {
|
|
|
3940
4276
|
);
|
|
3941
4277
|
header.append(titleWrap, meta, actions);
|
|
3942
4278
|
|
|
4279
|
+
const streamLines = outputLines.length ? outputLines : ["Waiting for release-aur output..."];
|
|
4280
|
+
const streamHeader = releaseNpmStreamHeader("Live AUR output stream", outputLines.length, { live: true });
|
|
3943
4281
|
const terminal = make("div", "release-npm-terminal");
|
|
3944
4282
|
terminal.setAttribute("role", "log");
|
|
3945
4283
|
terminal.setAttribute("aria-live", "polite");
|
|
3946
|
-
for (const line of
|
|
4284
|
+
for (const line of streamLines) {
|
|
3947
4285
|
appendReleaseNpmTerminalLine(terminal, line);
|
|
3948
4286
|
}
|
|
3949
4287
|
|
|
3950
4288
|
const controls = make("div", "release-npm-controls", details.controls || "Controls: /release-aur toggle expands/collapses · /release-aur abort stops subprocess");
|
|
3951
|
-
node.append(header, terminal, controls);
|
|
4289
|
+
node.append(header, streamHeader, terminal, controls);
|
|
3952
4290
|
requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
3953
4291
|
return node;
|
|
3954
4292
|
}
|
|
@@ -3972,11 +4310,13 @@ function renderReleaseAurLogWidget() {
|
|
|
3972
4310
|
actions.append(releaseNpmActionButton("Close log", "/release-aur logs close"));
|
|
3973
4311
|
header.append(titleWrap, meta, actions);
|
|
3974
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);
|
|
3975
4315
|
const terminal = make("div", "release-npm-terminal");
|
|
3976
|
-
for (const line of
|
|
4316
|
+
for (const line of logLines) {
|
|
3977
4317
|
appendReleaseNpmTerminalLine(terminal, line);
|
|
3978
4318
|
}
|
|
3979
|
-
node.append(header, terminal);
|
|
4319
|
+
node.append(header, streamHeader, terminal);
|
|
3980
4320
|
requestAnimationFrame(() => { terminal.scrollTop = terminal.scrollHeight; });
|
|
3981
4321
|
return node;
|
|
3982
4322
|
}
|
|
@@ -4112,24 +4452,24 @@ function renderGitWorkflow() {
|
|
|
4112
4452
|
elements.gitWorkflowCancelButton.disabled = false;
|
|
4113
4453
|
|
|
4114
4454
|
if (gitWorkflow.step === "add") {
|
|
4115
|
-
addGitWorkflowAction("Run git add .", runGitAdd, "primary", false);
|
|
4455
|
+
addGitWorkflowAction("Run git add .", () => runGitAdd(), "primary", false);
|
|
4116
4456
|
} else if (gitWorkflow.step === "generate") {
|
|
4117
|
-
addGitWorkflowAction("Run /git-staged-msg", runGitMessagePrompt, "primary", false);
|
|
4457
|
+
addGitWorkflowAction("Run /git-staged-msg", () => runGitMessagePrompt(), "primary", false);
|
|
4118
4458
|
addGitWorkflowAction("Preview current message files", () => loadGitWorkflowMessage({ requireFresh: false }), "", false);
|
|
4119
4459
|
} else if (gitWorkflow.step === "generating") {
|
|
4120
4460
|
addGitWorkflowAction("Refresh message preview", () => loadGitWorkflowMessage({ requireFresh: true }), "", false);
|
|
4121
4461
|
} else if (gitWorkflow.step === "message") {
|
|
4122
4462
|
addGitWorkflowAction("Commit short", () => commitGitWorkflow("short"), "primary", false);
|
|
4123
4463
|
addGitWorkflowAction("Commit long", () => commitGitWorkflow("long"), "primary", false);
|
|
4124
|
-
addGitWorkflowAction("Regenerate", runGitMessagePrompt, "", false);
|
|
4464
|
+
addGitWorkflowAction("Regenerate", () => runGitMessagePrompt(), "", false);
|
|
4125
4465
|
} else if (gitWorkflow.step === "push") {
|
|
4126
|
-
addGitWorkflowAction("Run git push", pushGitWorkflow, "primary", false);
|
|
4466
|
+
addGitWorkflowAction("Run git push", () => pushGitWorkflow(), "primary", false);
|
|
4127
4467
|
} else if (gitWorkflow.step === "done") {
|
|
4128
4468
|
addGitWorkflowAction("Close", () => setGitWorkflow({ active: false }), "primary", false);
|
|
4129
|
-
addGitWorkflowAction("Start another", startGitWorkflow, "", false);
|
|
4469
|
+
addGitWorkflowAction("Start another", () => startGitWorkflow(), "", false);
|
|
4130
4470
|
} else if (["cancelled", "error"].includes(gitWorkflow.step)) {
|
|
4131
4471
|
addGitWorkflowAction("Close", () => setGitWorkflow({ active: false }), "primary", false);
|
|
4132
|
-
addGitWorkflowAction("Restart", startGitWorkflow, "", false);
|
|
4472
|
+
addGitWorkflowAction("Restart", () => startGitWorkflow(), "", false);
|
|
4133
4473
|
}
|
|
4134
4474
|
}
|
|
4135
4475
|
|
|
@@ -4157,8 +4497,7 @@ function failGitWorkflow(error, step, { tabId = activeTabId } = {}) {
|
|
|
4157
4497
|
}, { tabId });
|
|
4158
4498
|
}
|
|
4159
4499
|
|
|
4160
|
-
function startGitWorkflow() {
|
|
4161
|
-
const tabId = activeTabId;
|
|
4500
|
+
function startGitWorkflow(tabId = activeTabId) {
|
|
4162
4501
|
if (!tabId) return;
|
|
4163
4502
|
if (!isOptionalFeatureEnabled("gitWorkflow")) {
|
|
4164
4503
|
const tabContext = activeTabContext(tabId);
|
|
@@ -4182,8 +4521,7 @@ function startGitWorkflow() {
|
|
|
4182
4521
|
}, { tabId });
|
|
4183
4522
|
}
|
|
4184
4523
|
|
|
4185
|
-
async function cancelGitWorkflow() {
|
|
4186
|
-
const tabId = activeTabId;
|
|
4524
|
+
async function cancelGitWorkflow(tabId = gitWorkflowActionTabId()) {
|
|
4187
4525
|
const tabContext = activeTabContext(tabId);
|
|
4188
4526
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4189
4527
|
if (!workflow?.active) return;
|
|
@@ -4198,8 +4536,7 @@ async function cancelGitWorkflow() {
|
|
|
4198
4536
|
if (shouldAbortPi && isCurrentTabContext(tabContext)) scheduleAbortStateChecks();
|
|
4199
4537
|
}
|
|
4200
4538
|
|
|
4201
|
-
async function runGitAdd() {
|
|
4202
|
-
const tabId = activeTabId;
|
|
4539
|
+
async function runGitAdd(tabId = gitWorkflowActionTabId()) {
|
|
4203
4540
|
const tabContext = activeTabContext(tabId);
|
|
4204
4541
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4205
4542
|
if (!workflow) return;
|
|
@@ -4215,10 +4552,11 @@ async function runGitAdd() {
|
|
|
4215
4552
|
}
|
|
4216
4553
|
}
|
|
4217
4554
|
|
|
4218
|
-
async function runGitMessagePrompt() {
|
|
4219
|
-
const tabId = activeTabId;
|
|
4555
|
+
async function runGitMessagePrompt(tabId = gitWorkflowActionTabId()) {
|
|
4220
4556
|
const tabContext = activeTabContext(tabId);
|
|
4221
|
-
|
|
4557
|
+
const targetTab = tabs.find((tab) => tab.id === tabId);
|
|
4558
|
+
const targetBusy = tabId === activeTabId ? !!currentState?.isStreaming : activityForTab(targetTab).isWorking;
|
|
4559
|
+
if (targetBusy) {
|
|
4222
4560
|
failGitWorkflow(new Error("Pi is currently running. Wait for it to finish or abort before generating a staged commit message."), "generate", { tabId });
|
|
4223
4561
|
return;
|
|
4224
4562
|
}
|
|
@@ -4241,7 +4579,8 @@ async function runGitMessagePrompt() {
|
|
|
4241
4579
|
if (isCurrentTabContext(tabContext)) scheduleRefreshState(120, tabContext);
|
|
4242
4580
|
setTimeout(() => {
|
|
4243
4581
|
const currentWorkflow = gitWorkflowForTab(tabId, { create: false });
|
|
4244
|
-
|
|
4582
|
+
const targetStillBusy = tabId === activeTabId && currentState?.isStreaming;
|
|
4583
|
+
if (isCurrentGitWorkflowRun(runId, tabId) && currentWorkflow?.step === "generating" && !targetStillBusy) {
|
|
4245
4584
|
loadGitWorkflowMessage({ requireFresh: true, retries: 1, runId, tabId });
|
|
4246
4585
|
}
|
|
4247
4586
|
}, 2500);
|
|
@@ -4283,8 +4622,7 @@ async function loadGitWorkflowMessage({ requireFresh = false, retries = 0, runId
|
|
|
4283
4622
|
}
|
|
4284
4623
|
}
|
|
4285
4624
|
|
|
4286
|
-
async function commitGitWorkflow(variant) {
|
|
4287
|
-
const tabId = activeTabId;
|
|
4625
|
+
async function commitGitWorkflow(variant, tabId = gitWorkflowActionTabId()) {
|
|
4288
4626
|
const tabContext = activeTabContext(tabId);
|
|
4289
4627
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4290
4628
|
if (!workflow) return;
|
|
@@ -4300,8 +4638,7 @@ async function commitGitWorkflow(variant) {
|
|
|
4300
4638
|
}
|
|
4301
4639
|
}
|
|
4302
4640
|
|
|
4303
|
-
async function pushGitWorkflow() {
|
|
4304
|
-
const tabId = activeTabId;
|
|
4641
|
+
async function pushGitWorkflow(tabId = gitWorkflowActionTabId()) {
|
|
4305
4642
|
const tabContext = activeTabContext(tabId);
|
|
4306
4643
|
const workflow = gitWorkflowForTab(tabId, { create: false });
|
|
4307
4644
|
if (!workflow) return;
|
|
@@ -4321,19 +4658,31 @@ function resumeGitWorkflowForActiveTab(tabContext = activeTabContext()) {
|
|
|
4321
4658
|
if (!isCurrentTabContext(tabContext)) return;
|
|
4322
4659
|
bindGitWorkflowToActiveTab();
|
|
4323
4660
|
renderGitWorkflow();
|
|
4324
|
-
|
|
4661
|
+
const workflowTabId = gitWorkflowActionTabId();
|
|
4662
|
+
if (workflowTabId === tabContext.tabId && gitWorkflow.active && gitWorkflow.step === "generating" && !currentState?.isStreaming) {
|
|
4325
4663
|
const retryDelayMs = Math.max(0, 2500 - (Date.now() - (gitWorkflow.messageRequestedAt || 0)));
|
|
4326
4664
|
if (retryDelayMs > 0) {
|
|
4327
4665
|
setTimeout(() => resumeGitWorkflowForActiveTab(tabContext), retryDelayMs);
|
|
4328
4666
|
return;
|
|
4329
4667
|
}
|
|
4330
|
-
loadGitWorkflowMessage({ requireFresh: true, retries: 3, runId: gitWorkflow.runId, tabId:
|
|
4668
|
+
loadGitWorkflowMessage({ requireFresh: true, retries: 3, runId: gitWorkflow.runId, tabId: workflowTabId });
|
|
4331
4669
|
}
|
|
4332
4670
|
}
|
|
4333
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
|
+
|
|
4334
4680
|
function renderQueue(event) {
|
|
4335
|
-
const
|
|
4336
|
-
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;
|
|
4337
4686
|
if (steering.length === 0 && followUp.length === 0) {
|
|
4338
4687
|
elements.queueBox.textContent = "No queued messages.";
|
|
4339
4688
|
elements.queueBox.classList.add("muted");
|
|
@@ -4343,9 +4692,32 @@ function renderQueue(event) {
|
|
|
4343
4692
|
const lines = [];
|
|
4344
4693
|
if (steering.length) lines.push(`Steering (${steering.length}):`, ...steering.map((item) => `• ${item}`));
|
|
4345
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).");
|
|
4346
4696
|
elements.queueBox.textContent = lines.join("\n");
|
|
4347
4697
|
}
|
|
4348
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
|
+
|
|
4349
4721
|
function appendText(parent, text, className = "text-block") {
|
|
4350
4722
|
const block = make("pre", className);
|
|
4351
4723
|
block.textContent = text || "";
|
|
@@ -4907,11 +5279,12 @@ function renderContent(parent, content, { markdown = false } = {}) {
|
|
|
4907
5279
|
if (markdown) appendMarkdown(parent, stripTodoProgressLines(text));
|
|
4908
5280
|
else appendText(parent, text);
|
|
4909
5281
|
} else if (part.type === "thinking") {
|
|
4910
|
-
|
|
5282
|
+
const thinking = visibleThinkingText(assistantThinkingText(part));
|
|
5283
|
+
if (!thinkingOutputVisible || !thinking) continue;
|
|
4911
5284
|
const details = make("details", "thinking-block");
|
|
4912
5285
|
details.open = true;
|
|
4913
5286
|
details.append(make("summary", undefined, "thinking"));
|
|
4914
|
-
appendText(details,
|
|
5287
|
+
appendText(details, thinking, "thinking-text");
|
|
4915
5288
|
parent.append(details);
|
|
4916
5289
|
} else if (part.type === "toolCall") {
|
|
4917
5290
|
const details = make("details");
|
|
@@ -4947,6 +5320,13 @@ function assistantThinkingText(part) {
|
|
|
4947
5320
|
return typeof part.content === "string" ? part.content : "";
|
|
4948
5321
|
}
|
|
4949
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
|
+
|
|
4950
5330
|
function isAssistantToolCallPart(part) {
|
|
4951
5331
|
return !!(part && typeof part === "object" && (part.type === "toolCall" || part.toolCall));
|
|
4952
5332
|
}
|
|
@@ -5008,8 +5388,8 @@ function assistantDisplayMessages(message) {
|
|
|
5008
5388
|
const part = content[index];
|
|
5009
5389
|
const isThinkingPart = part && typeof part === "object" && (part.type === "thinking" || typeof part.thinking === "string");
|
|
5010
5390
|
if (isThinkingPart) {
|
|
5011
|
-
const thinking = assistantThinkingText(part)
|
|
5012
|
-
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 });
|
|
5013
5393
|
continue;
|
|
5014
5394
|
}
|
|
5015
5395
|
if (isAssistantToolCallPart(part)) {
|
|
@@ -5051,6 +5431,136 @@ function stickyUserPromptPreview(message) {
|
|
|
5051
5431
|
return stickyUserPromptPreviewText(messageUserPromptText(message));
|
|
5052
5432
|
}
|
|
5053
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
|
+
|
|
5054
5564
|
function loadLastUserPromptCache() {
|
|
5055
5565
|
try {
|
|
5056
5566
|
const raw = JSON.parse(localStorage.getItem(LAST_USER_PROMPT_STORAGE_KEY) || "{}");
|
|
@@ -5126,8 +5636,18 @@ function stickyUserPromptViewportGap() {
|
|
|
5126
5636
|
|
|
5127
5637
|
function resetChatOutput() {
|
|
5128
5638
|
liveToolCards.clear();
|
|
5129
|
-
|
|
5130
|
-
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
|
+
}
|
|
5131
5651
|
}
|
|
5132
5652
|
|
|
5133
5653
|
function userPromptTargets() {
|
|
@@ -5339,7 +5859,7 @@ function appendToolOutput(parent, text, { label = "output", previewLines = 10, p
|
|
|
5339
5859
|
const lines = clean.split(/\r?\n/);
|
|
5340
5860
|
if (lines.length > previewLines) {
|
|
5341
5861
|
const details = make("details", "tool-output-details");
|
|
5342
|
-
details.open = open;
|
|
5862
|
+
details.open = open || toolOutputGloballyExpanded;
|
|
5343
5863
|
details.append(make("summary", "tool-output-summary", `${label} (${lines.length} lines; expand)`));
|
|
5344
5864
|
appendText(details, clean, "code-block tool-output-code");
|
|
5345
5865
|
parent.append(details);
|
|
@@ -5449,6 +5969,37 @@ function appendToolRawDetails(parent, tool) {
|
|
|
5449
5969
|
parent.append(details);
|
|
5450
5970
|
}
|
|
5451
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
|
+
|
|
5452
6003
|
function renderBashToolExecution(parent, tool) {
|
|
5453
6004
|
const command = toolArgText(tool.args, "command", "");
|
|
5454
6005
|
const timeout = toolArgValue(tool.args, "timeout");
|
|
@@ -5550,9 +6101,15 @@ function liveToolRunMessage(run) {
|
|
|
5550
6101
|
|
|
5551
6102
|
function applyToolExecutionBubbleState(bubble, message) {
|
|
5552
6103
|
const status = toolExecutionStatus(message);
|
|
5553
|
-
|
|
5554
|
-
bubble.classList.
|
|
5555
|
-
|
|
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"));
|
|
5556
6113
|
if (message.toolCallId) {
|
|
5557
6114
|
const id = String(message.toolCallId);
|
|
5558
6115
|
bubble.dataset.toolCallId = id;
|
|
@@ -5615,7 +6172,7 @@ function reuseToolExecutionBubble(reusableToolCards, message, { streaming = fals
|
|
|
5615
6172
|
bubble.removeAttribute("data-user-prompt");
|
|
5616
6173
|
}
|
|
5617
6174
|
if (!streaming && !transient) renderActionFeedbackControls(bubble, message, messageIndex);
|
|
5618
|
-
|
|
6175
|
+
appendChatMessageBubble(bubble);
|
|
5619
6176
|
return { bubble, body };
|
|
5620
6177
|
}
|
|
5621
6178
|
|
|
@@ -5629,10 +6186,13 @@ function updateLiveToolCard(bubble, message) {
|
|
|
5629
6186
|
if (role) role.textContent = messageTitle(message);
|
|
5630
6187
|
const timestamp = header?.querySelector(".muted");
|
|
5631
6188
|
if (timestamp) timestamp.textContent = formatDate(message.timestamp);
|
|
6189
|
+
const nextRenderSignature = toolExecutionRenderSignature(message);
|
|
6190
|
+
if (bubble._toolRenderSignature === nextRenderSignature && body.childElementCount > 0) return true;
|
|
5632
6191
|
const detailsOpenState = captureToolDetailsOpenState(body);
|
|
5633
6192
|
body.replaceChildren();
|
|
5634
6193
|
renderToolExecution(body, message);
|
|
5635
6194
|
restoreToolDetailsOpenState(body, detailsOpenState);
|
|
6195
|
+
bubble._toolRenderSignature = nextRenderSignature;
|
|
5636
6196
|
return true;
|
|
5637
6197
|
}
|
|
5638
6198
|
|
|
@@ -5779,9 +6339,10 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
5779
6339
|
if (message.isError) bubble.classList.add("error");
|
|
5780
6340
|
} else if (message.role === "toolExecution") {
|
|
5781
6341
|
renderToolExecution(body, message);
|
|
6342
|
+
bubble._toolRenderSignature = toolExecutionRenderSignature(message);
|
|
5782
6343
|
} else if (message.role === "thinking") {
|
|
5783
|
-
const thinkingText = message.thinking || textFromContent(message.content);
|
|
5784
|
-
if (thinkingOutputVisible &&
|
|
6344
|
+
const thinkingText = visibleThinkingText(message.thinking || textFromContent(message.content));
|
|
6345
|
+
if (thinkingOutputVisible && thinkingText) appendText(body, thinkingText, "thinking-text");
|
|
5785
6346
|
} else if (message.role === "toolCall") {
|
|
5786
6347
|
appendText(body, JSON.stringify(message.arguments ?? message.content ?? {}, null, 2), "code-block");
|
|
5787
6348
|
} else if (message.role === "assistantEvent") {
|
|
@@ -5792,7 +6353,7 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
5792
6353
|
|
|
5793
6354
|
if (isCollapsibleOutput) {
|
|
5794
6355
|
const details = make("details", "message-collapse");
|
|
5795
|
-
if (message.isError) details.open = true;
|
|
6356
|
+
if (message.isError || toolOutputGloballyExpanded) details.open = true;
|
|
5796
6357
|
details.append(header, body);
|
|
5797
6358
|
bubble.append(details);
|
|
5798
6359
|
if (message.role === "toolResult" && !message.isError) {
|
|
@@ -5806,7 +6367,7 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
5806
6367
|
bubble.append(header, body);
|
|
5807
6368
|
}
|
|
5808
6369
|
if (!streaming && !transient) renderActionFeedbackControls(bubble, message, messageIndex);
|
|
5809
|
-
|
|
6370
|
+
appendChatMessageBubble(bubble);
|
|
5810
6371
|
return { bubble, body };
|
|
5811
6372
|
}
|
|
5812
6373
|
|
|
@@ -5849,11 +6410,11 @@ function appendTranscriptMessage(message, { streaming = false, messageIndex = -1
|
|
|
5849
6410
|
}
|
|
5850
6411
|
|
|
5851
6412
|
function stateHasRunIndicatorActivity(state = currentState) {
|
|
5852
|
-
return !!state?.isStreaming || !!state?.isCompacting;
|
|
6413
|
+
return !!state?.isStreaming || !!state?.isCompacting || isUserBashActive();
|
|
5853
6414
|
}
|
|
5854
6415
|
|
|
5855
6416
|
function runIndicatorIsActive() {
|
|
5856
|
-
return runIndicatorLocallyActive || stateHasRunIndicatorActivity(currentState);
|
|
6417
|
+
return runIndicatorLocallyActive || stateHasRunIndicatorActivity(currentState) || isUserBashActive();
|
|
5857
6418
|
}
|
|
5858
6419
|
|
|
5859
6420
|
function clearRunIndicatorGraceCheck() {
|
|
@@ -5926,22 +6487,24 @@ function stopRunIndicatorTicker() {
|
|
|
5926
6487
|
runIndicatorTimer = null;
|
|
5927
6488
|
}
|
|
5928
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
|
+
|
|
5929
6506
|
function ensureRunIndicatorBubble() {
|
|
5930
|
-
if (runIndicatorBubble
|
|
5931
|
-
runIndicatorBubble = make("article", "message runIndicator run-indicator-message streaming");
|
|
5932
|
-
runIndicatorBubble.setAttribute("aria-live", "polite");
|
|
5933
|
-
runIndicatorBubble.setAttribute("aria-label", "Agent is running:");
|
|
5934
|
-
|
|
5935
|
-
const body = make("div", "message-body");
|
|
5936
|
-
const row = make("div", "run-indicator-row");
|
|
5937
|
-
const pulse = make("span", "run-indicator-pulse");
|
|
5938
|
-
pulse.setAttribute("aria-hidden", "true");
|
|
5939
|
-
runIndicatorText = make("span", "run-indicator-text");
|
|
5940
|
-
runIndicatorMeta = make("span", "run-indicator-meta");
|
|
5941
|
-
row.append(pulse, runIndicatorText, runIndicatorMeta);
|
|
5942
|
-
body.append(row);
|
|
5943
|
-
runIndicatorBubble.append(body);
|
|
5944
|
-
}
|
|
6507
|
+
if (!runIndicatorBubble || !runIndicatorText || !runIndicatorMeta) createRunIndicatorBubble();
|
|
5945
6508
|
if (elements.chat.lastElementChild !== runIndicatorBubble) elements.chat.append(runIndicatorBubble);
|
|
5946
6509
|
}
|
|
5947
6510
|
|
|
@@ -5949,9 +6512,11 @@ function updateRunIndicatorBubble() {
|
|
|
5949
6512
|
if (!runIndicatorIsActive()) return;
|
|
5950
6513
|
if (!runIndicatorStartedAt) runIndicatorStartedAt = performance.now();
|
|
5951
6514
|
ensureRunIndicatorBubble();
|
|
5952
|
-
|
|
6515
|
+
const headline = runIndicatorHeadline();
|
|
6516
|
+
if (runIndicatorText.textContent !== headline) runIndicatorText.textContent = headline;
|
|
5953
6517
|
const detail = runIndicatorDetail();
|
|
5954
|
-
|
|
6518
|
+
const meta = runIndicatorShowsElapsed() ? `${detail} · run time ${formatRunIndicatorElapsed()}` : detail;
|
|
6519
|
+
if (runIndicatorMeta.textContent !== meta) runIndicatorMeta.textContent = meta;
|
|
5955
6520
|
}
|
|
5956
6521
|
|
|
5957
6522
|
function removeRunIndicatorBubble() {
|
|
@@ -6124,6 +6689,7 @@ function renderAllMessages({ preserveScroll = false } = {}) {
|
|
|
6124
6689
|
});
|
|
6125
6690
|
}
|
|
6126
6691
|
rememberActionEntries(transcriptItems);
|
|
6692
|
+
applyToolOutputExpansionToDom();
|
|
6127
6693
|
renderRunIndicator({ scroll: false });
|
|
6128
6694
|
updateStickyUserPromptButton();
|
|
6129
6695
|
if (shouldFollow) scrollChatToBottom({ force: true });
|
|
@@ -6135,12 +6701,13 @@ function renderAllMessages({ preserveScroll = false } = {}) {
|
|
|
6135
6701
|
updateStickyUserPromptButton();
|
|
6136
6702
|
}
|
|
6137
6703
|
|
|
6138
|
-
function addTransientMessage({ role = "notice", title, content, level = "info" }) {
|
|
6704
|
+
function addTransientMessage({ role = "notice", title, content, level = "info", ...details }) {
|
|
6139
6705
|
transientMessages.push({
|
|
6140
6706
|
role,
|
|
6141
6707
|
title,
|
|
6142
6708
|
level,
|
|
6143
6709
|
content,
|
|
6710
|
+
...details,
|
|
6144
6711
|
timestamp: Date.now(),
|
|
6145
6712
|
});
|
|
6146
6713
|
if (transientMessages.length > 80) transientMessages.splice(0, transientMessages.length - 80);
|
|
@@ -6954,6 +7521,7 @@ function renderMessages(messages) {
|
|
|
6954
7521
|
latestMessages = messages || [];
|
|
6955
7522
|
cleanupLiveToolRunsForMessages(latestMessages);
|
|
6956
7523
|
syncLastUserPromptFromMessages(latestMessages);
|
|
7524
|
+
syncPromptHistoryFromMessages(latestMessages);
|
|
6957
7525
|
renderAllMessages();
|
|
6958
7526
|
renderFooter();
|
|
6959
7527
|
renderFeedbackTray();
|
|
@@ -7037,9 +7605,9 @@ function ensureStreamingThinkingBubble() {
|
|
|
7037
7605
|
return true;
|
|
7038
7606
|
}
|
|
7039
7607
|
|
|
7040
|
-
function showStreamingThinking(
|
|
7608
|
+
function showStreamingThinking(initialText = "") {
|
|
7041
7609
|
if (!ensureStreamingThinkingBubble()) return;
|
|
7042
|
-
if (!streamThinking.textContent) streamThinking.textContent =
|
|
7610
|
+
if (initialText && !streamThinking.textContent) streamThinking.textContent = initialText;
|
|
7043
7611
|
}
|
|
7044
7612
|
|
|
7045
7613
|
function resetStreamBubble() {
|
|
@@ -7055,7 +7623,7 @@ function resetStreamBubble() {
|
|
|
7055
7623
|
}
|
|
7056
7624
|
|
|
7057
7625
|
function thinkingDeltaText(update) {
|
|
7058
|
-
return update.delta || update.thinking || update.content || "";
|
|
7626
|
+
return visibleThinkingText(update.delta || update.thinking || update.content || "");
|
|
7059
7627
|
}
|
|
7060
7628
|
|
|
7061
7629
|
function assistantStreamingMessage(event) {
|
|
@@ -7082,37 +7650,38 @@ function assistantThinkingTextFromMessage(message) {
|
|
|
7082
7650
|
if (!Array.isArray(content)) return null;
|
|
7083
7651
|
const parts = content
|
|
7084
7652
|
.filter((part) => part && typeof part === "object" && (part.type === "thinking" || typeof part.thinking === "string"))
|
|
7085
|
-
.map((part) => assistantThinkingText(part))
|
|
7653
|
+
.map((part) => visibleThinkingText(assistantThinkingText(part)))
|
|
7086
7654
|
.filter((text) => text.trim());
|
|
7087
7655
|
return parts.length ? parts.join("\n\n") : "";
|
|
7088
7656
|
}
|
|
7089
7657
|
|
|
7090
7658
|
function setStreamingThinkingText(text) {
|
|
7091
|
-
|
|
7659
|
+
const thinking = visibleThinkingText(text);
|
|
7660
|
+
if (!thinkingOutputVisible || !thinking) return false;
|
|
7092
7661
|
showStreamingThinking("");
|
|
7093
|
-
if (streamThinking) streamThinking.textContent =
|
|
7662
|
+
if (streamThinking) streamThinking.textContent = thinking;
|
|
7663
|
+
return true;
|
|
7094
7664
|
}
|
|
7095
7665
|
|
|
7096
7666
|
function syncStreamingThinkingFromMessage(event, { placeholder = "" } = {}) {
|
|
7097
7667
|
if (!thinkingOutputVisible) return true;
|
|
7098
7668
|
const text = assistantThinkingTextFromMessage(assistantStreamingMessage(event));
|
|
7099
7669
|
if (text === null) return false;
|
|
7100
|
-
|
|
7101
|
-
return true;
|
|
7670
|
+
return setStreamingThinkingText(text || placeholder);
|
|
7102
7671
|
}
|
|
7103
7672
|
|
|
7104
7673
|
function handleMessageUpdate(event) {
|
|
7105
7674
|
const update = event.assistantMessageEvent || {};
|
|
7106
7675
|
if (update.type === "thinking_start") {
|
|
7107
7676
|
setRunIndicatorActivity("Thinking…", { scroll: false });
|
|
7108
|
-
syncStreamingThinkingFromMessage(event
|
|
7677
|
+
syncStreamingThinkingFromMessage(event);
|
|
7109
7678
|
scrollChatToBottom();
|
|
7110
7679
|
} else if (update.type === "thinking_delta") {
|
|
7111
7680
|
const delta = thinkingDeltaText(update);
|
|
7112
7681
|
currentRunStreamChars += delta.length;
|
|
7113
7682
|
setRunIndicatorActivity("Thinking…", { scroll: false });
|
|
7114
7683
|
const synced = syncStreamingThinkingFromMessage(event);
|
|
7115
|
-
if (thinkingOutputVisible && (!synced ||
|
|
7684
|
+
if (thinkingOutputVisible && delta && (!synced || !streamThinking?.textContent)) {
|
|
7116
7685
|
showStreamingThinking("");
|
|
7117
7686
|
if (streamThinking?.textContent === "Thinking…") streamThinking.textContent = "";
|
|
7118
7687
|
if (streamThinking) streamThinking.textContent += delta;
|
|
@@ -7732,6 +8301,35 @@ async function refreshAll(tabContext = activeTabContext()) {
|
|
|
7732
8301
|
resumeGitWorkflowForActiveTab(tabContext);
|
|
7733
8302
|
}
|
|
7734
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
|
+
|
|
7735
8333
|
async function openToNetwork() {
|
|
7736
8334
|
if (latestNetwork?.open) {
|
|
7737
8335
|
await closeNetworkAccess();
|
|
@@ -7806,6 +8404,216 @@ async function closeNetworkAccess() {
|
|
|
7806
8404
|
}
|
|
7807
8405
|
}
|
|
7808
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
|
+
|
|
7809
8617
|
async function sendPrompt(kind = "prompt", explicitMessage) {
|
|
7810
8618
|
const usesPromptInput = explicitMessage === undefined;
|
|
7811
8619
|
const rawMessage = usesPromptInput ? elements.promptInput.value : explicitMessage;
|
|
@@ -7816,6 +8624,11 @@ async function sendPrompt(kind = "prompt", explicitMessage) {
|
|
|
7816
8624
|
const attachments = usesPromptInput ? [...attachmentsForTab(targetTabId)] : [];
|
|
7817
8625
|
if (!originalMessage && attachments.length === 0) return;
|
|
7818
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
|
+
}
|
|
7819
8632
|
|
|
7820
8633
|
const targetWasStreaming = !!currentState?.isStreaming;
|
|
7821
8634
|
const busyBehavior = elements.busyBehavior.value || "followUp";
|
|
@@ -7834,7 +8647,10 @@ async function sendPrompt(kind = "prompt", explicitMessage) {
|
|
|
7834
8647
|
message = composeMessageWithAttachments(originalMessage, prepared.uploadedFiles, prepared.inlineImageIds);
|
|
7835
8648
|
const bodyBase = { message };
|
|
7836
8649
|
if (prepared.images.length) bodyBase.images = prepared.images;
|
|
7837
|
-
if (
|
|
8650
|
+
if (!message.startsWith("/")) {
|
|
8651
|
+
rememberPromptHistory(message, { tabId: targetTabId });
|
|
8652
|
+
if (kind === "prompt") rememberLastUserPrompt(message, { tabId: targetTabId });
|
|
8653
|
+
}
|
|
7838
8654
|
if (startsRun && isCurrentTabContext(tabContext)) setRunIndicatorActivity("Sending prompt to Pi…");
|
|
7839
8655
|
|
|
7840
8656
|
let response;
|
|
@@ -7860,12 +8676,15 @@ async function sendPrompt(kind = "prompt", explicitMessage) {
|
|
|
7860
8676
|
}
|
|
7861
8677
|
if (targetStillActive && response?.command === "native_slash_command" && response.data?.copyText) {
|
|
7862
8678
|
try {
|
|
7863
|
-
await
|
|
8679
|
+
await copyText(response.data.copyText);
|
|
7864
8680
|
} catch (error) {
|
|
7865
8681
|
response.data.message = `${response.data.message || "Copy requested, but clipboard access failed."}\n\nClipboard access failed: ${error.message}\n\n${response.data.copyText}`;
|
|
7866
8682
|
response.data.level = "warn";
|
|
7867
8683
|
}
|
|
7868
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
|
+
}
|
|
7869
8688
|
if (targetStillActive && response?.command === "native_slash_command" && response.data?.message) {
|
|
7870
8689
|
addTransientMessage({ role: "native", title: message.split(/\s+/, 1)[0], content: response.data.message, level: response.data.level || "info" });
|
|
7871
8690
|
}
|
|
@@ -8071,7 +8890,7 @@ function handleEvent(event) {
|
|
|
8071
8890
|
switch (event.type) {
|
|
8072
8891
|
case "webui_connected":
|
|
8073
8892
|
addEvent(`connected to ${event.tabTitle || "terminal"} for ${event.cwd}`);
|
|
8074
|
-
|
|
8893
|
+
scheduleForegroundReconcile("event stream reconnect", 0);
|
|
8075
8894
|
break;
|
|
8076
8895
|
case "webui_tab_renamed":
|
|
8077
8896
|
applyTabMetadata(event.tab || { id: event.tabId, title: event.tabTitle, activity: event.tabActivity });
|
|
@@ -8166,6 +8985,7 @@ function handleEvent(event) {
|
|
|
8166
8985
|
scheduleRefreshState();
|
|
8167
8986
|
scheduleRefreshMessages();
|
|
8168
8987
|
scheduleRefreshFooter();
|
|
8988
|
+
scheduleRefreshCodexUsage(2200);
|
|
8169
8989
|
renderFeedbackTray();
|
|
8170
8990
|
{
|
|
8171
8991
|
const workflowTabId = event.tabId || activeTabId;
|
|
@@ -8258,7 +9078,7 @@ function handleEvent(event) {
|
|
|
8258
9078
|
syncActiveTabActivityFromState(currentState);
|
|
8259
9079
|
syncRunIndicatorFromState(currentState);
|
|
8260
9080
|
renderStatus();
|
|
8261
|
-
} 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)) {
|
|
8262
9082
|
if (event.command === "new_session") {
|
|
8263
9083
|
const tabId = event.tabId || activeTabId;
|
|
8264
9084
|
forgetLastUserPrompt(tabId);
|
|
@@ -8330,7 +9150,7 @@ publishMenuContainer?.addEventListener("focusout", () => {
|
|
|
8330
9150
|
});
|
|
8331
9151
|
elements.releaseNpmButton.addEventListener("click", () => runPublishWorkflow("/release-npm"));
|
|
8332
9152
|
elements.releaseAurButton.addEventListener("click", () => runPublishWorkflow("/release-aur"));
|
|
8333
|
-
elements.gitWorkflowCancelButton.addEventListener("click", cancelGitWorkflow);
|
|
9153
|
+
elements.gitWorkflowCancelButton.addEventListener("click", () => cancelGitWorkflow());
|
|
8334
9154
|
elements.nativeCommandDialog.addEventListener("close", () => {
|
|
8335
9155
|
elements.nativeCommandSearch.oninput = null;
|
|
8336
9156
|
nativeCommandTabId = null;
|
|
@@ -8349,8 +9169,17 @@ async function abortActiveRun({ source = "button" } = {}) {
|
|
|
8349
9169
|
abortRequestInFlight = true;
|
|
8350
9170
|
resetAbortLongPressAffordance();
|
|
8351
9171
|
updateComposerModeButtons();
|
|
9172
|
+
const hadActiveBash = isUserBashActive(tabContext.tabId);
|
|
8352
9173
|
const hadActiveRun = runIndicatorIsActive();
|
|
8353
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
|
+
}
|
|
8354
9183
|
if (hadActiveRun) setRunIndicatorActivity(`Abort requested${source === "escape" ? " from Esc" : source === "long-press" ? " from long-press" : ""}; checking whether Pi stopped…`);
|
|
8355
9184
|
await api("/api/abort", { method: "POST", body: {}, tabId: tabContext.tabId });
|
|
8356
9185
|
if (!isCurrentTabContext(tabContext)) return;
|
|
@@ -8470,6 +9299,7 @@ if (elements.backgroundClearButton) {
|
|
|
8470
9299
|
elements.backgroundClearButton.addEventListener("click", () => clearCustomBackground().catch((error) => addEvent(error.message || String(error), "error")));
|
|
8471
9300
|
}
|
|
8472
9301
|
elements.openNetworkButton.addEventListener("click", openToNetwork);
|
|
9302
|
+
elements.stopServerButton.addEventListener("click", stopServer);
|
|
8473
9303
|
elements.agentDoneNotificationsToggle.addEventListener("change", () => {
|
|
8474
9304
|
setAgentDoneNotificationsEnabled(elements.agentDoneNotificationsToggle.checked, {
|
|
8475
9305
|
requestPermission: elements.agentDoneNotificationsToggle.checked,
|
|
@@ -8528,6 +9358,72 @@ document.addEventListener("pointermove", (event) => {
|
|
|
8528
9358
|
}
|
|
8529
9359
|
rememberPointerPosition(event);
|
|
8530
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));
|
|
8531
9427
|
window.addEventListener("keydown", (event) => {
|
|
8532
9428
|
if (event.key !== "Escape") return;
|
|
8533
9429
|
if (elements.dialog?.open || elements.pathPickerDialog?.open) return;
|
|
@@ -8561,6 +9457,9 @@ window.addEventListener("keydown", (event) => {
|
|
|
8561
9457
|
}
|
|
8562
9458
|
});
|
|
8563
9459
|
|
|
9460
|
+
elements.refreshCodexUsageButton?.addEventListener("click", () => {
|
|
9461
|
+
refreshCodexUsage({ forceAuthRefresh: true }).finally(() => scheduleRefreshCodexUsage());
|
|
9462
|
+
});
|
|
8564
9463
|
elements.pathPickerAddFastPickButton.addEventListener("click", () => addCurrentFastPick().catch((error) => addEvent(error.message, "error")));
|
|
8565
9464
|
elements.pathPickerCancelButton.addEventListener("click", () => closePathPicker(null));
|
|
8566
9465
|
elements.pathPickerChooseButton.addEventListener("click", () => closePathPicker(pathPickerState?.cwd || null));
|
|
@@ -8585,6 +9484,7 @@ elements.composer.addEventListener("dragleave", handleComposerDragLeave);
|
|
|
8585
9484
|
elements.composer.addEventListener("drop", handleComposerDrop);
|
|
8586
9485
|
|
|
8587
9486
|
elements.promptInput.addEventListener("keydown", (event) => {
|
|
9487
|
+
if (event.defaultPrevented) return;
|
|
8588
9488
|
if (shouldSendPromptFromEnter(event)) {
|
|
8589
9489
|
event.preventDefault();
|
|
8590
9490
|
hideCommandSuggestions();
|
|
@@ -8613,9 +9513,18 @@ elements.promptInput.addEventListener("keydown", (event) => {
|
|
|
8613
9513
|
hideCommandSuggestions();
|
|
8614
9514
|
}
|
|
8615
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
|
+
}
|
|
8616
9524
|
});
|
|
8617
9525
|
|
|
8618
9526
|
elements.promptInput.addEventListener("input", () => {
|
|
9527
|
+
resetPromptHistoryNavigation();
|
|
8619
9528
|
resizePromptInput();
|
|
8620
9529
|
renderCommandSuggestions();
|
|
8621
9530
|
});
|
|
@@ -8624,6 +9533,7 @@ elements.promptInput.addEventListener("focus", () => {
|
|
|
8624
9533
|
setTimeout(updateVisualViewportVars, 0);
|
|
8625
9534
|
});
|
|
8626
9535
|
elements.promptInput.addEventListener("click", () => {
|
|
9536
|
+
resetPromptHistoryNavigation();
|
|
8627
9537
|
updateVisualViewportVars();
|
|
8628
9538
|
syncMobileChatToBottomForInput();
|
|
8629
9539
|
renderCommandSuggestions();
|
|
@@ -8644,6 +9554,7 @@ focusPromptInput({ defer: true });
|
|
|
8644
9554
|
updateComposerModeButtons();
|
|
8645
9555
|
updateOptionalFeatureAvailability();
|
|
8646
9556
|
loadLastUserPromptCache();
|
|
9557
|
+
loadPromptHistoryCache();
|
|
8647
9558
|
installViewportHandlers();
|
|
8648
9559
|
currentThemeName = storedThemeName();
|
|
8649
9560
|
renderBackgroundControl();
|
|
@@ -8654,9 +9565,11 @@ initializeThemes().catch((error) => {
|
|
|
8654
9565
|
initializeFastPicks().catch((error) => addEvent(`failed to initialize path fast picks: ${error.message}`, "error"));
|
|
8655
9566
|
restoreAgentDoneNotificationsSetting();
|
|
8656
9567
|
restoreThinkingVisibilitySetting();
|
|
9568
|
+
restoreToolOutputExpansionSetting();
|
|
8657
9569
|
restoreSidePanelSectionState();
|
|
8658
9570
|
bindSidePanelSectionToggles();
|
|
8659
9571
|
restoreSidePanelState();
|
|
9572
|
+
initializeCodexUsage();
|
|
8660
9573
|
bindMobileViewChanges();
|
|
8661
9574
|
registerPwaServiceWorker();
|
|
8662
9575
|
renderServerOfflinePanel();
|