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

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 (106) hide show
  1. package/dist/{ConnectedDataExplorerComponent-C_hj7Poi.js → ConnectedDataExplorerComponent-C4yCN38h.js} +1 -1
  2. package/dist/{ErrorBoundary-CvxlC3F1.js → ErrorBoundary-CewXPyL5.js} +64 -69
  3. package/dist/{abnfDiagram-VCTEODGH-DzrZZXRD.js → abnfDiagram-VCTEODGH-o6Ay6Q63.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-BhiwwlVg.js} +1 -1
  6. package/dist/{chat-ui-DFxNgzXF.js → chat-ui-CbgR5ivy.js} +1964 -1740
  7. package/dist/{chunk-IMKFNOWR-B7xytD4B.js → chunk-IMKFNOWR-WMSMf4Nj.js} +1 -1
  8. package/dist/{chunk-SHT3W25Y-I8ZFP5L1.js → chunk-SHT3W25Y-BRfADrzD.js} +1 -1
  9. package/dist/{chunk-SVP7TREG-Di-uAPdT.js → chunk-SVP7TREG-CuK5QFre.js} +1 -1
  10. package/dist/{chunk-TICWLB2K-BFpUy0ud.js → chunk-TICWLB2K-Bcun_jhJ.js} +1 -1
  11. package/dist/{classDiagram-ZZMXUADV-Cqm4JGpx.js → classDiagram-ZZMXUADV-CgWDbUDT.js} +2 -2
  12. package/dist/{classDiagram-v2-VYDZK3BY-DFHRJpN3.js → classDiagram-v2-VYDZK3BY-DYmc9SFU.js} +2 -2
  13. package/dist/{code-visibility-SRM1JiD_.js → code-visibility-BKBl9Rh6.js} +9 -9
  14. package/dist/{common-yiRhZ6ZZ.js → common-N3Jd_6m9.js} +5944 -5626
  15. package/dist/{cynefinDiagram-5FMLGOSQ-cuGOeTBC.js → cynefinDiagram-5FMLGOSQ-PPKWipjT.js} +1 -1
  16. package/dist/{dagre-GXQ25YYZ-DJ76aC4k.js → dagre-GXQ25YYZ-niRBZh--.js} +1 -1
  17. package/dist/{data-grid-overlay-editor-DLTzcQ4O.js → data-grid-overlay-editor-Cp59CrqZ.js} +1 -1
  18. package/dist/{diagram-S7CK7UJ4-DLLc0sw7.js → diagram-S7CK7UJ4-DPiP9rot.js} +1 -1
  19. package/dist/{diagram-UQ7AKVKN-Tz_d6qmk.js → diagram-UQ7AKVKN-B0WZuaxV.js} +1 -1
  20. package/dist/{diagram-VX7I27RA-D7SzPrDN.js → diagram-VX7I27RA-Rn42WBuN.js} +1 -1
  21. package/dist/{diagram-Z3DM3KII-qr37XV58.js → diagram-Z3DM3KII-BB93--ry.js} +1 -1
  22. package/dist/{dropdown-menu-CkJ7ErY6.js → dropdown-menu-BpImEfn3.js} +1211 -1205
  23. package/dist/{ebnfDiagram-PWID7BFC-BKOQb4J0.js → ebnfDiagram-PWID7BFC-V4t5h6mZ.js} +2 -2
  24. package/dist/{erDiagram-RLTQ6QDP-CM15Vq68.js → erDiagram-RLTQ6QDP-C1MsAKXd.js} +1 -1
  25. package/dist/{error-banner-DbRFOd2O.js → error-banner-DPc9eIw5.js} +3 -3
  26. package/dist/{error-entries-CMoaUX9V.js → error-entries-CJGdD2ky.js} +49 -42
  27. package/dist/{feedback-button-DBUrE4OL.js → feedback-button-Qc1f9iPq.js} +70 -70
  28. package/dist/{flowDiagram-HODETNUW-Dt3N5yFT.js → flowDiagram-HODETNUW-DHjeJP96.js} +2 -2
  29. package/dist/{glide-data-editor-Cfv4HeOh.js → glide-data-editor-hCq3K0sd.js} +2042 -2041
  30. package/dist/{glide-utils-Ba9WD2IN.js → glide-utils-DC3sk6EX.js} +28 -34
  31. package/dist/{globals-dIbBIR5-.js → globals-B2_uMYDY.js} +1 -1
  32. package/dist/{html-to-image-D_5CMreP.js → html-to-image-S7spRQCi.js} +5526 -5828
  33. package/dist/{infoDiagram-27XIBGKW-C7axJMCl.js → infoDiagram-27XIBGKW-Dovv6iKx.js} +1 -1
  34. package/dist/{ishikawaDiagram-5VMMS53U-CdC9Eur7.js → ishikawaDiagram-5VMMS53U-D2nyygsc.js} +1 -1
  35. package/dist/{kanban-definition-UXKFOSKX-BHnMIgD7.js → kanban-definition-UXKFOSKX-KyVx0uTY.js} +1 -1
  36. package/dist/main.js +1288 -1287
  37. package/dist/{mermaid-DFYN_PXd.js → mermaid-B-jKSwGt.js} +33 -33
  38. package/dist/{mindmap-definition-YA3MSWOX-BagHoCGU.js → mindmap-definition-YA3MSWOX-Bz9tKQFl.js} +1 -1
  39. package/dist/{number-overlay-editor-CTiJp5Mj.js → number-overlay-editor-CfNYvbdz.js} +1 -1
  40. package/dist/{pegDiagram-XKGWAZYB-8IIZczip.js → pegDiagram-XKGWAZYB-C_qpq6Tv.js} +2 -2
  41. package/dist/pencil-DcT8AE0h.js +10 -0
  42. package/dist/{pieDiagram-E7YTZNPT-CE6dKBIr.js → pieDiagram-E7YTZNPT-Bp9ucgI4.js} +1 -1
  43. package/dist/{process-output-CGFNVC60.js → process-output-f34bkXTm.js} +2 -2
  44. package/dist/{railroadDiagram-O6MQD6OU-CkexPq3b.js → railroadDiagram-O6MQD6OU-tq-VEH6j.js} +2 -2
  45. package/dist/{requirementDiagram-BXWQKSXE-B0-OkuAe.js → requirementDiagram-BXWQKSXE-DW-vHadb.js} +1 -1
  46. package/dist/{reveal-component-DH09ygHo.js → reveal-component-CUxv-kkP.js} +9 -9
  47. package/dist/{sequenceDiagram-WJ2MYXX4-Dxysg8DX.js → sequenceDiagram-WJ2MYXX4-Dv9U9qRP.js} +1 -1
  48. package/dist/{stateDiagram-D77RDMKH-DTEN3-OJ.js → stateDiagram-D77RDMKH-DTmPQ0EB.js} +2 -2
  49. package/dist/{stateDiagram-v2-MP3YSRHH-vktk3Xc5.js → stateDiagram-v2-MP3YSRHH-CkCif0Iw.js} +2 -2
  50. package/dist/style.css +1 -1
  51. package/dist/{swimlanes-42K2YHIH-Cjveh9OM.js → swimlanes-42K2YHIH-21kCq6l8.js} +1 -1
  52. package/dist/{swimlanesDiagram-VR7AAH4N-Bdr7Ev_2.js → swimlanesDiagram-VR7AAH4N-BpRJP6KU.js} +2 -2
  53. package/dist/{timeline-definition-24CTP7MA-KIuDcefn.js → timeline-definition-24CTP7MA-Ckgh4lS4.js} +1 -1
  54. package/dist/{toast-CY1_Qv41.js → toast-DpczUryP.js} +1 -1
  55. package/dist/{vega-component-C-lqjV0q.js → vega-component-CF9kI6C4.js} +1 -1
  56. package/dist/{vennDiagram-4TSXK5OY-DrIOUHfb.js → vennDiagram-4TSXK5OY-CDei2Bt3.js} +1 -1
  57. package/dist/{wardleyDiagram-VM6X3IG4-CJNjf4OH.js → wardleyDiagram-VM6X3IG4-CXavT601.js} +1 -1
  58. package/dist/{xychartDiagram-S5SC5T6Z-DEshFho-.js → xychartDiagram-S5SC5T6Z-CvJhcZri.js} +1 -1
  59. package/package.json +1 -1
  60. package/src/components/chat/__tests__/chat-display.test.tsx +157 -86
  61. package/src/components/chat/__tests__/chat-utils.test.ts +42 -0
  62. package/src/components/chat/__tests__/turn-timing.test.ts +84 -0
  63. package/src/components/chat/__tests__/use-assistant-turn.test.ts +95 -0
  64. package/src/components/chat/__tests__/user-message.test.tsx +27 -0
  65. package/src/components/chat/acp/agent-panel.tsx +2 -0
  66. package/src/components/chat/agent-activity.tsx +132 -0
  67. package/src/components/chat/chat-display.tsx +125 -96
  68. package/src/components/chat/chat-panel.tsx +123 -106
  69. package/src/components/chat/chat-utils.ts +12 -24
  70. package/src/components/chat/tool-call/__tests__/tool-labels.test.ts +76 -0
  71. package/src/components/chat/tool-call/__tests__/tool-step.test.tsx +57 -0
  72. package/src/components/chat/tool-call/shared.ts +6 -0
  73. package/src/components/chat/tool-call/tool-call-view.tsx +22 -76
  74. package/src/components/chat/tool-call/tool-labels.ts +172 -0
  75. package/src/components/chat/tool-call/tool-step.tsx +145 -0
  76. package/src/components/chat/turn-timing.ts +73 -0
  77. package/src/components/chat/use-assistant-turn.ts +65 -0
  78. package/src/components/chat/use-chat-generation.ts +5 -4
  79. package/src/components/chat/user-message.tsx +145 -0
  80. package/src/components/editor/actions/__tests__/useCellActionButton.test.tsx +18 -1
  81. package/src/components/editor/actions/useCellActionButton.tsx +3 -0
  82. package/src/components/editor/ai/completion-handlers.tsx +0 -37
  83. package/src/components/editor/cell/StagedAICell.tsx +51 -144
  84. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +193 -32
  85. package/src/components/editor/chrome/wrapper/app-chrome.tsx +2 -2
  86. package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +145 -81
  87. package/src/components/editor/chrome/wrapper/pending-ai-changes-badge.tsx +25 -0
  88. package/src/components/editor/chrome/wrapper/sidebar.tsx +6 -0
  89. package/src/components/editor/links/cell-link.tsx +4 -4
  90. package/src/components/editor/notebook-cell.tsx +3 -11
  91. package/src/components/ui/toast.tsx +1 -1
  92. package/src/core/ai/__tests__/staged-cells.test.ts +74 -126
  93. package/src/core/ai/staged-cells.ts +48 -38
  94. package/src/core/cells/scrollCellIntoView.ts +6 -0
  95. package/src/core/codemirror/placeholder/__tests__/extensions.test.ts +59 -0
  96. package/src/core/codemirror/placeholder/extensions.ts +23 -0
  97. package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +34 -0
  98. package/src/core/session/useNotebookSession.tsx +7 -2
  99. package/src/css/app/Cell.css +7 -36
  100. package/src/plugins/impl/chat/chat-ui.tsx +2 -3
  101. package/src/plugins/impl/common/error-banner.tsx +5 -3
  102. package/src/components/chat/reasoning-accordion.tsx +0 -61
  103. package/src/components/chat/tool-call/tool-activity-group.tsx +0 -49
  104. package/src/components/chat/tool-call/tool-error-card.tsx +0 -81
  105. package/src/components/chat/tool-call/tool-history-row.tsx +0 -147
  106. package/src/components/icons/loading-ellipsis.tsx +0 -46
@@ -9,7 +9,7 @@ import "./chunk-4HAMMTFA-D4eFPyFh.js";
9
9
  import "./chunk-GVQU2GXP-Cwbt-lF0.js";
10
10
  import "./chunk-L3NEJ4N5-n-zgIVSy.js";
11
11
  import { o as n } from "./chunk-OSK3NFVY-DXa-Cxbn.js";
12
- import { c as r } from "./mermaid-DFYN_PXd.js";
12
+ import { c as r } from "./mermaid-B-jKSwGt.js";
13
13
  //#region ../node_modules/.pnpm/mermaid@11.17.2/node_modules/mermaid/dist/chunks/mermaid.core/swimlanes-42K2YHIH.mjs
14
14
  var i = 5, a = 1e-5, o = 1e-6;
15
15
  function s(e) {
@@ -9,11 +9,11 @@ import "./chunk-4HAMMTFA-D4eFPyFh.js";
9
9
  import "./chunk-GVQU2GXP-Cwbt-lF0.js";
10
10
  import "./chunk-L3NEJ4N5-n-zgIVSy.js";
11
11
  import "./chunk-OSK3NFVY-DXa-Cxbn.js";
12
- import "./mermaid-DFYN_PXd.js";
12
+ import "./mermaid-B-jKSwGt.js";
13
13
  import "./chunk-F27PBJKO-ufseIn5y.js";
14
14
  import "./chunk-XXDRQBXY-Dnl5VBqG.js";
15
15
  import "./chunk-POPQ4Y6H-hYOXbYH_.js";
16
- import { r as t, t as n } from "./chunk-SHT3W25Y-I8ZFP5L1.js";
16
+ import { r as t, t as n } from "./chunk-SHT3W25Y-BRfADrzD.js";
17
17
  //#region ../node_modules/.pnpm/mermaid@11.17.2/node_modules/mermaid/dist/chunks/mermaid.core/swimlanesDiagram-VR7AAH4N.mjs
18
18
  var r = n({
19
19
  defaultLayout: "swimlane",
@@ -3,7 +3,7 @@ import { p as t } from "./chunk-75Z2AOVW-D7gxIs2Q.js";
3
3
  import { n, t as r } from "./src-CA_m4ONl.js";
4
4
  import { n as i, t as a } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
5
5
  import { $ as o, J as s, a as c, b as l, et as u, nt as d, o as f, x as p } from "./chunk-DU6HZSFF-BwiyaNpg.js";
6
- import { p as m } from "./mermaid-DFYN_PXd.js";
6
+ import { p as m } from "./mermaid-B-jKSwGt.js";
7
7
  //#region ../node_modules/.pnpm/mermaid@11.17.2/node_modules/mermaid/dist/chunks/mermaid.core/timeline-definition-24CTP7MA.mjs
8
8
  var h = (function() {
9
9
  var e = /* @__PURE__ */ i(function(e, t, n, r) {
@@ -405,7 +405,7 @@ var $ = a(), ve = I, ye = S.forwardRef((e, n) => {
405
405
  ye.displayName = V.displayName;
406
406
  var be = {
407
407
  default: "bg-background border forced-colors:border-[CanvasText] forced-colors:bg-[Canvas] forced-colors:text-[CanvasText]",
408
- danger: "group destructive text-error border-destructive bg-(--red-1) shadow-md-solid shadow-error forced-colors:border-[CanvasText] forced-colors:bg-[Canvas] forced-colors:text-[CanvasText]"
408
+ danger: "group destructive text-(--red-11) border-destructive bg-(--red-1) shadow-md-solid shadow-error forced-colors:border-[CanvasText] forced-colors:bg-[Canvas] forced-colors:text-[CanvasText]"
409
409
  }, xe = r("data-[swipe=move]:transition-none group relative pointer-events-auto flex w-full items-start justify-between gap-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-(--radix-toast-swipe-move-x) data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-(--radix-toast-swipe-end-x) data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full sm:data-[state=open]:slide-in-from-bottom-full data-[state=closed]:slide-out-to-right-full forced-color-adjust-none", {
410
410
  variants: { variant: be },
411
411
  defaultVariants: { variant: "default" }
@@ -5,7 +5,7 @@ import { t as o } from "./compiler-runtime-CldRdSsv.js";
5
5
  import { t as s } from "./tooltip-BfcDQd2j.js";
6
6
  import { l as c, t as l } from "./config-CmeG_Pb6.js";
7
7
  import { t as u } from "./jsx-runtime-3BD1kT0s.js";
8
- import { n as d } from "./error-banner-DbRFOd2O.js";
8
+ import { n as d } from "./error-banner-DPc9eIw5.js";
9
9
  import { t as f } from "./debounce-DrhhziJv.js";
10
10
  import { n as p } from "./useTheme-BZ2GGLgm.js";
11
11
  import { D as ee } from "./utils-DXkj82sK.js";
@@ -3,7 +3,7 @@ import { t } from "./src-CA_m4ONl.js";
3
3
  import { n } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
4
4
  import { $ as r, H as i, K as a, U as o, a as s, b as c, c as l, et as u, f as d, nt as f, tt as p, v as m, w as h, y as g } from "./chunk-DU6HZSFF-BwiyaNpg.js";
5
5
  import { t as _ } from "./rough.esm-CtV9AnV1.js";
6
- import { p as v } from "./mermaid-DFYN_PXd.js";
6
+ import { p as v } from "./mermaid-B-jKSwGt.js";
7
7
  //#region ../node_modules/.pnpm/khroma@2.1.0/node_modules/khroma/dist/methods/transparentize.js
8
8
  var y = (e, t) => p(e, "a", -t), b = 1e-10;
9
9
  function x(e, t) {
@@ -2,7 +2,7 @@ import { i as e } from "./chunk-75Z2AOVW-D7gxIs2Q.js";
2
2
  import { n as t } from "./src-CA_m4ONl.js";
3
3
  import { n } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
4
4
  import { D as r, H as i, K as a, U as o, a as s, b as c, c as l, v as u, w as d, x as f, y as p } from "./chunk-DU6HZSFF-BwiyaNpg.js";
5
- import { p as m } from "./mermaid-DFYN_PXd.js";
5
+ import { p as m } from "./mermaid-B-jKSwGt.js";
6
6
  import { n as h } from "./mermaid-parser.core-DxuSfYwJ.js";
7
7
  import { t as g } from "./chunk-JWPE2WC7-DJvv1t8U.js";
8
8
  //#region ../node_modules/.pnpm/mermaid@11.17.2/node_modules/mermaid/dist/chunks/mermaid.core/wardleyDiagram-VM6X3IG4.mjs
@@ -9,7 +9,7 @@ import { n as s } from "./chunk-Y2CYZVJY-DKKS1VyV.js";
9
9
  import { D as c, H as l, K as u, U as d, a as f, b as p, c as m, f as h, v as g, w as _, y as v, z as y } from "./chunk-DU6HZSFF-BwiyaNpg.js";
10
10
  import "./chunk-PWAF6VOD-CQw5qATZ.js";
11
11
  import { t as b } from "./chunk-GMAD6QVW-BhaJx0X3.js";
12
- import { p as x } from "./mermaid-DFYN_PXd.js";
12
+ import { p as x } from "./mermaid-B-jKSwGt.js";
13
13
  //#region ../node_modules/.pnpm/d3-scale@4.0.2/node_modules/d3-scale/src/band.js
14
14
  function S() {
15
15
  var e = t().unknown(void 0), r = e.domain, i = e.range, o = 0, s = 1, c, l, u = !1, d = 0, f = 0, p = .5;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.25.3-dev266",
3
+ "version": "0.25.3-dev267",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -3,7 +3,7 @@
3
3
  import { fireEvent, render, screen } from "@testing-library/react";
4
4
  import type { ToolUIPart, UIMessage } from "ai";
5
5
  import { describe, expect, it, vi } from "vitest";
6
- import { renderUIMessage } from "../chat-display";
6
+ import { getAnswerText, renderUIMessage } from "../chat-display";
7
7
 
8
8
  vi.mock("../tool-call/tool-call-view", () => ({
9
9
  ToolCallView: ({ toolName }: { toolName: string }) => <div>{toolName}</div>,
@@ -12,114 +12,185 @@ vi.mock("../../markdown/markdown-renderer", () => ({
12
12
  MarkdownRenderer: ({ content }: { content: string }) => <p>{content}</p>,
13
13
  }));
14
14
 
15
- const done = (name = "edit_notebook_tool"): ToolUIPart => ({
16
- type: `tool-${name}`,
17
- toolCallId: crypto.randomUUID(),
18
- state: "output-available",
19
- input: {},
20
- output: {},
15
+ const tool = (
16
+ name = "edit_notebook_tool",
17
+ state: ToolUIPart["state"] = "output-available",
18
+ ): ToolUIPart =>
19
+ ({
20
+ type: `tool-${name}`,
21
+ toolCallId: crypto.randomUUID(),
22
+ state,
23
+ input: {},
24
+ output: {},
25
+ approval: state === "approval-requested" ? { id: "approval" } : undefined,
26
+ }) as ToolUIPart;
27
+
28
+ const reasoning = (text: string): UIMessage["parts"][number] => ({
29
+ type: "reasoning",
30
+ text,
31
+ state: "done",
21
32
  });
22
33
 
23
- function show(parts: UIMessage["parts"], isActive = false) {
34
+ function show(
35
+ parts: UIMessage["parts"],
36
+ {
37
+ isActive = false,
38
+ metadata,
39
+ }: { isActive?: boolean; metadata?: Record<string, number | boolean> } = {},
40
+ ) {
24
41
  return render(
25
- renderUIMessage({
26
- message: { id: "message", role: "assistant", parts },
27
- isStreamingReasoning: false,
28
- isLast: true,
29
- isActive,
30
- }),
42
+ <div>
43
+ {renderUIMessage({
44
+ message: { id: "message", role: "assistant", parts, metadata },
45
+ isLast: true,
46
+ isActive,
47
+ })}
48
+ </div>,
31
49
  );
32
50
  }
33
51
 
34
- describe("chat tool activity", () => {
35
- it("collapses consecutive edits across SDK step boundaries and exposes details on demand", () => {
36
- show([done(), { type: "step-start" }, done(), done(), done()]);
52
+ describe("assistant turn rendering", () => {
53
+ it("folds interleaved reasoning, narration, and tools into one block above the answer", () => {
54
+ show([
55
+ reasoning("Plan the cells"),
56
+ tool("get_lightweight_cell_map"),
57
+ { type: "step-start" },
58
+ { type: "text", text: "Adding the function now." },
59
+ reasoning("Write fib"),
60
+ tool(),
61
+ { type: "text", text: "Added `fib`." },
62
+ ]);
63
+
37
64
  const summary = screen.getByRole("button", {
38
- name: "Edited notebook · 4 operations",
65
+ name: "Worked through 2 steps",
39
66
  });
40
67
  expect(summary.getAttribute("aria-expanded")).toBe("false");
41
- expect(screen.queryByText("tool-edit_notebook_tool")).toBeNull();
68
+ expect(screen.getByText("Added `fib`.")).toBeTruthy();
69
+ expect(screen.queryByText("Plan the cells")).toBeNull();
70
+
42
71
  fireEvent.click(summary);
43
- expect(screen.getAllByText("tool-edit_notebook_tool")).toHaveLength(4);
72
+ expect(screen.getByText("Plan the cells")).toBeTruthy();
73
+ expect(screen.getByText("Adding the function now.")).toBeTruthy();
74
+ expect(screen.getByText("tool-get_lightweight_cell_map")).toBeTruthy();
75
+ expect(screen.getByText("tool-edit_notebook_tool")).toBeTruthy();
44
76
  });
45
77
 
46
- it("starts a new group after prose and summarizes mixed tool calls", () => {
47
- show([
48
- done(),
49
- { type: "text", text: "Checking the result." },
50
- done("lint_notebook"),
51
- {
52
- type: "dynamic-tool",
53
- toolName: "get_lightweight_cell_map",
54
- toolCallId: "dynamic",
55
- state: "output-available",
56
- input: {},
57
- output: {},
58
- },
59
- ]);
60
- expect(screen.getByText("Checking the result.")).toBeTruthy();
78
+ it("reports how long the turn took", () => {
79
+ const view = show([tool(), { type: "text", text: "Done." }], {
80
+ metadata: { startedAt: 0, finishedAt: 65_000 },
81
+ });
61
82
  expect(
62
- screen.getByRole("button", { name: "Edited notebook · 1 operation" }),
83
+ screen.getByRole("button", { name: "Worked for 1m 5s" }),
63
84
  ).toBeTruthy();
85
+ view.unmount();
86
+
87
+ show([reasoning("Hmm"), { type: "text", text: "Hi." }], {
88
+ metadata: { startedAt: 0, finishedAt: 8_000 },
89
+ });
90
+ expect(screen.getByRole("button", { name: "Thought for 8s" })).toBeTruthy();
91
+ });
92
+
93
+ it("stays open while running and collapses once the answer is in", () => {
94
+ const parts: UIMessage["parts"] = [
95
+ reasoning("Plan"),
96
+ tool("edit_notebook_tool", "input-available"),
97
+ ];
98
+ const view = show(parts, { isActive: true });
99
+ expect(screen.getByRole("status").textContent).toBe("Working…");
100
+ expect(screen.getByText("tool-edit_notebook_tool")).toBeTruthy();
101
+
102
+ view.rerender(
103
+ <div>
104
+ {renderUIMessage({
105
+ message: {
106
+ id: "message",
107
+ role: "assistant",
108
+ parts: [
109
+ ...parts.slice(0, 1),
110
+ tool(),
111
+ { type: "text", text: "Done." },
112
+ ],
113
+ },
114
+ isLast: true,
115
+ isActive: false,
116
+ })}
117
+ </div>,
118
+ );
119
+ const summary = screen.getByRole("button", {
120
+ name: "Worked through 1 step",
121
+ });
122
+ expect(summary.getAttribute("aria-expanded")).toBe("false");
123
+ });
124
+
125
+ it("leaves a stopped turn open since there is no answer to read", () => {
126
+ show([reasoning("Plan"), tool("edit_notebook_tool", "input-available")]);
127
+ const summary = screen.getByRole("button", { name: "Stopped" });
128
+ expect(summary.getAttribute("aria-expanded")).toBe("true");
129
+ });
130
+
131
+ it("labels a stopped turn even without pending tools", () => {
132
+ show([reasoning("Plan"), { type: "text", text: "Partial" }], {
133
+ metadata: { startedAt: 0, finishedAt: 3_000, stopped: true },
134
+ });
64
135
  expect(
65
- screen.getByRole("button", { name: "Completed 2 steps" }),
136
+ screen.getByRole("button", { name: "Stopped after 3s" }),
66
137
  ).toBeTruthy();
67
138
  });
68
139
 
69
- it("keeps errors, denials, and approval requests outside collapsed groups", () => {
140
+ it("flags failed steps in the summary", () => {
70
141
  show([
71
- done(),
72
- {
73
- type: "tool-failed",
74
- toolCallId: "error",
75
- state: "output-error",
76
- input: {},
77
- errorText: "Failed",
78
- },
79
- {
80
- type: "tool-approval",
81
- toolCallId: "approval",
82
- state: "approval-requested",
83
- input: {},
84
- approval: { id: "approval" },
85
- },
86
- {
87
- type: "tool-denied",
88
- toolCallId: "denied",
89
- state: "output-denied",
90
- input: {},
91
- approval: { id: "denied", approved: false },
92
- },
93
- done(),
142
+ tool("lint_notebook", "output-error"),
143
+ { type: "text", text: "Hm." },
94
144
  ]);
95
- expect(screen.getByText("tool-failed")).toBeTruthy();
96
- expect(screen.getByText("tool-approval")).toBeTruthy();
97
- expect(screen.getByText("tool-denied")).toBeTruthy();
98
- expect(
99
- screen.getAllByRole("button", { name: "Edited notebook · 1 operation" }),
100
- ).toHaveLength(2);
145
+ expect(screen.getByRole("button", { name: /1 failed/ })).toBeTruthy();
101
146
  });
102
147
 
103
- it("distinguishes running edits from interrupted activity", () => {
104
- const parts: UIMessage["parts"] = [
105
- done(),
106
- {
107
- type: "tool-edit_notebook_tool",
108
- toolCallId: "pending",
109
- state: "input-available",
110
- input: {},
111
- },
112
- ];
113
- const view = show(parts, true);
114
- expect(
115
- screen.getByRole("button", { name: "Editing notebook…" }),
116
- ).toBeTruthy();
117
- view.unmount();
118
- show(parts);
148
+ it("shows a thinking indicator before any content arrives", () => {
149
+ show([reasoning("")], { isActive: true });
150
+ expect(screen.getByRole("status").textContent).toBe("Thinking…");
151
+ expect(screen.queryByRole("button")).toBeNull();
152
+ });
153
+
154
+ it("keeps pending approvals, and the text explaining them, outside the block", () => {
155
+ show([
156
+ reasoning("Needs approval"),
157
+ { type: "text", text: "I'll run a query." },
158
+ tool("run_query", "approval-requested"),
159
+ ]);
160
+ expect(screen.getByText("I'll run a query.")).toBeTruthy();
161
+ expect(screen.getByText("tool-run_query")).toBeTruthy();
119
162
  expect(
120
- screen.getByRole("button", {
121
- name: "Stopped before completing all steps",
122
- }),
163
+ screen.getByRole("button", { name: "Thought process" }),
123
164
  ).toBeTruthy();
124
165
  });
166
+
167
+ it("renders plain answers without an activity block", () => {
168
+ show([{ type: "text", text: "Hello!" }]);
169
+ expect(screen.getByText("Hello!")).toBeTruthy();
170
+ expect(screen.queryByRole("button")).toBeNull();
171
+ });
172
+ });
173
+
174
+ describe("getAnswerText", () => {
175
+ it("copies the visible answer, not the narration folded into the block", () => {
176
+ const message: UIMessage = {
177
+ id: "m",
178
+ role: "assistant",
179
+ parts: [
180
+ { type: "text", text: "Adding the function now." },
181
+ tool(),
182
+ { type: "text", text: "Added `fib`." },
183
+ ],
184
+ };
185
+ expect(getAnswerText(message)).toBe("Added `fib`.");
186
+ });
187
+
188
+ it("falls back to the narration when there is no answer", () => {
189
+ const message: UIMessage = {
190
+ id: "m",
191
+ role: "assistant",
192
+ parts: [{ type: "text", text: "Starting." }, tool()],
193
+ };
194
+ expect(getAnswerText(message)).toBe("Starting.");
195
+ });
125
196
  });
@@ -9,6 +9,7 @@ import {
9
9
  hasPendingToolCalls,
10
10
  hasUnresolvedToolCalls,
11
11
  shouldFlushQueue,
12
+ willAutoContinue,
12
13
  } from "../chat-utils";
13
14
 
14
15
  /**
@@ -560,3 +561,44 @@ describe("handleToolCall", () => {
560
561
  },
561
562
  );
562
563
  });
564
+
565
+ describe("willAutoContinue", () => {
566
+ const toolPart = (state: string) =>
567
+ ({
568
+ type: "tool-run_query",
569
+ toolCallId: "call-1",
570
+ state,
571
+ input: {},
572
+ output: {},
573
+ approval: { id: "approval-1" },
574
+ }) as unknown as UIMessage["parts"][number];
575
+
576
+ it.each([
577
+ ["input-available", true],
578
+ ["output-available", true],
579
+ ["approval-requested", false],
580
+ ["input-streaming", false],
581
+ ])("returns %s → %s", (state, expected) => {
582
+ expect(
583
+ willAutoContinue([
584
+ userMessage("run it"),
585
+ assistantToolMessage([toolPart(state)]),
586
+ ]),
587
+ ).toBe(expected);
588
+ });
589
+
590
+ it.each(["output-available", "input-available"])(
591
+ "returns false once text follows a tool that is %s",
592
+ (state) => {
593
+ expect(
594
+ willAutoContinue([
595
+ userMessage("run it"),
596
+ assistantToolMessage([
597
+ toolPart(state),
598
+ { type: "text", text: "Done." },
599
+ ]),
600
+ ]),
601
+ ).toBe(false);
602
+ },
603
+ );
604
+ });
@@ -0,0 +1,84 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { UIMessage } from "ai";
4
+ import { describe, expect, it } from "vitest";
5
+ import { formatDuration, getTurnTiming, withTurnTiming } from "../turn-timing";
6
+
7
+ const user: UIMessage = { id: "u", role: "user", parts: [] };
8
+ const assistant = (metadata?: unknown): UIMessage => ({
9
+ id: "a",
10
+ role: "assistant",
11
+ parts: [],
12
+ metadata,
13
+ });
14
+
15
+ describe("withTurnTiming", () => {
16
+ it("stamps the turn start and finish on the trailing assistant message", () => {
17
+ const [, stamped] = withTurnTiming(
18
+ [user, assistant()],
19
+ { startedAt: 100, stopped: false },
20
+ 500,
21
+ );
22
+ expect(getTurnTiming(stamped)).toEqual({
23
+ startedAt: 100,
24
+ finishedAt: 500,
25
+ stopped: false,
26
+ });
27
+ });
28
+
29
+ it("prefers the live turn's start, which moves forward after approvals", () => {
30
+ const first = withTurnTiming(
31
+ [user, assistant({ other: 1 })],
32
+ { startedAt: 100, stopped: false },
33
+ 500,
34
+ );
35
+ const [, stamped] = withTurnTiming(
36
+ first,
37
+ { startedAt: 400, stopped: true },
38
+ 900,
39
+ );
40
+ expect(stamped.metadata).toEqual({
41
+ other: 1,
42
+ startedAt: 400,
43
+ finishedAt: 900,
44
+ stopped: true,
45
+ });
46
+ });
47
+
48
+ it("keeps the stored start when no turn is live", () => {
49
+ const first = withTurnTiming(
50
+ [user, assistant()],
51
+ { startedAt: 100, stopped: false },
52
+ 500,
53
+ );
54
+ const [, stamped] = withTurnTiming(
55
+ first,
56
+ { startedAt: null, stopped: false },
57
+ 900,
58
+ );
59
+ expect(getTurnTiming(stamped)?.startedAt).toBe(100);
60
+ });
61
+
62
+ it("leaves messages untouched without an assistant reply or start time", () => {
63
+ const pending = [user];
64
+ const turn = { startedAt: 100, stopped: false };
65
+ expect(withTurnTiming(pending, turn, 500)).toBe(pending);
66
+ const unknownStart = [user, assistant()];
67
+ expect(
68
+ withTurnTiming(unknownStart, { ...turn, startedAt: null }, 500),
69
+ ).toBe(unknownStart);
70
+ });
71
+ });
72
+
73
+ describe("formatDuration", () => {
74
+ it.each([
75
+ [200, "1s"],
76
+ [8_400, "8s"],
77
+ [60_000, "1m"],
78
+ [65_000, "1m 5s"],
79
+ [3_600_000, "1h"],
80
+ [3_780_000, "1h 3m"],
81
+ ])("%d ms → %s", (ms, expected) => {
82
+ expect(formatDuration(ms)).toBe(expected);
83
+ });
84
+ });
@@ -0,0 +1,95 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { act, renderHook } from "@testing-library/react";
4
+ import type { UIMessage } from "ai";
5
+ import { describe, expect, it } from "vitest";
6
+ import { useAssistantTurn } from "../use-assistant-turn";
7
+
8
+ const awaitingTool: UIMessage[] = [
9
+ { id: "u", role: "user", parts: [{ type: "text", text: "go" }] },
10
+ {
11
+ id: "a",
12
+ role: "assistant",
13
+ parts: [
14
+ {
15
+ type: "tool-lint_notebook",
16
+ toolCallId: "call",
17
+ state: "input-available",
18
+ input: {},
19
+ },
20
+ ],
21
+ },
22
+ ];
23
+
24
+ function setup() {
25
+ return renderHook(() => useAssistantTurn());
26
+ }
27
+
28
+ const idle = { isLoading: false, hasError: false, messages: awaitingTool };
29
+
30
+ describe("useAssistantTurn", () => {
31
+ it("stays active between requests while a tool is still running", () => {
32
+ const { result } = setup();
33
+ expect(result.current.isActive(idle)).toBe(false);
34
+ act(() => result.current.start());
35
+ expect(result.current.isActive(idle)).toBe(true);
36
+ expect(result.current.startedAt).toEqual(expect.any(Number));
37
+ });
38
+
39
+ it("ends on stop, error, or chat switch", () => {
40
+ const { result } = setup();
41
+ act(() => result.current.start());
42
+ act(() => {
43
+ result.current.finish(awaitingTool, { isAbort: true, isError: false });
44
+ });
45
+ expect(result.current.isActive(idle)).toBe(false);
46
+
47
+ act(() => result.current.start());
48
+ act(() => {
49
+ result.current.finish(awaitingTool, { isAbort: false, isError: true });
50
+ });
51
+ expect(result.current.isActive(idle)).toBe(false);
52
+
53
+ act(() => result.current.start());
54
+ expect(result.current.isActive({ ...idle, hasError: true })).toBe(false);
55
+
56
+ act(() => result.current.reset());
57
+ expect(result.current.isActive(idle)).toBe(false);
58
+ expect(result.current.startedAt).toBeNull();
59
+ });
60
+
61
+ it("stamps the turn's start on the reply", () => {
62
+ const { result } = setup();
63
+ act(() => result.current.start());
64
+ const [, reply] = result.current.finish(awaitingTool, {
65
+ isAbort: false,
66
+ isError: false,
67
+ });
68
+ expect(reply.metadata).toMatchObject({
69
+ startedAt: result.current.startedAt,
70
+ });
71
+ });
72
+
73
+ it("excludes time spent waiting on an approval", () => {
74
+ const reply: UIMessage = {
75
+ id: "a",
76
+ role: "assistant",
77
+ parts: [],
78
+ metadata: { startedAt: 1_000, finishedAt: 4_000 },
79
+ };
80
+ const { result } = setup();
81
+ act(() => result.current.resume(reply));
82
+ expect(Date.now() - (result.current.startedAt ?? 0)).toBeGreaterThanOrEqual(
83
+ 3_000,
84
+ );
85
+ expect(Date.now() - (result.current.startedAt ?? 0)).toBeLessThan(4_000);
86
+ });
87
+
88
+ it("stops auto-continuing once the user stops the turn", () => {
89
+ const { result } = setup();
90
+ act(() => result.current.start());
91
+ act(() => result.current.stop());
92
+ expect(result.current.hasEnded).toBe(true);
93
+ expect(result.current.isActive(idle)).toBe(false);
94
+ });
95
+ });
@@ -0,0 +1,27 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { render, screen } from "@testing-library/react";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { TooltipProvider } from "@/components/ui/tooltip";
6
+ import { UserMessage } from "../user-message";
7
+
8
+ describe("UserMessage", () => {
9
+ it("shows context mentions as chips instead of raw URIs", () => {
10
+ render(
11
+ <TooltipProvider>
12
+ <UserMessage
13
+ message={{
14
+ id: "u",
15
+ role: "user",
16
+ parts: [{ type: "text", text: "Plot @variable://df please" }],
17
+ }}
18
+ canEdit={true}
19
+ onEdit={vi.fn()}
20
+ />
21
+ </TooltipProvider>,
22
+ );
23
+ const chip = screen.getByTitle("variable://df");
24
+ expect(chip.textContent).toBe("@df");
25
+ expect(screen.queryByText(/@variable:\/\//)).toBeNull();
26
+ });
27
+ });
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import { PendingAICells } from "@/components/editor/chrome/wrapper/pending-ai-cells";
3
4
  import { useAtom } from "jotai";
4
5
  import {
5
6
  BotMessageSquareIcon,
@@ -1246,6 +1247,7 @@ const AgentPanel: React.FC = () => {
1246
1247
  </div>
1247
1248
  )}
1248
1249
 
1250
+ {!isLoading && <PendingAICells />}
1249
1251
  <PromptArea
1250
1252
  isLoading={isLoading}
1251
1253
  activeSessionId={activeSessionId}