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

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-By2DNeOL.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-Byb3BN4h.js} +4 -4
  17. package/dist/{common-JXDK9IjO.js → common-CgWyYqpC.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-BP_fYnBa.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-DPWFUVWl.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-GvhMU_kg.js} +10 -10
  38. package/dist/{glide-utils-CQddGpa-.js → glide-utils-qbkRZpEy.js} +3 -3
  39. package/dist/{globals-8EK96XwF.js → globals-RlhgvYCL.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-Dg1EfcEs.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-UPs4Rd1M.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
@@ -3,6 +3,7 @@
3
3
  import type {
4
4
  ChatAddToolApproveResponseFunction,
5
5
  DataUIPart,
6
+ DynamicToolUIPart,
6
7
  ToolUIPart,
7
8
  UIMessage,
8
9
  } from "ai";
@@ -11,9 +12,10 @@ import React from "react";
11
12
  import { renderHTML } from "@/plugins/core/RenderHTML";
12
13
  import { logNever } from "@/utils/assertNever";
13
14
  import { Logger } from "@/utils/Logger";
14
- import { MarkdownRenderer } from "../markdown/markdown-renderer";
15
+ import { ChatMarkdown } from "./chat-markdown";
15
16
  import { AttachmentRenderer, SourceChip } from "./chat-components";
16
17
  import { ReasoningAccordion } from "./reasoning-accordion";
18
+ import { ToolActivityGroup } from "./tool-call/tool-activity-group";
17
19
  import { ToolCallView } from "./tool-call/tool-call-view";
18
20
 
19
21
  export const renderUIMessage = ({
@@ -30,9 +32,52 @@ export const renderUIMessage = ({
30
32
  isActive: boolean;
31
33
  addToolApprovalResponse?: ChatAddToolApproveResponseFunction;
32
34
  }) => {
33
- return (
34
- <>{message.parts.map((part, index) => renderUIMessagePart(part, index))}</>
35
- );
35
+ const content: React.ReactNode[] = [];
36
+ let activity: { part: ToolUIPart | DynamicToolUIPart; index: number }[] = [];
37
+
38
+ const flushActivity = () => {
39
+ if (activity.length === 0) {
40
+ return;
41
+ }
42
+ content.push(
43
+ <ToolActivityGroup
44
+ key={activity[0].index}
45
+ tools={activity.map(({ part }) => ({
46
+ toolName: part.type === "dynamic-tool" ? part.toolName : part.type,
47
+ state: part.state,
48
+ }))}
49
+ isActive={isActive && isLast}
50
+ >
51
+ {activity.map(({ part, index }) => renderUIMessagePart(part, index))}
52
+ </ToolActivityGroup>,
53
+ );
54
+ activity = [];
55
+ };
56
+
57
+ message.parts.forEach((part, index) => {
58
+ // SDK step boundaries and invisible parts should not split visible activity.
59
+ if (
60
+ part.type === "step-start" ||
61
+ isDataPart(part) ||
62
+ (part.type === "text" && !part.text.trim())
63
+ ) {
64
+ return;
65
+ }
66
+ if (
67
+ (isToolPart(part) || part.type === "dynamic-tool") &&
68
+ part.state !== "output-error" &&
69
+ part.state !== "output-denied" &&
70
+ part.state !== "approval-requested"
71
+ ) {
72
+ activity.push({ part, index });
73
+ return;
74
+ }
75
+ flushActivity();
76
+ content.push(renderUIMessagePart(part, index));
77
+ });
78
+ flushActivity();
79
+
80
+ return content;
36
81
 
37
82
  function renderUIMessagePart(
38
83
  part: UIMessage["parts"][number],
@@ -46,7 +91,7 @@ export const renderUIMessage = ({
46
91
  toolName={part.type}
47
92
  result={part.output}
48
93
  errorText={part.state === "output-error" ? part.errorText : undefined}
49
- className="my-2"
94
+ className="my-1"
50
95
  state={part.state}
51
96
  input={part.input}
52
97
  approval={part.approval}
@@ -75,7 +120,7 @@ export const renderUIMessage = ({
75
120
  </React.Fragment>
76
121
  );
77
122
  }
78
- return <MarkdownRenderer key={index} content={part.text} />;
123
+ return <ChatMarkdown key={index} content={part.text} />;
79
124
  case "reasoning":
80
125
  return (
81
126
  <ReasoningAccordion
@@ -92,7 +92,12 @@ export const ChatHistoryPopover: React.FC<ChatHistoryPopoverProps> = ({
92
92
  <Popover open={open} onOpenChange={setOpen}>
93
93
  <Tooltip content="Previous chats">
94
94
  <PopoverTrigger asChild={true}>
95
- <Button variant="text" size="icon" aria-label="Previous chats">
95
+ <Button
96
+ className="chat-control"
97
+ variant="text"
98
+ size="icon"
99
+ aria-label="Previous chats"
100
+ >
96
101
  <ClockIcon className="h-4 w-4" />
97
102
  </Button>
98
103
  </PopoverTrigger>
@@ -0,0 +1,15 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { useAtomValue } from "jotai";
4
+ import { MarkdownRenderer } from "@/components/markdown/markdown-renderer";
5
+ import { aiAtom } from "@/core/config/config";
6
+
7
+ export function ChatMarkdown({ content }: { content: string }) {
8
+ const mode = useAtomValue(aiAtom)?.mode;
9
+ return (
10
+ <MarkdownRenderer
11
+ content={content}
12
+ allowNotebookInsertion={mode !== "agent" && mode !== "code_mode"}
13
+ />
14
+ );
15
+ }
@@ -13,15 +13,14 @@ import {
13
13
  import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai";
14
14
  import {
15
15
  BotMessageSquareIcon,
16
- ArrowRightIcon,
17
16
  Loader2,
18
17
  PlusIcon,
19
- SparklesIcon,
20
18
  SettingsIcon,
21
19
  } from "lucide-react";
22
- import { memo, useEffect, useRef, useState } from "react";
20
+ import { memo, useEffect, useMemo, useRef, useState } from "react";
23
21
  import useEvent from "react-use-event-hook";
24
22
  import { Button } from "@/components/ui/button";
23
+ import { LoadingEllipsis } from "@/components/icons/loading-ellipsis";
25
24
  import { replaceMessagesInChat } from "@/core/ai/chat-utils";
26
25
  import { useAIConfigActions } from "@/core/ai/config";
27
26
  import { AI_SDK_UI_THROTTLE_MS } from "@/core/ai/constants";
@@ -50,20 +49,19 @@ import { AIModelDropdown } from "../ai/ai-model-dropdown";
50
49
  import { useOpenSettingsToTab } from "../app-config/state";
51
50
  import {
52
51
  PairWithAgentBanner,
53
- PairWithAgentButton,
52
+ usePairWithAgentModal,
54
53
  } from "../editor/actions/pair-with-agent";
55
- import { PairWithAgentModal } from "../editor/actions/pair-with-agent-modal";
56
- import { PromptInput } from "../editor/ai/add-cell-with-ai";
54
+ import { PromptInput } from "../editor/ai/prompt-input";
57
55
  import {
58
56
  addContextCompletion,
59
- CONTEXT_TRIGGER,
60
57
  isContextAttachment,
61
58
  resolveChatContext,
62
59
  } from "../editor/ai/completion-utils";
63
60
  import { StreamingChunkTransport } from "../editor/ai/transport/chat-transport";
61
+ import { PanelHeaderActions } from "../editor/chrome/panels/panel-header";
64
62
  import { PanelEmptyState } from "../editor/chrome/panels/empty-state";
65
63
  import { CopyClipboardIcon } from "../icons/copy-icon";
66
- import { useImperativeModal } from "../modal/ImperativeModal";
64
+ import "./chat-typography.css";
67
65
  import { MCPStatusIndicator } from "../mcp/mcp-status-indicator";
68
66
  import { Tooltip, TooltipProvider } from "../ui/tooltip";
69
67
  import {
@@ -100,6 +98,7 @@ import { getCodes } from "@/core/codemirror/copilot/getCodes";
100
98
  import { focusInputAndMoveToEnd } from "@/core/codemirror/utils";
101
99
  import ScrollToBottomButton from "./acp/scroll-to-bottom-button";
102
100
  import { useChatControllerId } from "./use-chat-controller-id";
101
+ import { useChatGeneration } from "./use-chat-generation";
103
102
 
104
103
  // Default mode for the AI
105
104
  const DEFAULT_MODE = "ask";
@@ -116,7 +115,7 @@ const QueuedMessageDisplay = memo(
116
115
 
117
116
  return (
118
117
  <div className="flex justify-end">
119
- <div className="w-[95%] bg-background border border-dashed p-2 rounded-sm opacity-70">
118
+ <div className="w-[95%] bg-background border border-dashed p-2 rounded-[10px] opacity-70">
120
119
  {fileParts.map((filePart, idx) => (
121
120
  <AttachmentRenderer attachment={filePart} key={idx} />
122
121
  ))}
@@ -149,40 +148,41 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
149
148
  const { handleClick } = useOpenSettingsToTab();
150
149
 
151
150
  return (
152
- <div className="flex border-b px-2 py-1 justify-between shrink-0 items-center">
153
- <Tooltip content="New chat">
154
- <Button
155
- aria-label="New chat"
156
- variant="text"
157
- size="icon"
158
- onClick={onNewChat}
159
- >
160
- <PlusIcon className="h-4 w-4" />
161
- </Button>
162
- </Tooltip>
163
- <div className="flex items-center gap-2">
164
- <Tooltip content="Pair your agent">
165
- <PairWithAgentButton className="mr-1" label="Pair" />
166
- </Tooltip>
167
- <MCPStatusIndicator />
168
- <Tooltip content="AI Settings">
151
+ <PanelHeaderActions>
152
+ <div className="mo-ai-panel flex w-full shrink-0 items-center font-sans text-sm">
153
+ <div className="flex w-full items-center gap-1">
169
154
  <Button
170
- variant="text"
171
- size="xs"
172
- className="hover:bg-foreground/10 py-2"
173
- onClick={() => handleClick("ai")}
155
+ variant="ghost"
156
+ size="sm"
157
+ className="chat-control gap-1.5 font-normal"
158
+ onClick={onNewChat}
174
159
  >
175
- <SettingsIcon className="h-4 w-4" />
160
+ <PlusIcon className="size-4" aria-hidden={true} />
161
+ New chat
176
162
  </Button>
177
- </Tooltip>
178
- <ChatHistoryPopover
179
- activeChatId={activeChatId}
180
- setActiveChat={setActiveChat}
181
- onDeleteChats={onDeleteChats}
182
- isLoading={isLoading}
183
- />
163
+ <ChatHistoryPopover
164
+ activeChatId={activeChatId}
165
+ setActiveChat={setActiveChat}
166
+ onDeleteChats={onDeleteChats}
167
+ isLoading={isLoading}
168
+ />
169
+ <div className="ml-auto flex items-center gap-2">
170
+ <MCPStatusIndicator />
171
+ <Tooltip content="AI Settings">
172
+ <Button
173
+ className="chat-control"
174
+ aria-label="AI Settings"
175
+ variant="ghost"
176
+ size="icon"
177
+ onClick={() => handleClick("ai")}
178
+ >
179
+ <SettingsIcon className="size-4" aria-hidden={true} />
180
+ </Button>
181
+ </Tooltip>
182
+ </div>
183
+ </div>
184
184
  </div>
185
- </div>
185
+ </PanelHeaderActions>
186
186
  );
187
187
  };
188
188
 
@@ -216,14 +216,14 @@ const ChatMessageDisplay: React.FC<ChatMessageProps> = memo(
216
216
  );
217
217
 
218
218
  return (
219
- <div className="w-[95%] bg-background border p-1 rounded-sm">
219
+ <div className="w-[95%] bg-background border p-1 rounded-[10px]">
220
220
  {fileParts?.map((filePart, idx) => (
221
221
  <AttachmentRenderer attachment={filePart} key={idx} />
222
222
  ))}
223
223
  <PromptInput
224
224
  key={message.id}
225
225
  value={content}
226
- placeholder="Type your message..."
226
+ placeholder="Type your message. Use @ to reference variables and cells."
227
227
  onChange={() => {
228
228
  // noop
229
229
  }}
@@ -249,7 +249,7 @@ const ChatMessageDisplay: React.FC<ChatMessageProps> = memo(
249
249
 
250
250
  return (
251
251
  <div className="w-[95%] wrap-break-word">
252
- <div className="absolute right-1 top-1 opacity-0 group-hover:opacity-100 transition-opacity">
252
+ <div className="absolute right-1 top-1 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity">
253
253
  <CopyClipboardIcon className="h-3 w-3" value={content || ""} />
254
254
  </div>
255
255
  {renderUIMessage({
@@ -311,22 +311,22 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
311
311
 
312
312
  return (
313
313
  <TooltipProvider>
314
- <div className="px-3 py-2 border-t border-border/20 flex flex-wrap items-center gap-1">
315
- <div className="flex flex-wrap items-center gap-1">
314
+ <div className="chat-input-footer px-3 py-2 border-t border-border/20 flex items-center gap-2">
315
+ <div className="chat-input-selectors flex min-w-0 flex-1 items-center gap-1">
316
316
  <NotebookAccessSelector
317
317
  value={currentMode}
318
318
  onValueChange={saveModeChange}
319
319
  />
320
320
  <AIModelDropdown
321
321
  placeholder="Model"
322
- triggerClassName="h-6 text-xs shadow-none! ring-0! bg-muted hover:bg-muted/30 rounded-sm max-w-[200px]"
322
+ triggerClassName="chat-control chat-selector h-6 text-xs rounded-sm min-w-0 max-w-[200px] flex-1"
323
323
  iconSize="small"
324
324
  showAddCustomModelDocs={true}
325
325
  forRole="chat"
326
326
  />
327
- <CapabilitiesPopover />
328
327
  </div>
329
- <div className="flex flex-row ml-auto">
328
+ <div className="chat-input-actions flex shrink-0 items-center gap-0.5 ml-auto">
329
+ <CapabilitiesPopover />
330
330
  <AddContextButton
331
331
  handleAddContext={onAddContext}
332
332
  isLoading={isLoading}
@@ -354,9 +354,7 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
354
354
  ChatInputFooter.displayName = "ChatInputFooter";
355
355
 
356
356
  interface ChatInputProps {
357
- placeholder?: string;
358
357
  input: string;
359
- inputClassName?: string;
360
358
  setInput: (value: string) => void;
361
359
  onSubmit: (e: KeyboardEvent | undefined, value: string) => void;
362
360
  inputRef: React.RefObject<ReactCodeMirrorRef | null>;
@@ -369,9 +367,7 @@ interface ChatInputProps {
369
367
 
370
368
  const ChatInput: React.FC<ChatInputProps> = memo(
371
369
  ({
372
- placeholder,
373
370
  input,
374
- inputClassName,
375
371
  setInput,
376
372
  onSubmit,
377
373
  inputRef,
@@ -381,6 +377,20 @@ const ChatInput: React.FC<ChatInputProps> = memo(
381
377
  onAddFiles,
382
378
  onClose,
383
379
  }) => {
380
+ const openPairWithAgentModal = usePairWithAgentModal();
381
+ const placeholderAction = useMemo(
382
+ () =>
383
+ isWasm()
384
+ ? undefined
385
+ : {
386
+ beforeText: "Type your message or ",
387
+ linkText: "pair your own coding agent",
388
+ afterText: ". Use @ to reference variables and cells.",
389
+ onClick: openPairWithAgentModal,
390
+ },
391
+ [openPairWithAgentModal],
392
+ );
393
+
384
394
  const handleSendClick = useEvent(() => {
385
395
  if (input.trim()) {
386
396
  onSubmit(undefined, input);
@@ -388,8 +398,8 @@ const ChatInput: React.FC<ChatInputProps> = memo(
388
398
  });
389
399
 
390
400
  return (
391
- <div className="relative shrink-0 flex flex-col border-t">
392
- <div className={cn("px-2 py-1.5", inputClassName)}>
401
+ <div className="relative shrink-0 flex flex-col">
402
+ <div className="px-2 py-1.5">
393
403
  <PromptInput
394
404
  className="max-h-[400px]"
395
405
  inputRef={inputRef}
@@ -398,7 +408,8 @@ const ChatInput: React.FC<ChatInputProps> = memo(
398
408
  onSubmit={onSubmit}
399
409
  onClose={onClose}
400
410
  onAddFiles={onAddFiles}
401
- placeholder={placeholder || "Type your message..."}
411
+ placeholder="Type your message. Use @ to reference variables and cells."
412
+ placeholderAction={placeholderAction}
402
413
  />
403
414
  </div>
404
415
  <ChatInputFooter
@@ -417,26 +428,6 @@ const ChatInput: React.FC<ChatInputProps> = memo(
417
428
 
418
429
  ChatInput.displayName = "ChatInput";
419
430
 
420
- const PairWithAgentCallout: React.FC<{
421
- onPairWithAgent: () => void;
422
- }> = ({ onPairWithAgent }) => {
423
- if (isWasm()) {
424
- return null;
425
- }
426
-
427
- return (
428
- <Button
429
- variant="text"
430
- className="gap-1.5 text-sm text-link hover:underline"
431
- onClick={onPairWithAgent}
432
- >
433
- <SparklesIcon className="h-3.5 w-3.5 shrink-0" />
434
- <span>Pair your own agent with this notebook</span>
435
- <ArrowRightIcon className="h-3 w-3 shrink-0" />
436
- </Button>
437
- );
438
- };
439
-
440
431
  const ChatPanel = () => {
441
432
  const aiConfigured = useAtomValue(aiModelConfiguredAtom);
442
433
  const { handleClick } = useOpenSettingsToTab();
@@ -494,7 +485,6 @@ const ChatPanelBody = () => {
494
485
  });
495
486
  const runtimeManager = useRuntimeManager();
496
487
  const { invokeAiTool } = useRequestClient();
497
- const { openModal, closeModal } = useImperativeModal();
498
488
 
499
489
  const activeChatId = activeChat?.id;
500
490
  const { chatControllerId, renewDraftChatId } =
@@ -638,7 +628,7 @@ const ChatPanelBody = () => {
638
628
  },
639
629
  );
640
630
 
641
- const isLoading = status === "submitted" || status === "streaming";
631
+ const isLoading = useChatGeneration(status);
642
632
  // useEvent reads the latest loading state, even when called after an await
643
633
  // (e.g. resolving @-context) that outlives the original render's closure.
644
634
  const submitOrQueue = useEvent((parts: ChatMessagePart[]) => {
@@ -784,10 +774,6 @@ const ChatPanelBody = () => {
784
774
  });
785
775
  });
786
776
 
787
- const handlePairWithAgent = useEvent(() => {
788
- openModal(<PairWithAgentModal onClose={closeModal} />);
789
- });
790
-
791
777
  const handleMessageEdit = useEvent(
792
778
  async (index: number, newValue: string) => {
793
779
  const editedMessage = messages[index];
@@ -878,14 +864,20 @@ const ChatPanelBody = () => {
878
864
  return;
879
865
  }
880
866
  setPendingPrompt(null);
881
- const { prompt, submit } = pendingPrompt;
867
+ const { prompt, submit, append } = pendingPrompt;
868
+ const applyPrompt = (draft: string) =>
869
+ append
870
+ ? draft.includes(prompt.trim())
871
+ ? draft
872
+ : `${draft}${draft ? " " : ""}${prompt}`
873
+ : prompt;
882
874
  if (submit) {
883
875
  void submitPendingPrompt(prompt);
884
876
  } else if (isNewThread) {
885
- setNewThreadInput(prompt);
877
+ setNewThreadInput(applyPrompt);
886
878
  focusInputAndMoveToEnd(newThreadInputRef);
887
879
  } else {
888
- setInput(prompt);
880
+ setInput(applyPrompt);
889
881
  focusInputAndMoveToEnd(newMessageInputRef);
890
882
  }
891
883
  }, [pendingPrompt, setPendingPrompt, isNewThread, submitPendingPrompt]);
@@ -893,10 +885,8 @@ const ChatPanelBody = () => {
893
885
  const chatInput = isNewThread ? (
894
886
  <ChatInput
895
887
  key="new-thread-input"
896
- placeholder={`Ask anything, ${CONTEXT_TRIGGER} to include context about tables or dataframes`}
897
888
  input={newThreadInput}
898
889
  inputRef={newThreadInputRef}
899
- inputClassName="px-1 py-0"
900
890
  setInput={setNewThreadInput}
901
891
  onSubmit={handleNewThreadSubmit}
902
892
  isLoading={isLoading}
@@ -920,12 +910,7 @@ const ChatPanelBody = () => {
920
910
  );
921
911
 
922
912
  const filesPills = files && files.length > 0 && (
923
- <div
924
- className={cn(
925
- "flex flex-row gap-1 flex-wrap p-1",
926
- isNewThread && "py-2 px-1",
927
- )}
928
- >
913
+ <div className="flex flex-row gap-1 flex-wrap px-3 pt-2">
929
914
  {files?.map((file) => (
930
915
  <FileAttachmentPill
931
916
  file={file}
@@ -936,8 +921,15 @@ const ChatPanelBody = () => {
936
921
  </div>
937
922
  );
938
923
 
924
+ const composer = (
925
+ <div className="@container rounded-[10px] border border-border bg-background">
926
+ {filesPills}
927
+ {chatInput}
928
+ </div>
929
+ );
930
+
939
931
  return (
940
- <div className="flex flex-col h-[calc(100%-53px)]">
932
+ <div className="mo-ai-panel flex flex-col flex-1 min-h-0 font-prose text-sm leading-6">
941
933
  <TooltipProvider>
942
934
  <ChatHeader
943
935
  onNewChat={handleNewChat}
@@ -950,20 +942,10 @@ const ChatPanelBody = () => {
950
942
 
951
943
  <div className="flex-1 flex flex-col overflow-hidden relative min-h-0">
952
944
  <div
953
- className="flex-1 px-3 bg-(--slate-1) gap-4 py-3 flex flex-col overflow-y-auto"
945
+ className="flex-1 px-3 bg-background gap-4 py-3 flex flex-col overflow-y-auto"
954
946
  ref={scrollContainerRef}
955
947
  onScroll={handleScroll}
956
948
  >
957
- {isNewThread && (
958
- <div className="flex flex-col gap-2">
959
- <div className="rounded-md border bg-background">
960
- {filesPills}
961
- {chatInput}
962
- </div>
963
- <PairWithAgentCallout onPairWithAgent={handlePairWithAgent} />
964
- </div>
965
- )}
966
-
967
949
  {messages.map((message, idx) => (
968
950
  <ChatMessageDisplay
969
951
  key={message.id}
@@ -981,12 +963,6 @@ const ChatPanelBody = () => {
981
963
  <QueuedMessageDisplay key={message.id} message={message} />
982
964
  ))}
983
965
 
984
- {isLoading && (
985
- <div className="flex justify-center py-4">
986
- <Loader2 className="h-4 w-4 animate-spin" />
987
- </div>
988
- )}
989
-
990
966
  {error && (
991
967
  <div className="flex items-center justify-center space-x-2 mb-4">
992
968
  <ErrorBanner error={error || new Error("Unknown error")} />
@@ -1015,20 +991,30 @@ const ChatPanelBody = () => {
1015
991
  </div>
1016
992
 
1017
993
  {isLoading && (
1018
- <div className="w-full flex justify-center items-center z-20 border-t">
1019
- <Button variant="linkDestructive" size="sm" onClick={stop}>
1020
- Stop
994
+ <div className="flex shrink-0 items-center gap-2 px-3 pt-2 text-xs text-muted-foreground">
995
+ <div className="flex items-center gap-1.5">
996
+ <div aria-hidden={true}>
997
+ <LoadingEllipsis
998
+ size={3}
999
+ gap="gap-0.5"
1000
+ className="motion-reduce:[&_svg]:animate-none"
1001
+ />
1002
+ </div>
1003
+ <output>Working</output>
1004
+ </div>
1005
+ <span aria-hidden={true} className="h-3 w-px bg-border" />
1006
+ <Button
1007
+ variant="text"
1008
+ size="xs"
1009
+ className="chat-control"
1010
+ onClick={stop}
1011
+ >
1012
+ Stop generation
1021
1013
  </Button>
1022
1014
  </div>
1023
1015
  )}
1024
1016
 
1025
- {/* For existing threads, we place the chat input at the bottom */}
1026
- {!isNewThread && (
1027
- <>
1028
- {filesPills}
1029
- {chatInput}
1030
- </>
1031
- )}
1017
+ <div className="shrink-0 p-3">{composer}</div>
1032
1018
  </div>
1033
1019
  );
1034
1020
  };
@@ -0,0 +1,108 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ .mo-ai-panel {
4
+ .chat-control {
5
+ color: var(--muted-foreground);
6
+ background: transparent;
7
+ border-color: transparent;
8
+ box-shadow: none;
9
+ opacity: 1;
10
+
11
+ &.chat-selector {
12
+ color: var(--foreground);
13
+ background: var(--slate-2);
14
+ border-color: var(--border);
15
+ }
16
+
17
+ &:not(:disabled):is(:hover, [data-state="open"], [data-active="true"]) {
18
+ color: var(--foreground);
19
+ background: var(--slate-3);
20
+ }
21
+
22
+ &[data-active="true"] {
23
+ border-color: var(--border);
24
+ }
25
+
26
+ &:focus-visible {
27
+ outline: 1px solid var(--muted-foreground);
28
+ outline-offset: 2px;
29
+ }
30
+
31
+ &:disabled {
32
+ opacity: 0.5;
33
+ }
34
+ }
35
+
36
+ .lucide-chevron-down {
37
+ width: 0.75rem;
38
+ height: 0.75rem;
39
+ stroke-width: 1.5;
40
+ opacity: 0.5;
41
+ }
42
+
43
+ .mo-markdown-renderer {
44
+ font-family: inherit;
45
+ font-size: 0.875rem;
46
+ line-height: 1.5;
47
+
48
+ :is(h1, h2, h3, h4, h5, h6) {
49
+ font-family: inherit;
50
+ font-size: 0.875rem;
51
+ font-weight: 600;
52
+ line-height: 1.5;
53
+ margin-block: 1rem 0.5rem;
54
+ }
55
+
56
+ :is(h1, h2) {
57
+ font-size: 1rem;
58
+ }
59
+
60
+ > :first-child {
61
+ margin-top: 0;
62
+ }
63
+
64
+ :is(pre, code, .cm-editor) {
65
+ font-family: var(--monospace-font);
66
+ }
67
+
68
+ .cm-editor {
69
+ font-size: 0.8125rem;
70
+ line-height: 1.5;
71
+ }
72
+ }
73
+
74
+ .mo-ai-prompt {
75
+ font-family: var(--text-font), sans-serif;
76
+ .cm-clickable-placeholder:hover {
77
+ color: var(--foreground);
78
+ }
79
+
80
+ .cm-placeholder {
81
+ font-family: var(--text-font), sans-serif;
82
+ }
83
+
84
+ .cm-editor,
85
+ .cm-scroller {
86
+ font-family: inherit;
87
+ font-size: inherit;
88
+ line-height: inherit;
89
+ }
90
+ }
91
+ }
92
+
93
+ @container (max-width: 380px) {
94
+ .chat-input-footer {
95
+ flex-wrap: wrap;
96
+ }
97
+
98
+ .chat-input-selectors {
99
+ flex-basis: 100%;
100
+ }
101
+ }
102
+
103
+ @container (max-width: 360px) {
104
+ .sidebar-panel-actions:not(:empty) {
105
+ order: 1;
106
+ flex-basis: 100%;
107
+ }
108
+ }