@bubblebrain-ai/bubble 0.0.13 → 0.0.14

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 (75) 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/context/compact.js +9 -3
  10. package/dist/context/projector.js +27 -12
  11. package/dist/debug-trace.d.ts +27 -0
  12. package/dist/debug-trace.js +385 -0
  13. package/dist/feishu/agent-host/approval-card.js +9 -0
  14. package/dist/feishu/serve.js +7 -1
  15. package/dist/main.js +28 -0
  16. package/dist/model-catalog.js +1 -0
  17. package/dist/orchestrator/default-hooks.js +19 -8
  18. package/dist/orchestrator/hooks.d.ts +1 -0
  19. package/dist/prompt/environment.js +2 -0
  20. package/dist/prompt/reminders.d.ts +5 -6
  21. package/dist/prompt/reminders.js +8 -9
  22. package/dist/prompt/runtime.js +2 -2
  23. package/dist/provider-openai-codex.d.ts +7 -0
  24. package/dist/provider-openai-codex.js +265 -124
  25. package/dist/provider-registry.d.ts +2 -0
  26. package/dist/provider-registry.js +58 -9
  27. package/dist/provider.d.ts +3 -0
  28. package/dist/provider.js +5 -1
  29. package/dist/session-log.js +13 -1
  30. package/dist/slash-commands/commands.js +12 -0
  31. package/dist/slash-commands/types.d.ts +2 -0
  32. package/dist/stats/usage.d.ts +52 -0
  33. package/dist/stats/usage.js +414 -0
  34. package/dist/tools/apply-patch.d.ts +9 -0
  35. package/dist/tools/apply-patch.js +330 -0
  36. package/dist/tools/bash.js +205 -44
  37. package/dist/tools/edit-apply.d.ts +5 -2
  38. package/dist/tools/edit-apply.js +221 -31
  39. package/dist/tools/edit.js +12 -3
  40. package/dist/tools/file-mutation-queue.d.ts +1 -0
  41. package/dist/tools/file-mutation-queue.js +12 -1
  42. package/dist/tools/index.d.ts +2 -0
  43. package/dist/tools/index.js +7 -1
  44. package/dist/tools/patch-apply.d.ts +41 -0
  45. package/dist/tools/patch-apply.js +312 -0
  46. package/dist/tools/server-manager.d.ts +36 -0
  47. package/dist/tools/server-manager.js +234 -0
  48. package/dist/tools/server.d.ts +6 -0
  49. package/dist/tools/server.js +245 -0
  50. package/dist/tools/write.d.ts +3 -6
  51. package/dist/tools/write.js +26 -46
  52. package/dist/tui/display-history.d.ts +1 -0
  53. package/dist/tui/display-history.js +5 -4
  54. package/dist/tui/edit-diff.js +6 -1
  55. package/dist/tui/model-picker-data.d.ts +10 -0
  56. package/dist/tui/model-picker-data.js +32 -0
  57. package/dist/tui/run.js +632 -89
  58. package/dist/tui/tool-renderers/fallback.js +1 -1
  59. package/dist/tui/tool-renderers/write-preview.js +2 -0
  60. package/dist/tui/trace-groups.js +10 -3
  61. package/dist/tui-ink/app.js +1 -4
  62. package/dist/tui-ink/approval/approval-dialog.js +7 -1
  63. package/dist/tui-ink/display-history.d.ts +1 -0
  64. package/dist/tui-ink/display-history.js +5 -4
  65. package/dist/tui-ink/message-list.js +14 -8
  66. package/dist/tui-ink/trace-groups.js +1 -1
  67. package/dist/tui-opentui/app.js +2 -0
  68. package/dist/tui-opentui/approval/approval-dialog.js +7 -1
  69. package/dist/tui-opentui/display-history.d.ts +1 -0
  70. package/dist/tui-opentui/display-history.js +5 -4
  71. package/dist/tui-opentui/edit-diff.js +6 -1
  72. package/dist/tui-opentui/message-list.js +6 -3
  73. package/dist/tui-opentui/trace-groups.js +10 -3
  74. package/dist/types.d.ts +12 -2
  75. package/package.json +1 -1
package/dist/tui/run.js CHANGED
@@ -9,10 +9,11 @@ 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 { collectUsageStatsBundle, formatStatsPanelBody } from "../stats/usage.js";
16
17
  import { parseSkillInvocation } from "../skills/invocation.js";
17
18
  import { registry as slashRegistry } from "../slash-commands/index.js";
18
19
  import { sourceRank } from "../slash-commands/unified.js";
@@ -24,6 +25,7 @@ import { markdownInlineSegments } from "./markdown-inline.js";
24
25
  import { hashString } from "./render-signature.js";
25
26
  import { findToolRenderer } from "./tool-renderers/registry.js";
26
27
  import { writeToolKey } from "./tool-renderers/write.js";
28
+ import { discoverModelProviderGroups, getVisibleModelProviders, localModelsForProvider, } from "./model-picker-data.js";
27
29
  import { formatWritePreview, isWritePreviewTool } from "./tool-renderers/write-preview.js";
28
30
  import { extractStreamingArgsHint } from "./streaming-tool-args.js";
29
31
  import { getNextPermissionMode, PERMISSION_MODE_INFO } from "../permission/mode.js";
@@ -396,6 +398,7 @@ function OpenTuiApp(props) {
396
398
  redrawProviderDialog();
397
399
  redrawApprovalPanel();
398
400
  redrawQuestionPanel();
401
+ redrawStatsPanel();
399
402
  redrawFeishuSetupPanel();
400
403
  setSidebarTick((tick) => tick + 1);
401
404
  renderer.requestRender();
@@ -483,6 +486,7 @@ function OpenTuiApp(props) {
483
486
  const [pendingQuestion, setPendingQuestion] = createSignal();
484
487
  const [pendingFeedback, setPendingFeedback] = createSignal();
485
488
  const [pendingFeishuSetup, setPendingFeishuSetup] = createSignal();
489
+ let statsPanel;
486
490
  const questionSyncTimers = new Set();
487
491
  let feishuSetupAbortController;
488
492
  let pendingApprovalRef;
@@ -490,6 +494,8 @@ function OpenTuiApp(props) {
490
494
  const [approvalOptionIdx, setApprovalOptionIdx] = createSignal(0);
491
495
  let picker;
492
496
  let providerDialog;
497
+ let providerDialogModelItems;
498
+ let providerDialogModelRefreshId = 0;
493
499
  let previousPickerForKey;
494
500
  let homePromptRef;
495
501
  let sessionPromptRef;
@@ -546,6 +552,17 @@ function OpenTuiApp(props) {
546
552
  let feedbackPreviewShell;
547
553
  let feedbackPreviewText;
548
554
  let feedbackFooterText;
555
+ let statsRoot;
556
+ let statsPanelBox;
557
+ let statsTitle;
558
+ let statsEsc;
559
+ let statsTab7Box;
560
+ let statsTab30Box;
561
+ let statsTab7Text;
562
+ let statsTab30Text;
563
+ let statsBodyScroll;
564
+ let statsBodyText;
565
+ let statsFooterText;
549
566
  let feishuSetupRoot;
550
567
  let feishuSetupPanel;
551
568
  let feishuSetupTitle;
@@ -713,6 +730,12 @@ function OpenTuiApp(props) {
713
730
  feedbackRoot?.focus();
714
731
  }, 0);
715
732
  }
733
+ function focusStatsPanel() {
734
+ setTimeout(() => {
735
+ if (statsPanel)
736
+ statsRoot?.focus();
737
+ }, 0);
738
+ }
716
739
  function focusFeishuSetupPanel() {
717
740
  setTimeout(() => {
718
741
  const state = pendingFeishuSetup();
@@ -852,7 +875,7 @@ function OpenTuiApp(props) {
852
875
  return false;
853
876
  return !!event.shift;
854
877
  };
855
- const canInsertPromptNewline = () => !pendingApproval() && !pendingPlan() && !pendingQuestion() && !pendingFeedback() && !pendingFeishuSetup();
878
+ const canInsertPromptNewline = () => !pendingApproval() && !pendingPlan() && !pendingQuestion() && !pendingFeedback() && !statsPanel && !pendingFeishuSetup();
856
879
  const sidebarFits = () => dimensions().width > SESSION_SIDEBAR_WIDTH + 40;
857
880
  const sidebarVisible = () => {
858
881
  if (!sessionActive())
@@ -1193,6 +1216,7 @@ function OpenTuiApp(props) {
1193
1216
  switch (request.type) {
1194
1217
  case "bash": return "Bash";
1195
1218
  case "edit": return "Edit";
1219
+ case "patch": return "Patch";
1196
1220
  case "write": return "Write";
1197
1221
  case "lsp": return "Lsp";
1198
1222
  }
@@ -1798,6 +1822,71 @@ function OpenTuiApp(props) {
1798
1822
  }
1799
1823
  return false;
1800
1824
  }
1825
+ function openStatsPanel() {
1826
+ picker = undefined;
1827
+ providerDialog = undefined;
1828
+ redrawProviderDialog();
1829
+ statsPanel = {
1830
+ range: "30d",
1831
+ bundle: collectUsageStatsBundle(),
1832
+ };
1833
+ activePrompt()?.clear();
1834
+ activePrompt()?.blur();
1835
+ promptText = "";
1836
+ syncStatsUI(true);
1837
+ }
1838
+ function closeStatsPanel() {
1839
+ statsPanel = undefined;
1840
+ syncStatsUI(false);
1841
+ restorePromptAfterModal();
1842
+ if (queuedInputCount() > 0)
1843
+ scheduleQueuedInputDrain();
1844
+ }
1845
+ function syncStatsUI(focus = false) {
1846
+ redrawStatsPanel();
1847
+ syncPromptSurfaces();
1848
+ redrawDock();
1849
+ rootBox?.requestRender();
1850
+ scrollbox?.requestRender();
1851
+ if (focus || statsPanel)
1852
+ focusStatsPanel();
1853
+ }
1854
+ function setStatsRange(range) {
1855
+ if (!statsPanel || statsPanel.range === range)
1856
+ return;
1857
+ statsPanel = { ...statsPanel, range };
1858
+ redrawStatsPanel();
1859
+ }
1860
+ function handleStatsKey(event) {
1861
+ if (!statsPanel)
1862
+ return false;
1863
+ const name = keyNameFromEvent(event);
1864
+ if (name === "escape") {
1865
+ closeStatsPanel();
1866
+ event.preventDefault?.();
1867
+ event.stopPropagation?.();
1868
+ return true;
1869
+ }
1870
+ if (name === "left" || name === "h") {
1871
+ setStatsRange("7d");
1872
+ event.preventDefault?.();
1873
+ event.stopPropagation?.();
1874
+ return true;
1875
+ }
1876
+ if (name === "right" || name === "l") {
1877
+ setStatsRange("30d");
1878
+ event.preventDefault?.();
1879
+ event.stopPropagation?.();
1880
+ return true;
1881
+ }
1882
+ if (name === "tab") {
1883
+ setStatsRange(statsPanel.range === "30d" ? "7d" : "30d");
1884
+ event.preventDefault?.();
1885
+ event.stopPropagation?.();
1886
+ return true;
1887
+ }
1888
+ return true;
1889
+ }
1801
1890
  function openFeishuSetup() {
1802
1891
  picker = undefined;
1803
1892
  providerDialog = undefined;
@@ -2048,6 +2137,8 @@ function OpenTuiApp(props) {
2048
2137
  return "question";
2049
2138
  if (pendingFeedback())
2050
2139
  return "feedback";
2140
+ if (statsPanel)
2141
+ return "stats";
2051
2142
  if (providerDialog)
2052
2143
  return "provider";
2053
2144
  if (pendingFeishuSetup())
@@ -2067,6 +2158,8 @@ function OpenTuiApp(props) {
2067
2158
  return handleQuestionKey(event);
2068
2159
  case "feedback":
2069
2160
  return handleFeedbackKey(event);
2161
+ case "stats":
2162
+ return handleStatsKey(event);
2070
2163
  case "provider":
2071
2164
  return handleProviderDialogKey(event);
2072
2165
  case "feishu":
@@ -2084,6 +2177,8 @@ function OpenTuiApp(props) {
2084
2177
  }
2085
2178
  if (owner === "feedback")
2086
2179
  return pendingFeedback()?.stage !== "edit";
2180
+ if (owner === "stats")
2181
+ return true;
2087
2182
  if (owner === "feishu")
2088
2183
  return pendingFeishuSetup()?.kind !== "binding";
2089
2184
  return false;
@@ -2223,14 +2318,14 @@ function OpenTuiApp(props) {
2223
2318
  return currentTranscriptMessages(extra).some((message) => hasRenderableMessage(message, effectiveShowThinking()));
2224
2319
  }
2225
2320
  function isHomeSurfaceActive(extra) {
2226
- return !hasTranscriptMessages(extra) && !pendingPlan() && !pendingQuestion() && !pendingFeedback() && !pendingFeishuSetup();
2321
+ return !hasTranscriptMessages(extra) && !pendingPlan() && !pendingQuestion() && !pendingFeedback() && !statsPanel && !pendingFeishuSetup();
2227
2322
  }
2228
2323
  function syncPromptSurfaces(focus = false) {
2229
2324
  const homeActive = isHomeSurfaceActive(streamingDisplay);
2230
2325
  const nextSessionActive = !homeActive;
2231
2326
  const surfaceChanged = sessionActive() !== nextSessionActive;
2232
2327
  setSessionActive(nextSessionActive);
2233
- const modalComposerHidden = !!pendingQuestion() || !!pendingFeedback() || !!pendingFeishuSetup();
2328
+ const modalComposerHidden = !!pendingQuestion() || !!pendingFeedback() || !!statsPanel || !!pendingFeishuSetup();
2234
2329
  if (homeSurfaceShell)
2235
2330
  homeSurfaceShell.visible = homeActive;
2236
2331
  if (homeComposerShell)
@@ -2333,6 +2428,18 @@ function OpenTuiApp(props) {
2333
2428
  redrawTranscriptWithQueuedDisplays();
2334
2429
  return changed;
2335
2430
  }
2431
+ function removeQueuedUserDisplay(displayId) {
2432
+ if (!displayId)
2433
+ return false;
2434
+ const beforeDisplayCount = displayMessages.length;
2435
+ const beforeQueuedCount = queuedDisplayMessages.length;
2436
+ displayMessages = displayMessages.filter((message) => message.clientId !== displayId);
2437
+ queuedDisplayMessages = queuedDisplayMessages.filter((message) => message.clientId !== displayId);
2438
+ const changed = displayMessages.length !== beforeDisplayCount || queuedDisplayMessages.length !== beforeQueuedCount;
2439
+ if (changed)
2440
+ redrawTranscriptWithQueuedDisplays();
2441
+ return changed;
2442
+ }
2336
2443
  function promoteQueuedUserDisplay(displayId, fallbackContent) {
2337
2444
  if (!displayId)
2338
2445
  return false;
@@ -2425,6 +2532,7 @@ function OpenTuiApp(props) {
2425
2532
  || pendingPlan()
2426
2533
  || pendingQuestion()
2427
2534
  || pendingFeedback()
2535
+ || statsPanel
2428
2536
  || providerDialog
2429
2537
  || picker) {
2430
2538
  return;
@@ -2522,6 +2630,10 @@ function OpenTuiApp(props) {
2522
2630
  function cancelActiveAgentRun() {
2523
2631
  if (!activeRun || activeRun.abortController.signal.aborted)
2524
2632
  return false;
2633
+ traceEvent("tui_running_cancel", {
2634
+ runId: activeRun.id,
2635
+ pendingQueuedInputs: queuedInputCount(),
2636
+ }, { surface: "tui" });
2525
2637
  clearRunningCancelHint();
2526
2638
  activeRun.abortController.abort(new AgentAbortError("Agent run cancelled by user."));
2527
2639
  setNotice("Agent run cancelled");
@@ -2556,6 +2668,7 @@ function OpenTuiApp(props) {
2556
2668
  if (!activeRun || activeRun.abortController.signal.aborted)
2557
2669
  return false;
2558
2670
  const shouldCancel = armRunningCancelHint(activeRun);
2671
+ traceKeyRoute(event ? "key" : "raw", name, !shouldCancel ? "armed_cancel" : "confirm_cancel");
2559
2672
  if (!shouldCancel) {
2560
2673
  if (event)
2561
2674
  preventGlobalKey(event);
@@ -2573,33 +2686,72 @@ function OpenTuiApp(props) {
2573
2686
  if (!isRunning() || activeModalKeyOwner())
2574
2687
  return false;
2575
2688
  queuePromptFromComposer({ notice: "Queued next message" });
2689
+ traceKeyRoute(event ? "key" : "raw", name, "queued_next_message");
2576
2690
  if (event)
2577
2691
  preventGlobalKey(event);
2578
2692
  return true;
2579
2693
  }
2694
+ function traceKeyRoute(source, name, result) {
2695
+ const shouldTrace = result !== "unhandled"
2696
+ || name === "escape"
2697
+ || name === "enter"
2698
+ || name === "tab"
2699
+ || name === "up"
2700
+ || name === "down"
2701
+ || name === "left"
2702
+ || name === "right"
2703
+ || name === "ctrl-c"
2704
+ || !!activeModalKeyOwner()
2705
+ || isRunning();
2706
+ if (!shouldTrace)
2707
+ return;
2708
+ traceEvent("tui_key_route", {
2709
+ source,
2710
+ key: name,
2711
+ result,
2712
+ modalOwner: activeModalKeyOwner(),
2713
+ running: isRunning(),
2714
+ activeRunId: activeRun?.id,
2715
+ pendingApproval: !!pendingApproval(),
2716
+ pendingPlan: !!pendingPlan(),
2717
+ pendingQuestion: !!pendingQuestion(),
2718
+ providerDialog: !!providerDialog,
2719
+ picker: !!picker,
2720
+ }, { surface: "tui" });
2721
+ }
2580
2722
  function routeGlobalRawSequence(sequence) {
2581
2723
  if (isCtrlCSequence(sequence)) {
2582
2724
  void requestExit({ direct: true });
2725
+ traceKeyRoute("raw", "ctrl-c", "exit");
2583
2726
  return true;
2584
2727
  }
2585
2728
  const name = keyNameFromSequence(sequence);
2586
2729
  const modalName = modalKeyNameFromSequence(sequence);
2587
- if (routeModalRawSequence(sequence))
2730
+ if (routeModalRawSequence(sequence)) {
2731
+ traceKeyRoute("raw", modalName || name, "modal");
2588
2732
  return true;
2733
+ }
2589
2734
  if (routeRunningCancel(name))
2590
2735
  return true;
2591
2736
  if (routeRunningQueue(modalName))
2592
2737
  return true;
2593
- if (cycleModeFromRawSequence(sequence))
2738
+ if (cycleModeFromRawSequence(sequence)) {
2739
+ traceKeyRoute("raw", name, "mode_cycle");
2594
2740
  return true;
2741
+ }
2742
+ traceKeyRoute("raw", name, "unhandled");
2595
2743
  return false;
2596
2744
  }
2597
2745
  function routeGlobalKeyEvent(event) {
2598
- if (routeCtrlCExit(event))
2746
+ if (routeCtrlCExit(event)) {
2747
+ traceKeyRoute("key", "ctrl-c", "exit");
2599
2748
  return true;
2749
+ }
2600
2750
  const name = keyNameFromEvent(event);
2601
- if (routeModalKey(event))
2751
+ if (routeModalKey(event)) {
2752
+ traceKeyRoute("key", name, "modal");
2602
2753
  return true;
2754
+ }
2603
2755
  if (routeRunningCancel(name, event))
2604
2756
  return true;
2605
2757
  if (routeRunningQueue(name, event))
@@ -2609,25 +2761,32 @@ function OpenTuiApp(props) {
2609
2761
  if (event.ctrl && event.shift && name === "m") {
2610
2762
  openMcpReconnectPicker();
2611
2763
  event.preventDefault?.();
2764
+ traceKeyRoute("key", name, "mcp_picker");
2612
2765
  return true;
2613
2766
  }
2614
2767
  if (event.ctrl && name === "t" && !picker) {
2615
2768
  toggleThinkingVisibility();
2616
2769
  event.preventDefault?.();
2770
+ traceKeyRoute("key", name, "toggle_thinking");
2617
2771
  return true;
2618
2772
  }
2619
2773
  if (event.ctrl && name === "o" && !picker) {
2620
2774
  toggleVerboseTrace();
2621
2775
  event.preventDefault?.();
2776
+ traceKeyRoute("key", name, "toggle_verbose_trace");
2622
2777
  return true;
2623
2778
  }
2624
- if (cycleModeFromKey(event))
2779
+ if (cycleModeFromKey(event)) {
2780
+ traceKeyRoute("key", name, "mode_cycle");
2625
2781
  return true;
2782
+ }
2626
2783
  if (event.ctrl && name === "p" && !picker && !isRunning()) {
2627
2784
  openCommandPalette();
2628
2785
  event.preventDefault?.();
2786
+ traceKeyRoute("key", name, "command_palette");
2629
2787
  return true;
2630
2788
  }
2789
+ traceKeyRoute("key", name, "unhandled");
2631
2790
  return false;
2632
2791
  }
2633
2792
  function transcriptOptions() {
@@ -2739,6 +2898,7 @@ function OpenTuiApp(props) {
2739
2898
  sessionActive();
2740
2899
  syncSidebarChrome();
2741
2900
  redrawQuestionPanel();
2901
+ redrawStatsPanel();
2742
2902
  redrawFeishuSetupPanel();
2743
2903
  scrollbox?.requestRender();
2744
2904
  scheduleTranscriptScrollAfterUpdate(shouldFollow);
@@ -2800,6 +2960,13 @@ function OpenTuiApp(props) {
2800
2960
  dock?.requestRender();
2801
2961
  }
2802
2962
  function openProviderDialog(step = "providers", providerId) {
2963
+ if (step === "models") {
2964
+ providerDialogModelItems = undefined;
2965
+ }
2966
+ else {
2967
+ providerDialogModelRefreshId++;
2968
+ providerDialogModelItems = undefined;
2969
+ }
2803
2970
  const items = providerDialogItemsFor(step, providerId);
2804
2971
  picker = undefined;
2805
2972
  providerDialog = {
@@ -2815,9 +2982,14 @@ function OpenTuiApp(props) {
2815
2982
  redrawDock();
2816
2983
  redrawProviderDialog();
2817
2984
  setTimeout(() => providerDialogInput?.focus(), 0);
2985
+ if (step === "models") {
2986
+ void refreshProviderDialogModelItems(providerId, items);
2987
+ }
2818
2988
  }
2819
2989
  function closeProviderDialog() {
2820
2990
  providerDialog = undefined;
2991
+ providerDialogModelRefreshId++;
2992
+ providerDialogModelItems = undefined;
2821
2993
  providerDialogRoot && (providerDialogRoot.visible = false);
2822
2994
  providerDialogPanel && (providerDialogPanel.visible = false);
2823
2995
  providerDialogRoot?.requestRender();
@@ -2833,6 +3005,9 @@ function OpenTuiApp(props) {
2833
3005
  if (step === "skills")
2834
3006
  return buildSkillItems();
2835
3007
  if (step === "models") {
3008
+ if (providerDialogModelItems?.key === modelPickerCacheKey(providerId)) {
3009
+ return providerDialogModelItems.items;
3010
+ }
2836
3011
  const modelItems = buildPickerItems("model", providerId);
2837
3012
  if (modelItems.length || providerId)
2838
3013
  return modelItems;
@@ -2843,6 +3018,34 @@ function OpenTuiApp(props) {
2843
3018
  }
2844
3019
  return [];
2845
3020
  }
3021
+ function modelPickerCacheKey(providerId) {
3022
+ return providerId || "__all__";
3023
+ }
3024
+ async function refreshProviderDialogModelItems(providerId, localItems) {
3025
+ const refreshId = ++providerDialogModelRefreshId;
3026
+ const cacheKey = modelPickerCacheKey(providerId);
3027
+ const localPreferredIndex = preferredPickerIndex("model", localItems);
3028
+ try {
3029
+ const remoteItems = await buildRemoteModelPickerItems(providerId);
3030
+ if (refreshId !== providerDialogModelRefreshId)
3031
+ return;
3032
+ if (remoteItems.length === 0)
3033
+ return;
3034
+ const state = providerDialog;
3035
+ if (!state || state.step !== "models" || modelPickerCacheKey(state.providerId) !== cacheKey)
3036
+ return;
3037
+ providerDialogModelItems = { key: cacheKey, items: remoteItems };
3038
+ const remotePreferredIndex = preferredPickerIndex("model", remoteItems);
3039
+ const nextIndex = state.index === localPreferredIndex
3040
+ ? remotePreferredIndex
3041
+ : Math.min(state.index, Math.max(0, remoteItems.length - 1));
3042
+ providerDialog = { ...state, index: nextIndex };
3043
+ redrawProviderDialog();
3044
+ }
3045
+ catch {
3046
+ // Keep the already-rendered local catalog when remote model discovery fails.
3047
+ }
3048
+ }
2846
3049
  function providerDialogFilteredItems(state = providerDialog) {
2847
3050
  if (!state || state.step === "key")
2848
3051
  return [];
@@ -3434,6 +3637,127 @@ function OpenTuiApp(props) {
3434
3637
  feedbackRoot.requestRender();
3435
3638
  feedbackInput?.requestRender();
3436
3639
  }
3640
+ function redrawStatsPanel() {
3641
+ if (!statsRoot)
3642
+ return;
3643
+ const state = statsPanel;
3644
+ if (!state) {
3645
+ statsRoot.visible = false;
3646
+ statsPanelBox && (statsPanelBox.visible = false);
3647
+ statsRoot.requestRender();
3648
+ return;
3649
+ }
3650
+ const terminalWidth = dimensions().width;
3651
+ const terminalHeight = dimensions().height;
3652
+ const width = Math.max(56, Math.min(84, terminalWidth - 4));
3653
+ const bodyWidth = Math.max(48, width - 8);
3654
+ const stats = state.bundle.ranges[state.range];
3655
+ const body = formatStatsPanelBody(stats, bodyWidth);
3656
+ const bodyLines = body.split("\n");
3657
+ const height = Math.min(Math.max(22, bodyLines.length + 7), Math.max(18, terminalHeight - 4));
3658
+ const bodyHeight = Math.max(8, height - 8);
3659
+ statsRoot.visible = true;
3660
+ statsRoot.width = terminalWidth;
3661
+ statsRoot.height = terminalHeight;
3662
+ statsRoot.left = 0;
3663
+ statsRoot.top = 0;
3664
+ statsRoot.backgroundColor = modalBackdropColor();
3665
+ if (statsPanelBox) {
3666
+ statsPanelBox.visible = true;
3667
+ statsPanelBox.width = width;
3668
+ statsPanelBox.height = height;
3669
+ statsPanelBox.left = Math.max(0, Math.floor((terminalWidth - width) / 2));
3670
+ statsPanelBox.top = Math.max(0, Math.floor((terminalHeight - height) / 3));
3671
+ statsPanelBox.backgroundColor = theme.backgroundPanel;
3672
+ statsPanelBox.borderColor = theme.backgroundPanel;
3673
+ }
3674
+ if (statsTitle)
3675
+ statsTitle.content = "Stats";
3676
+ if (statsEsc)
3677
+ statsEsc.content = "esc";
3678
+ syncStatsTab(statsTab7Box, statsTab7Text, state.range === "7d", "7 days");
3679
+ syncStatsTab(statsTab30Box, statsTab30Text, state.range === "30d", "30 days");
3680
+ if (statsBodyText) {
3681
+ statsBodyText.content = statsPanelBodyStyledText(stats, bodyWidth);
3682
+ statsBodyText.width = bodyWidth;
3683
+ }
3684
+ if (statsBodyScroll) {
3685
+ statsBodyScroll.width = bodyWidth;
3686
+ statsBodyScroll.height = bodyHeight;
3687
+ statsBodyScroll.requestRender();
3688
+ }
3689
+ if (statsFooterText) {
3690
+ statsFooterText.content = statsFooterHint(state.range);
3691
+ statsFooterText.width = bodyWidth;
3692
+ statsFooterText.bg = theme.backgroundPanel;
3693
+ }
3694
+ statsPanelBox?.requestRender();
3695
+ statsRoot.requestRender();
3696
+ }
3697
+ function statsFooterHint(range) {
3698
+ return `left/right:range|tab:toggle|esc:close|view:${range}`;
3699
+ }
3700
+ function statsPanelBodyStyledText(stats, width) {
3701
+ const chunks = [];
3702
+ const lines = formatStatsPanelBody(stats, width).split("\n");
3703
+ for (let index = 0; index < lines.length; index += 1) {
3704
+ appendStatsPanelLine(chunks, lines[index]);
3705
+ if (index < lines.length - 1)
3706
+ chunks.push(fg(theme.text)("\n"));
3707
+ }
3708
+ return new StyledText(chunks);
3709
+ }
3710
+ function appendStatsPanelLine(chunks, line) {
3711
+ if (isStatsHeatmapWeekdayLine(line)) {
3712
+ chunks.push(fg(theme.text)(line.slice(0, 5)));
3713
+ appendStatsHeatmapDots(chunks, line.slice(5));
3714
+ return;
3715
+ }
3716
+ if (line.trim() === "Less . o O @ More") {
3717
+ appendStatsHeatmapLegend(chunks, line.length - line.trimStart().length);
3718
+ return;
3719
+ }
3720
+ chunks.push(fg(theme.text)(line));
3721
+ }
3722
+ function isStatsHeatmapWeekdayLine(line) {
3723
+ return /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) /.test(line);
3724
+ }
3725
+ function appendStatsHeatmapDots(chunks, text) {
3726
+ const colors = statsHeatmapDotColors();
3727
+ const colorByLevel = {
3728
+ ".": colors[0],
3729
+ o: colors[1],
3730
+ O: colors[2],
3731
+ "@": colors[3],
3732
+ };
3733
+ for (const char of text) {
3734
+ const color = colorByLevel[char];
3735
+ chunks.push(color ? fg(color)("•") : fg(theme.text)(char));
3736
+ }
3737
+ }
3738
+ function appendStatsHeatmapLegend(chunks, indent) {
3739
+ const colors = statsHeatmapDotColors();
3740
+ chunks.push(fg(theme.textMuted)(`${" ".repeat(indent)}Less `));
3741
+ colors.forEach((color, index) => {
3742
+ if (index > 0)
3743
+ chunks.push(fg(theme.textMuted)(" "));
3744
+ chunks.push(fg(color)("•"));
3745
+ });
3746
+ chunks.push(fg(theme.textMuted)(" More"));
3747
+ }
3748
+ function statsHeatmapDotColors() {
3749
+ return isLightTheme()
3750
+ ? ["#D9B98E", "#BE7D37", "#A56218", theme.warning]
3751
+ : ["#6B471D", "#9D6728", "#D18830", theme.warning];
3752
+ }
3753
+ function syncStatsTab(box, text, active, label) {
3754
+ if (box)
3755
+ box.backgroundColor = active ? theme.primary : theme.backgroundElement;
3756
+ if (text) {
3757
+ text.content = label;
3758
+ text.fg = active ? contrastText(theme.primary) : theme.textMuted;
3759
+ }
3760
+ }
3437
3761
  function redrawFeishuSetupPanel() {
3438
3762
  if (!feishuSetupRoot)
3439
3763
  return;
@@ -4340,6 +4664,7 @@ function OpenTuiApp(props) {
4340
4664
  toggleSidebar,
4341
4665
  setSidebarMode: applySidebarMode,
4342
4666
  openFeedback,
4667
+ openStats: openStatsPanel,
4343
4668
  });
4344
4669
  if (!handled)
4345
4670
  return false;
@@ -4518,6 +4843,60 @@ function OpenTuiApp(props) {
4518
4843
  await openPicker(item.after.mode, item.after.providerId);
4519
4844
  }
4520
4845
  }
4846
+ function buildLocalModelPickerItems(providerId) {
4847
+ const groups = getVisibleModelProviders(registry, providerId).map((provider) => ({
4848
+ provider,
4849
+ models: localModelsForProvider(registry, provider),
4850
+ }));
4851
+ return buildModelPickerItemsFromGroups(groups, providerId);
4852
+ }
4853
+ async function buildRemoteModelPickerItems(providerId) {
4854
+ const groups = await discoverModelProviderGroups(registry, providerId);
4855
+ return buildModelPickerItemsFromGroups(groups, providerId);
4856
+ }
4857
+ function buildModelPickerItemsFromGroups(groups, providerId) {
4858
+ const items = [];
4859
+ for (const { provider, models } of groups) {
4860
+ for (const model of models) {
4861
+ const reasoningLevels = getModelPickerReasoningLevels(provider.id, model.id);
4862
+ if (reasoningLevels.length > 0) {
4863
+ for (const level of reasoningLevels) {
4864
+ const isCurrent = props.agent.model === `${provider.id}:${model.id}` && props.agent.thinking === level;
4865
+ items.push({
4866
+ label: `${model.name} (${level})`,
4867
+ detail: isCurrent ? "(current)" : undefined,
4868
+ value: `${provider.id}:${model.id}`,
4869
+ command: `/model ${provider.id}:${model.id} --reasoning-effort ${level}`,
4870
+ category: provider.name,
4871
+ gutter: isCurrent ? "●" : undefined,
4872
+ });
4873
+ }
4874
+ continue;
4875
+ }
4876
+ const isCurrent = props.agent.model === `${provider.id}:${model.id}`;
4877
+ items.push({
4878
+ label: model.name,
4879
+ detail: isCurrent ? "(current)" : undefined,
4880
+ value: `${provider.id}:${model.id}`,
4881
+ command: `/model ${provider.id}:${model.id}`,
4882
+ category: provider.name,
4883
+ gutter: isCurrent ? "●" : undefined,
4884
+ });
4885
+ }
4886
+ }
4887
+ const currentModel = props.agent.model;
4888
+ if (!providerId && currentModel && !items.some((item) => item.value === currentModel)) {
4889
+ items.unshift({
4890
+ label: displayModel(currentModel),
4891
+ detail: "(current)",
4892
+ value: currentModel,
4893
+ command: `/model ${currentModel}`,
4894
+ category: "Recent",
4895
+ gutter: "●",
4896
+ });
4897
+ }
4898
+ return items;
4899
+ }
4521
4900
  function buildPickerItems(kind, providerId) {
4522
4901
  if (kind === "slash")
4523
4902
  return [];
@@ -4526,60 +4905,7 @@ function OpenTuiApp(props) {
4526
4905
  if (kind === "skill")
4527
4906
  return buildSkillItems();
4528
4907
  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;
4908
+ return buildLocalModelPickerItems(providerId);
4583
4909
  }
4584
4910
  if (kind === "provider") {
4585
4911
  return buildProviderConnectItems();
@@ -4761,6 +5087,15 @@ function OpenTuiApp(props) {
4761
5087
  redrawTranscript(undefined, nextMessages);
4762
5088
  const taskStartedAt = Date.now();
4763
5089
  const run = beginAgentRun();
5090
+ traceEvent("tui_agent_run_begin", {
5091
+ runId: run.id,
5092
+ input: summarizeTraceValue(actualInput),
5093
+ displayInput: summarizeTraceValue(displayInput),
5094
+ displayMessages: displayMessages.length,
5095
+ queuedInputs: queuedInputCount(),
5096
+ provider: activeProviderId,
5097
+ model: props.agent.apiModel,
5098
+ }, { surface: "tui" });
4764
5099
  let assistantContent = "";
4765
5100
  let assistantReasoning = "";
4766
5101
  const toolCalls = [];
@@ -4805,6 +5140,14 @@ function OpenTuiApp(props) {
4805
5140
  abortSignal: run.abortController.signal,
4806
5141
  inputController: run.inputController,
4807
5142
  })) {
5143
+ traceEvent("tui_agent_event", {
5144
+ runId: run.id,
5145
+ event: summarizeAgentEventForTrace(event),
5146
+ displayMessages: displayMessages.length,
5147
+ streamingChars: assistantContent.length,
5148
+ reasoningChars: assistantReasoning.length,
5149
+ toolCount: toolCalls.length,
5150
+ }, { surface: "tui" });
4808
5151
  if (event.type === "turn_start") {
4809
5152
  assistantContent = "";
4810
5153
  assistantReasoning = "";
@@ -5011,6 +5354,11 @@ function OpenTuiApp(props) {
5011
5354
  if (!runCancelled) {
5012
5355
  runError = error?.message || String(error);
5013
5356
  }
5357
+ traceEvent("tui_agent_run_error", {
5358
+ runId: run.id,
5359
+ cancelled: runCancelled,
5360
+ error: summarizeTraceError(error),
5361
+ }, { surface: "tui" });
5014
5362
  }
5015
5363
  finally {
5016
5364
  if (pendingStreamingRedrawTimer !== undefined) {
@@ -5020,8 +5368,19 @@ function OpenTuiApp(props) {
5020
5368
  pendingApprovalRef = undefined;
5021
5369
  setPendingApproval(undefined);
5022
5370
  setApprovalOptionIdx(0);
5371
+ traceEvent("tui_agent_run_end", {
5372
+ runId: run.id,
5373
+ cancelled: runCancelled,
5374
+ error: runError,
5375
+ displayMessages: displayMessages.length,
5376
+ queuedInputs: queuedInputCount(),
5377
+ }, { surface: "tui" });
5023
5378
  for (const pendingInput of run.inputController.clear()) {
5024
5379
  const pendingSteer = removePendingSteerInput(pendingInput.id);
5380
+ if (runCancelled) {
5381
+ removeQueuedUserDisplay(pendingSteer?.displayId);
5382
+ continue;
5383
+ }
5025
5384
  requeueRejectedSteer(pendingInput.content, pendingSteer?.displayId);
5026
5385
  }
5027
5386
  finishAgentRun(run);
@@ -5035,6 +5394,7 @@ function OpenTuiApp(props) {
5035
5394
  else if (runCancelled) {
5036
5395
  if (!notice())
5037
5396
  setNotice("Agent run cancelled");
5397
+ displayMessages = reconstructDisplayMessages(props.agent.messages);
5038
5398
  redrawTranscript();
5039
5399
  }
5040
5400
  else {
@@ -5074,13 +5434,13 @@ function OpenTuiApp(props) {
5074
5434
  return h("box", {
5075
5435
  ref: (ref) => {
5076
5436
  sessionComposerShell = ref;
5077
- ref.visible = !isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !pendingFeedback() && !pendingFeishuSetup();
5437
+ ref.visible = !isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !pendingFeedback() && !statsPanel && !pendingFeishuSetup();
5078
5438
  },
5079
5439
  width: "100%",
5080
5440
  paddingLeft: 2,
5081
5441
  paddingRight: 2,
5082
5442
  flexShrink: 0,
5083
- visible: !isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !pendingFeishuSetup(),
5443
+ visible: !isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !statsPanel && !pendingFeishuSetup(),
5084
5444
  }, renderPrompt({
5085
5445
  ref: (ref) => { sessionPromptRef = ref; },
5086
5446
  focused: !isHomeSurfaceActive(streamingDisplay),
@@ -5091,7 +5451,7 @@ function OpenTuiApp(props) {
5091
5451
  onKeyDown: handlePickerKey,
5092
5452
  onUiKeyDown: promptUiKeyDown,
5093
5453
  getText: readPromptText,
5094
- disabled: () => !!pendingFeedback(),
5454
+ disabled: () => !!pendingFeedback() || !!statsPanel,
5095
5455
  mode,
5096
5456
  registerModeLabel: registerPromptModeLabel,
5097
5457
  registerModelLabel: registerPromptModelLabel,
@@ -5106,6 +5466,8 @@ function OpenTuiApp(props) {
5106
5466
  return "Answer the question below";
5107
5467
  if (pendingFeedback())
5108
5468
  return "Describe feedback below";
5469
+ if (statsPanel)
5470
+ return "Stats panel is open";
5109
5471
  const plan = pendingPlan();
5110
5472
  if (plan)
5111
5473
  return "Press Enter to approve plan or Esc to reject";
@@ -5136,14 +5498,14 @@ function OpenTuiApp(props) {
5136
5498
  h("box", {
5137
5499
  ref: (ref) => {
5138
5500
  homeComposerShell = ref;
5139
- ref.visible = isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !pendingFeedback() && !pendingFeishuSetup();
5501
+ ref.visible = isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !pendingFeedback() && !statsPanel && !pendingFeishuSetup();
5140
5502
  },
5141
5503
  width: "100%",
5142
5504
  maxWidth: 75,
5143
5505
  zIndex: 1000,
5144
5506
  paddingTop: 1,
5145
5507
  flexShrink: 0,
5146
- visible: isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !pendingFeishuSetup(),
5508
+ visible: isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !statsPanel && !pendingFeishuSetup(),
5147
5509
  }, renderPrompt({
5148
5510
  ref: (ref) => {
5149
5511
  homePromptRef = ref;
@@ -5158,7 +5520,7 @@ function OpenTuiApp(props) {
5158
5520
  onKeyDown: handlePickerKey,
5159
5521
  onUiKeyDown: promptUiKeyDown,
5160
5522
  getText: readPromptText,
5161
- disabled: () => !!pendingFeedback(),
5523
+ disabled: () => !!pendingFeedback() || !!statsPanel,
5162
5524
  mode,
5163
5525
  registerModeLabel: registerPromptModeLabel,
5164
5526
  registerModelLabel: registerPromptModelLabel,
@@ -5173,6 +5535,8 @@ function OpenTuiApp(props) {
5173
5535
  return "Answer the question below";
5174
5536
  if (pendingFeedback())
5175
5537
  return "Describe feedback below";
5538
+ if (statsPanel)
5539
+ return "Stats panel is open";
5176
5540
  const plan = pendingPlan();
5177
5541
  if (plan)
5178
5542
  return "Press Enter to approve plan or Esc to reject";
@@ -5419,6 +5783,131 @@ function OpenTuiApp(props) {
5419
5783
  content: "ctrl+d submit · tab view payload · enter newline · esc cancel",
5420
5784
  })));
5421
5785
  }
5786
+ function renderStatsPanel() {
5787
+ return h("box", {
5788
+ ref: (ref) => {
5789
+ statsRoot = ref;
5790
+ redrawStatsPanel();
5791
+ },
5792
+ visible: false,
5793
+ focusable: true,
5794
+ position: "absolute",
5795
+ left: 0,
5796
+ top: 0,
5797
+ width: "100%",
5798
+ height: "100%",
5799
+ zIndex: 3050,
5800
+ backgroundColor: modalBackdropColor(),
5801
+ flexDirection: "column",
5802
+ onKeyDown: (event) => {
5803
+ if (handleStatsKey(event)) {
5804
+ event.preventDefault?.();
5805
+ event.stopPropagation?.();
5806
+ return true;
5807
+ }
5808
+ return false;
5809
+ },
5810
+ onMouseUp: () => closeStatsPanel(),
5811
+ }, h("box", {
5812
+ ref: (ref) => {
5813
+ statsPanelBox = ref;
5814
+ redrawStatsPanel();
5815
+ },
5816
+ visible: false,
5817
+ position: "absolute",
5818
+ width: 76,
5819
+ height: 24,
5820
+ backgroundColor: theme.backgroundPanel,
5821
+ flexDirection: "column",
5822
+ paddingTop: 1,
5823
+ onMouseUp: (event) => {
5824
+ event.stopPropagation?.();
5825
+ },
5826
+ }, [
5827
+ h("box", {
5828
+ flexDirection: "row",
5829
+ alignItems: "center",
5830
+ justifyContent: "space-between",
5831
+ paddingLeft: 4,
5832
+ paddingRight: 4,
5833
+ flexShrink: 0,
5834
+ }, h("text", {
5835
+ ref: (ref) => { statsTitle = ref; },
5836
+ fg: theme.text,
5837
+ content: "Stats",
5838
+ }), h("text", {
5839
+ ref: (ref) => { statsEsc = ref; },
5840
+ fg: theme.textMuted,
5841
+ content: "esc",
5842
+ onMouseUp: () => closeStatsPanel(),
5843
+ })),
5844
+ h("box", {
5845
+ flexDirection: "row",
5846
+ gap: 1,
5847
+ paddingLeft: 4,
5848
+ paddingRight: 4,
5849
+ paddingTop: 1,
5850
+ flexShrink: 0,
5851
+ }, h("box", {
5852
+ ref: (ref) => { statsTab7Box = ref; },
5853
+ paddingLeft: 1,
5854
+ paddingRight: 1,
5855
+ backgroundColor: theme.backgroundElement,
5856
+ onMouseUp: () => setStatsRange("7d"),
5857
+ }, h("text", {
5858
+ ref: (ref) => { statsTab7Text = ref; },
5859
+ fg: theme.textMuted,
5860
+ content: "7 days",
5861
+ })), h("box", {
5862
+ ref: (ref) => { statsTab30Box = ref; },
5863
+ paddingLeft: 1,
5864
+ paddingRight: 1,
5865
+ backgroundColor: theme.primary,
5866
+ onMouseUp: () => setStatsRange("30d"),
5867
+ }, h("text", {
5868
+ ref: (ref) => { statsTab30Text = ref; },
5869
+ fg: contrastText(theme.primary),
5870
+ content: "30 days",
5871
+ }))),
5872
+ h("box", {
5873
+ paddingLeft: 4,
5874
+ paddingRight: 4,
5875
+ paddingTop: 1,
5876
+ flexGrow: 1,
5877
+ minHeight: 0,
5878
+ }, h("scrollbox", {
5879
+ ref: (ref) => { statsBodyScroll = ref; },
5880
+ flexGrow: 1,
5881
+ minHeight: 0,
5882
+ height: 14,
5883
+ onMouseScroll: (event) => {
5884
+ event.stopPropagation?.();
5885
+ },
5886
+ }, h("text", {
5887
+ ref: (ref) => { statsBodyText = ref; },
5888
+ fg: theme.text,
5889
+ wrapMode: "none",
5890
+ content: "",
5891
+ }))),
5892
+ h("box", {
5893
+ flexDirection: "row",
5894
+ justifyContent: "space-between",
5895
+ paddingLeft: 4,
5896
+ paddingRight: 4,
5897
+ paddingTop: 1,
5898
+ paddingBottom: 1,
5899
+ flexShrink: 0,
5900
+ backgroundColor: theme.backgroundPanel,
5901
+ }, h("text", {
5902
+ ref: (ref) => { statsFooterText = ref; },
5903
+ fg: theme.textMuted,
5904
+ bg: theme.backgroundPanel,
5905
+ wrapMode: "none",
5906
+ truncate: true,
5907
+ content: "left/right:range|tab:toggle|esc:close|view:30d",
5908
+ })),
5909
+ ]));
5910
+ }
5422
5911
  function renderFeishuSetupPanel() {
5423
5912
  return h("box", {
5424
5913
  ref: (ref) => {
@@ -6293,6 +6782,7 @@ function OpenTuiApp(props) {
6293
6782
  registerTraceBadge: registerFooterTraceBadge,
6294
6783
  }),
6295
6784
  renderProviderDialog(),
6785
+ renderStatsPanel(),
6296
6786
  renderFeishuSetupPanel(),
6297
6787
  renderNoticeOverlay(),
6298
6788
  ]);
@@ -6952,7 +7442,7 @@ function createTraceGroupRenderable(ctx, group, syntaxStyle, width = 80) {
6952
7442
  }, children);
6953
7443
  }
6954
7444
  function shouldRenderTraceGroupAsRawTool(tool) {
6955
- return tool.name === "question" || tool.name === "todo_write" || tool.name === "edit";
7445
+ return tool.name === "question" || tool.name === "todo_write" || tool.name === "edit" || tool.name === "apply_patch";
6956
7446
  }
6957
7447
  function traceGroupDetailLines(group) {
6958
7448
  return group.previewLines.length > 0 ? group.previewLines : group.items;
@@ -7033,8 +7523,9 @@ function traceGroupRenderableSignature(group) {
7033
7523
  return [
7034
7524
  tool.id,
7035
7525
  tool.name,
7036
- tool.status ?? (tool.result === undefined ? "pending" : "completed"),
7526
+ tool.status ?? (tool.result === undefined && !tool.resultCollapsed ? "pending" : "completed"),
7037
7527
  tool.isError ? "error" : "ok",
7528
+ tool.resultCollapsed ? "collapsed" : "expanded",
7038
7529
  stableStringify(tool.args),
7039
7530
  tool.result ?? "",
7040
7531
  stableStringify(tool.metadata ?? null),
@@ -7046,8 +7537,9 @@ function toolRenderableSignature(tool, writeExpanded) {
7046
7537
  return [
7047
7538
  tool.id,
7048
7539
  tool.name,
7049
- tool.status ?? (tool.result === undefined ? "pending" : "completed"),
7540
+ tool.status ?? (tool.result === undefined && !tool.resultCollapsed ? "pending" : "completed"),
7050
7541
  tool.isError ? "error" : "ok",
7542
+ tool.resultCollapsed ? "collapsed" : "expanded",
7051
7543
  tool.streamingArgs ? "streaming-args" : "args-complete",
7052
7544
  writeExpanded ? "expanded" : "collapsed",
7053
7545
  hashString(stableStringify(tool.args)),
@@ -7174,16 +7666,41 @@ function createMarkdownList(ctx, token, palette, defaultFg) {
7174
7666
  flexShrink: 0,
7175
7667
  }, items.map((item, index) => {
7176
7668
  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
- });
7669
+ return createMarkdownListItem(ctx, item, marker, palette, defaultFg);
7185
7670
  }));
7186
7671
  }
7672
+ function createMarkdownListItem(ctx, item, marker, palette, defaultFg) {
7673
+ const tokens = Array.isArray(item?.tokens) ? item.tokens : [];
7674
+ const inlineTokens = tokens.filter((child) => !isMarkdownListToken(child));
7675
+ const nestedLists = tokens.filter(isMarkdownListToken);
7676
+ const fallback = tokens.length > 0 ? "" : (item?.text ?? "");
7677
+ const children = [];
7678
+ const line = markdownInlineToStyledText(inlineTokens, palette, fallback);
7679
+ children.push(createText(ctx, new StyledText([
7680
+ fg(theme.textMuted)(marker),
7681
+ ...line.chunks,
7682
+ ]), {
7683
+ fg: defaultFg,
7684
+ wrapMode: "word",
7685
+ flexShrink: 0,
7686
+ }));
7687
+ for (const nestedList of nestedLists) {
7688
+ const nested = createMarkdownList(ctx, nestedList, palette, defaultFg);
7689
+ if (!nested)
7690
+ continue;
7691
+ children.push(createBox(ctx, {
7692
+ flexDirection: "column",
7693
+ flexShrink: 0,
7694
+ paddingLeft: Math.max(2, marker.length),
7695
+ }, [nested]));
7696
+ }
7697
+ return children.length === 1
7698
+ ? children[0]
7699
+ : createBox(ctx, { flexDirection: "column", flexShrink: 0 }, children);
7700
+ }
7701
+ function isMarkdownListToken(token) {
7702
+ return token?.type === "list";
7703
+ }
7187
7704
  function markdownTokenInlineTokens(token) {
7188
7705
  if (Array.isArray(token?.tokens))
7189
7706
  return token.tokens;
@@ -7775,10 +8292,10 @@ function renderTool(tool, syntaxStyle, width = 80) {
7775
8292
  const icon = toolStateIcon(tool);
7776
8293
  const color = toolColor(tool);
7777
8294
  const diff = extractToolDiff(tool);
7778
- if (diff && !tool.isError && tool.name === "edit") {
8295
+ if (diff && !tool.resultCollapsed && !tool.isError && (tool.name === "edit" || tool.name === "apply_patch")) {
7779
8296
  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)));
7780
8297
  }
7781
- if (isWritePreviewTool(tool)) {
8298
+ if (!tool.resultCollapsed && isWritePreviewTool(tool)) {
7782
8299
  const hasContent = typeof tool.args.content === "string";
7783
8300
  const contentStr = hasContent ? String(tool.args.content) : "";
7784
8301
  const preview = hasContent ? formatWritePreview(contentStr, false) : null;
@@ -8237,7 +8754,7 @@ function appendRawToolTranscript(chunks, tool) {
8237
8754
  append(`${content}\n`, color);
8238
8755
  };
8239
8756
  appendLine("");
8240
- const icon = tool.name === "bash" ? "$" : tool.name === "edit" || tool.name === "write" ? "✎" : "●";
8757
+ const icon = tool.name === "bash" ? "$" : tool.name === "edit" || tool.name === "write" || tool.name === "apply_patch" ? "✎" : "●";
8241
8758
  const color = toolColor(tool);
8242
8759
  append(` ${icon} `, color);
8243
8760
  append(displayToolName(tool.name), color);
@@ -8405,6 +8922,18 @@ function getApprovalPanelMeta(request) {
8405
8922
  path: request.path,
8406
8923
  };
8407
8924
  }
8925
+ if (request.type === "patch") {
8926
+ return {
8927
+ icon: "→",
8928
+ title: `Patch ${path}`,
8929
+ subtitle: `${request.paths.length} file${request.paths.length === 1 ? "" : "s"}`,
8930
+ preview: request.diff || "No diff provided",
8931
+ previewHeight: 10,
8932
+ previewColor: request.diff ? theme.toolText : theme.textMuted,
8933
+ diff: request.diff,
8934
+ path: request.paths[0] ?? request.path,
8935
+ };
8936
+ }
8408
8937
  return {
8409
8938
  icon: "→",
8410
8939
  title: `Write ${path}`,
@@ -8483,7 +9012,7 @@ function toolColor(tool) {
8483
9012
  return theme.toolShell;
8484
9013
  if (tool.name === "read")
8485
9014
  return theme.toolRead;
8486
- if (tool.name === "write" || tool.name === "edit")
9015
+ if (tool.name === "write" || tool.name === "edit" || tool.name === "apply_patch")
8487
9016
  return theme.toolWrite;
8488
9017
  if (tool.name === "grep" || tool.name === "glob" || tool.name === "web_search" || tool.name === "web_fetch")
8489
9018
  return theme.toolSearch;
@@ -8494,6 +9023,7 @@ function displayToolName(name) {
8494
9023
  read: "Read",
8495
9024
  write: "Write",
8496
9025
  edit: "Edit",
9026
+ apply_patch: "Patch",
8497
9027
  bash: "Shell",
8498
9028
  grep: "Grep",
8499
9029
  glob: "Glob",
@@ -8526,16 +9056,29 @@ function toolHeader(tool) {
8526
9056
  const agentId = args.agent_id ?? (Array.isArray(args.agent_ids) ? `${args.agent_ids.length} agents` : undefined);
8527
9057
  return agentId ? `(${truncate(String(agentId), 64)})` : "";
8528
9058
  }
8529
- const value = args.path ?? args.command ?? args.pattern ?? args.url ?? args.query;
9059
+ const value = args.path ?? args.command ?? args.pattern ?? args.url ?? args.query ?? toolPath(tool);
8530
9060
  return value ? `(${truncate(String(value).replace(/\n/g, " "), 64)})` : "";
8531
9061
  }
8532
9062
  function toolPath(tool) {
8533
- const value = tool.args?.path ?? tool.args?.filePath;
9063
+ const value = tool.args?.path
9064
+ ?? tool.args?.filePath
9065
+ ?? tool.metadata?.path
9066
+ ?? (Array.isArray(tool.metadata?.paths) ? tool.metadata.paths[0] : undefined);
8534
9067
  return typeof value === "string" ? value : undefined;
8535
9068
  }
8536
9069
  function extractToolDiff(tool) {
9070
+ if (tool.resultCollapsed)
9071
+ return undefined;
9072
+ if (typeof tool.metadata?.diff === "string" && tool.metadata.diff.trim().length > 0) {
9073
+ return tool.metadata.diff.trim();
9074
+ }
8537
9075
  if (!tool.result)
8538
9076
  return undefined;
9077
+ if (tool.result.includes("✂") ||
9078
+ tool.result.includes("chars truncated") ||
9079
+ tool.result.includes("chars omitted for UI")) {
9080
+ return undefined;
9081
+ }
8539
9082
  const marker = "\n\nDiff:\n";
8540
9083
  const index = tool.result.indexOf(marker);
8541
9084
  if (index === -1)
@@ -8589,7 +9132,7 @@ function summarizeToolResult(tool) {
8589
9132
  const matches = typeof tool.metadata?.matches === "number" ? tool.metadata.matches : undefined;
8590
9133
  if (tool.name === "read")
8591
9134
  return "";
8592
- if (tool.name === "edit")
9135
+ if (tool.name === "edit" || tool.name === "apply_patch")
8593
9136
  return "patched file";
8594
9137
  if (tool.name === "write")
8595
9138
  return "wrote file";
@@ -8615,7 +9158,7 @@ function toolStateIcon(tool) {
8615
9158
  }
8616
9159
  if (tool.name === "bash")
8617
9160
  return "$";
8618
- if (tool.name === "edit")
9161
+ if (tool.name === "edit" || tool.name === "apply_patch")
8619
9162
  return "✎";
8620
9163
  if (tool.name === "write")
8621
9164
  return "✎";
@@ -8692,7 +9235,7 @@ function formatQuestionAnswer(answer) {
8692
9235
  return answer?.length ? answer.join(", ") : "(no answer)";
8693
9236
  }
8694
9237
  function isToolFinished(tool) {
8695
- return tool.status === "completed" || tool.status === "error" || tool.result !== undefined;
9238
+ return tool.status === "completed" || tool.status === "error" || tool.resultCollapsed === true || tool.result !== undefined;
8696
9239
  }
8697
9240
  function assistantStatusLabel(message) {
8698
9241
  if (message.status === "responding")