@firstpick/pi-package-webui 0.5.4 → 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/app.js CHANGED
@@ -257,10 +257,20 @@ let tabSeenCompletionSerials = new Map();
257
257
  let streamBubble = null;
258
258
  let streamText = null;
259
259
  let streamRawText = "";
260
+ let streamThinkingRawText = "";
261
+ let streamDerivedTextCache = { rawText: null, assistantText: "", thinkingFormat: null, finalText: "" };
260
262
  let streamBubbleVisibleSince = 0;
261
263
  let streamBubbleHideTimer = null;
262
264
  let streamTextRenderTimer = null;
265
+ let streamTextRenderFrame = null;
263
266
  let streamToolCallSeen = false;
267
+ let streamToolCallBubble = null;
268
+ let streamToolCallText = null;
269
+ let streamToolCallRawArguments = "";
270
+ let streamToolCallName = "";
271
+ let streamToolCallId = "";
272
+ let streamToolCallContentIndex = null;
273
+ let streamToolCallComplete = false;
264
274
  let streamThinkingBubble = null;
265
275
  let streamThinking = null;
266
276
  let streamMessageActive = false;
@@ -268,6 +278,8 @@ let runIndicatorBubble = null;
268
278
  let runIndicatorText = null;
269
279
  let runIndicatorMeta = null;
270
280
  let runIndicatorTimer = null;
281
+ let runIndicatorRenderFrame = null;
282
+ let runIndicatorRenderScroll = false;
271
283
  let runIndicatorGraceCheckTimer = null;
272
284
  let runIndicatorLastStateCheckAt = 0;
273
285
  let runIndicatorLocallyActive = false;
@@ -277,6 +289,7 @@ let refreshMessagesTimer = null;
277
289
  let refreshStateTimer = null;
278
290
  let refreshFooterTimer = null;
279
291
  let refreshTabsTimer = null;
292
+ let tabsRenderFrame = null;
280
293
  let foregroundReconcileTimer = null;
281
294
  let eventSource = null;
282
295
  let activeDialog = null;
@@ -366,6 +379,7 @@ let terminalTabsLayout = "top";
366
379
  let webuiSettings = {};
367
380
  let busyPromptBehavior = "followUp";
368
381
  let composerModeRenderSignature = "";
382
+ let composerModeButtonsFrame = null;
369
383
  let autocompleteMaxVisible = 12;
370
384
  let doubleEscapeAction = "none";
371
385
  let treeFilterMode = "default";
@@ -389,6 +403,11 @@ const contextUsageUnknownAfterCompactionByTab = new Map();
389
403
  let autoFollowChat = true;
390
404
  let chatFollowFrame = null;
391
405
  let chatFollowSettleTimer = null;
406
+ let chatFollowNeedsSettle = false;
407
+ let liveWidgetRenderFrame = null;
408
+ let liveTodoProgressSyncFrame = null;
409
+ let liveTodoProgressPendingText = "";
410
+ let liveTodoProgressPendingTabId = null;
392
411
  let lastChatProgrammaticScrollAt = 0;
393
412
  let chatUserScrollIntentUntil = 0;
394
413
  let mobileFooterExpanded = false;
@@ -397,7 +416,12 @@ let footerThinkingPickerOpen = false;
397
416
  let footerAutoCompactionToggleInFlight = false;
398
417
  let footerBranchPickerOpen = false;
399
418
  let footerBranchPickerState = { loading: false, error: "", branches: [], current: "", root: "", switching: "", tabId: null };
419
+ let footerBranchCreateDraft = { type: "", name: "" };
400
420
  let footerBranchPickerRequestSerial = 0;
421
+ let footerScopedModelDragKey = "";
422
+ let footerScopedModelLastDragOverKey = "";
423
+ let footerScopedModelPointerDrag = null;
424
+ let footerScopedModelSuppressClickUntil = 0;
401
425
  let publishMenuOpen = false;
402
426
  let maxVisualViewportHeight = 0;
403
427
  let abortRequestInFlight = false;
@@ -437,6 +461,8 @@ const SKILL_USAGE_STORAGE_KEY = "pi-webui-skill-usage-v1";
437
461
  const TERMINAL_TABS_LAYOUT_STORAGE_KEY = "pi-webui-terminal-tabs-layout";
438
462
  const TERMINAL_CUSTOM_GROUPS_STORAGE_KEY = "pi-webui-terminal-custom-groups-v1";
439
463
  const TERMINAL_TAB_DRAG_MIME = "application/x-pi-terminal-tab-id";
464
+ const FOOTER_SCOPED_MODEL_ORDER_STORAGE_KEY = "pi-webui-footer-scoped-model-order-v1";
465
+ const FOOTER_SCOPED_MODEL_POINTER_DRAG_THRESHOLD_PX = 6;
440
466
  const TOOL_OUTPUT_EXPANDED_STORAGE_KEY = "pi-webui-tool-output-expanded";
441
467
  const THEME_STORAGE_KEY = "pi-webui-theme";
442
468
  const CUSTOM_BACKGROUND_STORAGE_KEY = "pi-webui-custom-background";
@@ -534,7 +560,10 @@ const mobileViewMedia = window.matchMedia?.(MOBILE_VIEW_QUERY) || null;
534
560
  const sidePanelOverlayMedia = window.matchMedia?.(SIDE_PANEL_OVERLAY_QUERY) || mobileViewMedia;
535
561
  const statusEntries = new Map();
536
562
  const widgets = new Map();
563
+ const widgetsByTab = new Map();
537
564
  const todoProgressWidgetExpandedByTab = new Map();
565
+ const todoProgressGoalByTab = new Map();
566
+ const todoProgressSignatureByTab = new Map();
538
567
  const releaseNpmOutputExpandedByTab = new Map();
539
568
  const appRunnerDataByTab = new Map();
540
569
  const appRunnerInputDraftByRun = new Map();
@@ -887,6 +916,7 @@ const GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP = [
887
916
  "5. Return here to commit short, long, or typed input on that branch.",
888
917
  "6. Push and Create PR will push upstream, run /pr, let you review, then run gh pr create.",
889
918
  ].join("\n");
919
+ const GIT_BRANCH_TYPE_SUGGESTIONS = ["feat", "fix", "change", "perf", "test", "chore", "refactor", "docs", "style", "build", "ci", "revert"];
890
920
  const GIT_FOOTER_STATUS_SETUP_TOOLTIP = [
891
921
  "git-footer-status-setup:",
892
922
  "Store the GitHub username used when the Web UI initializes a no-repo directory.",
@@ -918,6 +948,12 @@ const GIT_INIT_STACK_TOOLTIP = [
918
948
  "Choose a known stack or type one. The value is saved in this browser.",
919
949
  "If left blank, Pi will inspect the codebase and fall back to sane default .gitignore patterns.",
920
950
  ].join("\n");
951
+ const MERMAID_MODULE_URL = "/vendor/mermaid/mermaid.esm.min.mjs";
952
+ const MERMAID_LANGUAGES = new Set(["mermaid", "mmd"]);
953
+ const MERMAID_MAX_TEXT_SIZE = 100_000;
954
+ let mermaidModulePromise = null;
955
+ let mermaidThemeSignature = "";
956
+ let mermaidRenderSequence = 0;
921
957
 
922
958
  function make(tag, className, text) {
923
959
  const node = document.createElement(tag);
@@ -1868,6 +1904,17 @@ function trackSkillsFromMessages(messages = latestMessages, tabId = activeTabId)
1868
1904
  for (const message of messages || []) trackSkillsFromMessage(tabId, message);
1869
1905
  }
1870
1906
 
1907
+ function assistantMessageUpdateType(event) {
1908
+ return event?.assistantMessageEvent?.type || "";
1909
+ }
1910
+
1911
+ function eventMayAffectSkillUsage(event) {
1912
+ const type = event?.type || "";
1913
+ return ["tool_execution_start", "tool_execution_update", "tool_execution_end"].includes(type)
1914
+ || (type === "message_update" && assistantMessageUpdateType(event) === "toolcall_start")
1915
+ || (type === "response" && event.command === "new_session");
1916
+ }
1917
+
1871
1918
  function trackSkillsFromEvent(event) {
1872
1919
  const tabId = event?.tabId || activeTabId;
1873
1920
  if (!tabId || !event) return;
@@ -1875,11 +1922,9 @@ function trackSkillsFromEvent(event) {
1875
1922
  trackSkillsFromToolInvocation(tabId, event.toolName, event.args, { sourcePrefix: `event:${event.type}` });
1876
1923
  return;
1877
1924
  }
1878
- if (event.type === "message_update") {
1925
+ if (assistantMessageUpdateType(event) === "toolcall_start") {
1879
1926
  const update = event.assistantMessageEvent || {};
1880
- if (update.type === "toolcall_start") {
1881
- trackSkillsFromToolInvocation(tabId, update.name || update.toolName || update.toolCall?.name, update.arguments || update.args || update.toolCall?.arguments || {}, { sourcePrefix: "event:message_update" });
1882
- }
1927
+ trackSkillsFromToolInvocation(tabId, update.name || update.toolName || update.toolCall?.name, update.arguments || update.args || update.toolCall?.arguments || {}, { sourcePrefix: "event:message_update" });
1883
1928
  return;
1884
1929
  }
1885
1930
  if (event.type === "response" && event.command === "new_session") {
@@ -2164,6 +2209,16 @@ function updateComposerModeButtons() {
2164
2209
  document.body.classList.toggle("pi-run-active", runActive || abortAvailable);
2165
2210
  }
2166
2211
 
2212
+ function scheduleComposerModeButtonsUpdate() {
2213
+ if (composerModeButtonsFrame !== null) return;
2214
+ const flush = () => {
2215
+ composerModeButtonsFrame = null;
2216
+ updateComposerModeButtons();
2217
+ };
2218
+ if (typeof requestAnimationFrame === "function") composerModeButtonsFrame = requestAnimationFrame(flush);
2219
+ else composerModeButtonsFrame = setTimeout(flush, 0);
2220
+ }
2221
+
2167
2222
  function isFooterPickerOpen() {
2168
2223
  return footerModelPickerOpen || footerThinkingPickerOpen || footerBranchPickerOpen;
2169
2224
  }
@@ -2501,7 +2556,7 @@ function messageCopyText(message, body = null) {
2501
2556
  return sections.join("\n\n").trimEnd() || messageCopyFallbackText(body);
2502
2557
  }
2503
2558
  if (message.role === "thinking") return visibleThinkingText(message.thinking || textFromContent(message.content)).trimEnd() || messageCopyFallbackText(body);
2504
- if (message.role === "toolCall") return JSON.stringify(message.arguments ?? message.content ?? {}, null, 2);
2559
+ if (message.role === "toolCall") return toolCallDisplayText(message).trimEnd() || messageCopyFallbackText(body);
2505
2560
  if (message.role === "assistantEvent") {
2506
2561
  return (typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? {}, null, 2)).trimEnd();
2507
2562
  }
@@ -4448,6 +4503,7 @@ function syncTabMetadata(nextTabs = []) {
4448
4503
  actionFeedbackByTab.delete(tabId);
4449
4504
  skillUsageByTab.delete(tabId);
4450
4505
  tabMessagesCache.delete(tabId);
4506
+ widgetsByTab.delete(tabId);
4451
4507
  clearGitWorkflowForTab(tabId);
4452
4508
  }
4453
4509
  }
@@ -4528,7 +4584,7 @@ function markTabWorkingLocally(tabId = activeTabId) {
4528
4584
  const previous = activityForTab(tab);
4529
4585
  const next = normalizeTabActivity({ ...previous, status: "working", isWorking: true });
4530
4586
  tabActivities.set(tabId, next);
4531
- if (tabActivityStateChanged(previous, next)) renderTabs();
4587
+ if (tabActivityStateChanged(previous, next)) scheduleTabsRender();
4532
4588
  return true;
4533
4589
  }
4534
4590
 
@@ -4538,7 +4594,7 @@ function markTabIdleLocally(tabId = activeTabId) {
4538
4594
  const previous = activityForTab(tab);
4539
4595
  const next = normalizeTabActivity({ ...previous, status: "idle", isWorking: false });
4540
4596
  tabActivities.set(tabId, next);
4541
- if (tabActivityStateChanged(previous, next)) renderTabs();
4597
+ if (tabActivityStateChanged(previous, next)) scheduleTabsRender();
4542
4598
  return true;
4543
4599
  }
4544
4600
 
@@ -4554,7 +4610,7 @@ function markTabDoneLocally(tabId = activeTabId) {
4554
4610
  lastCompletedAt: new Date().toISOString(),
4555
4611
  });
4556
4612
  tabActivities.set(tabId, next);
4557
- if (tabActivityStateChanged(previous, next)) renderTabs();
4613
+ if (tabActivityStateChanged(previous, next)) scheduleTabsRender();
4558
4614
  return true;
4559
4615
  }
4560
4616
 
@@ -4594,7 +4650,7 @@ function markTabOutputSeen(tabId = activeTabId, { force = false } = {}) {
4594
4650
  const previousSerial = tabSeenCompletionSerials.get(tabId) ?? 0;
4595
4651
  if (previousSerial >= completionSerial) return false;
4596
4652
  tabSeenCompletionSerials.set(tabId, completionSerial);
4597
- renderTabs();
4653
+ scheduleTabsRender();
4598
4654
  return true;
4599
4655
  }
4600
4656
 
@@ -4614,7 +4670,14 @@ function ingestEventTabActivity(event) {
4614
4670
  const next = setTabActivity(event.tabId, event.tabActivity);
4615
4671
  changed = tabActivityStateChanged(previous, next) || changed;
4616
4672
  }
4617
- if (changed) renderTabs();
4673
+ if (changed) scheduleTabsRender();
4674
+ }
4675
+
4676
+ function eventHasTabActivityPayload(event) {
4677
+ return !!(
4678
+ event?.tabId &&
4679
+ (event.tabTitle || event.tabActivity || Object.prototype.hasOwnProperty.call(event, "pendingExtensionUiRequestCount"))
4680
+ );
4618
4681
  }
4619
4682
 
4620
4683
  function trackAutoRetryStateFromEvent(event) {
@@ -4710,6 +4773,96 @@ function cancelPendingDialogs() {
4710
4773
  if (elements.dialog.open) elements.dialog.close();
4711
4774
  }
4712
4775
 
4776
+ function widgetCacheForTab(tabId = activeTabId, { create = true } = {}) {
4777
+ if (!tabId) return null;
4778
+ let cache = widgetsByTab.get(tabId);
4779
+ if (!cache && create) {
4780
+ cache = new Map();
4781
+ widgetsByTab.set(tabId, cache);
4782
+ }
4783
+ return cache || null;
4784
+ }
4785
+
4786
+ function cacheWidgetsForTab(tabId = activeTabId) {
4787
+ if (!tabId) return;
4788
+ if (widgets.size === 0) {
4789
+ widgetsByTab.delete(tabId);
4790
+ return;
4791
+ }
4792
+ widgetsByTab.set(tabId, new Map(widgets));
4793
+ }
4794
+
4795
+ function restoreWidgetsForActiveTab() {
4796
+ widgets.clear();
4797
+ const cache = widgetCacheForTab(activeTabId, { create: false });
4798
+ if (!cache) return;
4799
+ for (const [key, value] of cache) widgets.set(key, value);
4800
+ }
4801
+
4802
+ function todoProgressSignatureFromLines(lines = []) {
4803
+ const parsed = parseTodoProgressWidget(lines);
4804
+ if (!parsed) return JSON.stringify(lines.map(stripAnsi));
4805
+ return JSON.stringify({ goal: parsed.goal, done: parsed.done, total: parsed.total, partial: parsed.partial, items: parsed.items, footer: parsed.footer });
4806
+ }
4807
+
4808
+ function rememberTodoProgressGoalForTab(tabId, widgetKey, request) {
4809
+ if (widgetKey !== "todo-progress" || !tabId) return;
4810
+ if (!Array.isArray(request?.widgetLines)) {
4811
+ todoProgressGoalByTab.delete(tabId);
4812
+ todoProgressSignatureByTab.delete(tabId);
4813
+ return;
4814
+ }
4815
+ const parsed = parseTodoProgressWidget(request.widgetLines);
4816
+ if (parsed?.goal) todoProgressGoalByTab.set(tabId, parsed.goal);
4817
+ todoProgressSignatureByTab.set(tabId, todoProgressSignatureFromLines(request.widgetLines));
4818
+ }
4819
+
4820
+ function widgetRequestEquivalent(a, b) {
4821
+ const aHasLines = Array.isArray(a?.widgetLines);
4822
+ const bHasLines = Array.isArray(b?.widgetLines);
4823
+ if (aHasLines !== bHasLines) return false;
4824
+ if (!aHasLines) return true;
4825
+ if (a.widgetLines.length !== b.widgetLines.length) return false;
4826
+ return a.widgetLines.every((line, index) => String(line) === String(b.widgetLines[index]));
4827
+ }
4828
+
4829
+ function setWidgetForTab(tabId, widgetKey, request) {
4830
+ if (!widgetKey) return false;
4831
+ const targetTabId = tabId || activeTabId;
4832
+ const cache = widgetCacheForTab(targetTabId);
4833
+ const hasLines = Array.isArray(request?.widgetLines);
4834
+ const current = cache?.get(widgetKey) || (targetTabId === activeTabId ? widgets.get(widgetKey) : undefined);
4835
+ if (widgetRequestEquivalent(current, request)) return false;
4836
+
4837
+ rememberTodoProgressGoalForTab(targetTabId, widgetKey, request);
4838
+
4839
+ if (cache) {
4840
+ if (hasLines) cache.set(widgetKey, request);
4841
+ else cache.delete(widgetKey);
4842
+ if (cache.size === 0) widgetsByTab.delete(targetTabId);
4843
+ }
4844
+
4845
+ if (targetTabId === activeTabId) {
4846
+ if (hasLines) widgets.set(widgetKey, request);
4847
+ else widgets.delete(widgetKey);
4848
+ }
4849
+
4850
+ return true;
4851
+ }
4852
+
4853
+ function clearWidgetsForTab(tabId = activeTabId) {
4854
+ if (tabId) {
4855
+ widgetsByTab.delete(tabId);
4856
+ todoProgressGoalByTab.delete(tabId);
4857
+ todoProgressSignatureByTab.delete(tabId);
4858
+ }
4859
+ if (!tabId || tabId === activeTabId) widgets.clear();
4860
+ if (!tabId) {
4861
+ todoProgressGoalByTab.clear();
4862
+ todoProgressSignatureByTab.clear();
4863
+ }
4864
+ }
4865
+
4713
4866
  function resetActiveTabUi() {
4714
4867
  clearRefreshTimers();
4715
4868
  clearLiveToolRenderQueue();
@@ -4727,7 +4880,7 @@ function resetActiveTabUi() {
4727
4880
  latestMessagesSessionKey = "";
4728
4881
  clearRunIndicatorActivity({ render: false });
4729
4882
  statusEntries.clear();
4730
- widgets.clear();
4883
+ restoreWidgetsForActiveTab();
4731
4884
  transientMessages = [];
4732
4885
  liveToolRuns.clear();
4733
4886
  liveToolCards.clear();
@@ -5050,6 +5203,16 @@ function moveNewTabMenuFocus(delta) {
5050
5203
  items[nextIndex].focus({ preventScroll: true });
5051
5204
  }
5052
5205
 
5206
+ function scheduleTabsRender() {
5207
+ if (tabsRenderFrame !== null) return;
5208
+ const flush = () => {
5209
+ tabsRenderFrame = null;
5210
+ renderTabs();
5211
+ };
5212
+ if (typeof requestAnimationFrame === "function") tabsRenderFrame = requestAnimationFrame(flush);
5213
+ else tabsRenderFrame = setTimeout(flush, 0);
5214
+ }
5215
+
5053
5216
  function renderTabs() {
5054
5217
  if (deferUiRenderDuringPointerActivation("tabs", renderTabs)) return;
5055
5218
  const active = activeTab();
@@ -5108,6 +5271,7 @@ async function switchTab(tabId) {
5108
5271
  footerBranchPickerRequestSerial += 1;
5109
5272
  saveActiveDraft();
5110
5273
  cacheMessagesForTab(activeTabId);
5274
+ cacheWidgetsForTab(activeTabId);
5111
5275
  const tabContext = setActiveTabId(tabId, { remember: true });
5112
5276
  resetActiveTabUi();
5113
5277
  renderTabs();
@@ -7691,15 +7855,71 @@ function confirmFooterGitBranchAction(branch, { create = false, requireConfirm =
7691
7855
  return window.confirm(message);
7692
7856
  }
7693
7857
 
7694
- function promptFooterGitBranchName() {
7695
- const value = window.prompt("New git branch name:", "");
7696
- if (value === null) return "";
7697
- return cleanStatusText(value);
7858
+ function footerBranchCreateType(value = footerBranchCreateDraft.type) {
7859
+ return slugifyGitBranchPart(value);
7698
7860
  }
7699
7861
 
7700
- async function createFooterGitBranch() {
7701
- const branchName = promptFooterGitBranchName();
7702
- if (!branchName) return;
7862
+ function slugifyGitBranchName(value) {
7863
+ return cleanStatusText(value)
7864
+ .split("/")
7865
+ .map((part) => slugifyGitBranchPart(part))
7866
+ .filter(Boolean)
7867
+ .join("/");
7868
+ }
7869
+
7870
+ function footerBranchCreateName() {
7871
+ const type = footerBranchCreateType();
7872
+ const rawName = cleanStatusText(footerBranchCreateDraft.name);
7873
+ if (!type || !rawName) return "";
7874
+ return slugifyGitBranchName(`${type}/${rawName}`);
7875
+ }
7876
+
7877
+ function footerBranchCreatePreviewName() {
7878
+ const type = footerBranchCreateType() || "<type>";
7879
+ const rawName = cleanStatusText(footerBranchCreateDraft.name);
7880
+ const name = rawName ? slugifyGitBranchName(rawName) : "<branch-name>";
7881
+ return `${type}/${name}`;
7882
+ }
7883
+
7884
+ function updateFooterBranchCreateDraft(patch = {}) {
7885
+ const has = (key) => Object.prototype.hasOwnProperty.call(patch, key);
7886
+ footerBranchCreateDraft = {
7887
+ type: has("type") ? footerBranchCreateType(patch.type) : footerBranchCreateType(),
7888
+ name: has("name") ? String(patch.name || "") : footerBranchCreateDraft.name,
7889
+ };
7890
+ }
7891
+
7892
+ function quoteGitBranchForDisplay(branch) {
7893
+ return `'${String(branch || "").replace(/'/g, `'\\''`)}'`;
7894
+ }
7895
+
7896
+ function gitSwitchCreateCommandDisplay(branch) {
7897
+ return `git switch -c ${quoteGitBranchForDisplay(branch)}`;
7898
+ }
7899
+
7900
+ function footerBranchCreateTooltip(branchName = footerBranchCreateName()) {
7901
+ const command = gitSwitchCreateCommandDisplay(branchName || footerBranchCreatePreviewName());
7902
+ return [
7903
+ "Create new branch",
7904
+ "A branch is a safe workspace for your changes.",
7905
+ "",
7906
+ `This will run: ${command}`,
7907
+ "",
7908
+ "What happens:",
7909
+ "• creates the branch from the current code",
7910
+ "• switches this tab to that branch",
7911
+ "• does not commit, push, or delete anything",
7912
+ "",
7913
+ "Tip: use short lowercase words, e.g. fix/login-button.",
7914
+ ].join("\n");
7915
+ }
7916
+
7917
+ async function createFooterGitBranch(branch = footerBranchCreateName()) {
7918
+ const branchName = cleanStatusText(branch);
7919
+ if (!branchName) {
7920
+ addEvent("Enter a branch name before creating a new git branch.", "warn");
7921
+ return;
7922
+ }
7703
7923
  const tabContext = activeTabContext();
7704
7924
  if (!confirmFooterGitBranchAction(branchName, { create: true, requireConfirm: true, tabContext })) return;
7705
7925
  await applyFooterGitBranch(branchName, { create: true, tabContext, skipConfirm: true });
@@ -7720,6 +7940,7 @@ async function applyFooterGitBranch(branch, { create = false, tabContext = activ
7720
7940
  footerBranchPickerOpen = false;
7721
7941
  footerBranchPickerRequestSerial += 1;
7722
7942
  footerBranchPickerState = { ...footerBranchPickerState, loading: false, switching: "", current: switchedBranch };
7943
+ if (create) updateFooterBranchCreateDraft({ name: "" });
7723
7944
  applyOptimisticGitFooterBranch(switchedBranch, tabContext);
7724
7945
  addEvent(response.data?.created ? `Created and switched to git branch ${switchedBranch}.` : response.data?.switched === false ? `Already on git branch ${switchedBranch}.` : `Switched git branch to ${switchedBranch}.`, "info");
7725
7946
  requestGitFooterWebuiPayload(tabContext, { force: true });
@@ -7737,6 +7958,124 @@ async function applyFooterGitBranch(branch, { create = false, tabContext = activ
7737
7958
  }
7738
7959
  }
7739
7960
 
7961
+ function renderFooterBranchCreateForm(state = footerBranchPickerState) {
7962
+ const form = make("form", "footer-branch-create-form");
7963
+ form.setAttribute("aria-label", "Create new git branch");
7964
+
7965
+ const header = make("div", "footer-branch-create-header");
7966
+ header.append(make("strong", "footer-branch-create-title", "Create new branch"));
7967
+
7968
+ const fields = make("div", "footer-branch-create-fields");
7969
+ const typeField = make("div", "footer-branch-create-type-field");
7970
+ const typeInput = make("input", "footer-branch-create-dropdown-inputfield");
7971
+ typeInput.type = "text";
7972
+ typeInput.value = footerBranchCreateType();
7973
+ typeInput.placeholder = "type";
7974
+ typeInput.setAttribute("aria-label", "Branch type suggestion or custom prefix");
7975
+ typeInput.setAttribute("aria-haspopup", "listbox");
7976
+ typeInput.autocomplete = "off";
7977
+ typeInput.autocapitalize = "none";
7978
+ typeInput.spellcheck = false;
7979
+
7980
+ const typeSuggestions = make("div", "footer-branch-type-suggestions");
7981
+ typeSuggestions.setAttribute("role", "listbox");
7982
+ typeSuggestions.hidden = true;
7983
+ const renderTypeSuggestions = () => {
7984
+ const filter = slugifyGitBranchPart(typeInput.value);
7985
+ const matches = GIT_BRANCH_TYPE_SUGGESTIONS.filter((type) => !filter || type.includes(filter));
7986
+ const shown = matches.length ? matches : GIT_BRANCH_TYPE_SUGGESTIONS;
7987
+ typeSuggestions.replaceChildren(...shown.map((type) => {
7988
+ const button = make("button", `footer-branch-type-suggestion${footerBranchCreateType(typeInput.value) === type ? " active" : ""}`, type);
7989
+ button.type = "button";
7990
+ button.setAttribute("role", "option");
7991
+ button.setAttribute("aria-selected", footerBranchCreateType(typeInput.value) === type ? "true" : "false");
7992
+ button.addEventListener("pointerdown", (event) => event.preventDefault());
7993
+ button.addEventListener("click", () => {
7994
+ typeInput.value = type;
7995
+ updateFooterBranchCreateDraft({ type });
7996
+ updatePreview();
7997
+ typeSuggestions.hidden = true;
7998
+ nameInput.focus();
7999
+ });
8000
+ return button;
8001
+ }));
8002
+ };
8003
+
8004
+ const slash = make("span", "footer-branch-create-slash", "/");
8005
+
8006
+ const nameInput = make("input", "footer-branch-create-input-field");
8007
+ nameInput.type = "text";
8008
+ nameInput.value = footerBranchCreateDraft.name;
8009
+ nameInput.placeholder = "short-feature-name";
8010
+ nameInput.autocomplete = "off";
8011
+ nameInput.autocapitalize = "none";
8012
+ nameInput.spellcheck = false;
8013
+ nameInput.setAttribute("aria-label", "New branch name");
8014
+
8015
+ const submitButton = make("button", "footer-branch-create-submit", state.switching ? "Creating…" : "Create new branch");
8016
+ submitButton.type = "submit";
8017
+
8018
+ const preview = make("div", "footer-branch-create-preview");
8019
+ const updatePreview = () => {
8020
+ const branchName = footerBranchCreateName();
8021
+ preview.textContent = gitSwitchCreateCommandDisplay(branchName || footerBranchCreatePreviewName());
8022
+ const submitDisabled = Boolean(state.switching) || !branchName;
8023
+ submitButton.disabled = false;
8024
+ submitButton.classList.toggle("footer-branch-create-submit-disabled", submitDisabled);
8025
+ submitButton.setAttribute("aria-disabled", submitDisabled ? "true" : "false");
8026
+ submitButton.dataset.tooltip = footerBranchCreateTooltip(branchName);
8027
+ submitButton.setAttribute("aria-label", branchName ? `Create and switch to ${branchName}` : "Create new branch: enter both a branch type and name first");
8028
+ submitButton.removeAttribute("title");
8029
+ };
8030
+
8031
+ typeInput.addEventListener("focus", () => {
8032
+ renderTypeSuggestions();
8033
+ typeSuggestions.hidden = false;
8034
+ });
8035
+ typeInput.addEventListener("blur", () => {
8036
+ setTimeout(() => { typeSuggestions.hidden = true; }, 120);
8037
+ });
8038
+ typeInput.addEventListener("input", () => {
8039
+ updateFooterBranchCreateDraft({ type: typeInput.value });
8040
+ updatePreview();
8041
+ renderTypeSuggestions();
8042
+ typeSuggestions.hidden = false;
8043
+ });
8044
+ typeInput.addEventListener("keydown", (event) => {
8045
+ if (event.key === "Escape") {
8046
+ typeSuggestions.hidden = true;
8047
+ return;
8048
+ }
8049
+ if (event.key !== "/" && event.key !== "Enter") return;
8050
+ event.preventDefault();
8051
+ typeSuggestions.hidden = true;
8052
+ nameInput.focus();
8053
+ nameInput.select();
8054
+ });
8055
+ nameInput.addEventListener("input", () => {
8056
+ updateFooterBranchCreateDraft({ name: nameInput.value });
8057
+ updatePreview();
8058
+ });
8059
+ nameInput.addEventListener("keydown", (event) => {
8060
+ if (event.key !== "Enter") return;
8061
+ event.preventDefault();
8062
+ form.requestSubmit();
8063
+ });
8064
+ form.addEventListener("submit", (event) => {
8065
+ event.preventDefault();
8066
+ if (state.switching) return;
8067
+ const branchName = footerBranchCreateName();
8068
+ createFooterGitBranch(branchName).catch((error) => addEvent(error.message || String(error), "error"));
8069
+ });
8070
+
8071
+ updatePreview();
8072
+ renderTypeSuggestions();
8073
+ typeField.append(typeInput, typeSuggestions);
8074
+ fields.append(typeField, slash, nameInput, submitButton);
8075
+ form.append(header, fields, preview);
8076
+ return form;
8077
+ }
8078
+
7740
8079
  function renderFooterBranchPicker() {
7741
8080
  const picker = make("div", "footer-model-picker footer-branch-picker");
7742
8081
  picker.setAttribute("role", "listbox");
@@ -7753,7 +8092,7 @@ function renderFooterBranchPicker() {
7753
8092
  return picker;
7754
8093
  }
7755
8094
  if (state.loading && state.branches.length === 0) {
7756
- picker.append(make("div", "footer-model-picker-empty muted", "Loading local branches…"));
8095
+ picker.append(make("div", "footer-model-picker-empty muted", "Loading existing local branches… New branch creation is available."), renderFooterBranchCreateForm(state));
7757
8096
  return picker;
7758
8097
  }
7759
8098
 
@@ -7761,16 +8100,11 @@ function renderFooterBranchPicker() {
7761
8100
  if (!state.loading && !hasOtherBranches) {
7762
8101
  const empty = make("div", "footer-model-picker-empty muted");
7763
8102
  empty.append(make("strong", undefined, "No other local branches available."), make("span", undefined, " Create a branch from the current HEAD to continue."));
7764
- const createButton = make("button", "footer-model-option footer-branch-create-option");
7765
- createButton.type = "button";
7766
- createButton.append(
7767
- make("span", "footer-model-option-main", "Create new branch"),
7768
- make("span", "footer-model-option-name", "prompts for a name, confirms, then runs git switch -c"),
7769
- );
7770
- createButton.addEventListener("click", () => createFooterGitBranch().catch((error) => addEvent(error.message || String(error), "error")));
7771
- picker.append(empty, createButton);
8103
+ picker.append(empty);
7772
8104
  }
7773
8105
 
8106
+ picker.append(renderFooterBranchCreateForm(state));
8107
+
7774
8108
  for (const branch of state.branches) {
7775
8109
  const selected = branch.current || (!!state.current && branch.name === state.current);
7776
8110
  const disabled = selected || state.loading || !!state.switching;
@@ -7790,6 +8124,162 @@ function renderFooterBranchPicker() {
7790
8124
  return picker;
7791
8125
  }
7792
8126
 
8127
+ function footerScopedModelKey(model) {
8128
+ return model?.provider && model?.id ? `${model.provider}/${model.id}` : "";
8129
+ }
8130
+
8131
+ function readFooterScopedModelOrder() {
8132
+ try {
8133
+ const parsed = JSON.parse(localStorage.getItem(FOOTER_SCOPED_MODEL_ORDER_STORAGE_KEY) || "[]");
8134
+ return Array.isArray(parsed) ? parsed.filter((key) => typeof key === "string" && key.trim()) : [];
8135
+ } catch {
8136
+ return [];
8137
+ }
8138
+ }
8139
+
8140
+ function writeFooterScopedModelOrder(order) {
8141
+ try {
8142
+ localStorage.setItem(FOOTER_SCOPED_MODEL_ORDER_STORAGE_KEY, JSON.stringify([...new Set(order.filter(Boolean))]));
8143
+ } catch {}
8144
+ }
8145
+
8146
+ function orderedFooterScopedModels() {
8147
+ const order = readFooterScopedModelOrder();
8148
+ if (!order.length) return footerScopedModels;
8149
+ const rank = new Map(order.map((key, index) => [key, index]));
8150
+ return [...footerScopedModels].sort((a, b) => {
8151
+ const aRank = rank.has(footerScopedModelKey(a)) ? rank.get(footerScopedModelKey(a)) : Number.MAX_SAFE_INTEGER;
8152
+ const bRank = rank.has(footerScopedModelKey(b)) ? rank.get(footerScopedModelKey(b)) : Number.MAX_SAFE_INTEGER;
8153
+ return aRank - bRank;
8154
+ });
8155
+ }
8156
+
8157
+ function commitFooterScopedModelOrder(order, { render = true, focusKey = "" } = {}) {
8158
+ writeFooterScopedModelOrder(order);
8159
+ footerScopedModels = orderedFooterScopedModels();
8160
+ if (render) renderFooter();
8161
+ if (focusKey) {
8162
+ const movedButton = document.querySelector(`[data-footer-model-key="${CSS.escape(focusKey)}"]`);
8163
+ if (movedButton) movedButton.focus();
8164
+ }
8165
+ }
8166
+
8167
+ function reorderFooterScopedModel(fromKey, toKey, { focus = true } = {}) {
8168
+ if (!fromKey || !toKey || fromKey === toKey) return false;
8169
+ const models = orderedFooterScopedModels();
8170
+ const fromIndex = models.findIndex((model) => footerScopedModelKey(model) === fromKey);
8171
+ const toIndex = models.findIndex((model) => footerScopedModelKey(model) === toKey);
8172
+ if (fromIndex < 0 || toIndex < 0) return false;
8173
+ const [moved] = models.splice(fromIndex, 1);
8174
+ models.splice(toIndex, 0, moved);
8175
+ commitFooterScopedModelOrder(models.map(footerScopedModelKey), { focusKey: focus ? fromKey : "" });
8176
+ return true;
8177
+ }
8178
+
8179
+ function moveFooterScopedModelByOffset(modelKey, offset) {
8180
+ const models = orderedFooterScopedModels();
8181
+ const fromIndex = models.findIndex((model) => footerScopedModelKey(model) === modelKey);
8182
+ const toIndex = fromIndex + offset;
8183
+ if (fromIndex < 0 || toIndex < 0 || toIndex >= models.length) return false;
8184
+ const [moved] = models.splice(fromIndex, 1);
8185
+ models.splice(toIndex, 0, moved);
8186
+ commitFooterScopedModelOrder(models.map(footerScopedModelKey), { focusKey: modelKey });
8187
+ return true;
8188
+ }
8189
+
8190
+ function footerScopedModelButtons() {
8191
+ return [...document.querySelectorAll(".footer-model-picker .footer-model-option[data-footer-model-key]")];
8192
+ }
8193
+
8194
+ function footerScopedModelButtonFromPoint(clientX, clientY) {
8195
+ return document.elementFromPoint(clientX, clientY)?.closest?.(".footer-model-option[data-footer-model-key]") || null;
8196
+ }
8197
+
8198
+ function clearFooterScopedModelDragMarkers() {
8199
+ for (const button of footerScopedModelButtons()) {
8200
+ button.classList.remove("drag-over", "drag-over-before", "drag-over-after");
8201
+ }
8202
+ }
8203
+
8204
+ function commitVisibleFooterScopedModelOrder({ render = false, focusKey = "" } = {}) {
8205
+ const order = footerScopedModelButtons().map((button) => button.dataset.footerModelKey).filter(Boolean);
8206
+ if (!order.length) return;
8207
+ commitFooterScopedModelOrder(order, { render, focusKey });
8208
+ }
8209
+
8210
+ function moveVisibleFooterScopedModel(fromKey, targetButton, clientY) {
8211
+ if (!fromKey || !targetButton) return false;
8212
+ const sourceButton = document.querySelector(`[data-footer-model-key="${CSS.escape(fromKey)}"]`);
8213
+ if (!sourceButton || sourceButton === targetButton) return false;
8214
+ const parent = sourceButton.parentElement;
8215
+ if (!parent || targetButton.parentElement !== parent) return false;
8216
+ const targetKey = targetButton.dataset.footerModelKey || "";
8217
+ const rect = targetButton.getBoundingClientRect();
8218
+ const insertBefore = clientY < rect.top + rect.height / 2;
8219
+
8220
+ clearFooterScopedModelDragMarkers();
8221
+ targetButton.classList.add("drag-over", insertBefore ? "drag-over-before" : "drag-over-after");
8222
+ footerScopedModelLastDragOverKey = `${targetKey}:${insertBefore ? "before" : "after"}`;
8223
+
8224
+ if (insertBefore) parent.insertBefore(sourceButton, targetButton);
8225
+ else parent.insertBefore(sourceButton, targetButton.nextSibling);
8226
+ commitVisibleFooterScopedModelOrder({ render: false });
8227
+ return true;
8228
+ }
8229
+
8230
+ function beginFooterScopedModelPointerDrag(event, modelKey) {
8231
+ if (event.button !== 0 || !modelKey) return;
8232
+ footerScopedModelPointerDrag = { modelKey, pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, active: false };
8233
+ window.addEventListener("pointermove", updateFooterScopedModelPointerDrag, { capture: true });
8234
+ window.addEventListener("pointerup", endFooterScopedModelPointerDrag, { capture: true });
8235
+ window.addEventListener("pointercancel", endFooterScopedModelPointerDrag, { capture: true });
8236
+ }
8237
+
8238
+ function updateFooterScopedModelPointerDrag(event) {
8239
+ const drag = footerScopedModelPointerDrag;
8240
+ if (!drag || drag.pointerId !== event.pointerId) return;
8241
+ const distance = Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY);
8242
+ if (!drag.active && distance < FOOTER_SCOPED_MODEL_POINTER_DRAG_THRESHOLD_PX) return;
8243
+ event.preventDefault();
8244
+ if (!drag.active) {
8245
+ drag.active = true;
8246
+ footerScopedModelDragKey = drag.modelKey;
8247
+ footerScopedModelLastDragOverKey = "";
8248
+ clearTimeout(pointerActivationTimeout);
8249
+ pointerActivationTimeout = null;
8250
+ activePointerActivation = null;
8251
+ deferredUiRenderCallbacks.delete("footer");
8252
+ const sourceButton = document.querySelector(`[data-footer-model-key="${CSS.escape(drag.modelKey)}"]`);
8253
+ if (sourceButton) sourceButton.classList.add("dragging");
8254
+ }
8255
+ const targetButton = footerScopedModelButtonFromPoint(event.clientX, event.clientY);
8256
+ if (!targetButton || targetButton.dataset.footerModelKey === drag.modelKey) return;
8257
+ const rect = targetButton.getBoundingClientRect();
8258
+ const markerKey = `${targetButton.dataset.footerModelKey}:${event.clientY < rect.top + rect.height / 2 ? "before" : "after"}`;
8259
+ if (markerKey === footerScopedModelLastDragOverKey) return;
8260
+ moveVisibleFooterScopedModel(drag.modelKey, targetButton, event.clientY);
8261
+ }
8262
+
8263
+ function endFooterScopedModelPointerDrag(event) {
8264
+ const drag = footerScopedModelPointerDrag;
8265
+ if (!drag || drag.pointerId !== event.pointerId) return;
8266
+ window.removeEventListener("pointermove", updateFooterScopedModelPointerDrag, { capture: true });
8267
+ window.removeEventListener("pointerup", endFooterScopedModelPointerDrag, { capture: true });
8268
+ window.removeEventListener("pointercancel", endFooterScopedModelPointerDrag, { capture: true });
8269
+ const wasActive = drag.active;
8270
+ const sourceButton = document.querySelector(`[data-footer-model-key="${CSS.escape(drag.modelKey)}"]`);
8271
+ footerScopedModelPointerDrag = null;
8272
+ footerScopedModelDragKey = "";
8273
+ footerScopedModelLastDragOverKey = "";
8274
+ clearFooterScopedModelDragMarkers();
8275
+ if (sourceButton) sourceButton.classList.remove("dragging");
8276
+ if (wasActive) {
8277
+ footerScopedModelSuppressClickUntil = Date.now() + 250;
8278
+ event.preventDefault();
8279
+ commitVisibleFooterScopedModelOrder({ render: false, focusKey: drag.modelKey });
8280
+ }
8281
+ }
8282
+
7793
8283
  async function applyFooterModel(model) {
7794
8284
  if (!model?.provider || !model?.id) return;
7795
8285
  const tabContext = activeTabContext();
@@ -7815,7 +8305,7 @@ function renderFooterModelPicker() {
7815
8305
  picker.setAttribute("role", "listbox");
7816
8306
  picker.setAttribute("aria-label", "Scoped models");
7817
8307
  picker.append(make("div", "footer-model-picker-title", "Scoped models"));
7818
- picker.append(make("div", "footer-model-picker-source", footerScopedModelSource === "none" ? "No saved scope" : `Source: ${footerScopedModelSource}${footerScopedModelPatterns.length ? ` · ${footerScopedModelPatterns.join(", ")}` : ""}`));
8308
+ picker.append(make("div", "footer-model-picker-source", "Drag models to reorder · Alt+↑/↓ moves focused model"));
7819
8309
  if (footerScopedModels.length === 0) {
7820
8310
  const empty = make("div", "footer-model-picker-empty muted");
7821
8311
  empty.append(
@@ -7826,10 +8316,16 @@ function renderFooterModelPicker() {
7826
8316
  return picker;
7827
8317
  }
7828
8318
  const current = currentState?.model;
8319
+ footerScopedModels = orderedFooterScopedModels();
7829
8320
  for (const model of footerScopedModels) {
7830
8321
  const selected = current?.provider === model.provider && current?.id === model.id;
7831
- const button = make("button", `footer-model-option${selected ? " active" : ""}`);
8322
+ const modelKey = footerScopedModelKey(model);
8323
+ const dragging = footerScopedModelDragKey === modelKey;
8324
+ const dragOver = footerScopedModelDragKey && footerScopedModelLastDragOverKey === modelKey;
8325
+ const button = make("button", `footer-model-option${selected ? " active" : ""}${dragging ? " dragging" : ""}${dragOver ? " drag-over" : ""}`);
7832
8326
  button.type = "button";
8327
+ button.draggable = false;
8328
+ button.dataset.footerModelKey = modelKey;
7833
8329
  button.setAttribute("role", "option");
7834
8330
  button.setAttribute("aria-selected", selected ? "true" : "false");
7835
8331
  button.title = `${model.provider}/${model.id}${model.name ? ` · ${model.name}` : ""}`;
@@ -7837,7 +8333,19 @@ function renderFooterModelPicker() {
7837
8333
  make("span", "footer-model-option-main", `${model.provider}/${model.id}`),
7838
8334
  make("span", "footer-model-option-name", model.name || ""),
7839
8335
  );
7840
- button.addEventListener("click", () => applyFooterModel(model));
8336
+ button.addEventListener("click", (event) => {
8337
+ if (Date.now() < footerScopedModelSuppressClickUntil) {
8338
+ event.preventDefault();
8339
+ return;
8340
+ }
8341
+ applyFooterModel(model);
8342
+ });
8343
+ button.addEventListener("keydown", (event) => {
8344
+ if (!event.altKey || (event.key !== "ArrowUp" && event.key !== "ArrowDown")) return;
8345
+ event.preventDefault();
8346
+ moveFooterScopedModelByOffset(modelKey, event.key === "ArrowUp" ? -1 : 1);
8347
+ });
8348
+ button.addEventListener("pointerdown", (event) => beginFooterScopedModelPointerDrag(event, modelKey));
7841
8349
  picker.append(button);
7842
8350
  }
7843
8351
  return picker;
@@ -8630,13 +9138,27 @@ function renderReleaseDialogMessage(parent, text) {
8630
9138
  }
8631
9139
  }
8632
9140
 
9141
+ function textLines(raw) {
9142
+ const value = String(raw || "");
9143
+ const lines = [];
9144
+ let start = 0;
9145
+ for (let index = 0; index < value.length; index += 1) {
9146
+ if (value[index] !== "\n") continue;
9147
+ const end = index > start && value[index - 1] === "\r" ? index - 1 : index;
9148
+ lines.push(value.slice(start, end));
9149
+ start = index + 1;
9150
+ }
9151
+ if (start <= value.length) lines.push(value.slice(start));
9152
+ return lines;
9153
+ }
9154
+
8633
9155
  function stripTodoProgressLines(text, { streaming = false } = {}) {
8634
9156
  if (!isOptionalFeatureEnabled("todoProgressWidget")) return String(text || "");
8635
9157
  let inFence = false;
8636
9158
  const kept = [];
8637
9159
  const raw = String(text || "");
8638
9160
  const hasTrailingNewline = /\r?\n$/.test(raw);
8639
- const lines = raw.split(/\r?\n/);
9161
+ const lines = textLines(raw);
8640
9162
 
8641
9163
  lines.forEach((line, index) => {
8642
9164
  const isUnfinishedTail = streaming && !hasTrailingNewline && index === lines.length - 1;
@@ -8653,8 +9175,118 @@ function stripTodoProgressLines(text, { streaming = false } = {}) {
8653
9175
  return kept.join("\n").replace(/\n{3,}/g, "\n\n").trim();
8654
9176
  }
8655
9177
 
9178
+ function parseTodoProgressItemLine(line) {
9179
+ const match = String(line || "").match(/^\s*(?:(?:[-*]|\d+[.)])\s*)?\[( |x|X|-)\]\s+(.+)$/);
9180
+ if (!match) return null;
9181
+ const mark = match[1].toLowerCase();
9182
+ return { status: mark === "x" ? "done" : mark === "-" ? "partial" : "todo", text: match[2].trim() };
9183
+ }
9184
+
9185
+ function todoProgressStatusLabel(status) {
9186
+ if (status === "done") return "[x]";
9187
+ if (status === "partial") return "[-]";
9188
+ return "[ ]";
9189
+ }
9190
+
9191
+ function liveTodoProgressWidgetLinesFromText(text, tabId = activeTabId) {
9192
+ if (!isOptionalFeatureEnabled("todoProgressWidget")) return null;
9193
+ const raw = String(text || "");
9194
+ if (!raw.trim()) return null;
9195
+
9196
+ let inFence = false;
9197
+ let goal = "";
9198
+ let current = [];
9199
+ const blocks = [];
9200
+ const flush = () => {
9201
+ if (current.length) blocks.push(current);
9202
+ current = [];
9203
+ };
9204
+
9205
+ for (const line of textLines(raw)) {
9206
+ if (/^\s*```/.test(line)) {
9207
+ inFence = !inFence;
9208
+ flush();
9209
+ continue;
9210
+ }
9211
+ if (inFence) continue;
9212
+
9213
+ const clean = stripAnsi(line).trim();
9214
+ const goalMatch = clean.match(/^Goal\s*[::]\s*(.+)$/i);
9215
+ if (goalMatch?.[1]?.trim()) goal = goalMatch[1].trim();
9216
+
9217
+ const item = parseTodoProgressItemLine(clean);
9218
+ if (item) {
9219
+ current.push(item);
9220
+ continue;
9221
+ }
9222
+ flush();
9223
+ }
9224
+ flush();
9225
+
9226
+ const items = blocks.at(-1) || [];
9227
+ if (!items.length) return null;
9228
+
9229
+ if (goal && tabId) todoProgressGoalByTab.set(tabId, goal);
9230
+ const displayGoal = goal || (tabId ? todoProgressGoalByTab.get(tabId) : "") || "";
9231
+ const done = items.filter((item) => item.status === "done").length;
9232
+ const partial = items.filter((item) => item.status === "partial").length;
9233
+ const lines = [];
9234
+ if (displayGoal) lines.push(`Goal: ${displayGoal}`);
9235
+ lines.push(`Todo ${done}/${items.length} done${partial ? `, ${partial} partial` : ""}`);
9236
+ for (const item of items) lines.push(`${todoProgressStatusLabel(item.status)} ${item.text}`);
9237
+ return lines;
9238
+ }
9239
+
9240
+ // Coalesce live widget rebuilds to one per animation frame. The streaming
9241
+ // output handler calls into here on every text token, but the widget area
9242
+ // must not be torn down/rebuilt per token (that is UI reacting to the agent
9243
+ // output stream). One render per frame keeps streaming transcript-local.
9244
+ function scheduleLiveWidgetRender() {
9245
+ if (liveWidgetRenderFrame !== null) return;
9246
+ const flush = () => {
9247
+ liveWidgetRenderFrame = null;
9248
+ renderWidgets();
9249
+ };
9250
+ if (typeof requestAnimationFrame === "function") liveWidgetRenderFrame = requestAnimationFrame(flush);
9251
+ else liveWidgetRenderFrame = setTimeout(flush, 0);
9252
+ }
9253
+
9254
+ function scheduleLiveTodoProgressWidgetSync(text, tabId = activeTabId) {
9255
+ liveTodoProgressPendingText = String(text || "");
9256
+ liveTodoProgressPendingTabId = tabId || activeTabId;
9257
+ if (liveTodoProgressSyncFrame !== null) return;
9258
+ const flush = () => {
9259
+ const pendingText = liveTodoProgressPendingText;
9260
+ const pendingTabId = liveTodoProgressPendingTabId || activeTabId;
9261
+ liveTodoProgressSyncFrame = null;
9262
+ liveTodoProgressPendingText = "";
9263
+ liveTodoProgressPendingTabId = null;
9264
+ syncLiveTodoProgressWidgetFromText(pendingText, pendingTabId);
9265
+ };
9266
+ if (typeof requestAnimationFrame === "function") liveTodoProgressSyncFrame = requestAnimationFrame(flush);
9267
+ else liveTodoProgressSyncFrame = setTimeout(flush, 0);
9268
+ }
9269
+
9270
+ function syncLiveTodoProgressWidgetFromText(text, tabId = activeTabId) {
9271
+ const lines = liveTodoProgressWidgetLinesFromText(text, tabId);
9272
+ if (!lines) return false;
9273
+ const signature = todoProgressSignatureFromLines(lines);
9274
+ if (tabId && todoProgressSignatureByTab.get(tabId) === signature) return false;
9275
+ // liveTodoProgressWidgetLinesFromText already short-circuits unless the
9276
+ // feature is enabled, so detection is settled here. Do NOT run
9277
+ // updateOptionalFeatureAvailability() per token: it triggers git-footer
9278
+ // payload reconciliation and a full optional-feature control rebuild.
9279
+ // Availability is reconciled on command/state refreshes and RPC widget
9280
+ // updates instead, keeping streaming output decoupled from chrome UI.
9281
+ const changed = setWidgetForTab(tabId, "todo-progress", { method: "setWidget", widgetKey: "todo-progress", widgetLines: lines, tabId, live: true });
9282
+ if (changed && tabId === activeTabId) scheduleLiveWidgetRender();
9283
+ return changed;
9284
+ }
9285
+
8656
9286
  function parseTodoProgressWidget(lines) {
8657
9287
  const cleanLines = lines.map(stripAnsi).map((line) => line.trim()).filter(Boolean);
9288
+ const goalLine = cleanLines.find((line) => /^Goal\s*[::]/i.test(line));
9289
+ const goal = goalLine ? goalLine.replace(/^Goal\s*[::]\s*/i, "").trim() : "";
8658
9290
  const headerIndex = cleanLines.findIndex((line) => /^Todo\s+\d+\/\d+\s+done/i.test(line));
8659
9291
  if (headerIndex === -1) return null;
8660
9292
 
@@ -8665,16 +9297,16 @@ function parseTodoProgressWidget(lines) {
8665
9297
  const items = [];
8666
9298
  let footer = "";
8667
9299
  for (const line of cleanLines.slice(headerIndex + 1)) {
8668
- const item = line.match(/^\[( |x|X|-)\]\s+(.+)$/);
9300
+ const item = parseTodoProgressItemLine(line);
8669
9301
  if (item) {
8670
- const mark = item[1].toLowerCase();
8671
- items.push({ status: mark === "x" ? "done" : mark === "-" ? "partial" : "todo", text: item[2].trim() });
9302
+ items.push(item);
8672
9303
  } else if (/^Scroll\s+/i.test(line)) {
8673
9304
  footer = line;
8674
9305
  }
8675
9306
  }
8676
9307
 
8677
9308
  return {
9309
+ goal,
8678
9310
  done: Number.parseInt(match[1], 10) || 0,
8679
9311
  total: Number.parseInt(match[2], 10) || items.length,
8680
9312
  partial: Number.parseInt(match[3] || "0", 10) || 0,
@@ -8709,6 +9341,7 @@ function renderTodoProgressWidget(_key, lines) {
8709
9341
  const fill = make("span", "todo-widget-progress-fill");
8710
9342
  fill.style.width = `${percent}%`;
8711
9343
  progress.append(fill);
9344
+ if (todo.goal) summary.append(make("div", "todo-widget-goal", `Goal: ${todo.goal}`));
8712
9345
  summary.append(header, progress);
8713
9346
 
8714
9347
  const body = make("div", "todo-widget-body");
@@ -12888,14 +13521,201 @@ function appendMarkdownParagraph(parent, lines) {
12888
13521
  parent.append(paragraph);
12889
13522
  }
12890
13523
 
12891
- function appendMarkdownCodeBlock(parent, code, language = "") {
13524
+ function setMarkdownCodeCopyButtonState(button, copied) {
13525
+ clearTimeout(button._markdownCodeCopyResetTimer);
13526
+ const label = button._markdownCodeCopyDefaultLabel || "Copy";
13527
+ button.classList.toggle("copied", copied);
13528
+ button.textContent = copied ? "Copied" : label;
13529
+ button.title = copied ? "Copied code block" : `${label} code block`;
13530
+ button.setAttribute("aria-label", button.title);
13531
+ if (copied) {
13532
+ button._markdownCodeCopyResetTimer = setTimeout(() => setMarkdownCodeCopyButtonState(button, false), 1400);
13533
+ }
13534
+ }
13535
+
13536
+ async function copyMarkdownCodeBlock(button) {
13537
+ const wrapper = button.closest(".markdown-code-block");
13538
+ const codeNode = wrapper?.querySelector(":scope > pre.markdown-code > code, :scope > details.markdown-mermaid-source pre.markdown-code > code");
13539
+ const text = codeNode?.textContent || "";
13540
+ if (!text) {
13541
+ addEvent("code block has no text to copy", "warn");
13542
+ return;
13543
+ }
13544
+ button.disabled = true;
13545
+ try {
13546
+ await copyText(text);
13547
+ setMarkdownCodeCopyButtonState(button, true);
13548
+ } catch (error) {
13549
+ addEvent(`code block copy failed: ${error.message || String(error)}`, "warn");
13550
+ } finally {
13551
+ button.disabled = false;
13552
+ }
13553
+ }
13554
+
13555
+ function attachMarkdownCodeCopyButton(wrapper, label = "Copy") {
13556
+ if (!wrapper) return null;
13557
+ const existing = wrapper.querySelector(":scope > .markdown-code-copy-button");
13558
+ if (existing) return existing;
13559
+ const button = make("button", "markdown-code-copy-button", label);
13560
+ button.type = "button";
13561
+ button._markdownCodeCopyDefaultLabel = label;
13562
+ setMarkdownCodeCopyButtonState(button, false);
13563
+ button.addEventListener("click", (event) => {
13564
+ event.preventDefault();
13565
+ event.stopPropagation();
13566
+ copyMarkdownCodeBlock(button);
13567
+ });
13568
+ wrapper.classList.add("has-code-copy-action");
13569
+ wrapper.append(button);
13570
+ return button;
13571
+ }
13572
+
13573
+ function normalizedMarkdownLanguage(language) {
13574
+ return String(language || "").trim().toLowerCase();
13575
+ }
13576
+
13577
+ function isMermaidLanguage(language) {
13578
+ return MERMAID_LANGUAGES.has(normalizedMarkdownLanguage(language));
13579
+ }
13580
+
13581
+ function mermaidCssVar(styles, name, fallback) {
13582
+ return styles.getPropertyValue(name).trim() || fallback;
13583
+ }
13584
+
13585
+ function mermaidConfig() {
13586
+ const styles = getComputedStyle(document.documentElement);
13587
+ const text = mermaidCssVar(styles, "--ctp-text", "#cdd6f4");
13588
+ const subtext = mermaidCssVar(styles, "--ctp-subtext", "#bac2de");
13589
+ const surface = mermaidCssVar(styles, "--ctp-surface", "#313244");
13590
+ const base = mermaidCssVar(styles, "--ctp-base", "#1e1e2e");
13591
+ const crust = mermaidCssVar(styles, "--ctp-crust", "#11111b");
13592
+ const mauve = mermaidCssVar(styles, "--ctp-mauve", "#cba6f7");
13593
+ const blue = mermaidCssVar(styles, "--ctp-blue", "#89b4fa");
13594
+ const teal = mermaidCssVar(styles, "--ctp-teal", "#94e2d5");
13595
+ const yellow = mermaidCssVar(styles, "--ctp-yellow", "#f9e2af");
13596
+ const red = mermaidCssVar(styles, "--ctp-red", "#f38ba8");
13597
+ return {
13598
+ startOnLoad: false,
13599
+ securityLevel: "strict",
13600
+ logLevel: "error",
13601
+ maxTextSize: MERMAID_MAX_TEXT_SIZE,
13602
+ theme: "base",
13603
+ flowchart: { htmlLabels: false },
13604
+ themeVariables: {
13605
+ darkMode: true,
13606
+ background: "transparent",
13607
+ mainBkg: base,
13608
+ secondBkg: surface,
13609
+ primaryColor: surface,
13610
+ primaryTextColor: text,
13611
+ primaryBorderColor: mauve,
13612
+ secondaryColor: base,
13613
+ secondaryTextColor: text,
13614
+ secondaryBorderColor: blue,
13615
+ tertiaryColor: crust,
13616
+ tertiaryTextColor: text,
13617
+ tertiaryBorderColor: teal,
13618
+ lineColor: subtext,
13619
+ textColor: text,
13620
+ titleColor: teal,
13621
+ nodeTextColor: text,
13622
+ clusterBkg: crust,
13623
+ clusterBorder: mauve,
13624
+ edgeLabelBackground: base,
13625
+ noteBkgColor: crust,
13626
+ noteTextColor: text,
13627
+ noteBorderColor: yellow,
13628
+ errorBkgColor: crust,
13629
+ errorTextColor: red,
13630
+ fontFamily: 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
13631
+ },
13632
+ };
13633
+ }
13634
+
13635
+ function initializeMermaid(mermaid) {
13636
+ const config = mermaidConfig();
13637
+ const signature = JSON.stringify(config);
13638
+ if (signature !== mermaidThemeSignature) {
13639
+ mermaid.initialize(config);
13640
+ mermaidThemeSignature = signature;
13641
+ }
13642
+ }
13643
+
13644
+ async function loadMermaid() {
13645
+ if (!mermaidModulePromise) {
13646
+ mermaidModulePromise = import(MERMAID_MODULE_URL)
13647
+ .then((module) => module.default || module)
13648
+ .catch((error) => {
13649
+ mermaidModulePromise = null;
13650
+ throw error;
13651
+ });
13652
+ }
13653
+ const mermaid = await mermaidModulePromise;
13654
+ initializeMermaid(mermaid);
13655
+ return mermaid;
13656
+ }
13657
+
13658
+ function mermaidRenderErrorMessage(error) {
13659
+ return String(error?.str || error?.message || error || "Unknown Mermaid render error").trim();
13660
+ }
13661
+
13662
+ async function renderMermaidDiagram(diagram, status, source) {
13663
+ const token = `${Date.now().toString(36)}-${++mermaidRenderSequence}`;
13664
+ diagram.dataset.mermaidRenderToken = token;
13665
+ try {
13666
+ if (source.length > MERMAID_MAX_TEXT_SIZE) throw new Error(`Mermaid diagram is too large (${source.length} characters, max ${MERMAID_MAX_TEXT_SIZE}).`);
13667
+ const mermaid = await loadMermaid();
13668
+ const id = `mermaid-${token.replace(/[^a-z0-9_-]/gi, "-")}`;
13669
+ const { svg, bindFunctions } = await mermaid.render(id, source);
13670
+ if (!diagram.isConnected || diagram.dataset.mermaidRenderToken !== token) return;
13671
+ diagram.innerHTML = svg;
13672
+ bindFunctions?.(diagram);
13673
+ diagram.classList.add("rendered");
13674
+ status.textContent = "";
13675
+ status.hidden = true;
13676
+ } catch (error) {
13677
+ if (!diagram.isConnected || diagram.dataset.mermaidRenderToken !== token) return;
13678
+ diagram.classList.add("render-error");
13679
+ status.hidden = false;
13680
+ status.classList.add("error");
13681
+ status.textContent = `Mermaid render failed: ${mermaidRenderErrorMessage(error)}`;
13682
+ }
13683
+ }
13684
+
13685
+ function appendMarkdownMermaidBlock(parent, code) {
13686
+ const source = String(code || "").replace(/\n+$/g, "");
13687
+ const wrapper = make("div", "markdown-code-block markdown-mermaid-block");
13688
+ wrapper.append(make("div", "markdown-code-language", "mermaid"));
13689
+ const diagram = make("div", "markdown-mermaid-diagram");
13690
+ diagram.setAttribute("role", "img");
13691
+ diagram.setAttribute("aria-label", "Mermaid diagram");
13692
+ const status = make("div", "markdown-mermaid-status muted", "Rendering Mermaid diagram…");
13693
+ const sourceDetails = make("details", "markdown-mermaid-source");
13694
+ sourceDetails.append(make("summary", undefined, "Mermaid source"));
13695
+ const pre = make("pre", "code-block markdown-code");
13696
+ const codeNode = make("code", "language-mermaid");
13697
+ codeNode.textContent = source;
13698
+ pre.append(codeNode);
13699
+ sourceDetails.append(pre);
13700
+ wrapper.append(diagram, status, sourceDetails);
13701
+ attachMarkdownCodeCopyButton(wrapper, "Copy source");
13702
+ parent.append(wrapper);
13703
+ queueMicrotask(() => renderMermaidDiagram(diagram, status, source));
13704
+ }
13705
+
13706
+ function appendMarkdownCodeBlock(parent, code, language = "", { closed = true } = {}) {
13707
+ if (closed && isMermaidLanguage(language)) {
13708
+ appendMarkdownMermaidBlock(parent, code);
13709
+ return;
13710
+ }
12892
13711
  const wrapper = make("div", "markdown-code-block");
12893
13712
  if (language) wrapper.append(make("div", "markdown-code-language", language));
12894
13713
  const pre = make("pre", "code-block markdown-code");
12895
13714
  const codeNode = make("code", language ? `language-${language.replace(/[^a-z0-9_-]/gi, "")}` : "");
12896
- codeNode.textContent = code.replace(/\n+$/g, "");
13715
+ codeNode.textContent = String(code || "").replace(/\n+$/g, "");
12897
13716
  pre.append(codeNode);
12898
13717
  wrapper.append(pre);
13718
+ attachMarkdownCodeCopyButton(wrapper);
12899
13719
  parent.append(wrapper);
12900
13720
  }
12901
13721
 
@@ -12993,8 +13813,9 @@ function renderMarkdownInto(parent, text) {
12993
13813
  codeLines.push(lines[index]);
12994
13814
  index += 1;
12995
13815
  }
12996
- if (index < lines.length) index += 1;
12997
- appendMarkdownCodeBlock(parent, codeLines.join("\n"), language);
13816
+ const closed = index < lines.length;
13817
+ if (closed) index += 1;
13818
+ appendMarkdownCodeBlock(parent, codeLines.join("\n"), language, { closed });
12998
13819
  continue;
12999
13820
  }
13000
13821
  if (markdownTableSeparator(lines[index + 1]) && line.includes("|")) {
@@ -13098,16 +13919,21 @@ function streamingMarkdownStableBoundary(text) {
13098
13919
  return boundary;
13099
13920
  }
13100
13921
 
13922
+ function clearStreamingMarkdownBlock(block) {
13923
+ while (block.firstChild) block.firstChild.remove();
13924
+ }
13925
+
13101
13926
  function renderStreamingMarkdown(block, text) {
13102
13927
  let state = streamMarkdownState;
13103
13928
  if (!state || state.block !== block) {
13104
- block.replaceChildren();
13929
+ clearStreamingMarkdownBlock(block);
13105
13930
  state = streamMarkdownState = { block, stableText: "", tailNodes: [] };
13106
13931
  }
13107
13932
  if (!text.startsWith(state.stableText)) {
13108
- // Earlier content changed retroactively (e.g. todo-progress stripping);
13109
- // fall back to a full re-render for correctness.
13110
- block.replaceChildren();
13933
+ // Derived streaming text should be append-only; if a provider still sends a
13934
+ // retroactive rewrite, reset the streaming renderer without replaceChildren
13935
+ // so this path cannot tear down external chrome or widget nodes.
13936
+ clearStreamingMarkdownBlock(block);
13111
13937
  state.stableText = "";
13112
13938
  state.tailNodes = [];
13113
13939
  }
@@ -13548,6 +14374,30 @@ function assistantToolCallArguments(part) {
13548
14374
  return part?.arguments || part?.args || part?.input || part?.toolCall?.arguments || {};
13549
14375
  }
13550
14376
 
14377
+ function toolCallArgumentsText(value, { includeEmptyObject = true } = {}) {
14378
+ if (typeof value === "string") return value;
14379
+ if (value === undefined || value === null) return includeEmptyObject ? "{}" : "";
14380
+ if (typeof value === "object") {
14381
+ if (!includeEmptyObject && !Array.isArray(value) && Object.keys(value).length === 0) return "";
14382
+ try {
14383
+ return JSON.stringify(value, null, 2);
14384
+ } catch {
14385
+ return String(value);
14386
+ }
14387
+ }
14388
+ return String(value);
14389
+ }
14390
+
14391
+ function toolCallDisplayText(message, options = {}) {
14392
+ if (typeof message?.rawArguments === "string" && message.rawArguments.length > 0) return message.rawArguments;
14393
+ return toolCallArgumentsText(message?.arguments ?? message?.content ?? {}, options);
14394
+ }
14395
+
14396
+ function renderToolCallMessageBody(body, message, { placeholder = "{}" } = {}) {
14397
+ const text = toolCallDisplayText(message).trimEnd() || placeholder;
14398
+ appendText(body, text, "code-block tool-call-arguments");
14399
+ }
14400
+
13551
14401
  function assistantTextPartText(part) {
13552
14402
  if (!part || typeof part !== "object" || part.type !== "text") return "";
13553
14403
  if (typeof part.text === "string") return part.text;
@@ -14582,7 +15432,7 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
14582
15432
  const thinkingText = visibleThinkingText(message.thinking || textFromContent(message.content));
14583
15433
  if (thinkingOutputVisible && thinkingText) appendText(body, thinkingText, "thinking-text");
14584
15434
  } else if (message.role === "toolCall") {
14585
- appendText(body, JSON.stringify(message.arguments ?? message.content ?? {}, null, 2), "code-block");
15435
+ renderToolCallMessageBody(body, message);
14586
15436
  } else if (message.role === "assistantEvent") {
14587
15437
  appendText(body, typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? {}, null, 2), "code-block");
14588
15438
  } else {
@@ -14779,6 +15629,19 @@ function renderRunIndicator({ scroll = false } = {}) {
14779
15629
  if (shouldFollow) scrollChatToBottom();
14780
15630
  }
14781
15631
 
15632
+ function scheduleRunIndicatorRender({ scroll = false } = {}) {
15633
+ runIndicatorRenderScroll = runIndicatorRenderScroll || scroll;
15634
+ if (runIndicatorRenderFrame !== null) return;
15635
+ const flush = () => {
15636
+ const shouldScroll = runIndicatorRenderScroll;
15637
+ runIndicatorRenderFrame = null;
15638
+ runIndicatorRenderScroll = false;
15639
+ renderRunIndicator({ scroll: shouldScroll });
15640
+ };
15641
+ if (typeof requestAnimationFrame === "function") runIndicatorRenderFrame = requestAnimationFrame(flush);
15642
+ else runIndicatorRenderFrame = setTimeout(flush, 0);
15643
+ }
15644
+
14782
15645
  function setRunIndicatorActivity(activity, { active = true, scroll = true } = {}) {
14783
15646
  const wasLocallyActive = runIndicatorLocallyActive;
14784
15647
  const previousActivity = runIndicatorActivity;
@@ -14789,9 +15652,9 @@ function setRunIndicatorActivity(activity, { active = true, scroll = true } = {}
14789
15652
  }
14790
15653
  runIndicatorActivity = activity || runIndicatorActivity || "Waiting for output or action…";
14791
15654
  const needsRender = scroll || !hadRunIndicatorBubble || wasLocallyActive !== runIndicatorLocallyActive || previousActivity !== runIndicatorActivity;
14792
- if (needsRender) renderRunIndicator({ scroll });
15655
+ if (needsRender) scheduleRunIndicatorRender({ scroll });
14793
15656
  else if (runIndicatorIsActive()) startRunIndicatorTicker();
14794
- updateComposerModeButtons();
15657
+ scheduleComposerModeButtonsUpdate();
14795
15658
  if (active) scheduleRunIndicatorGraceCheck();
14796
15659
  }
14797
15660
 
@@ -15217,11 +16080,15 @@ function applyChatFollowScroll() {
15217
16080
  updateStickyUserPromptButton();
15218
16081
  }
15219
16082
 
15220
- function scheduleChatFollowScroll() {
16083
+ function scheduleChatFollowScroll({ settle = true } = {}) {
15221
16084
  if (chatFollowFrame === null) chatFollowFrame = requestAnimationFrame(applyChatFollowScroll);
16085
+ if (!settle) return;
16086
+ chatFollowNeedsSettle = true;
15222
16087
  clearTimeout(chatFollowSettleTimer);
15223
16088
  chatFollowSettleTimer = setTimeout(() => {
15224
16089
  chatFollowSettleTimer = null;
16090
+ if (!chatFollowNeedsSettle) return;
16091
+ chatFollowNeedsSettle = false;
15225
16092
  applyChatFollowScroll();
15226
16093
  }, CHAT_FOLLOW_SETTLE_DELAY_MS);
15227
16094
  }
@@ -15230,16 +16097,7 @@ function scrollChatToBottom({ force = false } = {}) {
15230
16097
  if (deferChatFollowScrollDuringPointerActivation({ force })) return;
15231
16098
  if (deferChatFollowScrollDuringInteractiveDropdown({ force })) return;
15232
16099
  if (force) autoFollowChat = true;
15233
- if (!autoFollowChat) {
15234
- updateJumpToLatestButton();
15235
- updateStickyUserPromptButton();
15236
- return;
15237
- }
15238
- lastChatProgrammaticScrollAt = performance.now();
15239
- setChatScrollTopInstant(elements.chat.scrollHeight);
15240
16100
  scheduleChatFollowScroll();
15241
- updateJumpToLatestButton();
15242
- updateStickyUserPromptButton();
15243
16101
  }
15244
16102
 
15245
16103
  function syncAutoFollowFromChatScroll() {
@@ -16774,6 +17632,8 @@ function cancelStreamBubbleHide() {
16774
17632
  function cancelStreamingAssistantTextRender() {
16775
17633
  clearTimeout(streamTextRenderTimer);
16776
17634
  streamTextRenderTimer = null;
17635
+ if (streamTextRenderFrame !== null && typeof cancelAnimationFrame === "function") cancelAnimationFrame(streamTextRenderFrame);
17636
+ streamTextRenderFrame = null;
16777
17637
  }
16778
17638
 
16779
17639
  function removeStreamBubble() {
@@ -16786,10 +17646,54 @@ function removeStreamBubble() {
16786
17646
  renderRunIndicator({ scroll: false });
16787
17647
  }
16788
17648
 
16789
- function streamRenderableAssistantText() {
17649
+ function resetStreamDerivedTextCache() {
17650
+ streamDerivedTextCache = { rawText: null, assistantText: "", thinkingFormat: null, finalText: "" };
17651
+ }
17652
+
17653
+ function setStreamRawText(text) {
17654
+ const nextText = String(text || "");
17655
+ if (nextText === streamRawText) return false;
17656
+ streamRawText = nextText;
17657
+ resetStreamDerivedTextCache();
17658
+ return true;
17659
+ }
17660
+
17661
+ function appendStreamRawText(delta) {
17662
+ const text = String(delta || "");
17663
+ if (!text) return false;
17664
+ streamRawText += text;
17665
+ resetStreamDerivedTextCache();
17666
+ return true;
17667
+ }
17668
+
17669
+ function streamingAssistantTextFallback(event) {
17670
+ // Fallback for legacy/summary events that do not carry a delta. The hot
17671
+ // text_delta path appends update.delta and avoids this accumulated-message scan.
17672
+ return assistantTextFromMessage(assistantStreamingMessage(event), { streaming: true });
17673
+ }
17674
+
17675
+ function syncStreamRawTextFromUpdate(event, update) {
17676
+ if (update.type === "text_delta") {
17677
+ const delta = update.delta ?? update.text ?? update.content ?? "";
17678
+ if (appendStreamRawText(delta)) return true;
17679
+ }
17680
+ if (typeof update.content === "string") return setStreamRawText(update.content);
17681
+ if (typeof update.text === "string") return setStreamRawText(update.text);
17682
+ const partialText = streamingAssistantTextFallback(event);
17683
+ return typeof partialText === "string" ? setStreamRawText(partialText) : false;
17684
+ }
17685
+
17686
+ function streamDerivedText() {
17687
+ if (streamDerivedTextCache.rawText === streamRawText) return streamDerivedTextCache;
16790
17688
  const assistantText = stripTodoProgressLines(streamRawText, { streaming: true });
16791
- const parsed = splitThinkingFormatText(assistantText, { streaming: true });
16792
- return parsed?.hasThinkingFormat ? stripTodoProgressLines(parsed.finalText, { streaming: true }) : assistantText;
17689
+ const thinkingFormat = splitThinkingFormatText(assistantText, { streaming: true });
17690
+ const finalText = thinkingFormat?.hasThinkingFormat ? stripTodoProgressLines(thinkingFormat.finalText, { streaming: true }) : assistantText;
17691
+ streamDerivedTextCache = { rawText: streamRawText, assistantText, thinkingFormat, finalText };
17692
+ return streamDerivedTextCache;
17693
+ }
17694
+
17695
+ function streamRenderableAssistantText() {
17696
+ return streamDerivedText().finalText;
16793
17697
  }
16794
17698
 
16795
17699
  function scheduleStreamBubbleHide() {
@@ -16804,8 +17708,8 @@ function scheduleStreamBubbleHide() {
16804
17708
  }, delayMs);
16805
17709
  }
16806
17710
 
16807
- function syncStreamingThinkingFormat(assistantText) {
16808
- const parsed = splitThinkingFormatText(assistantText, { streaming: true });
17711
+ function syncStreamingThinkingFormat() {
17712
+ const parsed = streamDerivedText().thinkingFormat;
16809
17713
  if (!parsed?.hasThinkingFormat) return null;
16810
17714
  const thinking = visibleThinkingText(parsed.thinkingText);
16811
17715
  if (thinking) setStreamingThinkingText(thinking);
@@ -16814,9 +17718,8 @@ function syncStreamingThinkingFormat(assistantText) {
16814
17718
  }
16815
17719
 
16816
17720
  function renderStreamingAssistantText() {
16817
- const assistantText = stripTodoProgressLines(streamRawText, { streaming: true });
16818
- const thinkingFormat = syncStreamingThinkingFormat(assistantText);
16819
- const finalText = thinkingFormat?.hasThinkingFormat ? stripTodoProgressLines(thinkingFormat.finalText, { streaming: true }) : assistantText;
17721
+ const thinkingFormat = syncStreamingThinkingFormat();
17722
+ const finalText = thinkingFormat?.hasThinkingFormat ? streamDerivedText().finalText : streamRenderableAssistantText();
16820
17723
  if (finalText) {
16821
17724
  ensureStreamBubble();
16822
17725
  renderStreamingMarkdown(streamText, finalText);
@@ -16825,19 +17728,72 @@ function renderStreamingAssistantText() {
16825
17728
  }
16826
17729
  }
16827
17730
 
16828
- function scheduleStreamingAssistantTextRender() {
16829
- if (streamTextRenderTimer) return;
16830
- streamTextRenderTimer = setTimeout(() => {
17731
+ function scheduleStreamingAssistantTextRender({ immediate = false } = {}) {
17732
+ if (streamTextRenderTimer || streamTextRenderFrame !== null) return;
17733
+ const flush = () => {
16831
17734
  streamTextRenderTimer = null;
17735
+ streamTextRenderFrame = null;
16832
17736
  renderStreamingAssistantText();
16833
- }, STREAM_OUTPUT_TOOLCALL_GUARD_MS);
17737
+ scheduleChatFollowScroll();
17738
+ };
17739
+ if (immediate && typeof requestAnimationFrame === "function") streamTextRenderFrame = requestAnimationFrame(flush);
17740
+ else streamTextRenderTimer = setTimeout(flush, immediate ? 0 : STREAM_OUTPUT_TOOLCALL_GUARD_MS);
16834
17741
  }
16835
17742
 
16836
17743
  function suppressStreamingAssistantTextBeforeToolCall() {
16837
- streamRawText = "";
17744
+ setStreamRawText("");
16838
17745
  removeStreamBubble();
16839
17746
  }
16840
17747
 
17748
+ function resetStreamingToolCallState({ remove = true } = {}) {
17749
+ if (remove) streamToolCallBubble?.remove();
17750
+ streamToolCallBubble = null;
17751
+ streamToolCallText = null;
17752
+ streamToolCallRawArguments = "";
17753
+ streamToolCallName = "";
17754
+ streamToolCallId = "";
17755
+ streamToolCallContentIndex = null;
17756
+ streamToolCallComplete = false;
17757
+ }
17758
+
17759
+ function streamingToolCallTitle() {
17760
+ const name = streamToolCallName || "tool";
17761
+ return `tool call: ${name}${streamToolCallComplete ? " (ready)" : " (building)"}`;
17762
+ }
17763
+
17764
+ function streamingToolCallMessage() {
17765
+ return {
17766
+ role: "toolCall",
17767
+ title: streamingToolCallTitle(),
17768
+ timestamp: Date.now(),
17769
+ toolName: streamToolCallName || "tool",
17770
+ toolCallId: streamToolCallId,
17771
+ rawArguments: streamToolCallRawArguments,
17772
+ content: streamToolCallRawArguments,
17773
+ };
17774
+ }
17775
+
17776
+ function renderStreamingToolCallCard({ scroll = false } = {}) {
17777
+ const message = streamingToolCallMessage();
17778
+ const displayText = streamToolCallRawArguments || (streamToolCallComplete ? "{}" : "(waiting for argument stream…)");
17779
+ if (!streamToolCallBubble?.parentElement || !streamToolCallText) {
17780
+ const created = appendMessage(message, { streaming: true });
17781
+ streamToolCallBubble = created.bubble;
17782
+ streamToolCallText = created.body.querySelector(".tool-call-arguments") || created.body.querySelector(".code-block");
17783
+ }
17784
+ streamToolCallBubble._copyMessage = message;
17785
+ const role = streamToolCallBubble.querySelector(":scope > .message-header .message-role");
17786
+ if (role && role.textContent !== message.title) role.textContent = message.title;
17787
+ if (streamToolCallText && streamToolCallText.textContent !== displayText) streamToolCallText.textContent = displayText;
17788
+ renderRunIndicator({ scroll: false });
17789
+ if (scroll) scrollChatToBottom();
17790
+ }
17791
+
17792
+ function removeStreamingToolCallCard() {
17793
+ resetStreamingToolCallState({ remove: true });
17794
+ renderRunIndicator({ scroll: false });
17795
+ }
17796
+
16841
17797
  function ensureStreamBubble() {
16842
17798
  cancelStreamBubbleHide();
16843
17799
  if (streamBubble?.parentElement === elements.chat) return;
@@ -16872,16 +17828,19 @@ function resetStreamBubble() {
16872
17828
  streamBubble = null;
16873
17829
  streamText = null;
16874
17830
  streamRawText = "";
17831
+ streamThinkingRawText = "";
17832
+ resetStreamDerivedTextCache();
16875
17833
  streamMarkdownState = null;
16876
17834
  streamBubbleVisibleSince = 0;
16877
17835
  streamToolCallSeen = false;
17836
+ resetStreamingToolCallState({ remove: true });
16878
17837
  streamThinkingBubble = null;
16879
17838
  streamThinking = null;
16880
17839
  streamMessageActive = false;
16881
17840
  }
16882
17841
 
16883
17842
  function liveStreamRenderActive() {
16884
- return streamMessageActive && currentState?.isStreaming === true && Boolean(streamBubble || streamThinkingBubble || streamRawText);
17843
+ return streamMessageActive && currentState?.isStreaming === true && Boolean(streamBubble || streamThinkingBubble || streamToolCallBubble || streamRawText || streamToolCallRawArguments);
16885
17844
  }
16886
17845
 
16887
17846
  /**
@@ -16892,15 +17851,19 @@ function liveStreamRenderActive() {
16892
17851
  function restoreStreamRenderAfterChatRebuild() {
16893
17852
  const thinkingText = streamThinking?.textContent || "";
16894
17853
  const thinkingComplete = streamThinkingBubble?.classList.contains("complete") === true;
17854
+ const toolCallWasVisible = !!(streamToolCallBubble?.parentElement === elements.chat || streamToolCallRawArguments || streamToolCallName);
16895
17855
  streamBubble = null;
16896
17856
  streamText = null;
16897
17857
  streamThinkingBubble = null;
16898
17858
  streamThinking = null;
17859
+ streamToolCallBubble = null;
17860
+ streamToolCallText = null;
16899
17861
  streamBubbleVisibleSince = 0;
16900
17862
  if (thinkingText && setStreamingThinkingText(thinkingText) && thinkingComplete) {
16901
17863
  streamThinkingBubble?.classList.add("complete");
16902
17864
  }
16903
- if (stripTodoProgressLines(streamRawText, { streaming: true })) renderStreamingAssistantText();
17865
+ if (toolCallWasVisible) renderStreamingToolCallCard();
17866
+ if (streamRenderableAssistantText()) renderStreamingAssistantText();
16904
17867
  }
16905
17868
 
16906
17869
  function thinkingDeltaText(update) {
@@ -16913,6 +17876,56 @@ function assistantStreamingMessage(event) {
16913
17876
  return partial?.role === "assistant" ? partial : null;
16914
17877
  }
16915
17878
 
17879
+ function assistantToolCallPartFromUpdate(event, update = event?.assistantMessageEvent || {}) {
17880
+ if (isAssistantToolCallPart(update.toolCall)) return update.toolCall;
17881
+ const message = assistantStreamingMessage(event);
17882
+ const content = message?.content;
17883
+ if (!Array.isArray(content)) return null;
17884
+ const contentIndex = Number(update.contentIndex);
17885
+ if (Number.isInteger(contentIndex) && isAssistantToolCallPart(content[contentIndex])) return content[contentIndex];
17886
+ for (let index = content.length - 1; index >= 0; index -= 1) {
17887
+ if (isAssistantToolCallPart(content[index])) return content[index];
17888
+ }
17889
+ return null;
17890
+ }
17891
+
17892
+ function streamToolCallNameFromUpdate(update, part) {
17893
+ const rawName = update.name || update.toolName || update.toolCall?.name || assistantToolCallName(part);
17894
+ const name = runIndicatorToolName(rawName);
17895
+ return name === "unknown" ? "tool" : name;
17896
+ }
17897
+
17898
+ function streamingToolCallContentIndexFromUpdate(update) {
17899
+ const contentIndex = Number(update.contentIndex);
17900
+ return Number.isInteger(contentIndex) ? contentIndex : null;
17901
+ }
17902
+
17903
+ function updateStreamingToolCallFromEvent(event, { reset = false, appendDelta = false, complete = false, scroll = false } = {}) {
17904
+ const update = event.assistantMessageEvent || {};
17905
+ const part = assistantToolCallPartFromUpdate(event, update);
17906
+ const contentIndex = streamingToolCallContentIndexFromUpdate(update);
17907
+ if (reset || (contentIndex !== null && streamToolCallContentIndex !== null && contentIndex !== streamToolCallContentIndex)) {
17908
+ resetStreamingToolCallState({ remove: true });
17909
+ }
17910
+ streamToolCallContentIndex = contentIndex ?? streamToolCallContentIndex;
17911
+ streamToolCallName = streamToolCallNameFromUpdate(update, part) || streamToolCallName || "tool";
17912
+ streamToolCallId = assistantToolCallId(update.toolCall || part) || streamToolCallId;
17913
+ streamToolCallComplete = !!complete;
17914
+
17915
+ const partArgumentText = toolCallArgumentsText(assistantToolCallArguments(update.toolCall || part), { includeEmptyObject: false });
17916
+ if (reset) streamToolCallRawArguments = partArgumentText || "";
17917
+ if (appendDelta && update.delta !== undefined && update.delta !== null) streamToolCallRawArguments += typeof update.delta === "string" ? update.delta : String(update.delta);
17918
+ if (!streamToolCallRawArguments && partArgumentText) streamToolCallRawArguments = partArgumentText;
17919
+ if (complete) {
17920
+ const finalArgumentText = toolCallArgumentsText(assistantToolCallArguments(update.toolCall || part), { includeEmptyObject: true });
17921
+ if (finalArgumentText && (finalArgumentText !== "{}" || !streamToolCallRawArguments)) streamToolCallRawArguments = finalArgumentText;
17922
+ if (!streamToolCallRawArguments) streamToolCallRawArguments = "{}";
17923
+ }
17924
+
17925
+ renderStreamingToolCallCard({ scroll });
17926
+ return streamToolCallName || "tool";
17927
+ }
17928
+
16916
17929
  function assistantTextFromMessage(message, { streaming = false } = {}) {
16917
17930
  void streaming;
16918
17931
  const content = message?.content;
@@ -16957,57 +17970,98 @@ function setStreamingThinkingText(text) {
16957
17970
  return true;
16958
17971
  }
16959
17972
 
16960
- function syncStreamingThinkingFromMessage(event, { placeholder = "" } = {}) {
17973
+ function streamingThinkingTextFallback(event) {
17974
+ return assistantThinkingTextFromMessage(assistantStreamingMessage(event), { streaming: true });
17975
+ }
17976
+
17977
+ function setStreamThinkingRawText(text) {
17978
+ const thinking = visibleThinkingText(text);
17979
+ if (thinking === streamThinkingRawText) return false;
17980
+ streamThinkingRawText = thinking;
17981
+ return true;
17982
+ }
17983
+
17984
+ function appendStreamThinkingText(delta) {
17985
+ const thinking = visibleThinkingText(delta);
17986
+ if (!thinking) return false;
17987
+ streamThinkingRawText += thinking;
17988
+ return true;
17989
+ }
17990
+
17991
+ function syncStreamingThinkingFromUpdate(event, update, { placeholder = "" } = {}) {
16961
17992
  if (!thinkingOutputVisible) return true;
16962
- const text = assistantThinkingTextFromMessage(assistantStreamingMessage(event), { streaming: true });
16963
- if (text === null) return false;
16964
- return setStreamingThinkingText(text || placeholder);
17993
+ const delta = thinkingDeltaText(update);
17994
+ if (update.type === "thinking_delta" && delta) {
17995
+ appendStreamThinkingText(delta);
17996
+ return setStreamingThinkingText(streamThinkingRawText || placeholder);
17997
+ }
17998
+ if (update.type === "thinking_start") {
17999
+ if (delta) setStreamThinkingRawText(delta);
18000
+ return streamThinkingRawText ? setStreamingThinkingText(streamThinkingRawText || placeholder) : false;
18001
+ }
18002
+ if (update.type === "thinking_end") {
18003
+ if (delta) setStreamThinkingRawText(delta);
18004
+ else {
18005
+ const fallback = streamingThinkingTextFallback(event);
18006
+ if (fallback !== null) setStreamThinkingRawText(fallback);
18007
+ }
18008
+ return setStreamingThinkingText(streamThinkingRawText || placeholder);
18009
+ }
18010
+ const fallback = streamingThinkingTextFallback(event);
18011
+ if (fallback === null) return false;
18012
+ setStreamThinkingRawText(fallback);
18013
+ return setStreamingThinkingText(streamThinkingRawText || placeholder);
16965
18014
  }
16966
18015
 
16967
18016
  function handleMessageUpdate(event) {
16968
18017
  const update = event.assistantMessageEvent || {};
16969
18018
  if (update.type === "thinking_start") {
16970
18019
  setRunIndicatorActivity("Thinking…", { scroll: false });
16971
- syncStreamingThinkingFromMessage(event);
16972
- scrollChatToBottom();
18020
+ syncStreamingThinkingFromUpdate(event, update);
18021
+ scheduleChatFollowScroll();
16973
18022
  } else if (update.type === "thinking_delta") {
16974
18023
  const delta = thinkingDeltaText(update);
16975
18024
  setRunIndicatorActivity("Thinking…", { scroll: false });
16976
- const synced = syncStreamingThinkingFromMessage(event);
18025
+ const synced = syncStreamingThinkingFromUpdate(event, update);
16977
18026
  if (thinkingOutputVisible && delta && (!synced || !streamThinking?.textContent)) {
16978
18027
  showStreamingThinking("");
16979
18028
  if (streamThinking?.textContent === "Thinking…") streamThinking.textContent = "";
16980
18029
  if (streamThinking) streamThinking.textContent += delta;
16981
18030
  }
16982
- scrollChatToBottom();
18031
+ scheduleChatFollowScroll();
16983
18032
  } else if (update.type === "thinking_end") {
16984
- const finalThinking = assistantThinkingTextFromMessage(assistantStreamingMessage(event), { streaming: true }) || thinkingDeltaText(update);
16985
- if (finalThinking) setStreamingThinkingText(finalThinking);
16986
- streamThinkingBubble?.classList.add("complete");
18033
+ if (syncStreamingThinkingFromUpdate(event, update)) streamThinkingBubble?.classList.add("complete");
16987
18034
  setRunIndicatorActivity("Finished thinking; waiting for the next output or action…", { scroll: false });
16988
18035
  } else if (update.type === "text_delta" || update.type === "text_end") {
16989
- const delta = update.type === "text_delta" ? update.delta || "" : "";
16990
- const partialText = assistantTextFromMessage(assistantStreamingMessage(event), { streaming: true });
16991
- if (typeof partialText === "string") streamRawText = partialText;
16992
- else if (update.type === "text_end" && typeof update.content === "string") streamRawText = update.content;
16993
- else streamRawText += delta;
18036
+ syncStreamRawTextFromUpdate(event, update);
18037
+ scheduleLiveTodoProgressWidgetSync(streamRawText, event.tabId || activeTabId);
16994
18038
  setRunIndicatorActivity("Writing response…", { scroll: false });
16995
- if (streamToolCallSeen || streamBubble) renderStreamingAssistantText();
16996
- else scheduleStreamingAssistantTextRender();
18039
+ scheduleStreamingAssistantTextRender({ immediate: !!(streamToolCallSeen || streamBubble) });
16997
18040
  // Streaming output must stay transcript-local. Full footer/status
16998
18041
  // reconciliation happens on message/state refreshes, not per token.
16999
- scrollChatToBottom();
18042
+ scheduleChatFollowScroll();
17000
18043
  } else if (update.type === "toolcall_start") {
17001
18044
  streamToolCallSeen = true;
17002
18045
  suppressStreamingAssistantTextBeforeToolCall();
17003
- const name = runIndicatorToolName(update.name || update.toolName || update.toolCall?.name);
17004
- setRunIndicatorActivity(`Preparing tool call: ${name}…`);
18046
+ const name = updateStreamingToolCallFromEvent(event, { reset: true, scroll: true });
18047
+ setRunIndicatorActivity(`Building tool call: ${name}…`, { scroll: false });
17005
18048
  addEvent(`tool call started in assistant message`, "info");
18049
+ } else if (update.type === "toolcall_delta") {
18050
+ streamToolCallSeen = true;
18051
+ suppressStreamingAssistantTextBeforeToolCall();
18052
+ const name = updateStreamingToolCallFromEvent(event, { appendDelta: true });
18053
+ setRunIndicatorActivity(`Building tool call: ${name}…`, { scroll: false });
18054
+ scheduleChatFollowScroll();
18055
+ } else if (update.type === "toolcall_end") {
18056
+ streamToolCallSeen = true;
18057
+ suppressStreamingAssistantTextBeforeToolCall();
18058
+ const name = updateStreamingToolCallFromEvent(event, { complete: true, scroll: true });
18059
+ setRunIndicatorActivity(`Tool call ready: ${name}; waiting to run…`, { scroll: false });
17006
18060
  } else if (update.type === "error") {
17007
18061
  setRunIndicatorActivity("Assistant stream reported an error…");
17008
18062
  appendMessage({ role: "error", title: "assistant error", timestamp: Date.now(), content: update.reason || update.errorMessage || "assistant error", level: "error" }, { streaming: true });
17009
18063
  renderRunIndicator({ scroll: false });
17010
- scrollChatToBottom();
18064
+ scheduleChatFollowScroll();
17011
18065
  }
17012
18066
  }
17013
18067
 
@@ -17304,6 +18358,7 @@ async function refreshModels(tabContext = activeTabContext()) {
17304
18358
  if (!isCurrentTabContext(tabContext)) return;
17305
18359
  availableModels = models;
17306
18360
  footerScopedModels = scopedModels;
18361
+ footerScopedModels = orderedFooterScopedModels();
17307
18362
  footerScopedModelPatterns = scopedModelPatterns;
17308
18363
  footerScopedModelSource = scopedModelSource;
17309
18364
  if (scopedModelError) addEvent(`failed to load scoped models: ${scopedModelError.message}`, "warn");
@@ -18605,18 +19660,17 @@ function handleExtensionUiRequest(request) {
18605
19660
  }
18606
19661
  case "setWidget": {
18607
19662
  const widgetKey = request.widgetKey || request.id;
19663
+ const requestTabId = request.tabId || activeTabId;
18608
19664
  if (widgetKey === "pi-remote-webui") {
18609
- widgets.delete(widgetKey);
19665
+ setWidgetForTab(requestTabId, widgetKey, { ...request, widgetLines: undefined });
18610
19666
  if (Array.isArray(request.widgetLines)) {
18611
19667
  mirrorRemoteWebuiWidgetToTranscript(widgetKey, request.widgetLines, request);
18612
19668
  showRemoteWebuiQrPopup(widgetKey, request.widgetLines, request);
18613
19669
  } else {
18614
19670
  closeRemoteWebuiQrPopup();
18615
19671
  }
18616
- } else if (Array.isArray(request.widgetLines)) {
18617
- widgets.set(widgetKey, request);
18618
19672
  } else {
18619
- widgets.delete(widgetKey);
19673
+ if (!setWidgetForTab(requestTabId, widgetKey, request)) return;
18620
19674
  }
18621
19675
  updateOptionalFeatureAvailability();
18622
19676
  renderWidgets();
@@ -18753,9 +19807,9 @@ function handleInactiveTabEvent(event) {
18753
19807
  }
18754
19808
 
18755
19809
  function handleEvent(event) {
18756
- ingestEventTabActivity(event);
18757
- trackAutoRetryStateFromEvent(event);
18758
- trackSkillsFromEvent(event);
19810
+ if (eventHasTabActivityPayload(event)) ingestEventTabActivity(event);
19811
+ if (event?.type === "auto_retry_start" || event?.type === "auto_retry_end") trackAutoRetryStateFromEvent(event);
19812
+ if (eventMayAffectSkillUsage(event)) trackSkillsFromEvent(event);
18759
19813
  if (!eventTargetsActiveTab(event)) {
18760
19814
  handleInactiveTabEvent(event);
18761
19815
  return;
@@ -18793,7 +19847,7 @@ function handleEvent(event) {
18793
19847
  addTransientMessage({ role: "native", title: "/reload", content: `${event.tabTitle || "terminal"} reloaded. Keybindings, extensions, skills, prompts, and themes were refreshed by restarting the RPC tab${event.sessionFile ? ` and resuming ${event.sessionFile}` : ""}.`, level: "info" });
18794
19848
  clearContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
18795
19849
  statusEntries.clear();
18796
- widgets.clear();
19850
+ clearWidgetsForTab(event.tabId || activeTabId);
18797
19851
  latestBtwWidgetPayload = null;
18798
19852
  btwWidgetDismissedId = "";
18799
19853
  btwWidgetComposerOpen = false;
@@ -18924,6 +19978,7 @@ function handleEvent(event) {
18924
19978
  case "tool_execution_start":
18925
19979
  streamToolCallSeen = true;
18926
19980
  suppressStreamingAssistantTextBeforeToolCall();
19981
+ removeStreamingToolCallCard();
18927
19982
  handleToolExecutionStart(event);
18928
19983
  setRunIndicatorActivity(`Running tool: ${runIndicatorToolName(event.toolName)}…`);
18929
19984
  addEvent(`tool ${event.toolName} started`);