@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/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 parsed = JSON.parse(localStorage.getItem(SIDE_PANEL_SECTION_STORAGE_KEY) || "[]");
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 new Set();
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
- for (const record of sidePanelSectionRecords()) {
459
- setSidePanelSectionCollapsed(record, collapsedIds.has(record.id), { persist: false });
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
- setSidePanelSectionCollapsed(record, !record.section.classList.contains("collapsed"));
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
- elements.queueBox.textContent = "No queued messages.";
2238
- elements.queueBox.classList.add("muted");
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 node = make("section", "widget todo-widget");
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(header, progress, list);
3790
- if (todo.footer) node.append(make("div", "todo-widget-footer", todo.footer));
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 (outputLines.length ? outputLines : ["Waiting for release output..."])) {
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 lines.slice(2).filter((line, index) => index > 0 || stripAnsi(line).trim())) {
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 (outputLines.length ? outputLines : ["Waiting for release-aur output..."])) {
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 lines.slice(2).filter((line, index) => index > 0 || stripAnsi(line).trim())) {
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
- if (currentState?.isStreaming) {
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
- if (isCurrentTabContext(tabContext) && isCurrentGitWorkflowRun(runId, tabId) && currentWorkflow?.step === "generating" && !currentState?.isStreaming) {
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
- if (gitWorkflow.active && gitWorkflow.step === "generating" && !currentState?.isStreaming) {
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: tabContext.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 steering = event?.steering || [];
4336
- const followUp = event?.followUp || [];
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
- if (!thinkingOutputVisible) continue;
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, part.thinking || "No thinking content was exposed by the provider.", "thinking-text");
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) || "No thinking content was exposed by the provider.";
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
- elements.chat.replaceChildren();
5130
- if (elements.stickyUserPromptButton) elements.chat.append(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
- bubble.classList.remove("tool-pending", "tool-running", "tool-success", "tool-error", "error");
5554
- bubble.classList.add(`tool-${status}`);
5555
- if (message.isError || status === "error") bubble.classList.add("error");
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
- elements.chat.append(bubble);
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 && (thinkingText || !streaming)) appendText(body, thinkingText || "No thinking content was exposed by the provider.", "thinking-text");
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
- elements.chat.append(bubble);
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?.parentElement !== elements.chat) {
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
- runIndicatorText.textContent = runIndicatorHeadline();
6515
+ const headline = runIndicatorHeadline();
6516
+ if (runIndicatorText.textContent !== headline) runIndicatorText.textContent = headline;
5953
6517
  const detail = runIndicatorDetail();
5954
- runIndicatorMeta.textContent = runIndicatorShowsElapsed() ? `${detail} · run time ${formatRunIndicatorElapsed()}` : detail;
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(placeholder = "Thinking…") {
7608
+ function showStreamingThinking(initialText = "") {
7041
7609
  if (!ensureStreamingThinkingBubble()) return;
7042
- if (!streamThinking.textContent) streamThinking.textContent = placeholder;
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
- if (!thinkingOutputVisible) return;
7659
+ const thinking = visibleThinkingText(text);
7660
+ if (!thinkingOutputVisible || !thinking) return false;
7092
7661
  showStreamingThinking("");
7093
- if (streamThinking) streamThinking.textContent = text;
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
- if (text || placeholder || streamThinkingBubble) setStreamingThinkingText(text || placeholder);
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, { placeholder: "Thinking…" });
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 || (!streamThinking?.textContent && delta))) {
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 (kind === "prompt" && !message.startsWith("/")) rememberLastUserPrompt(message, { tabId: targetTabId });
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 navigator.clipboard.writeText(response.data.copyText);
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
- refreshTabs().catch((error) => addEvent(error.message, "error"));
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();