@marimo-team/islands 0.19.8-dev5 → 0.19.8-dev50

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/{Combination-Bg-xN8JV.js → Combination-BTMrlhzT.js} +11 -10
  2. package/dist/{ConnectedDataExplorerComponent-DewsKLl2.js → ConnectedDataExplorerComponent-BAeQ8DWw.js} +11 -11
  3. package/dist/{ImageComparisonComponent-Bijp8beW.js → ImageComparisonComponent-DkEXPki_.js} +2 -2
  4. package/dist/{any-language-editor-DZc6NCTp.js → any-language-editor-D0UQItkS.js} +6 -6
  5. package/dist/{architectureDiagram-VXUJARFQ--NkyBn9Y.js → architectureDiagram-VXUJARFQ-DPPYVq8H.js} +4 -4
  6. package/dist/assets/__vite-browser-external-WSlCcXn_.js +1 -0
  7. package/dist/assets/worker-DUYMdbtA.js +73 -0
  8. package/dist/{blockDiagram-VD42YOAC-DEZZaTW0.js → blockDiagram-VD42YOAC-BA5N05Y9.js} +4 -4
  9. package/dist/{button-BWvsJ2Wr.js → button-Cy0ElmIm.js} +2 -2
  10. package/dist/{c4Diagram-YG6GDRKO-Bj7hwWCO.js → c4Diagram-YG6GDRKO-DJLzuGJJ.js} +3 -3
  11. package/dist/{channel-B_QrFrGg.js → channel-Dob5kWXR.js} +1 -1
  12. package/dist/{check-CM_kewwn.js → check-DkNR52Mm.js} +1 -1
  13. package/dist/{chunk-5FQGJX7Z-D5VFKHmt.js → chunk-5FQGJX7Z-BEb20Lzt.js} +3 -3
  14. package/dist/{chunk-ABZYJK2D-SZPYmRzN.js → chunk-ABZYJK2D-BXTC53mt.js} +1 -1
  15. package/dist/{chunk-ATLVNIR6-BI_WwH1o.js → chunk-ATLVNIR6-BJDjUR_c.js} +1 -1
  16. package/dist/{chunk-B4BG7PRW-BlI9Gm1l.js → chunk-B4BG7PRW-DzmUUpfH.js} +4 -4
  17. package/dist/{chunk-DI55MBZ5-BXxemMn5.js → chunk-DI55MBZ5-gTd3J8Tu.js} +4 -4
  18. package/dist/{chunk-EXTU4WIE-CzWtDV99.js → chunk-EXTU4WIE-DyoOs5QX.js} +1 -1
  19. package/dist/{chunk-JA3XYJ7Z-DQ-2ARfa.js → chunk-JA3XYJ7Z-BGnAIbOP.js} +2 -2
  20. package/dist/{chunk-JZLCHNYA-CVfjf2vv.js → chunk-JZLCHNYA-CIRgweVQ.js} +4 -4
  21. package/dist/{chunk-N4CR4FBY-BCZvQ7Jq.js → chunk-N4CR4FBY-DKSvXAIS.js} +5 -5
  22. package/dist/{chunk-QN33PNHL-DY_2Q2zl.js → chunk-QN33PNHL-B6zC8BTi.js} +1 -1
  23. package/dist/{chunk-QXUST7PY-BMCjAVR_.js → chunk-QXUST7PY-C7750n_u.js} +5 -5
  24. package/dist/{chunk-S3R3BYOJ-Ddu0H4Qa.js → chunk-S3R3BYOJ-CBkH6JZZ.js} +1 -1
  25. package/dist/{chunk-TZMSLE5B-C2wVlbMl.js → chunk-TZMSLE5B-DObGL7xi.js} +1 -1
  26. package/dist/{classDiagram-2ON5EDUG-D-g7zbyO.js → classDiagram-2ON5EDUG-B9pkKjjc.js} +9 -9
  27. package/dist/{classDiagram-v2-WZHVMYZB-C7v5zNRD.js → classDiagram-v2-WZHVMYZB-CRhhA0tV.js} +9 -9
  28. package/dist/{click-outside-container-BCN5BtVO.js → click-outside-container-DNfggvIW.js} +1 -1
  29. package/dist/{code-block-37QAKDTI-eUgXqGNG.js → code-block-37QAKDTI-u5kgjqmr.js} +2 -2
  30. package/dist/{compiler-runtime-DHFVbq0b.js → compiler-runtime-B_OLMU9S.js} +1 -1
  31. package/dist/{copy-B59Bw3-w.js → copy-DRaXIb_a.js} +3 -3
  32. package/dist/{dagre-6UL2VRFP-DKIPL74O.js → dagre-6UL2VRFP-C2C2XxsB.js} +6 -6
  33. package/dist/{data-grid-overlay-editor-COyFwFmE.js → data-grid-overlay-editor-BXqtz1ia.js} +4 -4
  34. package/dist/{diagram-PSM6KHXK-CVTrAZaP.js → diagram-PSM6KHXK-DHBY-94p.js} +5 -5
  35. package/dist/{diagram-QEK2KX5R-BqHBzu3x.js → diagram-QEK2KX5R-CgMshOwn.js} +3 -3
  36. package/dist/{diagram-S2PKOQOG-CJD6owcg.js → diagram-S2PKOQOG-F1KPva3Y.js} +3 -3
  37. package/dist/{dist-Co5PD8Fb.js → dist-BBYTEAvO.js} +1 -1
  38. package/dist/{erDiagram-Q2GNP2WA-CqOceSf9.js → erDiagram-Q2GNP2WA-18gGng8V.js} +9 -9
  39. package/dist/{error-banner-C7KLpECd.js → error-banner-D2zjeN_a.js} +5 -5
  40. package/dist/{esm-D4WO8J3G.js → esm-CgRNPmz8.js} +6 -6
  41. package/dist/{flowDiagram-NV44I4VS-K7-DUifo.js → flowDiagram-NV44I4VS-iHFiHYe0.js} +9 -9
  42. package/dist/{ganttDiagram-JELNMOA3-BwUFY9Nu.js → ganttDiagram-JELNMOA3-D7GixxiF.js} +2 -2
  43. package/dist/{gitGraphDiagram-NY62KEGX-CjGRtLb1.js → gitGraphDiagram-NY62KEGX-CJFHytRK.js} +2 -2
  44. package/dist/{glide-data-editor-C3T7HsLi.js → glide-data-editor-BYwb17Bf.js} +13 -13
  45. package/dist/{infoDiagram-WHAUD3N6-DNhmDn-6.js → infoDiagram-WHAUD3N6-B5Lkh3A9.js} +2 -2
  46. package/dist/{journeyDiagram-XKPGCS4Q-BOdK47P8.js → journeyDiagram-XKPGCS4Q-CV_9R9iP.js} +2 -2
  47. package/dist/{kanban-definition-3W4ZIXB7-A0JC9d0g.js → kanban-definition-3W4ZIXB7-Dp21D5Ym.js} +6 -6
  48. package/dist/{katex-DJyOeQ91.js → katex-CX2BKujk.js} +1 -1
  49. package/dist/{katex-Dm9nZf6A.js → katex-Db0k5oV_.js} +1 -1
  50. package/dist/{label-C4PtQcza.js → label-CxU5JNBW.js} +6 -6
  51. package/dist/main.js +2000 -1887
  52. package/dist/mermaid-4DMBBIKO-BhDCqnO1.js +6 -0
  53. package/dist/{mermaid-Bqp2Xw99.js → mermaid-B__BZSXU.js} +39 -39
  54. package/dist/{mhchem-BqdXeZVX.js → mhchem-w1tkUnWr.js} +1 -1
  55. package/dist/{mindmap-definition-VGOIOE7T-CS6nKN_L.js → mindmap-definition-VGOIOE7T-B_5mfdYp.js} +8 -8
  56. package/dist/{number-overlay-editor-Bz_bDJQb.js → number-overlay-editor-D-4WQAGX.js} +2 -2
  57. package/dist/{pieDiagram-ADFJNKIX-DSa60Grk.js → pieDiagram-ADFJNKIX-B-DGEopK.js} +3 -3
  58. package/dist/{quadrantDiagram-AYHSOK5B-CFnMbP2J.js → quadrantDiagram-AYHSOK5B-M_yRSIZn.js} +1 -1
  59. package/dist/{react-DdA8EBol.js → react-Bs6Z0kvn.js} +1 -1
  60. package/dist/{react-dom-DJW8xUDg.js → react-dom-CqtLRVZP.js} +2 -2
  61. package/dist/{react-plotly-jVjTu07w.js → react-plotly-BuRa9xtI.js} +1 -1
  62. package/dist/{react-vega-DgHpnZ04.js → react-vega-3WcLHYC7.js} +2 -2
  63. package/dist/{react-vega-CjiPWyw0.js → react-vega-DLFvGrpJ.js} +1 -1
  64. package/dist/{requirementDiagram-UZGBJVZJ-ytLQrFTk.js → requirementDiagram-UZGBJVZJ-9Wt82hOZ.js} +8 -8
  65. package/dist/{sankeyDiagram-TZEHDZUN-KQqXDoky.js → sankeyDiagram-TZEHDZUN-x_aTXZeN.js} +1 -1
  66. package/dist/{sequenceDiagram-WL72ISMW-ByLI04T5.js → sequenceDiagram-WL72ISMW-CXXmJqiQ.js} +3 -3
  67. package/dist/{slides-component-BVjvNo92.js → slides-component-Dp-y50K9.js} +4 -4
  68. package/dist/{spec-Dmb1KfK3.js → spec-HoYHAQo2.js} +6 -6
  69. package/dist/{stateDiagram-FKZM4ZOC-Dfz8vBbP.js → stateDiagram-FKZM4ZOC-CiSKS_Mx.js} +9 -9
  70. package/dist/{stateDiagram-v2-4FDKWEC3-DRYoLdT5.js → stateDiagram-v2-4FDKWEC3-A43Itnjp.js} +9 -9
  71. package/dist/style.css +1 -1
  72. package/dist/{timeline-definition-IT6M3QCI-CO48XU1B.js → timeline-definition-IT6M3QCI-DR26eWb4.js} +1 -1
  73. package/dist/{types-CzEZ3EWT.js → types-Bb-6p8hv.js} +8 -8
  74. package/dist/{useAsyncData-BjNwqCfS.js → useAsyncData-Dyq3DyOF.js} +3 -3
  75. package/dist/{useDeepCompareMemoize-CfoxVor3.js → useDeepCompareMemoize-BhZZsis0.js} +12 -8
  76. package/dist/{useIframeCapabilities-BBO_R0ww.js → useIframeCapabilities-DurI5SJh.js} +2 -2
  77. package/dist/{useTheme-BYG2SH8J.js → useTheme-SlKl8MlS.js} +5 -6
  78. package/dist/{vega-component-rDX7xwxH.js → vega-component-DCxUyPnb.js} +10 -10
  79. package/dist/{xychartDiagram-PRI3JC2R-CUIfjNVD.js → xychartDiagram-PRI3JC2R-BcVxCRox.js} +4 -4
  80. package/dist/{zod-DITCj31F.js → zod-bjADtMKr.js} +3 -3
  81. package/package.json +18 -18
  82. package/src/components/app-config/ai-config.tsx +11 -2
  83. package/src/components/app-config/optional-features.tsx +1 -1
  84. package/src/components/app-config/user-config-form.tsx +0 -54
  85. package/src/components/chat/__tests__/useFileState.test.tsx +93 -0
  86. package/src/components/chat/acp/__tests__/state.test.ts +69 -0
  87. package/src/components/chat/acp/agent-panel.tsx +26 -77
  88. package/src/components/chat/acp/state.ts +6 -6
  89. package/src/components/chat/chat-components.tsx +114 -1
  90. package/src/components/chat/chat-panel.tsx +79 -134
  91. package/src/components/chat/chat-utils.ts +42 -0
  92. package/src/components/data-table/__tests__/data-table.test.tsx +94 -2
  93. package/src/components/editor/actions/useCellActionButton.tsx +14 -1
  94. package/src/components/editor/ai/add-cell-with-ai.tsx +85 -53
  95. package/src/components/editor/ai/ai-completion-editor.tsx +15 -38
  96. package/src/components/editor/cell/CreateCellButton.tsx +2 -1
  97. package/src/components/editor/cell/code/cell-editor.tsx +12 -0
  98. package/src/components/editor/chrome/panels/packages-panel.tsx +12 -9
  99. package/src/components/editor/database/__tests__/__snapshots__/as-code.test.ts.snap +15 -0
  100. package/src/components/editor/database/__tests__/as-code.test.ts +8 -0
  101. package/src/components/editor/database/as-code.ts +3 -0
  102. package/src/components/editor/database/schemas.ts +9 -0
  103. package/src/components/editor/renderers/cell-array.tsx +2 -1
  104. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +12 -0
  105. package/src/components/pages/gallery-page.tsx +37 -6
  106. package/src/core/MarimoApp.tsx +12 -8
  107. package/src/core/ai/context/providers/file.ts +1 -1
  108. package/src/core/cells/__tests__/cells.test.ts +120 -0
  109. package/src/core/cells/__tests__/session.test.ts +37 -1
  110. package/src/core/cells/cells.ts +14 -0
  111. package/src/core/cells/session.ts +20 -8
  112. package/src/core/codemirror/language/languages/markdown.ts +7 -0
  113. package/src/core/config/feature-flag.tsx +0 -4
  114. package/src/core/dom/uiregistry.ts +4 -1
  115. package/src/core/islands/__tests__/bridge.test.ts +7 -2
  116. package/src/core/islands/bridge.ts +1 -1
  117. package/src/core/islands/main.ts +7 -0
  118. package/src/core/network/types.ts +2 -2
  119. package/src/core/run-app.tsx +11 -4
  120. package/src/core/static/__tests__/files.test.ts +195 -1
  121. package/src/core/static/files.ts +39 -9
  122. package/src/core/wasm/bridge.ts +1 -1
  123. package/src/core/websocket/useMarimoKernelConnection.tsx +5 -15
  124. package/src/plugins/core/registerReactComponent.tsx +9 -1
  125. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +164 -0
  126. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +93 -168
  127. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +37 -123
  128. package/src/plugins/impl/anywidget/__tests__/model.test.ts +128 -122
  129. package/src/{utils/__tests__/data-views.test.ts → plugins/impl/anywidget/__tests__/serialization.test.ts} +42 -96
  130. package/src/plugins/impl/anywidget/model.ts +348 -223
  131. package/src/plugins/impl/anywidget/schemas.ts +32 -0
  132. package/src/{utils/data-views.ts → plugins/impl/anywidget/serialization.ts} +13 -36
  133. package/src/plugins/impl/anywidget/types.ts +27 -0
  134. package/src/plugins/impl/chat/chat-ui.tsx +22 -20
  135. package/src/utils/Deferred.ts +21 -0
  136. package/src/utils/__tests__/blob.test.ts +3 -3
  137. package/src/utils/__tests__/id-tree.test.ts +22 -7
  138. package/src/utils/__tests__/mime-types.test.ts +8 -10
  139. package/src/utils/__tests__/url-parser.test.ts +22 -0
  140. package/src/utils/blob.ts +14 -27
  141. package/src/utils/id-tree.tsx +11 -19
  142. package/src/utils/json/base64.ts +38 -8
  143. package/src/utils/mime-types.ts +5 -5
  144. package/src/utils/url-parser.ts +1 -1
  145. package/dist/assets/__vite-browser-external-DRa9CT_O.js +0 -1
  146. package/dist/assets/worker-SqntmiwV.js +0 -73
  147. package/dist/mermaid-4DMBBIKO-o3xNphpD.js +0 -6
@@ -7,14 +7,14 @@ import type { ReactCodeMirrorRef } from "@uiw/react-codemirror";
7
7
  import { DefaultChatTransport, type FileUIPart, type TextUIPart } from "ai";
8
8
  import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai";
9
9
  import {
10
- AtSignIcon,
11
10
  BotMessageSquareIcon,
11
+ HatGlasses,
12
12
  Loader2,
13
- PaperclipIcon,
13
+ type LucideIcon,
14
+ MessageCircleIcon,
15
+ NotebookText,
14
16
  PlusIcon,
15
- SendIcon,
16
17
  SettingsIcon,
17
- SquareIcon,
18
18
  } from "lucide-react";
19
19
  import { memo, useEffect, useRef, useState } from "react";
20
20
  import useEvent from "react-use-event-hook";
@@ -29,7 +29,7 @@ import {
29
29
  } from "@/components/ui/select";
30
30
  import { replaceMessagesInChat } from "@/core/ai/chat-utils";
31
31
  import { useModelChange } from "@/core/ai/config";
32
- import { AiModelId, type ProviderId } from "@/core/ai/ids/ids";
32
+ import { AiModelId } from "@/core/ai/ids/ids";
33
33
  import { useStagedAICellsActions } from "@/core/ai/staged-cells";
34
34
  import {
35
35
  activeChatAtom,
@@ -45,7 +45,6 @@ import {
45
45
  import { useCellActions } from "@/core/cells/cells";
46
46
  import { aiAtom, aiEnabledAtom } from "@/core/config/config";
47
47
  import { DEFAULT_AI_MODEL } from "@/core/config/config-schema";
48
- import { FeatureFlagged } from "@/core/config/feature-flag";
49
48
  import { useRequestClient } from "@/core/network/requests";
50
49
  import { useRuntimeManager } from "@/core/runtime/config";
51
50
  import { ErrorBanner } from "@/plugins/impl/common/error-banner";
@@ -61,10 +60,14 @@ import {
61
60
  import { PanelEmptyState } from "../editor/chrome/panels/empty-state";
62
61
  import { CopyClipboardIcon } from "../icons/copy-icon";
63
62
  import { MCPStatusIndicator } from "../mcp/mcp-status-indicator";
64
- import { Input } from "../ui/input";
65
63
  import { Tooltip, TooltipProvider } from "../ui/tooltip";
66
- import { toast } from "../ui/use-toast";
67
- import { AttachmentRenderer, FileAttachmentPill } from "./chat-components";
64
+ import {
65
+ AddContextButton,
66
+ AttachFileButton,
67
+ AttachmentRenderer,
68
+ FileAttachmentPill,
69
+ SendButton,
70
+ } from "./chat-components";
68
71
  import { renderUIMessage } from "./chat-display";
69
72
  import { ChatHistoryPopover } from "./chat-history-popover";
70
73
  import {
@@ -74,21 +77,13 @@ import {
74
77
  handleToolCall,
75
78
  hasPendingToolCalls,
76
79
  isLastMessageReasoning,
80
+ PROVIDERS_THAT_SUPPORT_ATTACHMENTS,
81
+ useFileState,
77
82
  } from "./chat-utils";
78
83
 
79
84
  // Default mode for the AI
80
85
  const DEFAULT_MODE = "manual";
81
86
 
82
- // We need to modify the backend to support attachments for other providers
83
- // And other types
84
- const PROVIDERS_THAT_SUPPORT_ATTACHMENTS = new Set<ProviderId>([
85
- "openai",
86
- "google",
87
- "anthropic",
88
- ]);
89
- const SUPPORTED_ATTACHMENT_TYPES = ["image/*", "text/*"];
90
- const MAX_ATTACHMENT_SIZE = 1024 * 1024 * 50; // 50MB
91
-
92
87
  interface ChatHeaderProps {
93
88
  onNewChat: () => void;
94
89
  activeChatId: ChatId | undefined;
@@ -238,58 +233,72 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
238
233
  value: CopilotMode;
239
234
  label: string;
240
235
  subtitle: string;
236
+ Icon: LucideIcon;
241
237
  }[] = [
238
+ {
239
+ value: "manual",
240
+ label: "Manual",
241
+ subtitle: "Pure chat, no tool usage",
242
+ Icon: MessageCircleIcon,
243
+ },
242
244
  {
243
245
  value: "ask",
244
246
  label: "Ask",
245
247
  subtitle:
246
248
  "Use AI with access to read-only tools like documentation search",
247
- },
248
- {
249
- value: "manual",
250
- label: "Manual",
251
- subtitle: "Pure chat, no tool usage",
249
+ Icon: NotebookText,
252
250
  },
253
251
  {
254
252
  value: "agent",
255
253
  label: "Agent (beta)",
256
254
  subtitle: "Use AI with access to read and write tools",
255
+ Icon: HatGlasses,
257
256
  },
258
257
  ];
259
258
 
260
259
  const isAttachmentSupported =
261
260
  PROVIDERS_THAT_SUPPORT_ATTACHMENTS.has(currentProvider);
262
261
 
262
+ const CurrentModeIcon = modeOptions.find(
263
+ (o) => o.value === currentMode,
264
+ )?.Icon;
265
+
263
266
  return (
264
267
  <TooltipProvider>
265
268
  <div className="px-3 py-2 border-t border-border/20 flex flex-row flex-wrap items-center justify-between gap-1">
266
269
  <div className="flex items-center gap-2">
267
- <FeatureFlagged feature="chat_modes">
268
- <Select value={currentMode} onValueChange={saveModeChange}>
269
- <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">
270
- {currentMode}
271
- </SelectTrigger>
272
- <SelectContent>
273
- <SelectGroup>
274
- <SelectLabel>AI Mode</SelectLabel>
275
- {modeOptions.map((option) => (
276
- <SelectItem
277
- key={option.value}
278
- value={option.value}
279
- className="text-xs"
280
- >
281
- <div className="flex flex-col">
282
- {option.label}
283
- <div className="text-muted-foreground text-xs pt-1 block">
270
+ <Select value={currentMode} onValueChange={saveModeChange}>
271
+ <SelectTrigger className="h-6 text-xs border-border shadow-none! ring-0! bg-muted hover:bg-muted/30 py-0 px-2 gap-1.5">
272
+ {CurrentModeIcon && <CurrentModeIcon className="h-3 w-3" />}
273
+ <span className="capitalize">{currentMode}</span>
274
+ </SelectTrigger>
275
+ <SelectContent>
276
+ <SelectGroup>
277
+ <SelectLabel className="text-xs uppercase tracking-wider text-muted-foreground/70 font-medium">
278
+ AI Mode
279
+ </SelectLabel>
280
+ {modeOptions.map((option) => (
281
+ <SelectItem
282
+ key={option.value}
283
+ value={option.value}
284
+ className="text-xs py-1"
285
+ >
286
+ <div className="flex items-start gap-2.5">
287
+ <span className="mt-1 text-muted-foreground">
288
+ <option.Icon className="h-3 w-3" />
289
+ </span>
290
+ <div className="flex flex-col gap-0.5">
291
+ <span className="font-semibold">{option.label}</span>
292
+ <span className="text-muted-foreground">
284
293
  {option.subtitle}
285
- </div>
294
+ </span>
286
295
  </div>
287
- </SelectItem>
288
- ))}
289
- </SelectGroup>
290
- </SelectContent>
291
- </Select>
292
- </FeatureFlagged>
296
+ </div>
297
+ </SelectItem>
298
+ ))}
299
+ </SelectGroup>
300
+ </SelectContent>
301
+ </Select>
293
302
  <AIModelDropdown
294
303
  placeholder="Model"
295
304
  triggerClassName="h-6 text-xs shadow-none! ring-0! bg-muted hover:bg-muted/30 rounded-sm"
@@ -299,60 +308,23 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
299
308
  />
300
309
  </div>
301
310
  <div className="flex flex-row">
302
- <Tooltip content="Add context">
303
- <Button
304
- variant="text"
305
- size="icon"
306
- onClick={onAddContext}
307
- disabled={isLoading}
308
- >
309
- <AtSignIcon className="h-3.5 w-3.5" />
310
- </Button>
311
- </Tooltip>
311
+ <AddContextButton
312
+ handleAddContext={onAddContext}
313
+ isLoading={isLoading}
314
+ />
312
315
  {isAttachmentSupported && (
313
- <>
314
- <Tooltip content="Attach a file">
315
- <Button
316
- variant="text"
317
- size="icon"
318
- className="cursor-pointer"
319
- onClick={() => fileInputRef.current?.click()}
320
- title="Attach a file"
321
- disabled={isLoading}
322
- >
323
- <PaperclipIcon className="h-3.5 w-3.5" />
324
- </Button>
325
- </Tooltip>
326
- <Input
327
- ref={fileInputRef}
328
- type="file"
329
- multiple={true}
330
- hidden={true}
331
- onChange={(event) => {
332
- if (event.target.files) {
333
- onAddFiles([...event.target.files]);
334
- }
335
- }}
336
- accept={SUPPORTED_ATTACHMENT_TYPES.join(",")}
337
- />
338
- </>
316
+ <AttachFileButton
317
+ fileInputRef={fileInputRef}
318
+ isLoading={isLoading}
319
+ onAddFiles={onAddFiles}
320
+ />
339
321
  )}
340
-
341
- <Tooltip content={isLoading ? "Stop" : "Submit"}>
342
- <Button
343
- variant="text"
344
- size="sm"
345
- className="h-6 w-6 p-0 hover:bg-muted/30 cursor-pointer"
346
- onClick={isLoading ? onStop : onSendClick}
347
- disabled={isLoading ? false : isEmpty}
348
- >
349
- {isLoading ? (
350
- <SquareIcon className="h-3 w-3 fill-current" />
351
- ) : (
352
- <SendIcon className="h-3 w-3" />
353
- )}
354
- </Button>
355
- </Tooltip>
322
+ <SendButton
323
+ isLoading={isLoading}
324
+ onStop={onStop}
325
+ onSendClick={onSendClick}
326
+ isEmpty={isEmpty}
327
+ />
356
328
  </div>
357
329
  </div>
358
330
  </TooltipProvider>
@@ -456,7 +428,7 @@ const ChatPanelBody = () => {
456
428
  const [activeChat, setActiveChat] = useAtom(activeChatAtom);
457
429
  const [input, setInput] = useState("");
458
430
  const [newThreadInput, setNewThreadInput] = useState("");
459
- const [files, setFiles] = useState<File[]>();
431
+ const { files, addFiles, clearFiles, removeFile } = useFileState();
460
432
  const newThreadInputRef = useRef<ReactCodeMirrorRef>(null);
461
433
  const newMessageInputRef = useRef<ReactCodeMirrorRef>(null);
462
434
  const scrollContainerRef = useRef<HTMLDivElement>(null);
@@ -545,33 +517,6 @@ const ChatPanelBody = () => {
545
517
  },
546
518
  });
547
519
 
548
- const onAddFiles = useEvent((files: File[]) => {
549
- if (files.length === 0) {
550
- return;
551
- }
552
-
553
- let fileSize = 0;
554
- for (const file of files) {
555
- fileSize += file.size;
556
- }
557
-
558
- if (fileSize > MAX_ATTACHMENT_SIZE) {
559
- toast({
560
- title: "File size exceeds 50MB limit",
561
- description: "Please remove some files and try again.",
562
- });
563
- return;
564
- }
565
-
566
- setFiles((prev) => [...(prev ?? []), ...files]);
567
- });
568
-
569
- const removeFile = useEvent((file: File) => {
570
- if (files) {
571
- setFiles(files.filter((f) => f !== file));
572
- }
573
- });
574
-
575
520
  const isLoading = status === "submitted" || status === "streaming";
576
521
 
577
522
  // Check if we're currently streaming reasoning in the latest message
@@ -631,7 +576,7 @@ const ChatPanelBody = () => {
631
576
  ...(fileParts ?? []),
632
577
  ],
633
578
  });
634
- setFiles(undefined);
579
+ clearFiles();
635
580
  setInput("");
636
581
  };
637
582
 
@@ -639,7 +584,7 @@ const ChatPanelBody = () => {
639
584
  setActiveChat(null);
640
585
  setInput("");
641
586
  setNewThreadInput("");
642
- setFiles(undefined);
587
+ clearFiles();
643
588
  });
644
589
 
645
590
  const handleMessageEdit = useEvent((index: number, newValue: string) => {
@@ -670,7 +615,7 @@ const ChatPanelBody = () => {
670
615
  files: fileParts,
671
616
  });
672
617
  setInput("");
673
- setFiles(undefined);
618
+ clearFiles();
674
619
  },
675
620
  );
676
621
 
@@ -703,7 +648,7 @@ const ChatPanelBody = () => {
703
648
  isLoading={isLoading}
704
649
  onStop={stop}
705
650
  fileInputRef={fileInputRef}
706
- onAddFiles={onAddFiles}
651
+ onAddFiles={addFiles}
707
652
  onClose={handleOnCloseThread}
708
653
  />
709
654
  ) : (
@@ -716,7 +661,7 @@ const ChatPanelBody = () => {
716
661
  onStop={stop}
717
662
  onClose={() => newMessageInputRef.current?.editor?.blur()}
718
663
  fileInputRef={fileInputRef}
719
- onAddFiles={onAddFiles}
664
+ onAddFiles={addFiles}
720
665
  />
721
666
  );
722
667
 
@@ -777,7 +722,7 @@ const ChatPanelBody = () => {
777
722
 
778
723
  {error && (
779
724
  <div className="flex items-center justify-center space-x-2 mb-4">
780
- <ErrorBanner error={error} />
725
+ <ErrorBanner error={error || new Error("Unknown error")} />
781
726
  <Button variant="outline" size="sm" onClick={handleReload}>
782
727
  Retry
783
728
  </Button>
@@ -2,6 +2,9 @@
2
2
 
3
3
  import type { components } from "@marimo-team/marimo-api";
4
4
  import type { FileUIPart, ToolUIPart, UIMessage } from "ai";
5
+ import { useState } from "react";
6
+ import useEvent from "react-use-event-hook";
7
+ import type { ProviderId } from "@/core/ai/ids/ids";
5
8
  import type { ToolNotebookContext } from "@/core/ai/tools/base";
6
9
  import { FRONTEND_TOOL_REGISTRY } from "@/core/ai/tools/registry";
7
10
  import type {
@@ -11,6 +14,17 @@ import type {
11
14
  import { blobToString } from "@/utils/fileToBase64";
12
15
  import { Logger } from "@/utils/Logger";
13
16
  import { getAICompletionBodyWithAttachments } from "../editor/ai/completion-utils";
17
+ import { toast } from "../ui/use-toast";
18
+
19
+ // We need to modify the backend to support attachments for other providers
20
+ // And other types
21
+ export const PROVIDERS_THAT_SUPPORT_ATTACHMENTS = new Set<ProviderId>([
22
+ "openai",
23
+ "google",
24
+ "anthropic",
25
+ ]);
26
+ export const SUPPORTED_ATTACHMENT_TYPES = ["image/*", "text/*"];
27
+ const MAX_ATTACHMENT_SIZE = 1024 * 1024 * 50; // 50MB
14
28
 
15
29
  export function generateChatTitle(message: string): string {
16
30
  return message.length > 50 ? `${message.slice(0, 50)}...` : message;
@@ -198,3 +212,31 @@ export function hasPendingToolCalls(messages: UIMessage[]): boolean {
198
212
  // Only auto-send if we have completed tool calls and there is no reply yet
199
213
  return allToolCallsCompleted && !hasTextContent;
200
214
  }
215
+
216
+ export function useFileState() {
217
+ const [files, setFiles] = useState<File[]>([]);
218
+
219
+ const addFiles = useEvent((newFiles: File[]) => {
220
+ if (newFiles.length === 0) {
221
+ return;
222
+ }
223
+
224
+ const totalSize = newFiles.reduce((size, file) => size + file.size, 0);
225
+ if (totalSize > MAX_ATTACHMENT_SIZE) {
226
+ toast({
227
+ title: "File size exceeded",
228
+ description: "Attachments must be under 50 MB",
229
+ variant: "danger",
230
+ });
231
+ return;
232
+ }
233
+
234
+ setFiles((prev) => [...prev, ...newFiles]);
235
+ });
236
+
237
+ const clearFiles = () => setFiles([]);
238
+ const removeFile = (fileToRemove: File) =>
239
+ setFiles((prev) => prev.filter((f) => f !== fileToRemove));
240
+
241
+ return { files, addFiles, clearFiles, removeFile };
242
+ }
@@ -1,6 +1,11 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import type { ColumnDef, RowSelectionState } from "@tanstack/react-table";
3
- import { render, screen } from "@testing-library/react";
2
+ import type {
3
+ ColumnDef,
4
+ PaginationState,
5
+ RowSelectionState,
6
+ SortingState,
7
+ } from "@tanstack/react-table";
8
+ import { render, screen, within } from "@testing-library/react";
4
9
  import { describe, expect, it, vi } from "vitest";
5
10
  import { TooltipProvider } from "@/components/ui/tooltip";
6
11
  import { DataTable } from "../data-table";
@@ -95,4 +100,91 @@ describe("DataTable", () => {
95
100
  expect(rows[1]).toHaveAttribute("title", "Michael Scott");
96
101
  expect(rows[2]).toHaveAttribute("title", "Jim Halpert");
97
102
  });
103
+
104
+ it("should display updated data after rerender with manual sorting and pagination", () => {
105
+ // Simulates the bug from issue #8023:
106
+ // When a user sorts a table, rows that moved from page 2 to page 1
107
+ // don't visually refresh after the underlying data is updated.
108
+
109
+ interface RowData {
110
+ id: number;
111
+ status: string;
112
+ value: number;
113
+ }
114
+
115
+ // Initial data: 4 rows, page_size=3
116
+ const initialData: RowData[] = [
117
+ { id: 4, status: "pending", value: 40 },
118
+ { id: 3, status: "pending", value: 30 },
119
+ { id: 2, status: "pending", value: 20 },
120
+ ];
121
+
122
+ const columns: ColumnDef<RowData>[] = [
123
+ { id: "id", accessorFn: (row) => row.id, header: "id" },
124
+ { id: "status", accessorFn: (row) => row.status, header: "status" },
125
+ { id: "value", accessorFn: (row) => row.value, header: "value" },
126
+ ];
127
+
128
+ // Simulate sorted state (value descending) - manual sorting means
129
+ // data comes pre-sorted from backend
130
+ const sorting: SortingState = [{ id: "value", desc: true }];
131
+ const setSorting = vi.fn();
132
+
133
+ const paginationState: PaginationState = { pageIndex: 0, pageSize: 3 };
134
+ const setPaginationState = vi.fn();
135
+
136
+ const commonProps = {
137
+ columns,
138
+ selection: null as "single" | "multi" | null,
139
+ totalRows: 4,
140
+ totalColumns: 3,
141
+ pagination: true,
142
+ manualPagination: true,
143
+ paginationState,
144
+ setPaginationState,
145
+ manualSorting: true,
146
+ sorting,
147
+ setSorting,
148
+ };
149
+
150
+ const { rerender } = render(
151
+ <TooltipProvider>
152
+ <DataTable {...commonProps} data={initialData} />
153
+ </TooltipProvider>,
154
+ );
155
+
156
+ // Verify initial data is displayed - look for "pending" in cells
157
+ const rows = screen.getAllByRole("row");
158
+ // Row 0 is header, rows 1-3 are data rows
159
+ expect(rows).toHaveLength(4); // 1 header + 3 data rows
160
+ // All rows should show "pending"
161
+ expect(within(rows[1]).getByText("pending")).toBeTruthy();
162
+ expect(within(rows[2]).getByText("pending")).toBeTruthy();
163
+ expect(within(rows[3]).getByText("pending")).toBeTruthy();
164
+
165
+ // Now simulate data update: row with id=4 is now "approved"
166
+ // Backend returns sorted data with the update applied
167
+ const updatedData: RowData[] = [
168
+ { id: 4, status: "approved", value: 40 },
169
+ { id: 3, status: "pending", value: 30 },
170
+ { id: 2, status: "pending", value: 20 },
171
+ ];
172
+
173
+ // Rerender with updated data (same sorting, same pagination)
174
+ rerender(
175
+ <TooltipProvider>
176
+ <DataTable {...commonProps} data={updatedData} />
177
+ </TooltipProvider>,
178
+ );
179
+
180
+ // BUG: The row should show "approved" but might show stale "pending"
181
+ const updatedRows = screen.getAllByRole("row");
182
+ expect(updatedRows).toHaveLength(4);
183
+
184
+ // The first data row (id=4) should now show "approved"
185
+ expect(within(updatedRows[1]).getByText("approved")).toBeTruthy();
186
+ // Other rows should still show "pending"
187
+ expect(within(updatedRows[2]).getByText("pending")).toBeTruthy();
188
+ expect(within(updatedRows[3]).getByText("pending")).toBeTruthy();
189
+ });
98
190
  });
@@ -43,6 +43,7 @@ import type { CellData } from "@/core/cells/types";
43
43
  import { formatEditorViews } from "@/core/codemirror/format";
44
44
  import { toggleToLanguage } from "@/core/codemirror/language/commands";
45
45
  import { switchLanguage } from "@/core/codemirror/language/extension";
46
+ import { MARKDOWN_INITIAL_HIDE_CODE } from "@/core/codemirror/language/languages/markdown";
46
47
  import {
47
48
  aiEnabledAtom,
48
49
  appWidthAtom,
@@ -85,6 +86,7 @@ export function useCellActionButtons({ cell, closePopover }: Props) {
85
86
  sendToBottom,
86
87
  addColumnBreakpoint,
87
88
  clearCellOutput,
89
+ markUntouched,
88
90
  } = useCellActions();
89
91
  const splitCell = useSplitCellCallback();
90
92
  const runCell = useRunCell(cell?.cellId);
@@ -209,7 +211,7 @@ export function useCellActionButtons({ cell, closePopover }: Props) {
209
211
  icon: <MarkdownIcon />,
210
212
  label: "Convert to Markdown",
211
213
  hotkey: "cell.viewAsMarkdown",
212
- handle: () => {
214
+ handle: async () => {
213
215
  const editorView = getEditorView();
214
216
  if (!editorView) {
215
217
  return;
@@ -219,6 +221,17 @@ export function useCellActionButtons({ cell, closePopover }: Props) {
219
221
  language: "markdown",
220
222
  keepCodeAsIs: false,
221
223
  });
224
+ // Code stays visible until the user blurs the cell
225
+ if (!config.hide_code && MARKDOWN_INITIAL_HIDE_CODE) {
226
+ await saveCellConfig({
227
+ configs: { [cellId]: { hide_code: MARKDOWN_INITIAL_HIDE_CODE } },
228
+ });
229
+ updateCellConfig({
230
+ cellId,
231
+ config: { hide_code: MARKDOWN_INITIAL_HIDE_CODE },
232
+ });
233
+ markUntouched({ cellId });
234
+ }
222
235
  },
223
236
  hidden: isSetupCell,
224
237
  },