@marimo-team/islands 0.25.3-dev250 → 0.25.3-dev251

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 (147) hide show
  1. package/dist/{ConnectedDataExplorerComponent-BBihxHrr.js → ConnectedDataExplorerComponent-DGOAQFtf.js} +11 -11
  2. package/dist/{ErrorBoundary-Ks8L_DFj.js → ErrorBoundary-CZ19wGWT.js} +7 -7
  3. package/dist/{abnfDiagram-VCTEODGH-DBcJVxYP.js → abnfDiagram-VCTEODGH-DzrZZXRD.js} +2 -2
  4. package/dist/{any-language-editor-jGaR0EAo.js → any-language-editor-Be7dA2VK.js} +3 -3
  5. package/dist/{architectureDiagram-5GKGNRK7-BcHZXF4S.js → architectureDiagram-5GKGNRK7-iF_snyFL.js} +1 -1
  6. package/dist/{button-B6RjDKJx.js → button-0-RqY1DW.js} +2 -1
  7. package/dist/{capabilities-CkVzWJEX.js → capabilities-B6yy_CBN.js} +1 -1
  8. package/dist/{chat-ui-DgaVxFrc.js → chat-ui-zbfp80GW.js} +1547 -1482
  9. package/dist/{check-Z_vtI3OL.js → check-CSvxZk5I.js} +1 -1
  10. package/dist/{chunk-IMKFNOWR-CTZlwBAA.js → chunk-IMKFNOWR-B7xytD4B.js} +1 -1
  11. package/dist/{chunk-SHT3W25Y-53g_RZHm.js → chunk-SHT3W25Y-I8ZFP5L1.js} +1 -1
  12. package/dist/{chunk-SVP7TREG-P6abNGV_.js → chunk-SVP7TREG-Di-uAPdT.js} +1 -1
  13. package/dist/{chunk-TICWLB2K-BTJJqkyF.js → chunk-TICWLB2K-BFpUy0ud.js} +1 -1
  14. package/dist/{classDiagram-ZZMXUADV-BUGqOrTm.js → classDiagram-ZZMXUADV-Cqm4JGpx.js} +2 -2
  15. package/dist/{classDiagram-v2-VYDZK3BY-CXpQvO-s.js → classDiagram-v2-VYDZK3BY-DFHRJpN3.js} +2 -2
  16. package/dist/{code-visibility-CCCqFZYh.js → code-visibility-D2D_sL1T.js} +4 -4
  17. package/dist/{common-JXDK9IjO.js → common-DSIM5DHJ.js} +262 -262
  18. package/dist/{config-DbjzjwxX.js → config-mwEjAmvP.js} +2 -2
  19. package/dist/{copy-icon-CG57cxwb.js → copy-icon-DD8bdw4L.js} +3 -3
  20. package/dist/{createReducer-DePYJ0F3.js → createReducer-Bx81cw8_.js} +1 -1
  21. package/dist/{cynefinDiagram-5FMLGOSQ-DOcnl8wW.js → cynefinDiagram-5FMLGOSQ-cuGOeTBC.js} +1 -1
  22. package/dist/{dagre-GXQ25YYZ-CuHR9QfG.js → dagre-GXQ25YYZ-DJ76aC4k.js} +1 -1
  23. package/dist/{data-grid-overlay-editor-QAJSeJpH.js → data-grid-overlay-editor-ByfOIO6H.js} +1 -1
  24. package/dist/{diagram-S7CK7UJ4-BPiG4kso.js → diagram-S7CK7UJ4-DLLc0sw7.js} +1 -1
  25. package/dist/{diagram-UQ7AKVKN-_piBOvOC.js → diagram-UQ7AKVKN-Tz_d6qmk.js} +1 -1
  26. package/dist/{diagram-VX7I27RA-DCzVeJ8M.js → diagram-VX7I27RA-D7SzPrDN.js} +1 -1
  27. package/dist/{diagram-Z3DM3KII-D64CF1jQ.js → diagram-Z3DM3KII-qr37XV58.js} +1 -1
  28. package/dist/{dialog-DsZ49mmU.js → dialog-BHYPX87J.js} +3 -3
  29. package/dist/{dropdown-menu-BRiE7Vnp.js → dropdown-menu-Blu7V6Mk.js} +6 -6
  30. package/dist/{ebnfDiagram-PWID7BFC-BapQPML-.js → ebnfDiagram-PWID7BFC-BKOQb4J0.js} +2 -2
  31. package/dist/{erDiagram-RLTQ6QDP-CNQvC2_Q.js → erDiagram-RLTQ6QDP-CM15Vq68.js} +1 -1
  32. package/dist/{error-banner-IsonnvA0.js → error-banner-DbRFOd2O.js} +2 -2
  33. package/dist/{error-entries-CRPfxMqm.js → error-entries-BuIE3Ius.js} +9 -9
  34. package/dist/{feedback-button-Bg7zGcid.js → feedback-button-VT03EJ1h.js} +7 -7
  35. package/dist/{flowDiagram-HODETNUW-BBt6S6oe.js → flowDiagram-HODETNUW-Dt3N5yFT.js} +2 -2
  36. package/dist/{formats-BK4VefeF.js → formats-gGospKWJ.js} +2 -2
  37. package/dist/{glide-data-editor-BuFe7QfV.js → glide-data-editor-C94G7kD7.js} +10 -10
  38. package/dist/{glide-utils-CQddGpa-.js → glide-utils-qbkRZpEy.js} +3 -3
  39. package/dist/{globals-8EK96XwF.js → globals-D7JZ5hgJ.js} +2 -2
  40. package/dist/{html-to-image-Gx4q3ZoD.js → html-to-image-BXrAd5q8.js} +460 -455
  41. package/dist/{infoDiagram-27XIBGKW-Hd8Ifz82.js → infoDiagram-27XIBGKW-C7axJMCl.js} +1 -1
  42. package/dist/{ishikawaDiagram-5VMMS53U-CP7hnHWx.js → ishikawaDiagram-5VMMS53U-CdC9Eur7.js} +1 -1
  43. package/dist/{kanban-definition-UXKFOSKX-CX91sa9b.js → kanban-definition-UXKFOSKX-BHnMIgD7.js} +1 -1
  44. package/dist/{loader-BisjOLiW.js → loader-BVEtQ4F3.js} +1 -1
  45. package/dist/main.js +236 -236
  46. package/dist/{mermaid-Djb3Ni9t.js → mermaid-DFYN_PXd.js} +35 -35
  47. package/dist/{mindmap-definition-YA3MSWOX-DgCNo07O.js → mindmap-definition-YA3MSWOX-BagHoCGU.js} +1 -1
  48. package/dist/{number-overlay-editor-BDJF1c8h.js → number-overlay-editor-CdcscCHp.js} +1 -1
  49. package/dist/{pegDiagram-XKGWAZYB-SGJhnIKo.js → pegDiagram-XKGWAZYB-8IIZczip.js} +2 -2
  50. package/dist/{pieDiagram-E7YTZNPT-1stXIWCQ.js → pieDiagram-E7YTZNPT-CE6dKBIr.js} +1 -1
  51. package/dist/{process-output-D2ISIyIs.js → process-output-D2ZjxrHe.js} +4 -4
  52. package/dist/{railroadDiagram-O6MQD6OU-DY1mojzB.js → railroadDiagram-O6MQD6OU-CkexPq3b.js} +2 -2
  53. package/dist/{requirementDiagram-BXWQKSXE-gHzhUp0D.js → requirementDiagram-BXWQKSXE-B0-OkuAe.js} +1 -1
  54. package/dist/{reveal-component-B_hNJfj-.js → reveal-component-Cr9euMM2.js} +8 -8
  55. package/dist/{select-BSxlITBo.js → select-CcidPqlM.js} +5 -5
  56. package/dist/{sequenceDiagram-WJ2MYXX4-rjPtD9fB.js → sequenceDiagram-WJ2MYXX4-Dxysg8DX.js} +1 -1
  57. package/dist/{spec-Dq8RNplp.js → spec-ZwfZcgsm.js} +4 -4
  58. package/dist/{stateDiagram-D77RDMKH-DMNxjUxV.js → stateDiagram-D77RDMKH-DTEN3-OJ.js} +2 -2
  59. package/dist/{stateDiagram-v2-MP3YSRHH-CumAaqGo.js → stateDiagram-v2-MP3YSRHH-vktk3Xc5.js} +2 -2
  60. package/dist/{strings-BGW-fXVJ.js → strings-BYJNyKmg.js} +3 -3
  61. package/dist/style.css +1 -1
  62. package/dist/{swimlanes-42K2YHIH-BU14_Tej.js → swimlanes-42K2YHIH-Cjveh9OM.js} +1 -1
  63. package/dist/{swimlanesDiagram-VR7AAH4N-CieAnPqJ.js → swimlanesDiagram-VR7AAH4N-Bdr7Ev_2.js} +2 -2
  64. package/dist/{swiper-component-Dir1o8ml.js → swiper-component-Dvj-goaD.js} +2 -2
  65. package/dist/{timeline-definition-24CTP7MA-CrVj4E1K.js → timeline-definition-24CTP7MA-KIuDcefn.js} +1 -1
  66. package/dist/{toast-BWIPi3aF.js → toast-CkB5Jq9K.js} +4 -4
  67. package/dist/{tooltip-C3UnvsRe.js → tooltip-Bzl4k00l.js} +2 -2
  68. package/dist/{use-restore-focus-BWO0z1lo.js → use-restore-focus-CmflVLWJ.js} +1 -1
  69. package/dist/{useDateFormatter-ChB4eyim.js → useDateFormatter-CYo5fm8s.js} +2 -2
  70. package/dist/{useIframeCapabilities-cMNQ80CK.js → useIframeCapabilities-BIolGsF1.js} +1 -1
  71. package/dist/{useLifecycle-BquH9ET-.js → useLifecycle-GNgvNwI-.js} +2 -2
  72. package/dist/{useTheme-DgOLVISn.js → useTheme-BZ2GGLgm.js} +141 -139
  73. package/dist/{vega-component-Bwg_Kuh-.js → vega-component-DOtem7Hf.js} +7 -7
  74. package/dist/{vennDiagram-4TSXK5OY-ZLxQ8yhm.js → vennDiagram-4TSXK5OY-DrIOUHfb.js} +1 -1
  75. package/dist/{wardleyDiagram-VM6X3IG4-D88pcLY1.js → wardleyDiagram-VM6X3IG4-CJNjf4OH.js} +1 -1
  76. package/dist/{xychartDiagram-S5SC5T6Z-_13d4g8g.js → xychartDiagram-S5SC5T6Z-DEshFho-.js} +1 -1
  77. package/package.json +1 -1
  78. package/src/__tests__/fixtures/pair-config.ts +1 -1
  79. package/src/components/app-config/__tests__/state.test.tsx +50 -0
  80. package/src/components/app-config/ai-config.tsx +1 -1
  81. package/src/components/chat/__tests__/capabilities-popover.test.tsx +0 -1
  82. package/src/components/chat/__tests__/chat-display.test.tsx +125 -0
  83. package/src/components/chat/__tests__/chat-markdown.test.tsx +43 -0
  84. package/src/components/chat/__tests__/chat-utils.test.ts +51 -1
  85. package/src/components/chat/acp/__tests__/agent-panel-initialization.test.tsx +1 -1
  86. package/src/components/chat/acp/__tests__/agent-panel.test.tsx +22 -17
  87. package/src/components/chat/acp/agent-panel.tsx +4 -3
  88. package/src/components/chat/acp/blocks.tsx +10 -10
  89. package/src/components/chat/acp/common.tsx +4 -5
  90. package/src/components/chat/acp/model-selector.tsx +1 -1
  91. package/src/components/chat/capabilities-popover.tsx +1 -6
  92. package/src/components/chat/chat-components.tsx +7 -4
  93. package/src/components/chat/chat-display.tsx +51 -6
  94. package/src/components/chat/chat-history-popover.tsx +6 -1
  95. package/src/components/chat/chat-markdown.tsx +15 -0
  96. package/src/components/chat/chat-panel.tsx +105 -119
  97. package/src/components/chat/chat-typography.css +108 -0
  98. package/src/components/chat/chat-utils.ts +8 -3
  99. package/src/components/chat/notebook-access-selector.tsx +1 -1
  100. package/src/components/chat/reasoning-accordion.tsx +3 -3
  101. package/src/components/chat/tool-call/tool-activity-group.tsx +49 -0
  102. package/src/components/chat/tool-call/tool-history-row.tsx +3 -30
  103. package/src/components/chat/tool-call/tool-result.tsx +1 -1
  104. package/src/components/chat/use-chat-generation.ts +19 -0
  105. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +18 -37
  106. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +5 -8
  107. package/src/components/editor/actions/__tests__/pair-with-agent.test.tsx +5 -19
  108. package/src/components/editor/actions/pair-with-agent-commands.ts +0 -1
  109. package/src/components/editor/actions/pair-with-agent-modal.tsx +7 -11
  110. package/src/components/editor/actions/pair-with-agent.tsx +3 -31
  111. package/src/components/editor/actions/useCellActionButton.tsx +1 -1
  112. package/src/components/editor/ai/ai-completion-editor.tsx +1 -1
  113. package/src/components/editor/ai/prompt-input.tsx +189 -0
  114. package/src/components/editor/cell/StagedAICell.tsx +3 -3
  115. package/src/components/editor/cell/code/cell-editor.tsx +14 -1
  116. package/src/components/editor/chrome/panels/panel-header.tsx +13 -0
  117. package/src/components/editor/chrome/types.ts +1 -1
  118. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +53 -45
  119. package/src/components/editor/chrome/wrapper/__tests__/useAskAiAboutCell.test.tsx +58 -0
  120. package/src/components/editor/chrome/wrapper/app-chrome.tsx +89 -72
  121. package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +5 -8
  122. package/src/components/editor/chrome/wrapper/useAskAiAboutCell.ts +36 -0
  123. package/src/components/editor/renderers/__tests__/add-cell-buttons.test.tsx +18 -0
  124. package/src/components/editor/renderers/cell-array.tsx +10 -67
  125. package/src/components/markdown/markdown-renderer.tsx +35 -20
  126. package/src/components/mcp/mcp-status-indicator.tsx +18 -14
  127. package/src/core/ai/__tests__/staged-cells.test.ts +1 -467
  128. package/src/core/ai/context/context.ts +2 -0
  129. package/src/core/ai/context/providers/__tests__/cell.test.ts +49 -0
  130. package/src/core/ai/context/providers/cell.ts +54 -0
  131. package/src/core/ai/staged-cells.ts +4 -280
  132. package/src/core/ai/state.ts +3 -0
  133. package/src/core/codemirror/__tests__/setup.test.ts +32 -0
  134. package/src/core/codemirror/cells/state.ts +1 -0
  135. package/src/core/codemirror/cm.ts +5 -1
  136. package/src/core/codemirror/config/types.ts +1 -1
  137. package/src/core/codemirror/language/languages/python.ts +3 -3
  138. package/src/core/codemirror/placeholder/__tests__/extensions.test.ts +16 -0
  139. package/src/core/codemirror/placeholder/extensions.ts +57 -19
  140. package/src/core/hotkeys/hotkeys.ts +2 -1
  141. package/src/core/notebook/document/__tests__/codemirror.test.ts +1 -0
  142. package/src/plugins/impl/chat/__tests__/chat-prompts.test.tsx +1 -1
  143. package/src/plugins/impl/chat/chat-ui.tsx +1 -1
  144. package/src/components/editor/ai/__tests__/staged-cell-submission.test.ts +0 -106
  145. package/src/components/editor/ai/add-cell-with-ai.tsx +0 -506
  146. package/src/components/editor/ai/staged-cell-submission.ts +0 -46
  147. package/src/components/editor/cell/__tests__/StagedAICell.test.tsx +0 -66
@@ -6,7 +6,7 @@ import { pairConfigSchema } from "@/core/config/pair";
6
6
 
7
7
  // Read the Python-owned resource only in tests to check the shared contract.
8
8
  export const PAIR_CONFIG = pairConfigSchema.parse({
9
- command: "uvx marimo@latest",
9
+ command: "marimo",
10
10
  hostEnabled: true,
11
11
  templates: JSON.parse(
12
12
  readFileSync(
@@ -0,0 +1,50 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+ import { createStore, Provider } from "jotai";
5
+ import { describe, expect, it, vi } from "vitest";
6
+ import { TooltipProvider } from "@/components/ui/tooltip";
7
+ import { ConfigButton } from "../app-config-button";
8
+ import { aiSettingsSubTabAtom, useOpenSettingsToTab } from "../state";
9
+ import { activeUserConfigCategoryAtom } from "../user-config-form";
10
+
11
+ vi.mock("../user-config-form", async () => {
12
+ const { atom } = await import("jotai");
13
+ return {
14
+ activeUserConfigCategoryAtom: atom("editor"),
15
+ UserConfigForm: () => <div>Settings form</div>,
16
+ };
17
+ });
18
+ vi.mock("../app-config-form", () => ({
19
+ AppConfigForm: () => null,
20
+ }));
21
+
22
+ function OpenAiSettings() {
23
+ const { handleClick } = useOpenSettingsToTab();
24
+ return (
25
+ <button type="button" onClick={() => handleClick("ai")}>
26
+ AI Settings
27
+ </button>
28
+ );
29
+ }
30
+
31
+ describe("useOpenSettingsToTab", () => {
32
+ it("opens the settings dialog on the AI category from an external button", () => {
33
+ const store = createStore();
34
+ store.set(aiSettingsSubTabAtom, "ai-providers");
35
+ render(
36
+ <Provider store={store}>
37
+ <TooltipProvider>
38
+ <OpenAiSettings />
39
+ <ConfigButton />
40
+ </TooltipProvider>
41
+ </Provider>,
42
+ );
43
+
44
+ fireEvent.click(screen.getByRole("button", { name: "AI Settings" }));
45
+
46
+ expect(screen.getByRole("dialog", { name: "User settings" })).toBeVisible();
47
+ expect(store.get(activeUserConfigCategoryAtom)).toBe("ai");
48
+ expect(store.get(aiSettingsSubTabAtom)).toBe("ai-features");
49
+ });
50
+ });
@@ -1351,7 +1351,7 @@ export const AiAssistConfig: React.FC<AiConfigProps> = ({
1351
1351
  description={
1352
1352
  <span>
1353
1353
  Model to use for code editing with the{" "}
1354
- <Kbd className="inline">Generate with AI</Kbd> button.
1354
+ <Kbd className="inline">Edit with AI</Kbd> action.
1355
1355
  </span>
1356
1356
  }
1357
1357
  forRole="edit"
@@ -27,7 +27,6 @@ describe("CapabilitiesPopover", () => {
27
27
 
28
28
  expect(webSearch).toBeChecked();
29
29
  expect(trigger).toHaveAttribute("data-active", "true");
30
- expect(trigger).toHaveClass("bg-primary/15", "text-primary", "ring-1");
31
30
  expect(trigger).toHaveAttribute(
32
31
  "title",
33
32
  "Capabilities (web search enabled)",
@@ -0,0 +1,125 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+ import type { ToolUIPart, UIMessage } from "ai";
5
+ import { describe, expect, it, vi } from "vitest";
6
+ import { renderUIMessage } from "../chat-display";
7
+
8
+ vi.mock("../tool-call/tool-call-view", () => ({
9
+ ToolCallView: ({ toolName }: { toolName: string }) => <div>{toolName}</div>,
10
+ }));
11
+ vi.mock("../../markdown/markdown-renderer", () => ({
12
+ MarkdownRenderer: ({ content }: { content: string }) => <p>{content}</p>,
13
+ }));
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: {},
21
+ });
22
+
23
+ function show(parts: UIMessage["parts"], isActive = false) {
24
+ return render(
25
+ renderUIMessage({
26
+ message: { id: "message", role: "assistant", parts },
27
+ isStreamingReasoning: false,
28
+ isLast: true,
29
+ isActive,
30
+ }),
31
+ );
32
+ }
33
+
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()]);
37
+ const summary = screen.getByRole("button", {
38
+ name: "Edited notebook · 4 operations",
39
+ });
40
+ expect(summary.getAttribute("aria-expanded")).toBe("false");
41
+ expect(screen.queryByText("tool-edit_notebook_tool")).toBeNull();
42
+ fireEvent.click(summary);
43
+ expect(screen.getAllByText("tool-edit_notebook_tool")).toHaveLength(4);
44
+ });
45
+
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();
61
+ expect(
62
+ screen.getByRole("button", { name: "Edited notebook · 1 operation" }),
63
+ ).toBeTruthy();
64
+ expect(
65
+ screen.getByRole("button", { name: "Completed 2 steps" }),
66
+ ).toBeTruthy();
67
+ });
68
+
69
+ it("keeps errors, denials, and approval requests outside collapsed groups", () => {
70
+ 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(),
94
+ ]);
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);
101
+ });
102
+
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);
119
+ expect(
120
+ screen.getByRole("button", {
121
+ name: "Stopped before completing all steps",
122
+ }),
123
+ ).toBeTruthy();
124
+ });
125
+ });
@@ -0,0 +1,43 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { act, render, screen } from "@testing-library/react";
4
+ import { createStore, Provider } from "jotai";
5
+ import { expect, it, vi } from "vitest";
6
+ import { ChatMarkdown } from "../chat-markdown";
7
+
8
+ const { aiAtom } = await vi.hoisted(async () => {
9
+ const { atom } = await import("jotai");
10
+ return {
11
+ aiAtom: atom<{ mode?: "ask" | "manual" | "agent" | "code_mode" }>({}),
12
+ };
13
+ });
14
+ vi.mock("@/core/config/config", async (importOriginal) => ({
15
+ ...(await importOriginal<object>()),
16
+ aiAtom,
17
+ }));
18
+ vi.mock("@/plugins/impl/code/LazyAnyLanguageCodeMirror", () => ({
19
+ LazyAnyLanguageCodeMirror: ({ value }: { value: string }) => (
20
+ <pre>{value}</pre>
21
+ ),
22
+ }));
23
+
24
+ it("updates snippet actions when switching between read-only and editing modes", async () => {
25
+ const store = createStore();
26
+ render(
27
+ <Provider store={store}>
28
+ <ChatMarkdown content={"```python\nx = 1\n```"} />
29
+ </Provider>,
30
+ );
31
+ expect(
32
+ await screen.findByRole("button", { name: "Add to Notebook" }),
33
+ ).toBeInTheDocument();
34
+ for (const mode of ["agent", "code_mode", "ask", "manual"] as const) {
35
+ act(() => store.set(aiAtom, { mode }));
36
+ expect(screen.getByRole("button", { name: /^Copy$/ })).toBeInTheDocument();
37
+ expect(screen.getByText("x = 1")).toBeInTheDocument();
38
+ const insertButton = screen.queryByRole("button", {
39
+ name: "Add to Notebook",
40
+ });
41
+ expect(insertButton !== null).toBe(mode === "ask" || mode === "manual");
42
+ }
43
+ });
@@ -1,8 +1,11 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import type { UIMessage } from "ai";
4
- import { describe, expect, it } from "vitest";
4
+ import { createStore } from "jotai";
5
+ import { FRONTEND_TOOL_REGISTRY } from "@/core/ai/tools/registry";
6
+ import { describe, expect, it, vi } from "vitest";
5
7
  import {
8
+ handleToolCall,
6
9
  hasPendingToolCalls,
7
10
  hasUnresolvedToolCalls,
8
11
  shouldFlushQueue,
@@ -510,3 +513,50 @@ describe("shouldFlushQueue", () => {
510
513
  ).toBe(false);
511
514
  });
512
515
  });
516
+
517
+ describe("handleToolCall", () => {
518
+ it.each(["frontend", "backend", "exception"])(
519
+ "reports %s failures as errors rather than successful output",
520
+ async (source) => {
521
+ const hasTool = vi
522
+ .spyOn(FRONTEND_TOOL_REGISTRY, "has")
523
+ .mockReturnValue(source === "frontend");
524
+ const response = {
525
+ tool_name: "test_tool",
526
+ result: null,
527
+ error: "Edit failed",
528
+ };
529
+ const invokeFrontend = vi
530
+ .spyOn(FRONTEND_TOOL_REGISTRY, "invoke")
531
+ .mockResolvedValue(response);
532
+ const invokeAiTool = vi.fn().mockImplementation(async () => {
533
+ if (source === "exception") {
534
+ throw new Error("Edit failed");
535
+ }
536
+ return response;
537
+ });
538
+ const addToolOutput = vi.fn();
539
+ try {
540
+ await handleToolCall({
541
+ invokeAiTool,
542
+ addToolOutput,
543
+ toolCall: { toolName: "test_tool", toolCallId: "call-1", input: {} },
544
+ toolContext: {
545
+ store: createStore(),
546
+ addStagedCell: vi.fn(),
547
+ createCell: vi.fn(),
548
+ },
549
+ });
550
+ expect(addToolOutput).toHaveBeenCalledExactlyOnceWith({
551
+ tool: "test_tool",
552
+ toolCallId: "call-1",
553
+ state: "output-error",
554
+ errorText: "Edit failed",
555
+ });
556
+ } finally {
557
+ hasTool.mockRestore();
558
+ invokeFrontend.mockRestore();
559
+ }
560
+ },
561
+ );
562
+ });
@@ -52,7 +52,7 @@ vi.mock("use-acp", async (importOriginal) => ({
52
52
  clearNotifications: vi.fn(),
53
53
  }),
54
54
  }));
55
- vi.mock("@/components/editor/ai/add-cell-with-ai", () => ({
55
+ vi.mock("@/components/editor/ai/prompt-input", () => ({
56
56
  PromptInput: () => null,
57
57
  }));
58
58
  vi.mock("../session-tabs", () => ({ SessionTabs: () => null }));
@@ -12,7 +12,7 @@ import type { ComponentProps } from "react";
12
12
  import { JsonRpcError } from "use-acp";
13
13
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
14
14
  import { MockRequestClient } from "@/__mocks__/requests";
15
- import type { PromptInput } from "@/components/editor/ai/add-cell-with-ai";
15
+ import type { PromptInput } from "@/components/editor/ai/prompt-input";
16
16
  import { TooltipProvider } from "@/components/ui/tooltip";
17
17
  import { toast } from "@/components/ui/use-toast";
18
18
  import { pendingAiPromptAtom } from "@/core/ai/state";
@@ -54,7 +54,7 @@ vi.mock("use-acp", async (importOriginal) => ({
54
54
  useAcpClient: () => client,
55
55
  }));
56
56
 
57
- vi.mock("@/components/editor/ai/add-cell-with-ai", () => ({
57
+ vi.mock("@/components/editor/ai/prompt-input", () => ({
58
58
  PromptInput: ({
59
59
  value,
60
60
  onChange,
@@ -254,19 +254,24 @@ describe("AgentPanel prompt submission", () => {
254
254
  });
255
255
  });
256
256
 
257
- it("clears busy state when a cancelled prompt settles", async () => {
258
- const response = Promise.withResolvers<{ stopReason: string }>();
259
- agent.prompt.mockReturnValueOnce(response.promise);
260
- await renderPanel();
261
- submitPrompt();
262
- await waitFor(() => expect(agent.prompt).toHaveBeenCalledOnce());
263
- expect(screen.getByText("Agent is working...")).toBeInTheDocument();
264
- fireEvent.click(screen.getByRole("button", { name: "Stop" }));
265
- await act(async () => response.resolve({ stopReason: "cancelled" }));
266
- expect(agent.cancel).toHaveBeenCalledWith({ sessionId: "session-1" });
267
- expect(screen.queryByText("Agent is working...")).not.toBeInTheDocument();
268
- expect(
269
- screen.queryByRole("button", { name: "Dismiss" }),
270
- ).not.toBeInTheDocument();
271
- });
257
+ it.each([0, 1])(
258
+ "clears busy state after cancellation through Stop button %i",
259
+ async (buttonIndex) => {
260
+ const response = Promise.withResolvers<{ stopReason: string }>();
261
+ agent.prompt.mockReturnValueOnce(response.promise);
262
+ await renderPanel();
263
+ submitPrompt();
264
+ await waitFor(() => expect(agent.prompt).toHaveBeenCalledOnce());
265
+ expect(screen.getByText("Agent is working...")).toBeInTheDocument();
266
+ const stopButtons = screen.getAllByRole("button", { name: "Stop" });
267
+ expect(stopButtons).toHaveLength(2);
268
+ fireEvent.click(stopButtons[buttonIndex]);
269
+ await act(async () => response.resolve({ stopReason: "cancelled" }));
270
+ expect(agent.cancel).toHaveBeenCalledWith({ sessionId: "session-1" });
271
+ expect(screen.queryByText("Agent is working...")).not.toBeInTheDocument();
272
+ expect(
273
+ screen.queryByRole("button", { name: "Dismiss" }),
274
+ ).not.toBeInTheDocument();
275
+ },
276
+ );
272
277
  });
@@ -16,7 +16,7 @@ import {
16
16
  import {
17
17
  type AdditionalCompletions,
18
18
  PromptInput,
19
- } from "@/components/editor/ai/add-cell-with-ai";
19
+ } from "@/components/editor/ai/prompt-input";
20
20
  import { PanelEmptyState } from "@/components/editor/chrome/panels/empty-state";
21
21
  import { Spinner } from "@/components/icons/spinner";
22
22
  import { Button } from "@/components/ui/button";
@@ -39,6 +39,7 @@ import {
39
39
  } from "./state";
40
40
  import { AgentThread } from "./thread";
41
41
  import "./agent-panel.css";
42
+ import "../chat-typography.css";
42
43
  import type { Completion } from "@codemirror/autocomplete";
43
44
  import type { ReactCodeMirrorRef } from "@uiw/react-codemirror";
44
45
  import type {
@@ -441,7 +442,7 @@ const ModeSelector = memo<ModeSelectorProps>(
441
442
 
442
443
  return (
443
444
  <Select value={currentModeId} onValueChange={onModeChange}>
444
- <SelectTrigger className="h-6 text-xs border-border shadow-none! ring-0! bg-muted hover:bg-muted/30 py-0 px-2 gap-1 capitalize">
445
+ <SelectTrigger className="chat-control chat-selector h-6 text-xs border-border py-0 px-2 gap-1 capitalize">
445
446
  {currentMode?.label ?? currentModeId}
446
447
  </SelectTrigger>
447
448
  <SelectContent>
@@ -1266,7 +1267,7 @@ const AgentPanel: React.FC = () => {
1266
1267
  };
1267
1268
 
1268
1269
  return (
1269
- <div className="flex flex-col flex-1 overflow-hidden mo-agent-panel">
1270
+ <div className="mo-ai-panel flex flex-col flex-1 overflow-hidden mo-agent-panel font-prose text-sm leading-6">
1270
1271
  <AgentPanelHeader
1271
1272
  connectionState={connectionState}
1272
1273
  currentAgentId={selectedTab?.agentId}
@@ -167,11 +167,11 @@ export const ReadyToChatBlock = () => {
167
167
  return (
168
168
  <div className="flex-1 flex items-center justify-center h-full min-h-[200px] flex-col">
169
169
  <div className="text-center space-y-3">
170
- <div className="w-12 h-12 mx-auto rounded-full bg-(--blue-3) flex items-center justify-center">
171
- <BotMessageSquareIcon className="h-6 w-6 text-(--blue-10)" />
170
+ <div className="w-12 h-12 mx-auto rounded-full bg-muted flex items-center justify-center">
171
+ <BotMessageSquareIcon className="h-6 w-6 text-muted-foreground" />
172
172
  </div>
173
173
  <div>
174
- <h3 className="text-lg font-medium text-foreground mb-1">
174
+ <h3 className="text-base font-semibold text-foreground mb-1">
175
175
  Agent is connected
176
176
  </h3>
177
177
  <p className="text-sm text-muted-foreground">
@@ -203,10 +203,10 @@ export const ConnectionChangeBlock = (props: {
203
203
  icon: <WifiIcon className="h-4 w-4" />,
204
204
  title: "Connected to Agent",
205
205
  message: "Successfully established connection with the AI agent",
206
- bgColor: "bg-(--blue-2)",
207
- borderColor: "border-(--blue-6)",
208
- textColor: "text-(--blue-11)",
209
- iconColor: "text-(--blue-10)",
206
+ bgColor: "bg-transparent",
207
+ borderColor: "border-border",
208
+ textColor: "text-muted-foreground",
209
+ iconColor: "text-muted-foreground",
210
210
  };
211
211
  case "disconnected":
212
212
  return {
@@ -318,7 +318,7 @@ export const PlansBlock = (props: { data: PlanNotificationEvent[] }) => {
318
318
  plans = uniqueByTakeLast(plans, (item) => item.content);
319
319
 
320
320
  return (
321
- <div className="rounded-lg border bg-background p-2 text-xs">
321
+ <div className="rounded-lg border bg-background p-2 text-sm">
322
322
  <div className="flex items-center justify-between mb-2">
323
323
  <span className="font-medium text-muted-foreground">
324
324
  To-dos{" "}
@@ -345,7 +345,7 @@ export const PlansBlock = (props: { data: PlanNotificationEvent[] }) => {
345
345
  />
346
346
  <span
347
347
  className={cn(
348
- "text-xs",
348
+ "text-sm",
349
349
  item.status === "completed" &&
350
350
  "line-through text-muted-foreground",
351
351
  )}
@@ -361,7 +361,7 @@ export const PlansBlock = (props: { data: PlanNotificationEvent[] }) => {
361
361
 
362
362
  export const UserMessagesBlock = (props: { data: UserNotificationEvent[] }) => {
363
363
  return (
364
- <div className="flex flex-col gap-2 text-muted-foreground border p-2 bg-background rounded wrap-break-word overflow-x-hidden">
364
+ <div className="flex flex-col gap-2 text-foreground border p-2 bg-background rounded wrap-break-word overflow-x-hidden">
365
365
  <ContentBlocks data={props.data.map((item) => item.content)} />
366
366
  </div>
367
367
  );
@@ -45,7 +45,7 @@ export const SimpleAccordion: React.FC<SimpleAccordionProps> = ({
45
45
  case "error":
46
46
  return <XCircleIcon className="h-3 w-3 text-destructive" />;
47
47
  case "success":
48
- return <CheckCircleIcon className="h-3 w-3 text-(--blue-9)" />;
48
+ return null;
49
49
  default:
50
50
  return defaultIcon;
51
51
  }
@@ -61,9 +61,8 @@ export const SimpleAccordion: React.FC<SimpleAccordionProps> = ({
61
61
  <AccordionItem value="tool-call" className="border-0">
62
62
  <AccordionTrigger
63
63
  className={cn(
64
- "py-1 text-xs border-border shadow-none! ring-0! bg-muted hover:bg-muted/30 px-2 gap-1 rounded-sm [&[data-state=open]>svg]:rotate-180",
64
+ "h-auto pl-0 pr-2 py-1 gap-2 rounded text-xs font-normal text-muted-foreground hover:bg-(--slate-2) hover:text-foreground hover:no-underline",
65
65
  status === "error" && "text-destructive/80",
66
- status === "success" && "text-(--blue-8)",
67
66
  )}
68
67
  >
69
68
  <span className="flex items-center gap-1">
@@ -104,8 +103,8 @@ export const ConnectionStatus: React.FC<ConnectionStatusProps> = memo(
104
103
  return {
105
104
  icon: <WifiIcon className="h-3 w-3" />,
106
105
  label: "Connected",
107
- variant: "default" as const,
108
- className: "bg-(--blue-3) text-(--blue-11) border-(--blue-5)",
106
+ variant: "outline" as const,
107
+ className: "bg-transparent text-muted-foreground border-border",
109
108
  };
110
109
  case "connecting":
111
110
  return {
@@ -35,7 +35,7 @@ export const ModelSelector = memo<ModelSelectorProps>(
35
35
  onValueChange={onModelChange}
36
36
  disabled={disabled}
37
37
  >
38
- <SelectTrigger className="h-6 text-xs border-border shadow-none! ring-0! bg-muted hover:bg-muted/30 py-0 px-2 gap-1">
38
+ <SelectTrigger className="chat-control chat-selector h-6 text-xs border-border py-0 px-2 gap-1">
39
39
  {displayName}
40
40
  </SelectTrigger>
41
41
  <SelectContent>
@@ -11,7 +11,6 @@ import {
11
11
  } from "@/components/ui/popover";
12
12
  import { Switch } from "@/components/ui/switch";
13
13
  import { chatOptionsAtom } from "@/core/ai/state";
14
- import { cn } from "@/utils/cn";
15
14
 
16
15
  export const CapabilitiesPopover: React.FC = () => {
17
16
  const [chatOptions, setChatOptions] = useAtom(chatOptionsAtom);
@@ -28,11 +27,7 @@ export const CapabilitiesPopover: React.FC = () => {
28
27
  data-active={webSearchOn || undefined}
29
28
  variant="text"
30
29
  size="icon"
31
- className={cn(
32
- "h-6 w-6 shrink-0 bg-muted hover:bg-muted/30",
33
- webSearchOn &&
34
- "bg-primary/15 text-primary ring-1 ring-primary/30 opacity-100 hover:bg-primary/20",
35
- )}
30
+ className="chat-control h-6 w-6 shrink-0"
36
31
  >
37
32
  <SlidersHorizontalIcon className="h-3.5 w-3.5" />
38
33
  </Button>
@@ -8,7 +8,7 @@ import {
8
8
  ImageIcon,
9
9
  PaperclipIcon,
10
10
  SendHorizontalIcon,
11
- StopCircleIcon,
11
+ SquareIcon,
12
12
  XIcon,
13
13
  } from "lucide-react";
14
14
  import { useState } from "react";
@@ -88,19 +88,20 @@ export const SendButton = ({
88
88
  }) => {
89
89
  const loadingContent = showStopLabel ? (
90
90
  <div className="flex flex-row items-center gap-1 px-1.5">
91
- <span className="text-xs text-error">Stop</span>
91
+ <span className="text-xs">Stop</span>
92
92
  <Spinner size="small" />
93
93
  </div>
94
94
  ) : (
95
- <StopCircleIcon className="size-4 text-error" />
95
+ <SquareIcon className="size-3.5" aria-hidden={true} />
96
96
  );
97
97
 
98
98
  return (
99
99
  <Tooltip content={isLoading ? "Stop" : "Submit"}>
100
100
  <Button
101
+ aria-label={isLoading ? "Stop" : "Submit"}
101
102
  variant="text"
102
103
  size="sm"
103
- className="h-6 min-w-6 p-0 hover:bg-muted/30 cursor-pointer"
104
+ className="chat-control h-6 min-w-6 p-0 hover:bg-muted/30 cursor-pointer"
104
105
  onClick={isLoading ? onStop : onSendClick}
105
106
  disabled={isLoading ? false : isEmpty}
106
107
  >
@@ -124,6 +125,7 @@ export const AddContextButton = ({
124
125
  return (
125
126
  <Tooltip content="Add context">
126
127
  <Button
128
+ className="chat-control"
127
129
  aria-label="Add context"
128
130
  variant="text"
129
131
  size="icon"
@@ -149,6 +151,7 @@ export const AttachFileButton = ({
149
151
  <>
150
152
  <Tooltip content="Attach a file">
151
153
  <Button
154
+ className="chat-control"
152
155
  aria-label="Attach a file"
153
156
  variant="text"
154
157
  size="icon"