@marimo-team/islands 0.25.3-dev266 → 0.25.3-dev268

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 (184) hide show
  1. package/dist/{ConnectedDataExplorerComponent-C_hj7Poi.js → ConnectedDataExplorerComponent-eLHPh6uu.js} +9 -9
  2. package/dist/{ErrorBoundary-CvxlC3F1.js → ErrorBoundary-CQYAoD7X.js} +64 -69
  3. package/dist/{abnfDiagram-VCTEODGH-DzrZZXRD.js → abnfDiagram-VCTEODGH-hqd0IBtX.js} +2 -2
  4. package/dist/{any-language-editor-CFeWfC3o.js → any-language-editor-DnG26QXh.js} +1 -1
  5. package/dist/{architectureDiagram-5GKGNRK7-iF_snyFL.js → architectureDiagram-5GKGNRK7-BzLAFcc0.js} +1 -1
  6. package/dist/{strings-BCKpLFNU.js → assertNever-DIU3JJbt.js} +14 -14
  7. package/dist/{chat-ui-DFxNgzXF.js → chat-ui-BAnLgpTe.js} +1964 -1740
  8. package/dist/{chunk-IMKFNOWR-B7xytD4B.js → chunk-IMKFNOWR-DOeVvuyE.js} +1 -1
  9. package/dist/{chunk-SHT3W25Y-I8ZFP5L1.js → chunk-SHT3W25Y-DZoFI8wR.js} +1 -1
  10. package/dist/{chunk-SVP7TREG-Di-uAPdT.js → chunk-SVP7TREG-CWu86PSW.js} +1 -1
  11. package/dist/{chunk-TICWLB2K-BFpUy0ud.js → chunk-TICWLB2K-BrM0GoP3.js} +1 -1
  12. package/dist/{classDiagram-ZZMXUADV-Cqm4JGpx.js → classDiagram-ZZMXUADV-DYi4V2bB.js} +2 -2
  13. package/dist/{classDiagram-v2-VYDZK3BY-DFHRJpN3.js → classDiagram-v2-VYDZK3BY-tN7FIk-m.js} +2 -2
  14. package/dist/{code-visibility-SRM1JiD_.js → code-visibility-DcZ_RwaH.js} +44 -47
  15. package/dist/{common-yiRhZ6ZZ.js → common-BGeQilvh.js} +5846 -5519
  16. package/dist/{config-CmeG_Pb6.js → config-BsPruWMR.js} +2 -2
  17. package/dist/{createReducer-C_syF8Td.js → createReducer-DdtT_7ji.js} +1 -1
  18. package/dist/{cynefinDiagram-5FMLGOSQ-cuGOeTBC.js → cynefinDiagram-5FMLGOSQ-D5nCnDc-.js} +1 -1
  19. package/dist/{dagre-GXQ25YYZ-DJ76aC4k.js → dagre-GXQ25YYZ-BHkd7f_i.js} +1 -1
  20. package/dist/{data-grid-overlay-editor-DLTzcQ4O.js → data-grid-overlay-editor-Dc4lW4A3.js} +1 -1
  21. package/dist/{diagram-S7CK7UJ4-DLLc0sw7.js → diagram-S7CK7UJ4-D1ALppT2.js} +1 -1
  22. package/dist/{diagram-UQ7AKVKN-Tz_d6qmk.js → diagram-UQ7AKVKN-DLv0b83L.js} +1 -1
  23. package/dist/{diagram-VX7I27RA-D7SzPrDN.js → diagram-VX7I27RA-7XJ7J6qg.js} +1 -1
  24. package/dist/{diagram-Z3DM3KII-qr37XV58.js → diagram-Z3DM3KII-7M7zbHp_.js} +1 -1
  25. package/dist/{dialog-BtprQCUw.js → dialog-DzCj1qQv.js} +1 -1
  26. package/dist/{dropdown-menu-CkJ7ErY6.js → dropdown-menu-CdXE15an.js} +1214 -1208
  27. package/dist/{ebnfDiagram-PWID7BFC-BKOQb4J0.js → ebnfDiagram-PWID7BFC-DHH1qlmV.js} +2 -2
  28. package/dist/{erDiagram-RLTQ6QDP-CM15Vq68.js → erDiagram-RLTQ6QDP-fI_0dGef.js} +1 -1
  29. package/dist/{error-banner-DbRFOd2O.js → error-banner-DPc9eIw5.js} +3 -3
  30. package/dist/{error-entries-CMoaUX9V.js → error-entries-CxN-Nq3V.js} +524 -456
  31. package/dist/{feedback-button-DBUrE4OL.js → feedback-button-BqaeQcvX.js} +77 -77
  32. package/dist/{flowDiagram-HODETNUW-Dt3N5yFT.js → flowDiagram-HODETNUW-Brg9MA6i.js} +2 -2
  33. package/dist/{formats-gGospKWJ.js → formats-DbSvAPY-.js} +1 -1
  34. package/dist/{glide-data-editor-Cfv4HeOh.js → glide-data-editor-CiijPXA7.js} +2042 -2041
  35. package/dist/{glide-utils-Ba9WD2IN.js → glide-utils-BSw3SSHF.js} +29 -35
  36. package/dist/{globals-dIbBIR5-.js → globals-CpmSWKI_.js} +2 -2
  37. package/dist/{html-to-image-D_5CMreP.js → html-to-image-8sywF4my.js} +5452 -5751
  38. package/dist/{infoDiagram-27XIBGKW-C7axJMCl.js → infoDiagram-27XIBGKW-E9e403YT.js} +1 -1
  39. package/dist/{ishikawaDiagram-5VMMS53U-CdC9Eur7.js → ishikawaDiagram-5VMMS53U-C-RlYDRo.js} +1 -1
  40. package/dist/{kanban-definition-UXKFOSKX-BHnMIgD7.js → kanban-definition-UXKFOSKX-CLFl_uqK.js} +1 -1
  41. package/dist/main.js +6294 -6324
  42. package/dist/{mermaid-DFYN_PXd.js → mermaid-rd09e1JL.js} +35 -35
  43. package/dist/{mindmap-definition-YA3MSWOX-BagHoCGU.js → mindmap-definition-YA3MSWOX-BQ8TWtyB.js} +1 -1
  44. package/dist/{number-overlay-editor-CTiJp5Mj.js → number-overlay-editor-Ft4NiAEN.js} +1 -1
  45. package/dist/{pegDiagram-XKGWAZYB-8IIZczip.js → pegDiagram-XKGWAZYB-CN8UHFPi.js} +2 -2
  46. package/dist/pencil-DcT8AE0h.js +10 -0
  47. package/dist/{pieDiagram-E7YTZNPT-CE6dKBIr.js → pieDiagram-E7YTZNPT-DGuJ0EBd.js} +1 -1
  48. package/dist/{process-output-CGFNVC60.js → process-output-Dku5POvm.js} +3 -3
  49. package/dist/{railroadDiagram-O6MQD6OU-CkexPq3b.js → railroadDiagram-O6MQD6OU-BPDA8wy8.js} +2 -2
  50. package/dist/{requirementDiagram-BXWQKSXE-B0-OkuAe.js → requirementDiagram-BXWQKSXE-ChzLGbGW.js} +1 -1
  51. package/dist/{reveal-component-DH09ygHo.js → reveal-component-D0cfwbbl.js} +1094 -1094
  52. package/dist/{select-C33FVPPN.js → select-OxgozOC8.js} +1 -1
  53. package/dist/{sequenceDiagram-WJ2MYXX4-Dxysg8DX.js → sequenceDiagram-WJ2MYXX4-DFX-bE6O.js} +1 -1
  54. package/dist/{spec-DH1lmF7Q.js → spec-oJn0RhpQ.js} +2 -2
  55. package/dist/{stateDiagram-D77RDMKH-DTEN3-OJ.js → stateDiagram-D77RDMKH-BR7cI3wK.js} +2 -2
  56. package/dist/{stateDiagram-v2-MP3YSRHH-vktk3Xc5.js → stateDiagram-v2-MP3YSRHH-oGqB5UDI.js} +2 -2
  57. package/dist/{static-state-BldGd99T.js → static-state-CuV6Pc3a.js} +1 -1
  58. package/dist/style.css +1 -1
  59. package/dist/{swimlanes-42K2YHIH-Cjveh9OM.js → swimlanes-42K2YHIH-DwSZl1yC.js} +1 -1
  60. package/dist/{swimlanesDiagram-VR7AAH4N-Bdr7Ev_2.js → swimlanesDiagram-VR7AAH4N-BHlcd-Ri.js} +2 -2
  61. package/dist/{timeline-definition-24CTP7MA-KIuDcefn.js → timeline-definition-24CTP7MA-DMmP-G2O.js} +1 -1
  62. package/dist/{toast-CY1_Qv41.js → toast-DbBHmxmB.js} +2 -2
  63. package/dist/{useAsyncData-DsZMvNXn.js → useAsyncData-BYIAoMqn.js} +1 -1
  64. package/dist/{useDateFormatter-Dk_xFci4.js → useDateFormatter-ButWkdcK.js} +1 -1
  65. package/dist/{useDeepCompareMemoize-KMBBux8C.js → useDeepCompareMemoize-Bz0c5GN6.js} +1 -1
  66. package/dist/{useTheme-BZ2GGLgm.js → useTheme-DPLobSFs.js} +7 -7
  67. package/dist/{utils-DXkj82sK.js → utils-EBklsC5R.js} +31 -31
  68. package/dist/{vega-component-C-lqjV0q.js → vega-component-rxIxAAh9.js} +7 -7
  69. package/dist/{vennDiagram-4TSXK5OY-DrIOUHfb.js → vennDiagram-4TSXK5OY-Db8GtH1G.js} +1 -1
  70. package/dist/{wardleyDiagram-VM6X3IG4-CJNjf4OH.js → wardleyDiagram-VM6X3IG4-DmIiC3FU.js} +1 -1
  71. package/dist/{xychartDiagram-S5SC5T6Z-DEshFho-.js → xychartDiagram-S5SC5T6Z-DS9OPrNq.js} +1 -1
  72. package/package.json +1 -1
  73. package/src/__mocks__/executions.ts +195 -0
  74. package/src/__mocks__/notebook.ts +0 -1
  75. package/src/__tests__/CellStatus.test.tsx +19 -20
  76. package/src/components/chat/__tests__/chat-display.test.tsx +157 -86
  77. package/src/components/chat/__tests__/chat-utils.test.ts +42 -0
  78. package/src/components/chat/__tests__/turn-timing.test.ts +84 -0
  79. package/src/components/chat/__tests__/use-assistant-turn.test.ts +95 -0
  80. package/src/components/chat/__tests__/user-message.test.tsx +27 -0
  81. package/src/components/chat/acp/agent-panel.tsx +2 -0
  82. package/src/components/chat/agent-activity.tsx +132 -0
  83. package/src/components/chat/chat-display.tsx +125 -96
  84. package/src/components/chat/chat-panel.tsx +123 -106
  85. package/src/components/chat/chat-utils.ts +12 -24
  86. package/src/components/chat/tool-call/__tests__/tool-labels.test.ts +76 -0
  87. package/src/components/chat/tool-call/__tests__/tool-step.test.tsx +57 -0
  88. package/src/components/chat/tool-call/shared.ts +6 -0
  89. package/src/components/chat/tool-call/tool-call-view.tsx +22 -76
  90. package/src/components/chat/tool-call/tool-labels.ts +172 -0
  91. package/src/components/chat/tool-call/tool-step.tsx +145 -0
  92. package/src/components/chat/turn-timing.ts +73 -0
  93. package/src/components/chat/use-assistant-turn.ts +65 -0
  94. package/src/components/chat/use-chat-generation.ts +5 -4
  95. package/src/components/chat/user-message.tsx +145 -0
  96. package/src/components/dependency-graph/minimap-content.tsx +3 -2
  97. package/src/components/editor/__tests__/data-attributes.test.tsx +0 -7
  98. package/src/components/editor/__tests__/output-persistence.test.tsx +2 -2
  99. package/src/components/editor/actions/__tests__/useCellActionButton.test.tsx +19 -1
  100. package/src/components/editor/actions/useCellActionButton.tsx +10 -3
  101. package/src/components/editor/ai/completion-handlers.tsx +0 -37
  102. package/src/components/editor/alerts/stdin-blocking-alert.tsx +3 -1
  103. package/src/components/editor/cell/CellStatus.tsx +23 -19
  104. package/src/components/editor/cell/RunButton.tsx +16 -6
  105. package/src/components/editor/cell/StagedAICell.tsx +51 -144
  106. package/src/components/editor/cell/__tests__/useRunCells.test.ts +45 -0
  107. package/src/components/editor/cell/cell-context-menu.tsx +4 -1
  108. package/src/components/editor/cell/code/cell-editor.tsx +7 -5
  109. package/src/components/editor/cell/collapse.tsx +4 -0
  110. package/src/components/editor/cell/useRunCells.ts +18 -3
  111. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +193 -32
  112. package/src/components/editor/chrome/wrapper/app-chrome.tsx +2 -2
  113. package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +145 -81
  114. package/src/components/editor/chrome/wrapper/pending-ai-changes-badge.tsx +25 -0
  115. package/src/components/editor/chrome/wrapper/sidebar.tsx +8 -4
  116. package/src/components/editor/header/status.tsx +4 -2
  117. package/src/components/editor/links/cell-link.tsx +4 -4
  118. package/src/components/editor/navigation/navigation.ts +3 -2
  119. package/src/components/editor/notebook-cell.tsx +85 -94
  120. package/src/components/editor/renderers/grid-layout/__tests__/grid-layout.test.tsx +75 -0
  121. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +22 -16
  122. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -1
  123. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +12 -33
  124. package/src/components/editor/stdin-blocking-alert.tsx +3 -1
  125. package/src/components/scratchpad/scratchpad.tsx +4 -1
  126. package/src/components/slides/minimap.tsx +4 -2
  127. package/src/components/slides/reveal-component.tsx +14 -10
  128. package/src/components/slides/slide-cell-view.tsx +28 -24
  129. package/src/components/slides/slide.tsx +3 -4
  130. package/src/components/ui/toast.tsx +1 -1
  131. package/src/core/ai/__tests__/staged-cells.test.ts +74 -126
  132. package/src/core/ai/staged-cells.ts +48 -38
  133. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -389
  134. package/src/core/cells/__tests__/cell.test.ts +49 -154
  135. package/src/core/cells/__tests__/cells.test.ts +18 -127
  136. package/src/core/cells/__tests__/pending-delete-service.test.tsx +11 -20
  137. package/src/core/cells/__tests__/utils.test.ts +75 -247
  138. package/src/core/cells/actions.ts +5 -2
  139. package/src/core/cells/cell.ts +31 -133
  140. package/src/core/cells/cells.ts +14 -29
  141. package/src/core/cells/document.ts +3 -3
  142. package/src/core/cells/focus.ts +27 -6
  143. package/src/core/cells/layout.ts +1 -1
  144. package/src/core/cells/pending-delete-service.ts +5 -7
  145. package/src/core/cells/scrollCellIntoView.ts +6 -0
  146. package/src/core/cells/types.ts +13 -36
  147. package/src/core/cells/utils.ts +37 -68
  148. package/src/core/codemirror/placeholder/__tests__/extensions.test.ts +59 -0
  149. package/src/core/codemirror/placeholder/extensions.ts +23 -0
  150. package/src/core/edit-app.tsx +2 -1
  151. package/src/core/executions/__tests__/atoms.test.ts +99 -0
  152. package/src/core/executions/__tests__/select.test.ts +268 -0
  153. package/src/core/executions/atoms.ts +206 -0
  154. package/src/core/executions/hooks.ts +31 -0
  155. package/src/core/executions/index.ts +31 -0
  156. package/src/core/executions/select.ts +175 -0
  157. package/src/core/export/__tests__/hooks.test.ts +0 -5
  158. package/src/core/islands/bootstrap.ts +4 -1
  159. package/src/core/islands/components/output-wrapper.tsx +5 -5
  160. package/src/core/kernel/handlers.ts +2 -11
  161. package/src/core/kernel/state.ts +0 -15
  162. package/src/core/lifecycle/render-policy.ts +12 -26
  163. package/src/core/network/__tests__/requests-lazy.test.ts +5 -5
  164. package/src/core/network/requests-lazy.ts +3 -3
  165. package/src/core/notebook/operations.ts +1 -1
  166. package/src/core/run-app.tsx +2 -1
  167. package/src/core/runtime/__tests__/adapter.test.ts +3 -3
  168. package/src/core/runtime/adapter.ts +4 -4
  169. package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +34 -0
  170. package/src/core/session/useNotebookSession.tsx +10 -5
  171. package/src/core/wasm/bridge.ts +1 -1
  172. package/src/css/app/Cell.css +7 -36
  173. package/src/plugins/core/registerReactComponent.tsx +4 -13
  174. package/src/plugins/impl/chat/chat-ui.tsx +2 -3
  175. package/src/plugins/impl/common/error-banner.tsx +5 -3
  176. package/src/stories/cell-status.stories.tsx +17 -10
  177. package/src/stories/cell.stories.tsx +0 -31
  178. package/src/stories/layout/vertical/one-column.stories.tsx +0 -9
  179. package/src/utils/time.ts +10 -0
  180. package/src/components/chat/reasoning-accordion.tsx +0 -61
  181. package/src/components/chat/tool-call/tool-activity-group.tsx +0 -49
  182. package/src/components/chat/tool-call/tool-error-card.tsx +0 -81
  183. package/src/components/chat/tool-call/tool-history-row.tsx +0 -147
  184. package/src/components/icons/loading-ellipsis.tsx +0 -46
@@ -57,30 +57,6 @@ export async function convertToFileUIPart(
57
57
  return fileUIParts;
58
58
  }
59
59
 
60
- export function isLastMessageReasoning(messages: UIMessage[]): boolean {
61
- if (messages.length === 0) {
62
- return false;
63
- }
64
-
65
- const lastMessage = messages.at(-1);
66
- if (!lastMessage) {
67
- return false;
68
- }
69
-
70
- if (lastMessage.role !== "assistant" || !lastMessage.parts) {
71
- return false;
72
- }
73
-
74
- const parts = lastMessage.parts;
75
- if (parts.length === 0) {
76
- return false;
77
- }
78
-
79
- // Check if the last part is reasoning
80
- const lastPart = parts[parts.length - 1];
81
- return lastPart.type === "reasoning";
82
- }
83
-
84
60
  function stringifyTextParts(parts: UIMessage["parts"]): string {
85
61
  return parts
86
62
  .map((part) => (part.type === "text" ? part.text : ""))
@@ -198,6 +174,18 @@ export function hasPendingToolCalls(messages: UIMessage[]): boolean {
198
174
  );
199
175
  }
200
176
 
177
+ export function willAutoContinue(messages: UIMessage[]): boolean {
178
+ const lastMessage = messages.at(-1);
179
+ const lastPart =
180
+ lastMessage?.role === "assistant" ? lastMessage.parts.at(-1) : undefined;
181
+ return (
182
+ hasPendingToolCalls(messages) ||
183
+ (lastPart != null &&
184
+ isToolUIPart(lastPart) &&
185
+ lastPart.state === "input-available")
186
+ );
187
+ }
188
+
201
189
  /**
202
190
  * True when the assistant is still waiting on tool execution or user approval.
203
191
  * Unlike `hasPendingToolCalls` (ready to auto-resume), these states must block
@@ -0,0 +1,76 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import { describeToolCall } from "../tool-labels";
5
+
6
+ describe("describeToolCall", () => {
7
+ it("names notebook edits by operation and shows the first line of code", () => {
8
+ const input = {
9
+ edit: {
10
+ type: "add_cell",
11
+ code: "\n def fib(n):\n return n",
12
+ position: {},
13
+ },
14
+ };
15
+ expect(
16
+ describeToolCall("tool-edit_notebook_tool", input, "done"),
17
+ ).toMatchObject({
18
+ label: "Added cell",
19
+ detail: "def fib(n):",
20
+ });
21
+ expect(
22
+ describeToolCall("tool-edit_notebook_tool", input, "running").label,
23
+ ).toBe("Adding cell");
24
+ });
25
+
26
+ it("falls back to a generic edit label while the input is still streaming", () => {
27
+ expect(
28
+ describeToolCall("tool-edit_notebook_tool", { edit: {} }, "running"),
29
+ ).toEqual(expect.objectContaining({ label: "Editing notebook" }));
30
+ expect(
31
+ describeToolCall("tool-edit_notebook_tool", { edit: {} }, "running"),
32
+ ).not.toHaveProperty("detail");
33
+ });
34
+
35
+ it("ignores inherited object keys in tool names and edit types", () => {
36
+ expect(describeToolCall("tool-constructor", {}, "done")).toMatchObject({
37
+ label: "Called",
38
+ detail: "constructor",
39
+ });
40
+ expect(
41
+ describeToolCall(
42
+ "edit_notebook_tool",
43
+ { edit: { type: "toString" } },
44
+ "done",
45
+ ).label,
46
+ ).toBe("Edited notebook");
47
+ });
48
+
49
+ it("summarizes code-mode runs by their first meaningful line", () => {
50
+ const code =
51
+ "import marimo._code_mode as cm\n# add cells\ncm.add_cell('x = 1')";
52
+ expect(
53
+ describeToolCall("tool-execute_code", { code }, "done"),
54
+ ).toMatchObject({
55
+ label: "Ran code",
56
+ detail: "cm.add_cell('x = 1')",
57
+ });
58
+ expect(
59
+ describeToolCall("execute_code", { code: "import numpy as np" }, "done")
60
+ .detail,
61
+ ).toBe("import numpy as np");
62
+ });
63
+
64
+ it("uses plain language for known tools", () => {
65
+ expect(describeToolCall("lint_notebook", {}, "done").label).toBe(
66
+ "Linted notebook",
67
+ );
68
+ });
69
+
70
+ it("shows the raw name for unknown tools such as MCP tools", () => {
71
+ expect(describeToolCall("tool-search_docs", {}, "running")).toMatchObject({
72
+ label: "Calling",
73
+ detail: "search_docs",
74
+ });
75
+ });
76
+ });
@@ -0,0 +1,57 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+ import { describe, expect, it } from "vitest";
5
+ import { ToolStep } from "../tool-step";
6
+
7
+ const edit = { edit: { type: "add_cell", code: "x = 1", position: {} } };
8
+
9
+ describe("ToolStep", () => {
10
+ it("describes the call and reveals its input and output on demand", () => {
11
+ render(
12
+ <ToolStep
13
+ toolName="tool-edit_notebook_tool"
14
+ state="output-available"
15
+ input={edit}
16
+ result={{ status: "success" }}
17
+ />,
18
+ );
19
+ const row = screen.getByRole("button", { name: /Added cell/ });
20
+ expect(row.getAttribute("aria-expanded")).toBe("false");
21
+ expect(screen.queryByText("Tool Request")).toBeNull();
22
+
23
+ fireEvent.click(row);
24
+ expect(row.getAttribute("aria-expanded")).toBe("true");
25
+ expect(screen.getByText("Tool Request")).toBeTruthy();
26
+ expect(screen.getByText("Tool Result")).toBeTruthy();
27
+ });
28
+
29
+ it("shows the error text of a failed call", () => {
30
+ render(
31
+ <ToolStep
32
+ toolName="lint_notebook"
33
+ state="output-error"
34
+ input={{}}
35
+ errorText="Linter crashed"
36
+ />,
37
+ );
38
+ fireEvent.click(
39
+ screen.getByRole("button", { name: /Linted notebook.*Failed/ }),
40
+ );
41
+ expect(screen.getByText("Linter crashed")).toBeTruthy();
42
+ });
43
+
44
+ it("marks calls cut off by the end of the turn as stopped", () => {
45
+ render(
46
+ <ToolStep
47
+ toolName="tool-edit_notebook_tool"
48
+ state="input-available"
49
+ input={edit}
50
+ isActive={false}
51
+ />,
52
+ );
53
+ expect(
54
+ screen.getByRole("button", { name: /Adding cell.*Stopped/ }),
55
+ ).toBeTruthy();
56
+ });
57
+ });
@@ -8,6 +8,12 @@ export type ToolState = ToolUIPart["state"];
8
8
  // rather than exporting a named type, so we derive ours from there.
9
9
  export type ToolApproval = NonNullable<ToolUIPart["approval"]>;
10
10
 
11
+ export const PENDING_TOOL_STATES: ReadonlySet<ToolState> = new Set([
12
+ "input-streaming",
13
+ "input-available",
14
+ "approval-responded",
15
+ ]);
16
+
11
17
  export function formatToolName(toolName: string): string {
12
18
  return toolName.replace("tool-", "");
13
19
  }
@@ -2,11 +2,9 @@
2
2
 
3
3
  import type { ChatAddToolApproveResponseFunction } from "ai";
4
4
  import React from "react";
5
- import { logNever } from "@/utils/assertNever";
6
5
  import type { ToolApproval, ToolState } from "./shared";
7
6
  import { ToolApprovalCard } from "./tool-approval-card";
8
- import { ToolErrorCard } from "./tool-error-card";
9
- import { ToolHistoryRow } from "./tool-history-row";
7
+ import { ToolStep } from "./tool-step";
10
8
 
11
9
  interface ToolCallViewProps {
12
10
  toolName: string;
@@ -16,88 +14,36 @@ interface ToolCallViewProps {
16
14
  input?: unknown;
17
15
  approval?: ToolApproval;
18
16
  onApprove?: ChatAddToolApproveResponseFunction;
19
- index?: number;
20
17
  className?: string;
21
- /**
22
- * Whether this tool call belongs to the latest message in the conversation.
23
- * Used by error cards to decide whether to stay expanded (live) or collapse
24
- * (the user has moved on).
25
- */
26
- isLive?: boolean;
27
18
  isActive?: boolean;
28
19
  }
29
20
 
30
21
  export const ToolCallView: React.FC<ToolCallViewProps> = ({
31
22
  toolName,
32
23
  state,
33
- result,
34
- errorText,
35
- input,
36
24
  approval,
37
25
  onApprove,
38
- index,
39
- className,
40
- isLive = true,
41
- isActive = true,
26
+ ...props
42
27
  }) => {
43
- switch (state) {
44
- case "approval-requested":
45
- // Approval is a live, blocking action — render it as a prominent card
46
- // Fall back to history row if the wiring isn't
47
- // available (shouldn't happen in practice).
48
- if (approval != null && onApprove != null) {
49
- return (
50
- <ToolApprovalCard
51
- toolName={toolName}
52
- input={input}
53
- approval={approval}
54
- onApprove={onApprove}
55
- className={className}
56
- />
57
- );
58
- }
59
- return (
60
- <ToolHistoryRow
61
- toolName={toolName}
62
- state="input-available"
63
- input={input}
64
- index={index}
65
- className={className}
66
- isActive={isActive}
67
- />
68
- );
69
-
70
- case "output-error":
71
- return (
72
- <ToolErrorCard
73
- toolName={toolName}
74
- input={input}
75
- errorText={errorText}
76
- isLive={isLive}
77
- className={className}
78
- />
79
- );
80
-
81
- case "input-streaming":
82
- case "input-available":
83
- case "approval-responded":
84
- case "output-available":
85
- case "output-denied":
86
- return (
87
- <ToolHistoryRow
88
- toolName={toolName}
89
- state={state}
90
- input={input}
91
- result={result}
92
- approval={approval}
93
- index={index}
94
- className={className}
95
- isActive={isActive}
96
- />
97
- );
98
-
99
- default:
100
- logNever(state);
101
- return null;
28
+ if (state === "approval-requested") {
29
+ return approval != null && onApprove != null ? (
30
+ <ToolApprovalCard
31
+ toolName={toolName}
32
+ input={props.input}
33
+ approval={approval}
34
+ onApprove={onApprove}
35
+ className={props.className}
36
+ />
37
+ ) : (
38
+ <ToolStep {...props} toolName={toolName} state="input-available" />
39
+ );
102
40
  }
41
+ return (
42
+ <ToolStep
43
+ {...props}
44
+ toolName={toolName}
45
+ state={state}
46
+ approval={approval}
47
+ />
48
+ );
103
49
  };
@@ -0,0 +1,172 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import {
4
+ BookOpenIcon,
5
+ BugIcon,
6
+ DatabaseIcon,
7
+ FileSearchIcon,
8
+ ListChecksIcon,
9
+ type LucideIcon,
10
+ NetworkIcon,
11
+ NotebookIcon,
12
+ PencilIcon,
13
+ PlusIcon,
14
+ ScanSearchIcon,
15
+ SquareTerminalIcon,
16
+ Trash2Icon,
17
+ VariableIcon,
18
+ WrenchIcon,
19
+ } from "lucide-react";
20
+ import { formatToolName } from "./shared";
21
+
22
+ interface ToolDescription {
23
+ label: string;
24
+ detail?: string;
25
+ icon: LucideIcon;
26
+ }
27
+
28
+ type Phase = "running" | "done";
29
+
30
+ const KNOWN_TOOLS: Record<
31
+ string,
32
+ { running: string; done: string; icon: LucideIcon }
33
+ > = {
34
+ get_marimo_rules: {
35
+ running: "Reading marimo guidelines",
36
+ done: "Read marimo guidelines",
37
+ icon: BookOpenIcon,
38
+ },
39
+ get_active_notebooks: {
40
+ running: "Listing notebooks",
41
+ done: "Listed notebooks",
42
+ icon: NotebookIcon,
43
+ },
44
+ get_lightweight_cell_map: {
45
+ running: "Reading notebook",
46
+ done: "Read notebook",
47
+ icon: FileSearchIcon,
48
+ },
49
+ get_cell_runtime_data: {
50
+ running: "Inspecting cells",
51
+ done: "Inspected cells",
52
+ icon: ScanSearchIcon,
53
+ },
54
+ get_cell_outputs: {
55
+ running: "Reading cell outputs",
56
+ done: "Read cell outputs",
57
+ icon: ScanSearchIcon,
58
+ },
59
+ get_tables_and_variables: {
60
+ running: "Reading variables",
61
+ done: "Read variables",
62
+ icon: VariableIcon,
63
+ },
64
+ get_database_tables: {
65
+ running: "Reading database tables",
66
+ done: "Read database tables",
67
+ icon: DatabaseIcon,
68
+ },
69
+ get_notebook_errors: {
70
+ running: "Checking for errors",
71
+ done: "Checked for errors",
72
+ icon: BugIcon,
73
+ },
74
+ lint_notebook: {
75
+ running: "Linting notebook",
76
+ done: "Linted notebook",
77
+ icon: ListChecksIcon,
78
+ },
79
+ get_cell_dependency_graph: {
80
+ running: "Reading dependency graph",
81
+ done: "Read dependency graph",
82
+ icon: NetworkIcon,
83
+ },
84
+ };
85
+
86
+ const EDIT_LABELS = {
87
+ add_cell: { running: "Adding cell", done: "Added cell", icon: PlusIcon },
88
+ update_cell: {
89
+ running: "Editing cell",
90
+ done: "Edited cell",
91
+ icon: PencilIcon,
92
+ },
93
+ delete_cell: {
94
+ running: "Deleting cell",
95
+ done: "Deleted cell",
96
+ icon: Trash2Icon,
97
+ },
98
+ } as const;
99
+
100
+ export function describeToolCall(
101
+ toolName: string,
102
+ input: unknown,
103
+ phase: Phase,
104
+ ): ToolDescription {
105
+ const name = formatToolName(toolName);
106
+
107
+ if (name === "execute_code") {
108
+ return {
109
+ label: phase === "running" ? "Running code" : "Ran code",
110
+ detail: isRecord(input) ? summarizeCode(input.code) : undefined,
111
+ icon: SquareTerminalIcon,
112
+ };
113
+ }
114
+
115
+ if (name === "edit_notebook_tool") {
116
+ const edit = getEdit(input);
117
+ if (edit) {
118
+ const labels = EDIT_LABELS[edit.type];
119
+ return {
120
+ label: labels[phase],
121
+ detail: summarizeCode(edit.code),
122
+ icon: labels.icon,
123
+ };
124
+ }
125
+ return {
126
+ label: phase === "running" ? "Editing notebook" : "Edited notebook",
127
+ icon: PencilIcon,
128
+ };
129
+ }
130
+
131
+ if (Object.hasOwn(KNOWN_TOOLS, name)) {
132
+ const known = KNOWN_TOOLS[name];
133
+ return { label: known[phase], icon: known.icon };
134
+ }
135
+
136
+ return {
137
+ label: phase === "running" ? "Calling" : "Called",
138
+ detail: name,
139
+ icon: WrenchIcon,
140
+ };
141
+ }
142
+
143
+ function getEdit(
144
+ input: unknown,
145
+ ): { type: keyof typeof EDIT_LABELS; code?: string } | undefined {
146
+ if (!isRecord(input) || !isRecord(input.edit)) {
147
+ return undefined;
148
+ }
149
+ const { type, code } = input.edit;
150
+ if (typeof type !== "string" || !Object.hasOwn(EDIT_LABELS, type)) {
151
+ return undefined;
152
+ }
153
+ return {
154
+ type: type as keyof typeof EDIT_LABELS,
155
+ code: typeof code === "string" ? code : undefined,
156
+ };
157
+ }
158
+
159
+ function summarizeCode(code: unknown): string | undefined {
160
+ if (typeof code !== "string") {
161
+ return undefined;
162
+ }
163
+ const lines = code
164
+ .split("\n")
165
+ .map((line) => line.trim())
166
+ .filter((line) => line.length > 0);
167
+ return lines.find((line) => !/^(import\s|from\s|#)/.test(line)) ?? lines[0];
168
+ }
169
+
170
+ function isRecord(value: unknown): value is Record<string, unknown> {
171
+ return typeof value === "object" && value !== null;
172
+ }
@@ -0,0 +1,145 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import {
4
+ BanIcon,
5
+ ChevronRightIcon,
6
+ CircleSlashIcon,
7
+ Loader2Icon,
8
+ type LucideIcon,
9
+ XCircleIcon,
10
+ } from "lucide-react";
11
+ import React from "react";
12
+ import { cn } from "@/utils/cn";
13
+ import {
14
+ PENDING_TOOL_STATES,
15
+ type ToolApproval,
16
+ type ToolState,
17
+ } from "./shared";
18
+ import { ToolArgsRenderer } from "./tool-args";
19
+ import { describeToolCall } from "./tool-labels";
20
+ import { ResultRenderer } from "./tool-result";
21
+
22
+ type ToolStepState = Exclude<ToolState, "approval-requested">;
23
+
24
+ interface ToolStepProps {
25
+ toolName: string;
26
+ state: ToolStepState;
27
+ input?: unknown;
28
+ result?: unknown;
29
+ errorText?: string;
30
+ approval?: ToolApproval;
31
+ isActive?: boolean;
32
+ className?: string;
33
+ }
34
+
35
+ export const ToolStep: React.FC<ToolStepProps> = ({
36
+ toolName,
37
+ state,
38
+ input,
39
+ result,
40
+ errorText,
41
+ approval,
42
+ isActive = true,
43
+ className,
44
+ }) => {
45
+ const [open, setOpen] = React.useState(false);
46
+ const pending = PENDING_TOOL_STATES.has(state);
47
+ const { label, detail, icon } = describeToolCall(
48
+ toolName,
49
+ input,
50
+ pending ? "running" : "done",
51
+ );
52
+ const status = getStatus(state, pending && !isActive, icon);
53
+ const failed = state === "output-error";
54
+
55
+ return (
56
+ <div className={cn("w-full", className)}>
57
+ <button
58
+ type="button"
59
+ onClick={() => setOpen((prev) => !prev)}
60
+ aria-expanded={open}
61
+ className={cn(
62
+ "group/step -mx-1.5 flex w-fit max-w-full min-w-0 items-center gap-1.5 rounded-md px-1.5 py-0.5 text-left text-xs leading-4 text-muted-foreground transition-colors hover:bg-foreground/6 hover:text-foreground focus-visible:outline-1 focus-visible:outline-(--slate-8)",
63
+ open && "text-foreground",
64
+ failed && "text-(--red-11) hover:bg-(--red-3) hover:text-(--red-11)",
65
+ )}
66
+ >
67
+ <status.icon
68
+ aria-hidden={true}
69
+ className={cn(
70
+ // Offsets lucide's built-in padding.
71
+ "-ml-px size-3 shrink-0",
72
+ status.spinning && "animate-spin",
73
+ )}
74
+ />
75
+ <span className="shrink-0">{label}</span>
76
+ {detail && (
77
+ <code className="min-w-0 truncate font-mono text-[0.6875rem] opacity-80">
78
+ {detail}
79
+ </code>
80
+ )}
81
+ {status.suffix && (
82
+ <span className="shrink-0 opacity-80">· {status.suffix}</span>
83
+ )}
84
+ <ChevronRightIcon
85
+ aria-hidden={true}
86
+ className={cn(
87
+ "size-3 shrink-0 opacity-40 transition-transform group-hover/step:opacity-100",
88
+ open && "rotate-90 opacity-100",
89
+ )}
90
+ />
91
+ </button>
92
+ {open && (
93
+ <ToolStepDetails
94
+ state={state}
95
+ input={input}
96
+ result={result}
97
+ errorText={errorText}
98
+ approval={approval}
99
+ />
100
+ )}
101
+ </div>
102
+ );
103
+ };
104
+
105
+ function getStatus(
106
+ state: ToolStepState,
107
+ interrupted: boolean,
108
+ doneIcon: LucideIcon,
109
+ ): { icon: LucideIcon; suffix?: string; spinning?: boolean } {
110
+ if (interrupted) {
111
+ return { icon: CircleSlashIcon, suffix: "Stopped" };
112
+ }
113
+ switch (state) {
114
+ case "output-error":
115
+ return { icon: XCircleIcon, suffix: "Failed" };
116
+ case "output-denied":
117
+ return { icon: BanIcon, suffix: "Denied" };
118
+ case "output-available":
119
+ return { icon: doneIcon };
120
+ default:
121
+ return { icon: Loader2Icon, spinning: true };
122
+ }
123
+ }
124
+
125
+ const ToolStepDetails: React.FC<
126
+ Pick<ToolStepProps, "state" | "input" | "result" | "errorText" | "approval">
127
+ > = ({ state, input, result, errorText, approval }) => (
128
+ <div className="mt-1 mb-2 ml-5.5 space-y-3">
129
+ <ToolArgsRenderer input={input} />
130
+ {state === "output-available" && result != null && (
131
+ <ResultRenderer result={result} />
132
+ )}
133
+ {state === "output-error" && errorText && (
134
+ <pre className="max-h-64 overflow-auto rounded border border-(--red-6) bg-(--red-2) p-2 text-xs leading-relaxed whitespace-pre-wrap text-(--red-11) scrollbar-thin">
135
+ {errorText}
136
+ </pre>
137
+ )}
138
+ {state === "output-denied" && (
139
+ <p className="text-xs text-muted-foreground">
140
+ Tool execution was denied
141
+ {approval?.reason ? `: ${approval.reason}` : "."}
142
+ </p>
143
+ )}
144
+ </div>
145
+ );