@marimo-team/islands 0.23.12-dev2 → 0.23.12-dev21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/{ConnectedDataExplorerComponent-WqG-xX4l.js → ConnectedDataExplorerComponent-Du3_nUzI.js} +13 -13
  2. package/dist/{ErrorBoundary-BNx_OSVo.js → ErrorBoundary-DE6tzZf-.js} +2 -2
  3. package/dist/{any-language-editor-rPSlOll9.js → any-language-editor-DN1R-1KZ.js} +5 -5
  4. package/dist/{button-vQhauTmO.js → button-BacYv-bE.js} +7 -1
  5. package/dist/{capabilities-BEHzIS99.js → capabilities-D_4LYhSU.js} +1 -1
  6. package/dist/{chat-ui-k2kqhCv5.js → chat-ui-CsPewo4h.js} +16 -16
  7. package/dist/{check-nrzHDi45.js → check-C9OoNtR4.js} +1 -1
  8. package/dist/{code-visibility-DZ_6U5hT.js → code-visibility-DBnAQPtB.js} +664 -663
  9. package/dist/{copy-UhDed7D4.js → copy-COam1EG7.js} +2 -2
  10. package/dist/{dist-DYGLrbYQ.js → dist--2Bqjvs0.js} +2 -2
  11. package/dist/{error-banner-BHAkVFc2.js → error-banner-DFPfz_Qf.js} +2 -2
  12. package/dist/{esm-Bqu9AE2K.js → esm-M837UxV5.js} +1 -1
  13. package/dist/{extends-9Yl5BEcg.js → extends-9MVIxxRo.js} +4 -4
  14. package/dist/{formats-BV4bOfMI.js → formats-d6MhLuQ9.js} +4 -4
  15. package/dist/{glide-data-editor-BDTq6YUb.js → glide-data-editor-DkzAInWG.js} +9 -9
  16. package/dist/{html-to-image-C86pQALH.js → html-to-image-DXwLcQ6l.js} +95 -88
  17. package/dist/{input-AKkGXdyV.js → input-CbEz_aj_.js} +6 -6
  18. package/dist/{label-E3ZJXHu8.js → label-WfTSU8L4.js} +2 -2
  19. package/dist/{loader-YPuQvn1Y.js → loader-Boph2xIS.js} +1 -1
  20. package/dist/main.js +1753 -1626
  21. package/dist/{mermaid-QFAR9YgY.js → mermaid-CJW9vIyO.js} +5 -5
  22. package/dist/{process-output-nNw4OpSj.js → process-output-C6_e1pT_.js} +3 -3
  23. package/dist/{reveal-component-BxDb5eK0.js → reveal-component-DIzKQ1NS.js} +11 -11
  24. package/dist/{spec-B45_YCNI.js → spec-Bv-XlYiv.js} +4 -4
  25. package/dist/{strings-Cq2s9_EQ.js → strings-Dq_j3Rxw.js} +4 -4
  26. package/dist/style.css +2 -2
  27. package/dist/{swiper-component-BNa_4kh2.js → swiper-component-5HoSsPi1.js} +2 -2
  28. package/dist/{toDate-Do1xRzAo.js → toDate-D-l5s8nn.js} +3 -3
  29. package/dist/{tooltip-Bz3OAwrU.js → tooltip-Czds6Qr8.js} +3 -3
  30. package/dist/{types-D8gEGs4R.js → types-C2Ir191_.js} +1 -1
  31. package/dist/{useAsyncData-CL3o2p4i.js → useAsyncData-1Dhzjfwf.js} +1 -1
  32. package/dist/{useDateFormatter-BC6iSz9g.js → useDateFormatter-CMnRuVmN.js} +2 -2
  33. package/dist/{useDeepCompareMemoize-BPx2MuOK.js → useDeepCompareMemoize-CDWT3BDz.js} +1 -1
  34. package/dist/{useIframeCapabilities-C6Ta3EyP.js → useIframeCapabilities-DWIYvDh7.js} +1 -1
  35. package/dist/{useLifecycle-C3Ec71q0.js → useLifecycle-AHlswLw-.js} +3 -3
  36. package/dist/{useTheme-ZhT6uIu3.js → useTheme-BrYvK-_A.js} +2 -2
  37. package/dist/{vega-component-C3AWYGAL.js → vega-component-Pk6lyc_a.js} +10 -10
  38. package/dist/{zod-DXqkaI_w.js → zod-CijjQh4u.js} +1 -1
  39. package/package.json +3 -3
  40. package/src/components/ai/display-helpers.tsx +5 -5
  41. package/src/components/app-config/ai-config.tsx +5 -5
  42. package/src/components/app-config/mcp-config.tsx +3 -3
  43. package/src/components/chat/acp/agent-panel.tsx +3 -3
  44. package/src/components/chat/acp/blocks.tsx +36 -38
  45. package/src/components/chat/acp/common.tsx +12 -16
  46. package/src/components/chat/acp/scroll-to-bottom-button.tsx +1 -1
  47. package/src/components/chat/acp/session-tabs.tsx +2 -2
  48. package/src/components/chat/chat-history-popover.tsx +1 -1
  49. package/src/components/chat/chat-panel.tsx +47 -23
  50. package/src/components/data-table/TableBottomBar.tsx +4 -1
  51. package/src/components/data-table/columns.tsx +2 -2
  52. package/src/components/data-table/data-table.tsx +26 -17
  53. package/src/components/data-table/filter-pill-editor.tsx +1 -1
  54. package/src/components/dependency-graph/minimap-content.tsx +1 -1
  55. package/src/components/editor/RecoveryButton.tsx +1 -1
  56. package/src/components/editor/actions/pair-with-agent-modal.tsx +2 -2
  57. package/src/components/editor/actions/useNotebookActions.tsx +4 -4
  58. package/src/components/editor/ai/__tests__/completion-utils.test.ts +138 -2
  59. package/src/components/editor/ai/ai-completion-editor.tsx +1 -1
  60. package/src/components/editor/ai/completion-utils.ts +124 -21
  61. package/src/components/editor/cell/CreateCellButton.tsx +1 -1
  62. package/src/components/editor/chrome/panels/empty-state.tsx +1 -1
  63. package/src/components/editor/chrome/panels/outline/floating-outline.tsx +1 -1
  64. package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +1 -1
  65. package/src/components/editor/columns/cell-column.tsx +1 -1
  66. package/src/components/editor/columns/sortable-column.tsx +2 -2
  67. package/src/components/editor/output/MarimoErrorOutput.tsx +1 -1
  68. package/src/components/editor/output/TextOutput.tsx +2 -2
  69. package/src/components/home/components.tsx +4 -4
  70. package/src/components/icons/github.tsx +21 -0
  71. package/src/components/icons/youtube.tsx +21 -0
  72. package/src/components/slides/minimap.tsx +2 -2
  73. package/src/components/slides/reveal-component.tsx +1 -1
  74. package/src/components/storage/components.tsx +3 -7
  75. package/src/components/ui/alert.tsx +1 -1
  76. package/src/components/ui/command.tsx +2 -2
  77. package/src/components/ui/reorderable-list.tsx +1 -1
  78. package/src/components/ui/table.tsx +2 -5
  79. package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +67 -0
  80. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +47 -0
  81. package/src/core/codemirror/go-to-definition/commands.ts +47 -30
  82. package/src/core/codemirror/go-to-definition/utils.ts +0 -1
  83. package/src/core/codemirror/language/languages/sql/renderers.tsx +60 -68
  84. package/src/core/codemirror/reactive-references/__tests__/analyzer.test.ts +54 -0
  85. package/src/core/codemirror/reactive-references/analyzer.ts +44 -35
  86. package/src/core/hotkeys/hotkeys.ts +1 -0
  87. package/src/core/islands/__tests__/bridge.test.ts +25 -0
  88. package/src/core/islands/__tests__/parse.test.ts +585 -1
  89. package/src/core/islands/__tests__/test-utils.tsx +10 -1
  90. package/src/core/islands/bridge.ts +6 -1
  91. package/src/core/islands/constants.ts +2 -0
  92. package/src/core/islands/parse.ts +293 -13
  93. package/src/plugins/impl/DataTablePlugin.tsx +20 -1
  94. package/src/plugins/impl/FileBrowserPlugin.tsx +165 -74
  95. package/src/plugins/impl/MatrixPlugin.tsx +2 -2
  96. package/src/plugins/impl/TabsPlugin.tsx +1 -1
  97. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +141 -1
  98. package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +314 -0
  99. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +4 -1
  100. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +34 -0
  101. package/src/plugins/impl/anywidget/__tests__/model.test.ts +19 -0
  102. package/src/plugins/impl/anywidget/model.ts +15 -0
  103. package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +1 -1
  104. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +155 -98
  105. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +154 -1
  106. package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +10 -0
@@ -67,6 +67,8 @@ import { PromptInput } from "../editor/ai/add-cell-with-ai";
67
67
  import {
68
68
  addContextCompletion,
69
69
  CONTEXT_TRIGGER,
70
+ isContextAttachment,
71
+ resolveChatContext,
70
72
  } from "../editor/ai/completion-utils";
71
73
  import { PanelEmptyState } from "../editor/chrome/panels/empty-state";
72
74
  import { CopyClipboardIcon } from "../icons/copy-icon";
@@ -83,7 +85,6 @@ import {
83
85
  import { renderUIMessage } from "./chat-display";
84
86
  import { ChatHistoryPopover } from "./chat-history-popover";
85
87
  import {
86
- buildCompletionRequestBody,
87
88
  convertToFileUIPart,
88
89
  generateChatTitle,
89
90
  handleToolCall,
@@ -92,6 +93,7 @@ import {
92
93
  PROVIDERS_THAT_SUPPORT_ATTACHMENTS,
93
94
  useFileState,
94
95
  } from "./chat-utils";
96
+ import { getCodes } from "@/core/codemirror/copilot/getCodes";
95
97
 
96
98
  // Default mode for the AI
97
99
  const DEFAULT_MODE = "manual";
@@ -281,16 +283,13 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
281
283
  subtitle: "AI with access to read and write tools",
282
284
  Icon: HatGlasses,
283
285
  },
284
- ];
285
-
286
- if (import.meta.env.DEV) {
287
- modeOptions.push({
286
+ {
288
287
  value: "code_mode",
289
288
  label: "Code Mode (experimental)",
290
289
  subtitle: "AI with access to the notebook's kernel. Use with caution.",
291
290
  Icon: CodeIcon,
292
- });
293
- }
291
+ },
292
+ ];
294
293
 
295
294
  const isAttachmentSupported =
296
295
  PROVIDERS_THAT_SUPPORT_ATTACHMENTS.has(currentProvider);
@@ -538,9 +537,10 @@ const ChatPanelBody = () => {
538
537
  );
539
538
  }
540
539
 
541
- const completionBody = await buildCompletionRequestBody(
542
- options.messages,
543
- );
540
+ const completionBody = {
541
+ uiMessages: options.messages,
542
+ includeOtherCode: getCodes(""),
543
+ };
544
544
 
545
545
  // Call this here to ensure the value is not stale
546
546
  const chatMode = store.get(aiAtom)?.mode || DEFAULT_MODE;
@@ -629,6 +629,8 @@ const ChatPanelBody = () => {
629
629
  initialAttachments && initialAttachments.length > 0
630
630
  ? await convertToFileUIPart(initialAttachments)
631
631
  : undefined;
632
+ const { contextPart, attachments } =
633
+ await resolveChatContext(initialMessage);
632
634
 
633
635
  // Trigger AI conversation with append
634
636
  sendMessage({
@@ -638,7 +640,9 @@ const ChatPanelBody = () => {
638
640
  type: "text" as const,
639
641
  text: initialMessage,
640
642
  },
643
+ ...(contextPart ? [contextPart] : []),
641
644
  ...(fileParts ?? []),
645
+ ...attachments,
642
646
  ],
643
647
  });
644
648
  clearFiles();
@@ -656,17 +660,31 @@ const ChatPanelBody = () => {
656
660
  openModal(<PairWithAgentModal onClose={closeModal} />);
657
661
  });
658
662
 
659
- const handleMessageEdit = useEvent((index: number, newValue: string) => {
660
- const editedMessage = messages[index];
661
- const fileParts = editedMessage.parts?.filter((p) => p.type === "file");
662
-
663
- const messageId = editedMessage.id;
664
- sendMessage({
665
- messageId: messageId, // replace the message
666
- role: "user",
667
- parts: [{ type: "text", text: newValue }, ...fileParts],
668
- });
669
- });
663
+ const handleMessageEdit = useEvent(
664
+ async (index: number, newValue: string) => {
665
+ const editedMessage = messages[index];
666
+ // Keep the user's own uploaded files, but drop the previous @-context
667
+ // snapshot (data part + its attachments) so we can re-resolve a fresh,
668
+ // point-in-time snapshot from the edited text below.
669
+ const userFileParts =
670
+ editedMessage.parts?.filter(
671
+ (p) => p.type === "file" && !isContextAttachment(p),
672
+ ) ?? [];
673
+ const { contextPart, attachments } = await resolveChatContext(newValue);
674
+
675
+ const messageId = editedMessage.id;
676
+ sendMessage({
677
+ messageId: messageId, // replace the message
678
+ role: "user",
679
+ parts: [
680
+ { type: "text", text: newValue },
681
+ ...(contextPart ? [contextPart] : []),
682
+ ...userFileParts,
683
+ ...attachments,
684
+ ],
685
+ });
686
+ },
687
+ );
670
688
 
671
689
  const handleChatInputSubmit = useEvent(
672
690
  async (e: KeyboardEvent | undefined, newValue: string): Promise<void> => {
@@ -677,11 +695,17 @@ const ChatPanelBody = () => {
677
695
  storePrompt(newMessageInputRef.current.view);
678
696
  }
679
697
  const fileParts = files ? await convertToFileUIPart(files) : undefined;
698
+ const { contextPart, attachments } = await resolveChatContext(newValue);
680
699
 
681
700
  e?.preventDefault();
682
701
  sendMessage({
683
- text: newValue,
684
- files: fileParts,
702
+ role: "user",
703
+ parts: [
704
+ { type: "text", text: newValue },
705
+ ...(contextPart ? [contextPart] : []),
706
+ ...(fileParts ?? []),
707
+ ...attachments,
708
+ ],
685
709
  });
686
710
  setInput("");
687
711
  clearFiles();
@@ -3,6 +3,7 @@
3
3
 
4
4
  import type { RowSelectionState, Table } from "@tanstack/react-table";
5
5
  import { useLocale } from "react-aria";
6
+ import { isStaticNotebook } from "@/core/static/static-state";
6
7
  import type { GetRowIds } from "@/plugins/impl/DataTablePlugin";
7
8
  import { cn } from "@/utils/cn";
8
9
  import { Events } from "@/utils/events";
@@ -40,6 +41,8 @@ export const TableBottomBar = <TData,>({
40
41
  className,
41
42
  }: TableBottomBarProps<TData>) => {
42
43
  const { locale } = useLocale();
44
+ // Pagination fetches each page via a kernel RPC, absent in static exports.
45
+ const isStatic = isStaticNotebook();
43
46
  const handleSelectAllRows = (value: boolean) => {
44
47
  if (!onRowSelectionChange) {
45
48
  return;
@@ -171,7 +174,7 @@ export const TableBottomBar = <TData,>({
171
174
  <CellSelectionStats table={table} className="lg:hidden" />
172
175
  </div>
173
176
  <div className="ml-auto lg:ml-0 lg:justify-self-center flex items-center shrink-0">
174
- {pagination && (
177
+ {pagination && !isStatic && (
175
178
  <DataTablePagination
176
179
  table={table}
177
180
  tableLoading={tableLoading}
@@ -503,7 +503,7 @@ function getCellStyleClass({
503
503
  isNumeric && "tabular-nums",
504
504
  justify === "center" && "text-center",
505
505
  justify === "right" && "text-right",
506
- wrapped && `${COLUMN_WRAPPING_STYLES} break-words`,
506
+ wrapped && `${COLUMN_WRAPPING_STYLES} wrap-break-word`,
507
507
  );
508
508
  }
509
509
 
@@ -651,7 +651,7 @@ export function renderCellValue<TData, TValue>({
651
651
  selectCell={selectCell}
652
652
  rawStringValue={stringValue}
653
653
  edges={{ leading, trailing }}
654
- contentClassName="max-h-64 overflow-auto whitespace-pre-wrap break-words text-sm w-96"
654
+ contentClassName="max-h-64 overflow-auto whitespace-pre-wrap wrap-break-word text-sm w-96"
655
655
  buttonText="X"
656
656
  wrapped={isWrapped}
657
657
  >
@@ -25,6 +25,7 @@ import { useLocale } from "react-aria";
25
25
 
26
26
  import { Button } from "@/components/ui/button";
27
27
  import { Table } from "@/components/ui/table";
28
+ import { isStaticNotebook } from "@/core/static/static-state";
28
29
  import { Banner } from "@/plugins/impl/common/error-banner";
29
30
  import type {
30
31
  CalculateTopKRows,
@@ -182,6 +183,11 @@ const DataTableInternal = <TData,>({
182
183
  onViewedRowChange,
183
184
  renderTableExplorerPanel,
184
185
  }: DataTableProps<TData>) => {
186
+ // The top bar's controls (search, filters, column explorer, chart builder)
187
+ // all require a live kernel, which static exports don't have.
188
+ const isStatic = isStaticNotebook();
189
+ const showTableTopBar = !isStatic;
190
+
185
191
  const [showLoadingBar, setShowLoadingBar] = React.useState<boolean>(false);
186
192
  const { locale } = useLocale();
187
193
 
@@ -267,11 +273,12 @@ const DataTableInternal = <TData,>({
267
273
  }
268
274
  : {}),
269
275
  manualSorting: manualSorting,
276
+ enableSorting: !isStatic,
270
277
  enableMultiSort: true,
271
278
  getSortedRowModel: getSortedRowModel(),
272
279
  // filtering
273
280
  manualFiltering: true,
274
- enableColumnFilters: showFilters,
281
+ enableColumnFilters: showFilters && !isStatic,
275
282
  getFilteredRowModel: getFilteredRowModel(),
276
283
  onColumnFiltersChange: onFiltersChange,
277
284
  // selection
@@ -355,22 +362,24 @@ const DataTableInternal = <TData,>({
355
362
  part="table-wrapper"
356
363
  className={cn(className || "rounded-md border overflow-hidden")}
357
364
  >
358
- <TableTopBar
359
- table={table}
360
- showSearch={showSearch}
361
- searchQuery={searchQuery}
362
- onSearchQueryChange={onSearchQueryChange}
363
- reloading={reloading}
364
- showChartBuilder={showChartBuilder}
365
- isChartBuilderOpen={isChartBuilderOpen}
366
- toggleDisplayHeader={toggleDisplayHeader}
367
- showTableExplorer={showTableExplorer}
368
- togglePanel={togglePanel}
369
- isAnyPanelOpen={isAnyPanelOpen}
370
- downloadAs={downloadAs}
371
- sizeBytes={sizeBytes}
372
- sizeBytesIsLoading={sizeBytesIsLoading}
373
- />
365
+ {showTableTopBar && (
366
+ <TableTopBar
367
+ table={table}
368
+ showSearch={showSearch}
369
+ searchQuery={searchQuery}
370
+ onSearchQueryChange={onSearchQueryChange}
371
+ reloading={reloading}
372
+ showChartBuilder={showChartBuilder}
373
+ isChartBuilderOpen={isChartBuilderOpen}
374
+ toggleDisplayHeader={toggleDisplayHeader}
375
+ showTableExplorer={showTableExplorer}
376
+ togglePanel={togglePanel}
377
+ isAnyPanelOpen={isAnyPanelOpen}
378
+ downloadAs={downloadAs}
379
+ sizeBytes={sizeBytes}
380
+ sizeBytesIsLoading={sizeBytesIsLoading}
381
+ />
382
+ )}
374
383
  {allUserColumnsHidden && (
375
384
  <Banner className="mb-1 mx-2 rounded flex items-center justify-between">
376
385
  <span>All columns are hidden.</span>
@@ -435,7 +435,7 @@ const ValueSlot = <TData, TValue>({
435
435
  const v =
436
436
  value.kind === "multi-values" ? value : { kind: "multi-values" as const };
437
437
  return (
438
- <div className="min-w-[14rem] w-fit max-w-[24rem]">
438
+ <div className="min-w-56 w-fit max-w-[24rem]">
439
439
  <FilterByValuesPicker
440
440
  column={column}
441
441
  calculateTopKRows={calculateTopKRows}
@@ -107,7 +107,7 @@ const MinimapCell: React.FC<MinimapCellProps> = (props) => {
107
107
  <svg
108
108
  className={cn(
109
109
  "absolute overflow-visible top-[10.5px] left-[calc(var(--spacing-extra-small,8px)+31px)] pointer-events-none",
110
- isSelected ? "z-[1]" : "z-0",
110
+ isSelected ? "z-1" : "z-0",
111
111
  getTextColor({ cell, selectedCell }),
112
112
  )}
113
113
  width="1"
@@ -76,7 +76,7 @@ const RecoveryModal = (props: {
76
76
  Download unsaved changes?
77
77
  </DialogTitle>
78
78
  <DialogDescription
79
- className="markdown break-words"
79
+ className="markdown wrap-break-word"
80
80
  style={{ wordBreak: "break-word" }}
81
81
  >
82
82
  <div className="prose dark:prose-invert">
@@ -234,7 +234,7 @@ const CommandBlock: React.FC<{
234
234
  if (multiline) {
235
235
  return (
236
236
  <div className="relative rounded-md bg-muted">
237
- <pre className="max-h-64 overflow-auto whitespace-pre-wrap break-words px-3 py-2 pr-10 font-mono text-xs select-all">
237
+ <pre className="max-h-64 overflow-auto whitespace-pre-wrap wrap-break-word px-3 py-2 pr-10 font-mono text-xs select-all">
238
238
  {display ?? command}
239
239
  </pre>
240
240
  <Tooltip content="Copied!" open={copied}>
@@ -257,7 +257,7 @@ const CommandBlock: React.FC<{
257
257
 
258
258
  return (
259
259
  <div className="flex items-center gap-2 rounded-md bg-muted px-3 py-2 font-mono text-xs">
260
- <code className="flex-1 select-all break-words">
260
+ <code className="flex-1 select-all wrap-break-word">
261
261
  {display ?? command}
262
262
  </code>
263
263
  <Tooltip content="Copied!" open={copied}>
@@ -21,7 +21,6 @@ import {
21
21
  Files,
22
22
  FileTextIcon,
23
23
  FolderDownIcon,
24
- GithubIcon,
25
24
  GlobeIcon,
26
25
  HardDrive,
27
26
  Home,
@@ -39,7 +38,6 @@ import {
39
38
  SparklesIcon,
40
39
  Undo2Icon,
41
40
  XCircleIcon,
42
- YoutubeIcon,
43
41
  ZapIcon,
44
42
  } from "lucide-react";
45
43
  import {
@@ -47,7 +45,9 @@ import {
47
45
  useOpenSettingsToTab,
48
46
  } from "@/components/app-config/state";
49
47
  import { MarkdownIcon } from "@/components/editor/cell/code/icons";
48
+ import { GitHubIcon } from "@/components/icons/github";
50
49
  import { MarimoPlusIcon } from "@/components/icons/marimo-icons";
50
+ import { YouTubeIcon } from "@/components/icons/youtube";
51
51
  import { useImperativeModal } from "@/components/modal/ImperativeModal";
52
52
  import { renderShortcut } from "@/components/shortcuts/renderShortcut";
53
53
  import { PairWithAgentModal } from "@/components/editor/actions/pair-with-agent-modal";
@@ -655,7 +655,7 @@ export function useNotebookActions() {
655
655
  },
656
656
  },
657
657
  {
658
- icon: <GithubIcon size={14} strokeWidth={1.5} />,
658
+ icon: <GitHubIcon className="h-3.5 w-3.5" />,
659
659
  label: "GitHub",
660
660
  handle: () => {
661
661
  window.open(Constants.githubPage, "_blank");
@@ -669,7 +669,7 @@ export function useNotebookActions() {
669
669
  },
670
670
  },
671
671
  {
672
- icon: <YoutubeIcon size={14} strokeWidth={1.5} />,
672
+ icon: <YouTubeIcon className="h-3.5 w-3.5" />,
673
673
  label: "YouTube",
674
674
  handle: () => {
675
675
  window.open(Constants.youtube, "_blank");
@@ -1,6 +1,15 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import { beforeEach, describe, expect, it, type Mock, vi } from "vitest";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ it,
8
+ type Mock,
9
+ vi,
10
+ } from "vitest";
3
11
  import { variableName } from "@/__tests__/branded";
12
+ import * as aiContext from "@/core/ai/context/context";
4
13
  import { getCodes } from "@/core/codemirror/copilot/getCodes";
5
14
  import { dataSourceConnectionsAtom } from "@/core/datasets/data-source-connections";
6
15
  import { DUCKDB_ENGINE } from "@/core/datasets/engines";
@@ -8,7 +17,15 @@ import { datasetsAtom } from "@/core/datasets/state";
8
17
  import type { DatasetsState } from "@/core/datasets/types";
9
18
  import { store } from "@/core/state/jotai";
10
19
  import { variablesAtom } from "@/core/variables/state";
11
- import { codeToCells, getAICompletionBody } from "../completion-utils";
20
+ import type { FileUIPart, UIMessage } from "ai";
21
+ import {
22
+ codeToCells,
23
+ getAICompletionBody,
24
+ getAICompletionBodyWithAttachments,
25
+ isContextAttachment,
26
+ MARIMO_CONTEXT_PART_TYPE,
27
+ resolveChatContext,
28
+ } from "../completion-utils";
12
29
 
13
30
  // Mock getCodes function
14
31
  vi.mock("@/core/codemirror/copilot/getCodes", () => ({
@@ -350,6 +367,125 @@ describe("getAICompletionBody", () => {
350
367
  });
351
368
  });
352
369
 
370
+ describe("resolveChatContext", () => {
371
+ beforeEach(() => {
372
+ store.set(datasetsAtom, {
373
+ tables: [],
374
+ } as unknown as DatasetsState);
375
+ store.set(dataSourceConnectionsAtom, {
376
+ latestEngineSelected: DUCKDB_ENGINE,
377
+ connectionsMap: new Map(),
378
+ });
379
+ store.set(variablesAtom, {});
380
+ });
381
+
382
+ it("returns no context when the input has no @-mentions", async () => {
383
+ const result = await resolveChatContext("just a plain question");
384
+ expect(result).toEqual({ contextPart: null, attachments: [] });
385
+ });
386
+
387
+ it("returns no context part when @-mentions resolve to nothing", async () => {
388
+ const result = await resolveChatContext("look at @variable://ghost");
389
+ expect(result.contextPart).toBeNull();
390
+ expect(result.attachments).toEqual([]);
391
+ });
392
+
393
+ it("captures resolved @-context into a data part", async () => {
394
+ store.set(variablesAtom, {
395
+ [variableName("var1")]: {
396
+ name: variableName("var1"),
397
+ value: "string value",
398
+ dataType: "string",
399
+ declaredBy: [],
400
+ usedBy: [],
401
+ },
402
+ });
403
+
404
+ const result = await resolveChatContext("inspect @variable://var1");
405
+
406
+ expect(result.contextPart?.type).toBe(MARIMO_CONTEXT_PART_TYPE);
407
+ expect(result.contextPart?.data.contextIds).toEqual(["variable://var1"]);
408
+ expect(result.contextPart?.data.plainText).toMatchInlineSnapshot(
409
+ `"<variable name="var1" dataType="string">"string value"</variable>"`,
410
+ );
411
+ });
412
+ });
413
+
414
+ describe("isContextAttachment", () => {
415
+ type Part = UIMessage["parts"][number];
416
+
417
+ it("is true for a file part tagged as context", () => {
418
+ const part = {
419
+ type: "file",
420
+ mediaType: "image/png",
421
+ url: "data:image/png;base64,abc",
422
+ providerMetadata: { marimo: { source: "context" } },
423
+ } as Part;
424
+ expect(isContextAttachment(part)).toBe(true);
425
+ });
426
+
427
+ it("is false for a user-uploaded file part (no marker)", () => {
428
+ const part = {
429
+ type: "file",
430
+ mediaType: "image/png",
431
+ url: "data:image/png;base64,abc",
432
+ } as Part;
433
+ expect(isContextAttachment(part)).toBe(false);
434
+ });
435
+
436
+ it("is false for a file part with unrelated provider metadata", () => {
437
+ const part = {
438
+ type: "file",
439
+ mediaType: "image/png",
440
+ url: "data:image/png;base64,abc",
441
+ providerMetadata: { openai: { foo: "bar" } },
442
+ } as Part;
443
+ expect(isContextAttachment(part)).toBe(false);
444
+ });
445
+
446
+ it("is false for non-file parts", () => {
447
+ expect(isContextAttachment({ type: "text", text: "hi" } as Part)).toBe(
448
+ false,
449
+ );
450
+ });
451
+ });
452
+
453
+ describe("context attachment stamping", () => {
454
+ const rawAttachment: FileUIPart = {
455
+ type: "file",
456
+ mediaType: "image/png",
457
+ url: "data:image/png;base64,abc",
458
+ };
459
+
460
+ beforeEach(() => {
461
+ vi.spyOn(aiContext, "getAIContextRegistry").mockReturnValue({
462
+ parseAllContextIds: () => ["data://t1"],
463
+ formatContextForAI: () => '<data name="t1" />',
464
+ getAttachmentsForContext: async () => [rawAttachment],
465
+ } as unknown as ReturnType<typeof aiContext.getAIContextRegistry>);
466
+ });
467
+
468
+ afterEach(() => {
469
+ vi.restoreAllMocks();
470
+ });
471
+
472
+ it("stamps chat attachments as context-derived", async () => {
473
+ const { attachments } = await resolveChatContext("see @data://t1");
474
+ expect(attachments).toHaveLength(1);
475
+ expect(isContextAttachment(attachments[0])).toBe(true);
476
+ // The original attachment is left untouched (we return a stamped copy).
477
+ expect(rawAttachment.providerMetadata).toBeUndefined();
478
+ });
479
+
480
+ it("stamps completion attachments the same way as chat", async () => {
481
+ const { attachments } = await getAICompletionBodyWithAttachments({
482
+ input: "see @data://t1",
483
+ });
484
+ expect(attachments).toHaveLength(1);
485
+ expect(isContextAttachment(attachments[0])).toBe(true);
486
+ });
487
+ });
488
+
353
489
  describe("codeToCells", () => {
354
490
  it("should return empty array for empty string", () => {
355
491
  const code = "";
@@ -222,7 +222,7 @@ export const AiCompletionEditor: React.FC<Props> = ({
222
222
  showInputPrompt={showInputPrompt}
223
223
  setShowInputPrompt={setShowInputPrompt}
224
224
  runCell={runCell}
225
- className="mt-4 mb-3 w-128"
225
+ className="mt-4 mb-3 w-lg"
226
226
  />
227
227
  </div>
228
228
  );