@bubblebrain-ai/bubble 0.0.13 → 0.0.15

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.
Files changed (80) hide show
  1. package/dist/agent/execution-governor.js +1 -1
  2. package/dist/agent/tool-intent.js +1 -0
  3. package/dist/agent.d.ts +2 -0
  4. package/dist/agent.js +589 -316
  5. package/dist/approval/controller.d.ts +1 -0
  6. package/dist/approval/controller.js +20 -3
  7. package/dist/approval/tool-helper.js +2 -0
  8. package/dist/approval/types.d.ts +14 -1
  9. package/dist/cli.d.ts +3 -1
  10. package/dist/cli.js +12 -0
  11. package/dist/context/compact.js +9 -3
  12. package/dist/context/projector.js +27 -12
  13. package/dist/debug-trace.d.ts +27 -0
  14. package/dist/debug-trace.js +385 -0
  15. package/dist/feishu/agent-host/approval-card.js +9 -0
  16. package/dist/feishu/serve.js +7 -1
  17. package/dist/main.js +41 -0
  18. package/dist/model-catalog.js +1 -0
  19. package/dist/orchestrator/default-hooks.js +19 -8
  20. package/dist/orchestrator/hooks.d.ts +1 -0
  21. package/dist/prompt/environment.js +2 -0
  22. package/dist/prompt/reminders.d.ts +5 -6
  23. package/dist/prompt/reminders.js +8 -9
  24. package/dist/prompt/runtime.js +2 -2
  25. package/dist/provider-openai-codex.d.ts +7 -0
  26. package/dist/provider-openai-codex.js +265 -124
  27. package/dist/provider-registry.d.ts +2 -0
  28. package/dist/provider-registry.js +58 -9
  29. package/dist/provider.d.ts +3 -0
  30. package/dist/provider.js +5 -1
  31. package/dist/session-log.js +13 -1
  32. package/dist/slash-commands/commands.js +12 -0
  33. package/dist/slash-commands/types.d.ts +2 -0
  34. package/dist/stats/usage.d.ts +52 -0
  35. package/dist/stats/usage.js +414 -0
  36. package/dist/tools/apply-patch.d.ts +9 -0
  37. package/dist/tools/apply-patch.js +330 -0
  38. package/dist/tools/bash.js +205 -44
  39. package/dist/tools/edit-apply.d.ts +5 -2
  40. package/dist/tools/edit-apply.js +221 -31
  41. package/dist/tools/edit.js +12 -3
  42. package/dist/tools/file-mutation-queue.d.ts +1 -0
  43. package/dist/tools/file-mutation-queue.js +12 -1
  44. package/dist/tools/index.d.ts +2 -0
  45. package/dist/tools/index.js +7 -1
  46. package/dist/tools/patch-apply.d.ts +41 -0
  47. package/dist/tools/patch-apply.js +312 -0
  48. package/dist/tools/server-manager.d.ts +36 -0
  49. package/dist/tools/server-manager.js +234 -0
  50. package/dist/tools/server.d.ts +6 -0
  51. package/dist/tools/server.js +245 -0
  52. package/dist/tools/write.d.ts +3 -6
  53. package/dist/tools/write.js +26 -46
  54. package/dist/tui/display-history.d.ts +1 -0
  55. package/dist/tui/display-history.js +5 -4
  56. package/dist/tui/edit-diff.js +6 -1
  57. package/dist/tui/model-picker-data.d.ts +10 -0
  58. package/dist/tui/model-picker-data.js +32 -0
  59. package/dist/tui/run.d.ts +2 -0
  60. package/dist/tui/run.js +717 -122
  61. package/dist/tui/tool-renderers/fallback.js +1 -1
  62. package/dist/tui/tool-renderers/write-preview.js +2 -0
  63. package/dist/tui/trace-groups.js +10 -3
  64. package/dist/tui-ink/app.js +1 -4
  65. package/dist/tui-ink/approval/approval-dialog.js +7 -1
  66. package/dist/tui-ink/display-history.d.ts +1 -0
  67. package/dist/tui-ink/display-history.js +5 -4
  68. package/dist/tui-ink/message-list.js +14 -8
  69. package/dist/tui-ink/trace-groups.js +1 -1
  70. package/dist/tui-opentui/app.js +2 -0
  71. package/dist/tui-opentui/approval/approval-dialog.js +7 -1
  72. package/dist/tui-opentui/display-history.d.ts +1 -0
  73. package/dist/tui-opentui/display-history.js +5 -4
  74. package/dist/tui-opentui/edit-diff.js +6 -1
  75. package/dist/tui-opentui/message-list.js +6 -3
  76. package/dist/tui-opentui/trace-groups.js +10 -3
  77. package/dist/types.d.ts +12 -2
  78. package/dist/update/index.d.ts +46 -0
  79. package/dist/update/index.js +240 -0
  80. package/package.json +1 -1
package/dist/tui/run.js CHANGED
@@ -9,10 +9,12 @@ import { homedir } from "node:os";
9
9
  import { AgentAbortError } from "../agent.js";
10
10
  import { AgentRunInputQueue } from "../agent/input-controller.js";
11
11
  import { debugReasoningStream, summarizeDebugText } from "../reasoning-debug.js";
12
+ import { summarizeAgentEventForTrace, summarizeTraceError, summarizeTraceValue, traceEvent, } from "../debug-trace.js";
12
13
  import { BUILTIN_PROVIDERS, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
13
- import { listBuiltinModels } from "../model-catalog.js";
14
14
  import { calculateUsageCost } from "../model-pricing.js";
15
15
  import { getAvailableThinkingLevels } from "../provider-transform.js";
16
+ import { getCurrentVersion } from "../update/index.js";
17
+ import { collectUsageStatsBundle, formatStatsPanelBody } from "../stats/usage.js";
16
18
  import { parseSkillInvocation } from "../skills/invocation.js";
17
19
  import { registry as slashRegistry } from "../slash-commands/index.js";
18
20
  import { sourceRank } from "../slash-commands/unified.js";
@@ -24,6 +26,7 @@ import { markdownInlineSegments } from "./markdown-inline.js";
24
26
  import { hashString } from "./render-signature.js";
25
27
  import { findToolRenderer } from "./tool-renderers/registry.js";
26
28
  import { writeToolKey } from "./tool-renderers/write.js";
29
+ import { discoverModelProviderGroups, getVisibleModelProviders, localModelsForProvider, } from "./model-picker-data.js";
27
30
  import { formatWritePreview, isWritePreviewTool } from "./tool-renderers/write-preview.js";
28
31
  import { extractStreamingArgsHint } from "./streaming-tool-args.js";
29
32
  import { getNextPermissionMode, PERMISSION_MODE_INFO } from "../permission/mode.js";
@@ -179,7 +182,7 @@ const PROMPT_SCANNER_IDLE_FRAMES = [" "];
179
182
  const PROMPT_SCANNER_INTERVAL_MS = 80;
180
183
  const SESSION_SIDEBAR_WIDTH = 42;
181
184
  const SESSION_SIDEBAR_AUTO_WIDTH = 120;
182
- const PROVIDER_DIALOG_ROWS = 11;
185
+ const PROVIDER_DIALOG_ROWS = 13;
183
186
  const QUESTION_MAX_TABS = 4;
184
187
  const QUESTION_MAX_OPTIONS = 10;
185
188
  const QUESTION_MAX_CONFIRM_ROWS = 3;
@@ -396,6 +399,7 @@ function OpenTuiApp(props) {
396
399
  redrawProviderDialog();
397
400
  redrawApprovalPanel();
398
401
  redrawQuestionPanel();
402
+ redrawStatsPanel();
399
403
  redrawFeishuSetupPanel();
400
404
  setSidebarTick((tick) => tick + 1);
401
405
  renderer.requestRender();
@@ -483,6 +487,7 @@ function OpenTuiApp(props) {
483
487
  const [pendingQuestion, setPendingQuestion] = createSignal();
484
488
  const [pendingFeedback, setPendingFeedback] = createSignal();
485
489
  const [pendingFeishuSetup, setPendingFeishuSetup] = createSignal();
490
+ let statsPanel;
486
491
  const questionSyncTimers = new Set();
487
492
  let feishuSetupAbortController;
488
493
  let pendingApprovalRef;
@@ -490,6 +495,8 @@ function OpenTuiApp(props) {
490
495
  const [approvalOptionIdx, setApprovalOptionIdx] = createSignal(0);
491
496
  let picker;
492
497
  let providerDialog;
498
+ let providerDialogModelItems;
499
+ let providerDialogModelRefreshId = 0;
493
500
  let previousPickerForKey;
494
501
  let homePromptRef;
495
502
  let sessionPromptRef;
@@ -546,6 +553,17 @@ function OpenTuiApp(props) {
546
553
  let feedbackPreviewShell;
547
554
  let feedbackPreviewText;
548
555
  let feedbackFooterText;
556
+ let statsRoot;
557
+ let statsPanelBox;
558
+ let statsTitle;
559
+ let statsEsc;
560
+ let statsTab7Box;
561
+ let statsTab30Box;
562
+ let statsTab7Text;
563
+ let statsTab30Text;
564
+ let statsBodyScroll;
565
+ let statsBodyText;
566
+ let statsFooterText;
549
567
  let feishuSetupRoot;
550
568
  let feishuSetupPanel;
551
569
  let feishuSetupTitle;
@@ -713,6 +731,12 @@ function OpenTuiApp(props) {
713
731
  feedbackRoot?.focus();
714
732
  }, 0);
715
733
  }
734
+ function focusStatsPanel() {
735
+ setTimeout(() => {
736
+ if (statsPanel)
737
+ statsRoot?.focus();
738
+ }, 0);
739
+ }
716
740
  function focusFeishuSetupPanel() {
717
741
  setTimeout(() => {
718
742
  const state = pendingFeishuSetup();
@@ -852,7 +876,7 @@ function OpenTuiApp(props) {
852
876
  return false;
853
877
  return !!event.shift;
854
878
  };
855
- const canInsertPromptNewline = () => !pendingApproval() && !pendingPlan() && !pendingQuestion() && !pendingFeedback() && !pendingFeishuSetup();
879
+ const canInsertPromptNewline = () => !pendingApproval() && !pendingPlan() && !pendingQuestion() && !pendingFeedback() && !statsPanel && !pendingFeishuSetup();
856
880
  const sidebarFits = () => dimensions().width > SESSION_SIDEBAR_WIDTH + 40;
857
881
  const sidebarVisible = () => {
858
882
  if (!sessionActive())
@@ -1081,7 +1105,7 @@ function OpenTuiApp(props) {
1081
1105
  if (!safeSetText(ref, promptModeBadge()))
1082
1106
  promptModeLabels.delete(ref);
1083
1107
  };
1084
- const promptModelTitle = () => displayModel(props.agent.model) || "no model";
1108
+ const promptModelTitle = () => displayModelWithThinking(props.agent.model, props.agent.thinking) || "no model";
1085
1109
  const syncModelChrome = () => {
1086
1110
  if (uiDisposed)
1087
1111
  return;
@@ -1193,6 +1217,7 @@ function OpenTuiApp(props) {
1193
1217
  switch (request.type) {
1194
1218
  case "bash": return "Bash";
1195
1219
  case "edit": return "Edit";
1220
+ case "patch": return "Patch";
1196
1221
  case "write": return "Write";
1197
1222
  case "lsp": return "Lsp";
1198
1223
  }
@@ -1798,6 +1823,71 @@ function OpenTuiApp(props) {
1798
1823
  }
1799
1824
  return false;
1800
1825
  }
1826
+ function openStatsPanel() {
1827
+ picker = undefined;
1828
+ providerDialog = undefined;
1829
+ redrawProviderDialog();
1830
+ statsPanel = {
1831
+ range: "30d",
1832
+ bundle: collectUsageStatsBundle(),
1833
+ };
1834
+ activePrompt()?.clear();
1835
+ activePrompt()?.blur();
1836
+ promptText = "";
1837
+ syncStatsUI(true);
1838
+ }
1839
+ function closeStatsPanel() {
1840
+ statsPanel = undefined;
1841
+ syncStatsUI(false);
1842
+ restorePromptAfterModal();
1843
+ if (queuedInputCount() > 0)
1844
+ scheduleQueuedInputDrain();
1845
+ }
1846
+ function syncStatsUI(focus = false) {
1847
+ redrawStatsPanel();
1848
+ syncPromptSurfaces();
1849
+ redrawDock();
1850
+ rootBox?.requestRender();
1851
+ scrollbox?.requestRender();
1852
+ if (focus || statsPanel)
1853
+ focusStatsPanel();
1854
+ }
1855
+ function setStatsRange(range) {
1856
+ if (!statsPanel || statsPanel.range === range)
1857
+ return;
1858
+ statsPanel = { ...statsPanel, range };
1859
+ redrawStatsPanel();
1860
+ }
1861
+ function handleStatsKey(event) {
1862
+ if (!statsPanel)
1863
+ return false;
1864
+ const name = keyNameFromEvent(event);
1865
+ if (name === "escape") {
1866
+ closeStatsPanel();
1867
+ event.preventDefault?.();
1868
+ event.stopPropagation?.();
1869
+ return true;
1870
+ }
1871
+ if (name === "left" || name === "h") {
1872
+ setStatsRange("7d");
1873
+ event.preventDefault?.();
1874
+ event.stopPropagation?.();
1875
+ return true;
1876
+ }
1877
+ if (name === "right" || name === "l") {
1878
+ setStatsRange("30d");
1879
+ event.preventDefault?.();
1880
+ event.stopPropagation?.();
1881
+ return true;
1882
+ }
1883
+ if (name === "tab") {
1884
+ setStatsRange(statsPanel.range === "30d" ? "7d" : "30d");
1885
+ event.preventDefault?.();
1886
+ event.stopPropagation?.();
1887
+ return true;
1888
+ }
1889
+ return true;
1890
+ }
1801
1891
  function openFeishuSetup() {
1802
1892
  picker = undefined;
1803
1893
  providerDialog = undefined;
@@ -2048,6 +2138,8 @@ function OpenTuiApp(props) {
2048
2138
  return "question";
2049
2139
  if (pendingFeedback())
2050
2140
  return "feedback";
2141
+ if (statsPanel)
2142
+ return "stats";
2051
2143
  if (providerDialog)
2052
2144
  return "provider";
2053
2145
  if (pendingFeishuSetup())
@@ -2067,6 +2159,8 @@ function OpenTuiApp(props) {
2067
2159
  return handleQuestionKey(event);
2068
2160
  case "feedback":
2069
2161
  return handleFeedbackKey(event);
2162
+ case "stats":
2163
+ return handleStatsKey(event);
2070
2164
  case "provider":
2071
2165
  return handleProviderDialogKey(event);
2072
2166
  case "feishu":
@@ -2084,6 +2178,8 @@ function OpenTuiApp(props) {
2084
2178
  }
2085
2179
  if (owner === "feedback")
2086
2180
  return pendingFeedback()?.stage !== "edit";
2181
+ if (owner === "stats")
2182
+ return true;
2087
2183
  if (owner === "feishu")
2088
2184
  return pendingFeishuSetup()?.kind !== "binding";
2089
2185
  return false;
@@ -2223,14 +2319,14 @@ function OpenTuiApp(props) {
2223
2319
  return currentTranscriptMessages(extra).some((message) => hasRenderableMessage(message, effectiveShowThinking()));
2224
2320
  }
2225
2321
  function isHomeSurfaceActive(extra) {
2226
- return !hasTranscriptMessages(extra) && !pendingPlan() && !pendingQuestion() && !pendingFeedback() && !pendingFeishuSetup();
2322
+ return !hasTranscriptMessages(extra) && !pendingPlan() && !pendingQuestion() && !pendingFeedback() && !statsPanel && !pendingFeishuSetup();
2227
2323
  }
2228
2324
  function syncPromptSurfaces(focus = false) {
2229
2325
  const homeActive = isHomeSurfaceActive(streamingDisplay);
2230
2326
  const nextSessionActive = !homeActive;
2231
2327
  const surfaceChanged = sessionActive() !== nextSessionActive;
2232
2328
  setSessionActive(nextSessionActive);
2233
- const modalComposerHidden = !!pendingQuestion() || !!pendingFeedback() || !!pendingFeishuSetup();
2329
+ const modalComposerHidden = !!pendingQuestion() || !!pendingFeedback() || !!statsPanel || !!pendingFeishuSetup();
2234
2330
  if (homeSurfaceShell)
2235
2331
  homeSurfaceShell.visible = homeActive;
2236
2332
  if (homeComposerShell)
@@ -2333,6 +2429,18 @@ function OpenTuiApp(props) {
2333
2429
  redrawTranscriptWithQueuedDisplays();
2334
2430
  return changed;
2335
2431
  }
2432
+ function removeQueuedUserDisplay(displayId) {
2433
+ if (!displayId)
2434
+ return false;
2435
+ const beforeDisplayCount = displayMessages.length;
2436
+ const beforeQueuedCount = queuedDisplayMessages.length;
2437
+ displayMessages = displayMessages.filter((message) => message.clientId !== displayId);
2438
+ queuedDisplayMessages = queuedDisplayMessages.filter((message) => message.clientId !== displayId);
2439
+ const changed = displayMessages.length !== beforeDisplayCount || queuedDisplayMessages.length !== beforeQueuedCount;
2440
+ if (changed)
2441
+ redrawTranscriptWithQueuedDisplays();
2442
+ return changed;
2443
+ }
2336
2444
  function promoteQueuedUserDisplay(displayId, fallbackContent) {
2337
2445
  if (!displayId)
2338
2446
  return false;
@@ -2425,6 +2533,7 @@ function OpenTuiApp(props) {
2425
2533
  || pendingPlan()
2426
2534
  || pendingQuestion()
2427
2535
  || pendingFeedback()
2536
+ || statsPanel
2428
2537
  || providerDialog
2429
2538
  || picker) {
2430
2539
  return;
@@ -2522,6 +2631,10 @@ function OpenTuiApp(props) {
2522
2631
  function cancelActiveAgentRun() {
2523
2632
  if (!activeRun || activeRun.abortController.signal.aborted)
2524
2633
  return false;
2634
+ traceEvent("tui_running_cancel", {
2635
+ runId: activeRun.id,
2636
+ pendingQueuedInputs: queuedInputCount(),
2637
+ }, { surface: "tui" });
2525
2638
  clearRunningCancelHint();
2526
2639
  activeRun.abortController.abort(new AgentAbortError("Agent run cancelled by user."));
2527
2640
  setNotice("Agent run cancelled");
@@ -2556,6 +2669,7 @@ function OpenTuiApp(props) {
2556
2669
  if (!activeRun || activeRun.abortController.signal.aborted)
2557
2670
  return false;
2558
2671
  const shouldCancel = armRunningCancelHint(activeRun);
2672
+ traceKeyRoute(event ? "key" : "raw", name, !shouldCancel ? "armed_cancel" : "confirm_cancel");
2559
2673
  if (!shouldCancel) {
2560
2674
  if (event)
2561
2675
  preventGlobalKey(event);
@@ -2573,33 +2687,72 @@ function OpenTuiApp(props) {
2573
2687
  if (!isRunning() || activeModalKeyOwner())
2574
2688
  return false;
2575
2689
  queuePromptFromComposer({ notice: "Queued next message" });
2690
+ traceKeyRoute(event ? "key" : "raw", name, "queued_next_message");
2576
2691
  if (event)
2577
2692
  preventGlobalKey(event);
2578
2693
  return true;
2579
2694
  }
2695
+ function traceKeyRoute(source, name, result) {
2696
+ const shouldTrace = result !== "unhandled"
2697
+ || name === "escape"
2698
+ || name === "enter"
2699
+ || name === "tab"
2700
+ || name === "up"
2701
+ || name === "down"
2702
+ || name === "left"
2703
+ || name === "right"
2704
+ || name === "ctrl-c"
2705
+ || !!activeModalKeyOwner()
2706
+ || isRunning();
2707
+ if (!shouldTrace)
2708
+ return;
2709
+ traceEvent("tui_key_route", {
2710
+ source,
2711
+ key: name,
2712
+ result,
2713
+ modalOwner: activeModalKeyOwner(),
2714
+ running: isRunning(),
2715
+ activeRunId: activeRun?.id,
2716
+ pendingApproval: !!pendingApproval(),
2717
+ pendingPlan: !!pendingPlan(),
2718
+ pendingQuestion: !!pendingQuestion(),
2719
+ providerDialog: !!providerDialog,
2720
+ picker: !!picker,
2721
+ }, { surface: "tui" });
2722
+ }
2580
2723
  function routeGlobalRawSequence(sequence) {
2581
2724
  if (isCtrlCSequence(sequence)) {
2582
2725
  void requestExit({ direct: true });
2726
+ traceKeyRoute("raw", "ctrl-c", "exit");
2583
2727
  return true;
2584
2728
  }
2585
2729
  const name = keyNameFromSequence(sequence);
2586
2730
  const modalName = modalKeyNameFromSequence(sequence);
2587
- if (routeModalRawSequence(sequence))
2731
+ if (routeModalRawSequence(sequence)) {
2732
+ traceKeyRoute("raw", modalName || name, "modal");
2588
2733
  return true;
2734
+ }
2589
2735
  if (routeRunningCancel(name))
2590
2736
  return true;
2591
2737
  if (routeRunningQueue(modalName))
2592
2738
  return true;
2593
- if (cycleModeFromRawSequence(sequence))
2739
+ if (cycleModeFromRawSequence(sequence)) {
2740
+ traceKeyRoute("raw", name, "mode_cycle");
2594
2741
  return true;
2742
+ }
2743
+ traceKeyRoute("raw", name, "unhandled");
2595
2744
  return false;
2596
2745
  }
2597
2746
  function routeGlobalKeyEvent(event) {
2598
- if (routeCtrlCExit(event))
2747
+ if (routeCtrlCExit(event)) {
2748
+ traceKeyRoute("key", "ctrl-c", "exit");
2599
2749
  return true;
2750
+ }
2600
2751
  const name = keyNameFromEvent(event);
2601
- if (routeModalKey(event))
2752
+ if (routeModalKey(event)) {
2753
+ traceKeyRoute("key", name, "modal");
2602
2754
  return true;
2755
+ }
2603
2756
  if (routeRunningCancel(name, event))
2604
2757
  return true;
2605
2758
  if (routeRunningQueue(name, event))
@@ -2609,25 +2762,32 @@ function OpenTuiApp(props) {
2609
2762
  if (event.ctrl && event.shift && name === "m") {
2610
2763
  openMcpReconnectPicker();
2611
2764
  event.preventDefault?.();
2765
+ traceKeyRoute("key", name, "mcp_picker");
2612
2766
  return true;
2613
2767
  }
2614
2768
  if (event.ctrl && name === "t" && !picker) {
2615
2769
  toggleThinkingVisibility();
2616
2770
  event.preventDefault?.();
2771
+ traceKeyRoute("key", name, "toggle_thinking");
2617
2772
  return true;
2618
2773
  }
2619
2774
  if (event.ctrl && name === "o" && !picker) {
2620
2775
  toggleVerboseTrace();
2621
2776
  event.preventDefault?.();
2777
+ traceKeyRoute("key", name, "toggle_verbose_trace");
2622
2778
  return true;
2623
2779
  }
2624
- if (cycleModeFromKey(event))
2780
+ if (cycleModeFromKey(event)) {
2781
+ traceKeyRoute("key", name, "mode_cycle");
2625
2782
  return true;
2783
+ }
2626
2784
  if (event.ctrl && name === "p" && !picker && !isRunning()) {
2627
2785
  openCommandPalette();
2628
2786
  event.preventDefault?.();
2787
+ traceKeyRoute("key", name, "command_palette");
2629
2788
  return true;
2630
2789
  }
2790
+ traceKeyRoute("key", name, "unhandled");
2631
2791
  return false;
2632
2792
  }
2633
2793
  function transcriptOptions() {
@@ -2739,6 +2899,7 @@ function OpenTuiApp(props) {
2739
2899
  sessionActive();
2740
2900
  syncSidebarChrome();
2741
2901
  redrawQuestionPanel();
2902
+ redrawStatsPanel();
2742
2903
  redrawFeishuSetupPanel();
2743
2904
  scrollbox?.requestRender();
2744
2905
  scheduleTranscriptScrollAfterUpdate(shouldFollow);
@@ -2800,6 +2961,13 @@ function OpenTuiApp(props) {
2800
2961
  dock?.requestRender();
2801
2962
  }
2802
2963
  function openProviderDialog(step = "providers", providerId) {
2964
+ if (step === "models") {
2965
+ providerDialogModelItems = undefined;
2966
+ }
2967
+ else {
2968
+ providerDialogModelRefreshId++;
2969
+ providerDialogModelItems = undefined;
2970
+ }
2803
2971
  const items = providerDialogItemsFor(step, providerId);
2804
2972
  picker = undefined;
2805
2973
  providerDialog = {
@@ -2815,9 +2983,14 @@ function OpenTuiApp(props) {
2815
2983
  redrawDock();
2816
2984
  redrawProviderDialog();
2817
2985
  setTimeout(() => providerDialogInput?.focus(), 0);
2986
+ if (step === "models") {
2987
+ void refreshProviderDialogModelItems(providerId, items);
2988
+ }
2818
2989
  }
2819
2990
  function closeProviderDialog() {
2820
2991
  providerDialog = undefined;
2992
+ providerDialogModelRefreshId++;
2993
+ providerDialogModelItems = undefined;
2821
2994
  providerDialogRoot && (providerDialogRoot.visible = false);
2822
2995
  providerDialogPanel && (providerDialogPanel.visible = false);
2823
2996
  providerDialogRoot?.requestRender();
@@ -2833,6 +3006,9 @@ function OpenTuiApp(props) {
2833
3006
  if (step === "skills")
2834
3007
  return buildSkillItems();
2835
3008
  if (step === "models") {
3009
+ if (providerDialogModelItems?.key === modelPickerCacheKey(providerId)) {
3010
+ return providerDialogModelItems.items;
3011
+ }
2836
3012
  const modelItems = buildPickerItems("model", providerId);
2837
3013
  if (modelItems.length || providerId)
2838
3014
  return modelItems;
@@ -2843,6 +3019,59 @@ function OpenTuiApp(props) {
2843
3019
  }
2844
3020
  return [];
2845
3021
  }
3022
+ function modelPickerCacheKey(providerId) {
3023
+ return providerId || "__all__";
3024
+ }
3025
+ async function refreshProviderDialogModelItems(providerId, localItems) {
3026
+ const refreshId = ++providerDialogModelRefreshId;
3027
+ const cacheKey = modelPickerCacheKey(providerId);
3028
+ const localPreferredIndex = preferredPickerIndex("model", localItems);
3029
+ try {
3030
+ const remoteItems = await buildRemoteModelPickerItems(providerId);
3031
+ if (refreshId !== providerDialogModelRefreshId)
3032
+ return;
3033
+ if (remoteItems.length === 0)
3034
+ return;
3035
+ const state = providerDialog;
3036
+ if (!state || state.step !== "models" || modelPickerCacheKey(state.providerId) !== cacheKey)
3037
+ return;
3038
+ providerDialogModelItems = { key: cacheKey, items: remoteItems };
3039
+ const remotePreferredIndex = preferredPickerIndex("model", remoteItems);
3040
+ const nextIndex = state.index === localPreferredIndex
3041
+ ? remotePreferredIndex
3042
+ : Math.min(state.index, Math.max(0, remoteItems.length - 1));
3043
+ providerDialog = { ...state, index: nextIndex };
3044
+ redrawProviderDialog();
3045
+ }
3046
+ catch {
3047
+ // Keep the already-rendered local catalog when remote model discovery fails.
3048
+ }
3049
+ }
3050
+ function providerDialogMatchScore(item, query) {
3051
+ const label = (item.label || "").toLowerCase();
3052
+ const value = (item.value || "").toLowerCase();
3053
+ const haystack = [
3054
+ item.label,
3055
+ item.detail,
3056
+ item.value,
3057
+ item.category,
3058
+ item.footer,
3059
+ ].filter(Boolean).join(" ").toLowerCase();
3060
+ if (label.startsWith(query))
3061
+ return 100;
3062
+ if (label.includes(query))
3063
+ return 80;
3064
+ if (value.includes(query))
3065
+ return 60;
3066
+ if (haystack.includes(query))
3067
+ return 40;
3068
+ // Fuzzy (subsequence) match is a last resort, and only against label+value
3069
+ // so long provider descriptions (e.g. "platform.moonshot.cn") don't produce
3070
+ // spurious hits like "gpt" matching "kimi-k2-thinking".
3071
+ if (fuzzyMatch(`${label} ${value}`, query))
3072
+ return 20;
3073
+ return 0;
3074
+ }
2846
3075
  function providerDialogFilteredItems(state = providerDialog) {
2847
3076
  if (!state || state.step === "key")
2848
3077
  return [];
@@ -2850,16 +3079,12 @@ function OpenTuiApp(props) {
2850
3079
  const query = state.query.trim().toLowerCase();
2851
3080
  if (!query)
2852
3081
  return items;
2853
- return items.filter((item) => {
2854
- const haystack = [
2855
- item.label,
2856
- item.detail,
2857
- item.value,
2858
- item.category,
2859
- item.footer,
2860
- ].filter(Boolean).join(" ").toLowerCase();
2861
- return haystack.includes(query) || fuzzyMatch(haystack, query);
2862
- });
3082
+ const scored = items
3083
+ .map((item, order) => ({ item, order, score: providerDialogMatchScore(item, query) }))
3084
+ .filter((entry) => entry.score > 0);
3085
+ // Stable sort by score desc, preserving original catalog order within a tier.
3086
+ scored.sort((a, b) => b.score - a.score || a.order - b.order);
3087
+ return scored.map((entry) => entry.item);
2863
3088
  }
2864
3089
  function providerDialogVisibleRows(state = providerDialog) {
2865
3090
  if (!state)
@@ -2900,7 +3125,7 @@ function OpenTuiApp(props) {
2900
3125
  providerDialogRoot.requestRender();
2901
3126
  return;
2902
3127
  }
2903
- const width = Math.max(48, Math.min(60, dimensions().width - 2));
3128
+ const width = Math.max(56, Math.min(76, dimensions().width - 4));
2904
3129
  const height = PROVIDER_DIALOG_ROWS + 7;
2905
3130
  providerDialogRoot.visible = true;
2906
3131
  providerDialogRoot.width = dimensions().width;
@@ -3434,6 +3659,127 @@ function OpenTuiApp(props) {
3434
3659
  feedbackRoot.requestRender();
3435
3660
  feedbackInput?.requestRender();
3436
3661
  }
3662
+ function redrawStatsPanel() {
3663
+ if (!statsRoot)
3664
+ return;
3665
+ const state = statsPanel;
3666
+ if (!state) {
3667
+ statsRoot.visible = false;
3668
+ statsPanelBox && (statsPanelBox.visible = false);
3669
+ statsRoot.requestRender();
3670
+ return;
3671
+ }
3672
+ const terminalWidth = dimensions().width;
3673
+ const terminalHeight = dimensions().height;
3674
+ const width = Math.max(56, Math.min(84, terminalWidth - 4));
3675
+ const bodyWidth = Math.max(48, width - 8);
3676
+ const stats = state.bundle.ranges[state.range];
3677
+ const body = formatStatsPanelBody(stats, bodyWidth);
3678
+ const bodyLines = body.split("\n");
3679
+ const height = Math.min(Math.max(22, bodyLines.length + 7), Math.max(18, terminalHeight - 4));
3680
+ const bodyHeight = Math.max(8, height - 8);
3681
+ statsRoot.visible = true;
3682
+ statsRoot.width = terminalWidth;
3683
+ statsRoot.height = terminalHeight;
3684
+ statsRoot.left = 0;
3685
+ statsRoot.top = 0;
3686
+ statsRoot.backgroundColor = modalBackdropColor();
3687
+ if (statsPanelBox) {
3688
+ statsPanelBox.visible = true;
3689
+ statsPanelBox.width = width;
3690
+ statsPanelBox.height = height;
3691
+ statsPanelBox.left = Math.max(0, Math.floor((terminalWidth - width) / 2));
3692
+ statsPanelBox.top = Math.max(0, Math.floor((terminalHeight - height) / 3));
3693
+ statsPanelBox.backgroundColor = theme.backgroundPanel;
3694
+ statsPanelBox.borderColor = theme.backgroundPanel;
3695
+ }
3696
+ if (statsTitle)
3697
+ statsTitle.content = "Stats";
3698
+ if (statsEsc)
3699
+ statsEsc.content = "esc";
3700
+ syncStatsTab(statsTab7Box, statsTab7Text, state.range === "7d", "7 days");
3701
+ syncStatsTab(statsTab30Box, statsTab30Text, state.range === "30d", "30 days");
3702
+ if (statsBodyText) {
3703
+ statsBodyText.content = statsPanelBodyStyledText(stats, bodyWidth);
3704
+ statsBodyText.width = bodyWidth;
3705
+ }
3706
+ if (statsBodyScroll) {
3707
+ statsBodyScroll.width = bodyWidth;
3708
+ statsBodyScroll.height = bodyHeight;
3709
+ statsBodyScroll.requestRender();
3710
+ }
3711
+ if (statsFooterText) {
3712
+ statsFooterText.content = statsFooterHint(state.range);
3713
+ statsFooterText.width = bodyWidth;
3714
+ statsFooterText.bg = theme.backgroundPanel;
3715
+ }
3716
+ statsPanelBox?.requestRender();
3717
+ statsRoot.requestRender();
3718
+ }
3719
+ function statsFooterHint(range) {
3720
+ return `left/right:range|tab:toggle|esc:close|view:${range}`;
3721
+ }
3722
+ function statsPanelBodyStyledText(stats, width) {
3723
+ const chunks = [];
3724
+ const lines = formatStatsPanelBody(stats, width).split("\n");
3725
+ for (let index = 0; index < lines.length; index += 1) {
3726
+ appendStatsPanelLine(chunks, lines[index]);
3727
+ if (index < lines.length - 1)
3728
+ chunks.push(fg(theme.text)("\n"));
3729
+ }
3730
+ return new StyledText(chunks);
3731
+ }
3732
+ function appendStatsPanelLine(chunks, line) {
3733
+ if (isStatsHeatmapWeekdayLine(line)) {
3734
+ chunks.push(fg(theme.text)(line.slice(0, 5)));
3735
+ appendStatsHeatmapDots(chunks, line.slice(5));
3736
+ return;
3737
+ }
3738
+ if (line.trim() === "Less . o O @ More") {
3739
+ appendStatsHeatmapLegend(chunks, line.length - line.trimStart().length);
3740
+ return;
3741
+ }
3742
+ chunks.push(fg(theme.text)(line));
3743
+ }
3744
+ function isStatsHeatmapWeekdayLine(line) {
3745
+ return /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) /.test(line);
3746
+ }
3747
+ function appendStatsHeatmapDots(chunks, text) {
3748
+ const colors = statsHeatmapDotColors();
3749
+ const colorByLevel = {
3750
+ ".": colors[0],
3751
+ o: colors[1],
3752
+ O: colors[2],
3753
+ "@": colors[3],
3754
+ };
3755
+ for (const char of text) {
3756
+ const color = colorByLevel[char];
3757
+ chunks.push(color ? fg(color)("•") : fg(theme.text)(char));
3758
+ }
3759
+ }
3760
+ function appendStatsHeatmapLegend(chunks, indent) {
3761
+ const colors = statsHeatmapDotColors();
3762
+ chunks.push(fg(theme.textMuted)(`${" ".repeat(indent)}Less `));
3763
+ colors.forEach((color, index) => {
3764
+ if (index > 0)
3765
+ chunks.push(fg(theme.textMuted)(" "));
3766
+ chunks.push(fg(color)("•"));
3767
+ });
3768
+ chunks.push(fg(theme.textMuted)(" More"));
3769
+ }
3770
+ function statsHeatmapDotColors() {
3771
+ return isLightTheme()
3772
+ ? ["#D9B98E", "#BE7D37", "#A56218", theme.warning]
3773
+ : ["#6B471D", "#9D6728", "#D18830", theme.warning];
3774
+ }
3775
+ function syncStatsTab(box, text, active, label) {
3776
+ if (box)
3777
+ box.backgroundColor = active ? theme.primary : theme.backgroundElement;
3778
+ if (text) {
3779
+ text.content = label;
3780
+ text.fg = active ? contrastText(theme.primary) : theme.textMuted;
3781
+ }
3782
+ }
3437
3783
  function redrawFeishuSetupPanel() {
3438
3784
  if (!feishuSetupRoot)
3439
3785
  return;
@@ -4340,6 +4686,7 @@ function OpenTuiApp(props) {
4340
4686
  toggleSidebar,
4341
4687
  setSidebarMode: applySidebarMode,
4342
4688
  openFeedback,
4689
+ openStats: openStatsPanel,
4343
4690
  });
4344
4691
  if (!handled)
4345
4692
  return false;
@@ -4518,6 +4865,60 @@ function OpenTuiApp(props) {
4518
4865
  await openPicker(item.after.mode, item.after.providerId);
4519
4866
  }
4520
4867
  }
4868
+ function buildLocalModelPickerItems(providerId) {
4869
+ const groups = getVisibleModelProviders(registry, providerId).map((provider) => ({
4870
+ provider,
4871
+ models: localModelsForProvider(registry, provider),
4872
+ }));
4873
+ return buildModelPickerItemsFromGroups(groups, providerId);
4874
+ }
4875
+ async function buildRemoteModelPickerItems(providerId) {
4876
+ const groups = await discoverModelProviderGroups(registry, providerId);
4877
+ return buildModelPickerItemsFromGroups(groups, providerId);
4878
+ }
4879
+ function buildModelPickerItemsFromGroups(groups, providerId) {
4880
+ const items = [];
4881
+ for (const { provider, models } of groups) {
4882
+ for (const model of models) {
4883
+ const reasoningLevels = getModelPickerReasoningLevels(provider.id, model.id);
4884
+ if (reasoningLevels.length > 0) {
4885
+ for (const level of reasoningLevels) {
4886
+ const isCurrent = props.agent.model === `${provider.id}:${model.id}` && props.agent.thinking === level;
4887
+ items.push({
4888
+ label: `${model.name} (${level})`,
4889
+ detail: isCurrent ? "(current)" : undefined,
4890
+ value: `${provider.id}:${model.id}`,
4891
+ command: `/model ${provider.id}:${model.id} --reasoning-effort ${level}`,
4892
+ category: provider.name,
4893
+ gutter: isCurrent ? "●" : undefined,
4894
+ });
4895
+ }
4896
+ continue;
4897
+ }
4898
+ const isCurrent = props.agent.model === `${provider.id}:${model.id}`;
4899
+ items.push({
4900
+ label: model.name,
4901
+ detail: isCurrent ? "(current)" : undefined,
4902
+ value: `${provider.id}:${model.id}`,
4903
+ command: `/model ${provider.id}:${model.id}`,
4904
+ category: provider.name,
4905
+ gutter: isCurrent ? "●" : undefined,
4906
+ });
4907
+ }
4908
+ }
4909
+ const currentModel = props.agent.model;
4910
+ if (!providerId && currentModel && !items.some((item) => item.value === currentModel)) {
4911
+ items.unshift({
4912
+ label: displayModel(currentModel),
4913
+ detail: "(current)",
4914
+ value: currentModel,
4915
+ command: `/model ${currentModel}`,
4916
+ category: "Recent",
4917
+ gutter: "●",
4918
+ });
4919
+ }
4920
+ return items;
4921
+ }
4521
4922
  function buildPickerItems(kind, providerId) {
4522
4923
  if (kind === "slash")
4523
4924
  return [];
@@ -4526,60 +4927,7 @@ function OpenTuiApp(props) {
4526
4927
  if (kind === "skill")
4527
4928
  return buildSkillItems();
4528
4929
  if (kind === "model") {
4529
- const items = [];
4530
- for (const provider of registry.getEnabled()) {
4531
- if (providerId && provider.id !== providerId)
4532
- continue;
4533
- const customModels = registry.getModelConfig().getCustomModels(provider.id);
4534
- const builtinProviderId = provider.id === "openai" && provider.authType === "oauth"
4535
- ? "openai-codex"
4536
- : provider.id;
4537
- const models = customModels.length > 0
4538
- ? customModels
4539
- : listBuiltinModels(builtinProviderId).map((model) => ({
4540
- id: model.id,
4541
- name: model.name,
4542
- providerId: provider.id,
4543
- }));
4544
- for (const model of models) {
4545
- const reasoningLevels = getModelPickerReasoningLevels(provider.id, model.id);
4546
- if (reasoningLevels.length > 0) {
4547
- for (const level of reasoningLevels) {
4548
- const isCurrent = props.agent.model === `${provider.id}:${model.id}` && props.agent.thinking === level;
4549
- items.push({
4550
- label: `${model.name} (${level})`,
4551
- detail: isCurrent ? "(current)" : undefined,
4552
- value: `${provider.id}:${model.id}`,
4553
- command: `/model ${provider.id}:${model.id} --reasoning-effort ${level}`,
4554
- category: provider.name,
4555
- gutter: isCurrent ? "●" : undefined,
4556
- });
4557
- }
4558
- continue;
4559
- }
4560
- const isCurrent = props.agent.model === `${provider.id}:${model.id}`;
4561
- items.push({
4562
- label: model.name,
4563
- detail: isCurrent ? "(current)" : undefined,
4564
- value: `${provider.id}:${model.id}`,
4565
- command: `/model ${provider.id}:${model.id}`,
4566
- category: provider.name,
4567
- gutter: isCurrent ? "●" : undefined,
4568
- });
4569
- }
4570
- }
4571
- const currentModel = props.agent.model;
4572
- if (!providerId && currentModel && !items.some((item) => item.value === currentModel)) {
4573
- items.unshift({
4574
- label: displayModel(currentModel),
4575
- detail: "(current)",
4576
- value: currentModel,
4577
- command: `/model ${currentModel}`,
4578
- category: "Recent",
4579
- gutter: "●",
4580
- });
4581
- }
4582
- return items;
4930
+ return buildLocalModelPickerItems(providerId);
4583
4931
  }
4584
4932
  if (kind === "provider") {
4585
4933
  return buildProviderConnectItems();
@@ -4761,6 +5109,15 @@ function OpenTuiApp(props) {
4761
5109
  redrawTranscript(undefined, nextMessages);
4762
5110
  const taskStartedAt = Date.now();
4763
5111
  const run = beginAgentRun();
5112
+ traceEvent("tui_agent_run_begin", {
5113
+ runId: run.id,
5114
+ input: summarizeTraceValue(actualInput),
5115
+ displayInput: summarizeTraceValue(displayInput),
5116
+ displayMessages: displayMessages.length,
5117
+ queuedInputs: queuedInputCount(),
5118
+ provider: activeProviderId,
5119
+ model: props.agent.apiModel,
5120
+ }, { surface: "tui" });
4764
5121
  let assistantContent = "";
4765
5122
  let assistantReasoning = "";
4766
5123
  const toolCalls = [];
@@ -4805,6 +5162,14 @@ function OpenTuiApp(props) {
4805
5162
  abortSignal: run.abortController.signal,
4806
5163
  inputController: run.inputController,
4807
5164
  })) {
5165
+ traceEvent("tui_agent_event", {
5166
+ runId: run.id,
5167
+ event: summarizeAgentEventForTrace(event),
5168
+ displayMessages: displayMessages.length,
5169
+ streamingChars: assistantContent.length,
5170
+ reasoningChars: assistantReasoning.length,
5171
+ toolCount: toolCalls.length,
5172
+ }, { surface: "tui" });
4808
5173
  if (event.type === "turn_start") {
4809
5174
  assistantContent = "";
4810
5175
  assistantReasoning = "";
@@ -5011,6 +5376,11 @@ function OpenTuiApp(props) {
5011
5376
  if (!runCancelled) {
5012
5377
  runError = error?.message || String(error);
5013
5378
  }
5379
+ traceEvent("tui_agent_run_error", {
5380
+ runId: run.id,
5381
+ cancelled: runCancelled,
5382
+ error: summarizeTraceError(error),
5383
+ }, { surface: "tui" });
5014
5384
  }
5015
5385
  finally {
5016
5386
  if (pendingStreamingRedrawTimer !== undefined) {
@@ -5020,8 +5390,19 @@ function OpenTuiApp(props) {
5020
5390
  pendingApprovalRef = undefined;
5021
5391
  setPendingApproval(undefined);
5022
5392
  setApprovalOptionIdx(0);
5393
+ traceEvent("tui_agent_run_end", {
5394
+ runId: run.id,
5395
+ cancelled: runCancelled,
5396
+ error: runError,
5397
+ displayMessages: displayMessages.length,
5398
+ queuedInputs: queuedInputCount(),
5399
+ }, { surface: "tui" });
5023
5400
  for (const pendingInput of run.inputController.clear()) {
5024
5401
  const pendingSteer = removePendingSteerInput(pendingInput.id);
5402
+ if (runCancelled) {
5403
+ removeQueuedUserDisplay(pendingSteer?.displayId);
5404
+ continue;
5405
+ }
5025
5406
  requeueRejectedSteer(pendingInput.content, pendingSteer?.displayId);
5026
5407
  }
5027
5408
  finishAgentRun(run);
@@ -5035,6 +5416,7 @@ function OpenTuiApp(props) {
5035
5416
  else if (runCancelled) {
5036
5417
  if (!notice())
5037
5418
  setNotice("Agent run cancelled");
5419
+ displayMessages = reconstructDisplayMessages(props.agent.messages);
5038
5420
  redrawTranscript();
5039
5421
  }
5040
5422
  else {
@@ -5074,13 +5456,13 @@ function OpenTuiApp(props) {
5074
5456
  return h("box", {
5075
5457
  ref: (ref) => {
5076
5458
  sessionComposerShell = ref;
5077
- ref.visible = !isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !pendingFeedback() && !pendingFeishuSetup();
5459
+ ref.visible = !isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !pendingFeedback() && !statsPanel && !pendingFeishuSetup();
5078
5460
  },
5079
5461
  width: "100%",
5080
5462
  paddingLeft: 2,
5081
5463
  paddingRight: 2,
5082
5464
  flexShrink: 0,
5083
- visible: !isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !pendingFeishuSetup(),
5465
+ visible: !isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !statsPanel && !pendingFeishuSetup(),
5084
5466
  }, renderPrompt({
5085
5467
  ref: (ref) => { sessionPromptRef = ref; },
5086
5468
  focused: !isHomeSurfaceActive(streamingDisplay),
@@ -5091,7 +5473,7 @@ function OpenTuiApp(props) {
5091
5473
  onKeyDown: handlePickerKey,
5092
5474
  onUiKeyDown: promptUiKeyDown,
5093
5475
  getText: readPromptText,
5094
- disabled: () => !!pendingFeedback(),
5476
+ disabled: () => !!pendingFeedback() || !!statsPanel,
5095
5477
  mode,
5096
5478
  registerModeLabel: registerPromptModeLabel,
5097
5479
  registerModelLabel: registerPromptModelLabel,
@@ -5106,6 +5488,8 @@ function OpenTuiApp(props) {
5106
5488
  return "Answer the question below";
5107
5489
  if (pendingFeedback())
5108
5490
  return "Describe feedback below";
5491
+ if (statsPanel)
5492
+ return "Stats panel is open";
5109
5493
  const plan = pendingPlan();
5110
5494
  if (plan)
5111
5495
  return "Press Enter to approve plan or Esc to reject";
@@ -5132,18 +5516,22 @@ function OpenTuiApp(props) {
5132
5516
  paddingRight: 2,
5133
5517
  }, [
5134
5518
  h("box", { flexShrink: 0, flexDirection: "column", alignItems: "center" }, ...logoLines.map((line) => renderHomeLogoLine(line))),
5519
+ h("box", { flexShrink: 0, flexDirection: "column", alignItems: "center", paddingTop: 1 }, h("text", { fg: theme.textMuted, content: `v${getCurrentVersion()}` })),
5520
+ ...(props.options.updateNotice
5521
+ ? [h("box", { flexShrink: 0, flexDirection: "column", alignItems: "center" }, h("text", { fg: theme.accent, content: props.options.updateNotice }))]
5522
+ : []),
5135
5523
  h("box", { height: 1, minHeight: 0, flexShrink: 1 }),
5136
5524
  h("box", {
5137
5525
  ref: (ref) => {
5138
5526
  homeComposerShell = ref;
5139
- ref.visible = isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !pendingFeedback() && !pendingFeishuSetup();
5527
+ ref.visible = isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !pendingFeedback() && !statsPanel && !pendingFeishuSetup();
5140
5528
  },
5141
5529
  width: "100%",
5142
5530
  maxWidth: 75,
5143
5531
  zIndex: 1000,
5144
5532
  paddingTop: 1,
5145
5533
  flexShrink: 0,
5146
- visible: isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !pendingFeishuSetup(),
5534
+ visible: isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !statsPanel && !pendingFeishuSetup(),
5147
5535
  }, renderPrompt({
5148
5536
  ref: (ref) => {
5149
5537
  homePromptRef = ref;
@@ -5158,7 +5546,7 @@ function OpenTuiApp(props) {
5158
5546
  onKeyDown: handlePickerKey,
5159
5547
  onUiKeyDown: promptUiKeyDown,
5160
5548
  getText: readPromptText,
5161
- disabled: () => !!pendingFeedback(),
5549
+ disabled: () => !!pendingFeedback() || !!statsPanel,
5162
5550
  mode,
5163
5551
  registerModeLabel: registerPromptModeLabel,
5164
5552
  registerModelLabel: registerPromptModelLabel,
@@ -5173,6 +5561,8 @@ function OpenTuiApp(props) {
5173
5561
  return "Answer the question below";
5174
5562
  if (pendingFeedback())
5175
5563
  return "Describe feedback below";
5564
+ if (statsPanel)
5565
+ return "Stats panel is open";
5176
5566
  const plan = pendingPlan();
5177
5567
  if (plan)
5178
5568
  return "Press Enter to approve plan or Esc to reject";
@@ -5419,6 +5809,131 @@ function OpenTuiApp(props) {
5419
5809
  content: "ctrl+d submit · tab view payload · enter newline · esc cancel",
5420
5810
  })));
5421
5811
  }
5812
+ function renderStatsPanel() {
5813
+ return h("box", {
5814
+ ref: (ref) => {
5815
+ statsRoot = ref;
5816
+ redrawStatsPanel();
5817
+ },
5818
+ visible: false,
5819
+ focusable: true,
5820
+ position: "absolute",
5821
+ left: 0,
5822
+ top: 0,
5823
+ width: "100%",
5824
+ height: "100%",
5825
+ zIndex: 3050,
5826
+ backgroundColor: modalBackdropColor(),
5827
+ flexDirection: "column",
5828
+ onKeyDown: (event) => {
5829
+ if (handleStatsKey(event)) {
5830
+ event.preventDefault?.();
5831
+ event.stopPropagation?.();
5832
+ return true;
5833
+ }
5834
+ return false;
5835
+ },
5836
+ onMouseUp: () => closeStatsPanel(),
5837
+ }, h("box", {
5838
+ ref: (ref) => {
5839
+ statsPanelBox = ref;
5840
+ redrawStatsPanel();
5841
+ },
5842
+ visible: false,
5843
+ position: "absolute",
5844
+ width: 76,
5845
+ height: 24,
5846
+ backgroundColor: theme.backgroundPanel,
5847
+ flexDirection: "column",
5848
+ paddingTop: 1,
5849
+ onMouseUp: (event) => {
5850
+ event.stopPropagation?.();
5851
+ },
5852
+ }, [
5853
+ h("box", {
5854
+ flexDirection: "row",
5855
+ alignItems: "center",
5856
+ justifyContent: "space-between",
5857
+ paddingLeft: 4,
5858
+ paddingRight: 4,
5859
+ flexShrink: 0,
5860
+ }, h("text", {
5861
+ ref: (ref) => { statsTitle = ref; },
5862
+ fg: theme.text,
5863
+ content: "Stats",
5864
+ }), h("text", {
5865
+ ref: (ref) => { statsEsc = ref; },
5866
+ fg: theme.textMuted,
5867
+ content: "esc",
5868
+ onMouseUp: () => closeStatsPanel(),
5869
+ })),
5870
+ h("box", {
5871
+ flexDirection: "row",
5872
+ gap: 1,
5873
+ paddingLeft: 4,
5874
+ paddingRight: 4,
5875
+ paddingTop: 1,
5876
+ flexShrink: 0,
5877
+ }, h("box", {
5878
+ ref: (ref) => { statsTab7Box = ref; },
5879
+ paddingLeft: 1,
5880
+ paddingRight: 1,
5881
+ backgroundColor: theme.backgroundElement,
5882
+ onMouseUp: () => setStatsRange("7d"),
5883
+ }, h("text", {
5884
+ ref: (ref) => { statsTab7Text = ref; },
5885
+ fg: theme.textMuted,
5886
+ content: "7 days",
5887
+ })), h("box", {
5888
+ ref: (ref) => { statsTab30Box = ref; },
5889
+ paddingLeft: 1,
5890
+ paddingRight: 1,
5891
+ backgroundColor: theme.primary,
5892
+ onMouseUp: () => setStatsRange("30d"),
5893
+ }, h("text", {
5894
+ ref: (ref) => { statsTab30Text = ref; },
5895
+ fg: contrastText(theme.primary),
5896
+ content: "30 days",
5897
+ }))),
5898
+ h("box", {
5899
+ paddingLeft: 4,
5900
+ paddingRight: 4,
5901
+ paddingTop: 1,
5902
+ flexGrow: 1,
5903
+ minHeight: 0,
5904
+ }, h("scrollbox", {
5905
+ ref: (ref) => { statsBodyScroll = ref; },
5906
+ flexGrow: 1,
5907
+ minHeight: 0,
5908
+ height: 14,
5909
+ onMouseScroll: (event) => {
5910
+ event.stopPropagation?.();
5911
+ },
5912
+ }, h("text", {
5913
+ ref: (ref) => { statsBodyText = ref; },
5914
+ fg: theme.text,
5915
+ wrapMode: "none",
5916
+ content: "",
5917
+ }))),
5918
+ h("box", {
5919
+ flexDirection: "row",
5920
+ justifyContent: "space-between",
5921
+ paddingLeft: 4,
5922
+ paddingRight: 4,
5923
+ paddingTop: 1,
5924
+ paddingBottom: 1,
5925
+ flexShrink: 0,
5926
+ backgroundColor: theme.backgroundPanel,
5927
+ }, h("text", {
5928
+ ref: (ref) => { statsFooterText = ref; },
5929
+ fg: theme.textMuted,
5930
+ bg: theme.backgroundPanel,
5931
+ wrapMode: "none",
5932
+ truncate: true,
5933
+ content: "left/right:range|tab:toggle|esc:close|view:30d",
5934
+ })),
5935
+ ]));
5936
+ }
5422
5937
  function renderFeishuSetupPanel() {
5423
5938
  return h("box", {
5424
5939
  ref: (ref) => {
@@ -5507,7 +6022,8 @@ function OpenTuiApp(props) {
5507
6022
  width: "100%",
5508
6023
  value: "",
5509
6024
  placeholder: "",
5510
- fg: theme.text,
6025
+ textColor: theme.text,
6026
+ focusedTextColor: theme.text,
5511
6027
  backgroundColor: theme.backgroundElement,
5512
6028
  focusedBackgroundColor: theme.backgroundElement,
5513
6029
  cursorColor: theme.primary,
@@ -5652,7 +6168,7 @@ function OpenTuiApp(props) {
5652
6168
  },
5653
6169
  visible: false,
5654
6170
  position: "absolute",
5655
- width: 60,
6171
+ width: 76,
5656
6172
  height: PROVIDER_DIALOG_ROWS + 7,
5657
6173
  backgroundColor: theme.backgroundPanel,
5658
6174
  flexDirection: "column",
@@ -5689,7 +6205,8 @@ function OpenTuiApp(props) {
5689
6205
  width: "100%",
5690
6206
  value: "",
5691
6207
  placeholder: "Search",
5692
- fg: theme.textMuted,
6208
+ textColor: theme.text,
6209
+ focusedTextColor: theme.text,
5693
6210
  backgroundColor: theme.backgroundPanel,
5694
6211
  focusedBackgroundColor: theme.backgroundPanel,
5695
6212
  cursorColor: theme.primary,
@@ -5702,15 +6219,11 @@ function OpenTuiApp(props) {
5702
6219
  providerDialog = { ...state, apiKey: value, error: undefined };
5703
6220
  }
5704
6221
  else {
6222
+ const query = value.trim().toLowerCase();
5705
6223
  const items = providerDialogItemsFor(state.step, state.providerId).filter((item) => {
5706
- const query = value.trim().toLowerCase();
5707
6224
  if (!query)
5708
6225
  return true;
5709
- const haystack = [item.label, item.detail, item.value, item.category, item.footer]
5710
- .filter(Boolean)
5711
- .join(" ")
5712
- .toLowerCase();
5713
- return haystack.includes(query) || fuzzyMatch(haystack, query);
6226
+ return providerDialogMatchScore(item, query) > 0;
5714
6227
  });
5715
6228
  providerDialog = {
5716
6229
  ...state,
@@ -6293,6 +6806,7 @@ function OpenTuiApp(props) {
6293
6806
  registerTraceBadge: registerFooterTraceBadge,
6294
6807
  }),
6295
6808
  renderProviderDialog(),
6809
+ renderStatsPanel(),
6296
6810
  renderFeishuSetupPanel(),
6297
6811
  renderNoticeOverlay(),
6298
6812
  ]);
@@ -6564,7 +7078,7 @@ function renderMarkdownContent(content, syntaxStyle, options) {
6564
7078
  bg: theme.background,
6565
7079
  width: "100%",
6566
7080
  tableOptions: {
6567
- widthMode: "full",
7081
+ widthMode: "content",
6568
7082
  columnFitter: "balanced",
6569
7083
  wrapMode: "word",
6570
7084
  cellPadding: 1,
@@ -6784,7 +7298,13 @@ function syncMarkdownRenderable(markdown, content, streaming) {
6784
7298
  return;
6785
7299
  markdown.content = content;
6786
7300
  markdown.streaming = streaming;
6787
- markdown.clearCache();
7301
+ // While streaming, let OpenTUI's incremental markdown/code-block rendering do
7302
+ // its job — clearing the parse cache every delta forces the (syntax-
7303
+ // highlighted) code blocks to be rebuilt and re-highlighted on every token,
7304
+ // which is the source of the visible flicker on streamed code blocks. Clear
7305
+ // the cache only once streaming ends, to fully reparse the finalized content.
7306
+ if (!streaming)
7307
+ markdown.clearCache();
6788
7308
  }
6789
7309
  function updateAssistantPartEntries(entry, parts, options, streaming) {
6790
7310
  const partsBox = entry.refs.partsBox;
@@ -6952,7 +7472,7 @@ function createTraceGroupRenderable(ctx, group, syntaxStyle, width = 80) {
6952
7472
  }, children);
6953
7473
  }
6954
7474
  function shouldRenderTraceGroupAsRawTool(tool) {
6955
- return tool.name === "question" || tool.name === "todo_write" || tool.name === "edit";
7475
+ return tool.name === "question" || tool.name === "todo_write" || tool.name === "edit" || tool.name === "apply_patch";
6956
7476
  }
6957
7477
  function traceGroupDetailLines(group) {
6958
7478
  return group.previewLines.length > 0 ? group.previewLines : group.items;
@@ -7033,8 +7553,9 @@ function traceGroupRenderableSignature(group) {
7033
7553
  return [
7034
7554
  tool.id,
7035
7555
  tool.name,
7036
- tool.status ?? (tool.result === undefined ? "pending" : "completed"),
7556
+ tool.status ?? (tool.result === undefined && !tool.resultCollapsed ? "pending" : "completed"),
7037
7557
  tool.isError ? "error" : "ok",
7558
+ tool.resultCollapsed ? "collapsed" : "expanded",
7038
7559
  stableStringify(tool.args),
7039
7560
  tool.result ?? "",
7040
7561
  stableStringify(tool.metadata ?? null),
@@ -7046,8 +7567,9 @@ function toolRenderableSignature(tool, writeExpanded) {
7046
7567
  return [
7047
7568
  tool.id,
7048
7569
  tool.name,
7049
- tool.status ?? (tool.result === undefined ? "pending" : "completed"),
7570
+ tool.status ?? (tool.result === undefined && !tool.resultCollapsed ? "pending" : "completed"),
7050
7571
  tool.isError ? "error" : "ok",
7572
+ tool.resultCollapsed ? "collapsed" : "expanded",
7051
7573
  tool.streamingArgs ? "streaming-args" : "args-complete",
7052
7574
  writeExpanded ? "expanded" : "collapsed",
7053
7575
  hashString(stableStringify(tool.args)),
@@ -7121,7 +7643,7 @@ function createMarkdown(ctx, content, syntaxStyle, options) {
7121
7643
  width: "100%",
7122
7644
  flexShrink: 0,
7123
7645
  tableOptions: {
7124
- widthMode: "full",
7646
+ widthMode: "content",
7125
7647
  columnFitter: "balanced",
7126
7648
  wrapMode: "word",
7127
7649
  cellPadding: 1,
@@ -7174,16 +7696,41 @@ function createMarkdownList(ctx, token, palette, defaultFg) {
7174
7696
  flexShrink: 0,
7175
7697
  }, items.map((item, index) => {
7176
7698
  const marker = ordered ? `${start + index}. ` : "• ";
7177
- return createText(ctx, new StyledText([
7178
- fg(theme.textMuted)(marker),
7179
- ...markdownInlineToStyledText(markdownTokenInlineTokens(item), palette, item.text ?? "").chunks,
7180
- ]), {
7181
- fg: defaultFg,
7182
- wrapMode: "word",
7183
- flexShrink: 0,
7184
- });
7699
+ return createMarkdownListItem(ctx, item, marker, palette, defaultFg);
7185
7700
  }));
7186
7701
  }
7702
+ function createMarkdownListItem(ctx, item, marker, palette, defaultFg) {
7703
+ const tokens = Array.isArray(item?.tokens) ? item.tokens : [];
7704
+ const inlineTokens = tokens.filter((child) => !isMarkdownListToken(child));
7705
+ const nestedLists = tokens.filter(isMarkdownListToken);
7706
+ const fallback = tokens.length > 0 ? "" : (item?.text ?? "");
7707
+ const children = [];
7708
+ const line = markdownInlineToStyledText(inlineTokens, palette, fallback);
7709
+ children.push(createText(ctx, new StyledText([
7710
+ fg(theme.textMuted)(marker),
7711
+ ...line.chunks,
7712
+ ]), {
7713
+ fg: defaultFg,
7714
+ wrapMode: "word",
7715
+ flexShrink: 0,
7716
+ }));
7717
+ for (const nestedList of nestedLists) {
7718
+ const nested = createMarkdownList(ctx, nestedList, palette, defaultFg);
7719
+ if (!nested)
7720
+ continue;
7721
+ children.push(createBox(ctx, {
7722
+ flexDirection: "column",
7723
+ flexShrink: 0,
7724
+ paddingLeft: Math.max(2, marker.length),
7725
+ }, [nested]));
7726
+ }
7727
+ return children.length === 1
7728
+ ? children[0]
7729
+ : createBox(ctx, { flexDirection: "column", flexShrink: 0 }, children);
7730
+ }
7731
+ function isMarkdownListToken(token) {
7732
+ return token?.type === "list";
7733
+ }
7187
7734
  function markdownTokenInlineTokens(token) {
7188
7735
  if (Array.isArray(token?.tokens))
7189
7736
  return token.tokens;
@@ -7775,10 +8322,10 @@ function renderTool(tool, syntaxStyle, width = 80) {
7775
8322
  const icon = toolStateIcon(tool);
7776
8323
  const color = toolColor(tool);
7777
8324
  const diff = extractToolDiff(tool);
7778
- if (diff && !tool.isError && tool.name === "edit") {
7779
- return h("box", { paddingLeft: 3, marginTop: 1, flexDirection: "column", flexShrink: 0 }, h("text", { fg: color }, `${icon} ${displayToolName(tool.name)}${toolHeader(tool) ? ` ${toolHeader(tool)}` : ""}`), h("box", { paddingLeft: 1, marginTop: 1, border: ["left"], borderColor: theme.borderSubtle, flexDirection: "column", flexShrink: 0 }, renderDiffContent(diff, toolPath(tool), syntaxStyle, width)));
8325
+ if (diff && !tool.resultCollapsed && !tool.isError && (tool.name === "edit" || tool.name === "apply_patch")) {
8326
+ return h("box", { paddingLeft: 3, marginTop: 1, flexDirection: "column", flexShrink: 0 }, h("text", { fg: color }, `${icon} ${displayToolName(tool.name)}${toolHeader(tool) ? ` ${toolHeader(tool)}` : ""}`), h("box", { paddingLeft: 1, marginTop: 1, border: ["left"], borderColor: theme.borderSubtle, flexDirection: "column", flexShrink: 0, backgroundColor: theme.diffContextBg }, renderDiffContent(diff, toolPath(tool), syntaxStyle, width)));
7780
8327
  }
7781
- if (isWritePreviewTool(tool)) {
8328
+ if (!tool.resultCollapsed && isWritePreviewTool(tool)) {
7782
8329
  const hasContent = typeof tool.args.content === "string";
7783
8330
  const contentStr = hasContent ? String(tool.args.content) : "";
7784
8331
  const preview = hasContent ? formatWritePreview(contentStr, false) : null;
@@ -7884,10 +8431,18 @@ function pickerTitle(kind, providerId) {
7884
8431
  }
7885
8432
  }
7886
8433
  function getModelPickerReasoningLevels(providerId, modelId) {
7887
- if (providerId !== "deepseek" || (modelId !== "deepseek-v4-flash" && modelId !== "deepseek-v4-pro")) {
8434
+ // Only expand into one picker row per effort for models that genuinely have a
8435
+ // reasoning-effort spectrum: OpenAI's reasoning models (codex gpt-5.x:
8436
+ // off/minimal/low/medium/high/xhigh) and DeepSeek's v4 models. Other providers
8437
+ // (e.g. GLM, Moonshot/Kimi) only have a thinking on/off toggle, not an effort
8438
+ // control, so they stay as a single row.
8439
+ const isOpenAIReasoning = providerId === "openai" || providerId === "openai-codex";
8440
+ const isDeepseekReasoning = providerId === "deepseek" && (modelId === "deepseek-v4-flash" || modelId === "deepseek-v4-pro");
8441
+ if (!isOpenAIReasoning && !isDeepseekReasoning)
7888
8442
  return [];
7889
- }
7890
- return getAvailableThinkingLevels(providerId, modelId);
8443
+ const levels = getAvailableThinkingLevels(providerId, modelId);
8444
+ // gpt-4o and friends report only ["off"] — keep those as a single row too.
8445
+ return levels.length > 1 ? levels : [];
7891
8446
  }
7892
8447
  function displayModelWithThinking(model, thinkingLevel) {
7893
8448
  if (!model)
@@ -7895,7 +8450,10 @@ function displayModelWithThinking(model, thinkingLevel) {
7895
8450
  const { providerId, modelId } = decodeModel(model);
7896
8451
  if (!providerId)
7897
8452
  return displayModel(model);
7898
- const levels = getAvailableThinkingLevels(providerId, modelId);
8453
+ // Use the same scoping as the picker: only models with a real reasoning-effort
8454
+ // spectrum (OpenAI codex gpt-5.x, deepseek v4) get the "(level)" suffix. The
8455
+ // on/off thinking toggle on GLM / Moonshot(Kimi) is not an effort control.
8456
+ const levels = getModelPickerReasoningLevels(providerId, modelId);
7899
8457
  if (levels.length > 1 && thinkingLevel !== "off") {
7900
8458
  return `${displayModel(model)} (${thinkingLevel})`;
7901
8459
  }
@@ -8237,7 +8795,7 @@ function appendRawToolTranscript(chunks, tool) {
8237
8795
  append(`${content}\n`, color);
8238
8796
  };
8239
8797
  appendLine("");
8240
- const icon = tool.name === "bash" ? "$" : tool.name === "edit" || tool.name === "write" ? "✎" : "●";
8798
+ const icon = tool.name === "bash" ? "$" : tool.name === "edit" || tool.name === "write" || tool.name === "apply_patch" ? "✎" : "●";
8241
8799
  const color = toolColor(tool);
8242
8800
  append(` ${icon} `, color);
8243
8801
  append(displayToolName(tool.name), color);
@@ -8405,6 +8963,18 @@ function getApprovalPanelMeta(request) {
8405
8963
  path: request.path,
8406
8964
  };
8407
8965
  }
8966
+ if (request.type === "patch") {
8967
+ return {
8968
+ icon: "→",
8969
+ title: `Patch ${path}`,
8970
+ subtitle: `${request.paths.length} file${request.paths.length === 1 ? "" : "s"}`,
8971
+ preview: request.diff || "No diff provided",
8972
+ previewHeight: 10,
8973
+ previewColor: request.diff ? theme.toolText : theme.textMuted,
8974
+ diff: request.diff,
8975
+ path: request.paths[0] ?? request.path,
8976
+ };
8977
+ }
8408
8978
  return {
8409
8979
  icon: "→",
8410
8980
  title: `Write ${path}`,
@@ -8483,7 +9053,7 @@ function toolColor(tool) {
8483
9053
  return theme.toolShell;
8484
9054
  if (tool.name === "read")
8485
9055
  return theme.toolRead;
8486
- if (tool.name === "write" || tool.name === "edit")
9056
+ if (tool.name === "write" || tool.name === "edit" || tool.name === "apply_patch")
8487
9057
  return theme.toolWrite;
8488
9058
  if (tool.name === "grep" || tool.name === "glob" || tool.name === "web_search" || tool.name === "web_fetch")
8489
9059
  return theme.toolSearch;
@@ -8494,6 +9064,7 @@ function displayToolName(name) {
8494
9064
  read: "Read",
8495
9065
  write: "Write",
8496
9066
  edit: "Edit",
9067
+ apply_patch: "Patch",
8497
9068
  bash: "Shell",
8498
9069
  grep: "Grep",
8499
9070
  glob: "Glob",
@@ -8526,16 +9097,36 @@ function toolHeader(tool) {
8526
9097
  const agentId = args.agent_id ?? (Array.isArray(args.agent_ids) ? `${args.agent_ids.length} agents` : undefined);
8527
9098
  return agentId ? `(${truncate(String(agentId), 64)})` : "";
8528
9099
  }
8529
- const value = args.path ?? args.command ?? args.pattern ?? args.url ?? args.query;
9100
+ const value = args.path ?? args.command ?? args.pattern ?? args.url ?? args.query ?? toolPath(tool);
8530
9101
  return value ? `(${truncate(String(value).replace(/\n/g, " "), 64)})` : "";
8531
9102
  }
8532
9103
  function toolPath(tool) {
8533
- const value = tool.args?.path ?? tool.args?.filePath;
9104
+ const value = tool.args?.path
9105
+ ?? tool.args?.filePath
9106
+ ?? tool.metadata?.path
9107
+ ?? (Array.isArray(tool.metadata?.paths) ? tool.metadata.paths[0] : undefined);
8534
9108
  return typeof value === "string" ? value : undefined;
8535
9109
  }
9110
+ // Strip only leading/trailing newlines — NOT a full .trim(). A blank context
9111
+ // line in a unified diff is a single space (" "); plain .trim() would delete a
9112
+ // trailing blank context line, leaving the hunk body shorter than its @@ header
9113
+ // count and breaking the diff parser ("Added line count did not match").
9114
+ function stripDiffEdgeNewlines(diff) {
9115
+ return diff.replace(/^\n+/, "").replace(/\n+$/, "");
9116
+ }
8536
9117
  function extractToolDiff(tool) {
9118
+ if (tool.resultCollapsed)
9119
+ return undefined;
9120
+ if (typeof tool.metadata?.diff === "string" && tool.metadata.diff.trim().length > 0) {
9121
+ return stripDiffEdgeNewlines(tool.metadata.diff);
9122
+ }
8537
9123
  if (!tool.result)
8538
9124
  return undefined;
9125
+ if (tool.result.includes("✂") ||
9126
+ tool.result.includes("chars truncated") ||
9127
+ tool.result.includes("chars omitted for UI")) {
9128
+ return undefined;
9129
+ }
8539
9130
  const marker = "\n\nDiff:\n";
8540
9131
  const index = tool.result.indexOf(marker);
8541
9132
  if (index === -1)
@@ -8543,10 +9134,14 @@ function extractToolDiff(tool) {
8543
9134
  const rawDiff = tool.result.slice(index + marker.length);
8544
9135
  const diagnosticsIndex = rawDiff.search(/\n\nLSP diagnostics in /);
8545
9136
  const diff = diagnosticsIndex === -1 ? rawDiff : rawDiff.slice(0, diagnosticsIndex);
8546
- return diff.trim().length > 0 ? diff : undefined;
9137
+ return diff.trim().length > 0 ? stripDiffEdgeNewlines(diff) : undefined;
8547
9138
  }
8548
- function diffViewMode(width = 80) {
8549
- return width > 120 ? "split" : "unified";
9139
+ function diffViewMode(_width = 80) {
9140
+ // Always unified: split view pads the shorter side with empty filler rows that
9141
+ // OpenTUI's DiffRenderable leaves uncolored, which shows up as bright white
9142
+ // blocks in light mode. Unified view has no filler rows — every line is
9143
+ // add/remove/context and gets a background — so the edit area stays uniform.
9144
+ return "unified";
8550
9145
  }
8551
9146
  function filetype(filePath) {
8552
9147
  if (!filePath)
@@ -8589,7 +9184,7 @@ function summarizeToolResult(tool) {
8589
9184
  const matches = typeof tool.metadata?.matches === "number" ? tool.metadata.matches : undefined;
8590
9185
  if (tool.name === "read")
8591
9186
  return "";
8592
- if (tool.name === "edit")
9187
+ if (tool.name === "edit" || tool.name === "apply_patch")
8593
9188
  return "patched file";
8594
9189
  if (tool.name === "write")
8595
9190
  return "wrote file";
@@ -8615,7 +9210,7 @@ function toolStateIcon(tool) {
8615
9210
  }
8616
9211
  if (tool.name === "bash")
8617
9212
  return "$";
8618
- if (tool.name === "edit")
9213
+ if (tool.name === "edit" || tool.name === "apply_patch")
8619
9214
  return "✎";
8620
9215
  if (tool.name === "write")
8621
9216
  return "✎";
@@ -8692,7 +9287,7 @@ function formatQuestionAnswer(answer) {
8692
9287
  return answer?.length ? answer.join(", ") : "(no answer)";
8693
9288
  }
8694
9289
  function isToolFinished(tool) {
8695
- return tool.status === "completed" || tool.status === "error" || tool.result !== undefined;
9290
+ return tool.status === "completed" || tool.status === "error" || tool.resultCollapsed === true || tool.result !== undefined;
8696
9291
  }
8697
9292
  function assistantStatusLabel(message) {
8698
9293
  if (message.status === "responding")