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

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-02AuLxDs.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-CX0nM3qj.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 +91 -1
  59. package/src/components/editor/ai/ai-completion-editor.tsx +1 -1
  60. package/src/components/editor/ai/completion-utils.ts +86 -1
  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");
@@ -8,7 +8,14 @@ import { datasetsAtom } from "@/core/datasets/state";
8
8
  import type { DatasetsState } from "@/core/datasets/types";
9
9
  import { store } from "@/core/state/jotai";
10
10
  import { variablesAtom } from "@/core/variables/state";
11
- import { codeToCells, getAICompletionBody } from "../completion-utils";
11
+ import type { UIMessage } from "ai";
12
+ import {
13
+ codeToCells,
14
+ getAICompletionBody,
15
+ isContextAttachment,
16
+ MARIMO_CONTEXT_PART_TYPE,
17
+ resolveChatContext,
18
+ } from "../completion-utils";
12
19
 
13
20
  // Mock getCodes function
14
21
  vi.mock("@/core/codemirror/copilot/getCodes", () => ({
@@ -350,6 +357,89 @@ describe("getAICompletionBody", () => {
350
357
  });
351
358
  });
352
359
 
360
+ describe("resolveChatContext", () => {
361
+ beforeEach(() => {
362
+ store.set(datasetsAtom, {
363
+ tables: [],
364
+ } as unknown as DatasetsState);
365
+ store.set(dataSourceConnectionsAtom, {
366
+ latestEngineSelected: DUCKDB_ENGINE,
367
+ connectionsMap: new Map(),
368
+ });
369
+ store.set(variablesAtom, {});
370
+ });
371
+
372
+ it("returns no context when the input has no @-mentions", async () => {
373
+ const result = await resolveChatContext("just a plain question");
374
+ expect(result).toEqual({ contextPart: null, attachments: [] });
375
+ });
376
+
377
+ it("returns no context part when @-mentions resolve to nothing", async () => {
378
+ const result = await resolveChatContext("look at @variable://ghost");
379
+ expect(result.contextPart).toBeNull();
380
+ expect(result.attachments).toEqual([]);
381
+ });
382
+
383
+ it("captures resolved @-context into a data part", async () => {
384
+ store.set(variablesAtom, {
385
+ [variableName("var1")]: {
386
+ name: variableName("var1"),
387
+ value: "string value",
388
+ dataType: "string",
389
+ declaredBy: [],
390
+ usedBy: [],
391
+ },
392
+ });
393
+
394
+ const result = await resolveChatContext("inspect @variable://var1");
395
+
396
+ expect(result.contextPart?.type).toBe(MARIMO_CONTEXT_PART_TYPE);
397
+ expect(result.contextPart?.data.contextIds).toEqual(["variable://var1"]);
398
+ expect(result.contextPart?.data.plainText).toMatchInlineSnapshot(
399
+ `"<variable name="var1" dataType="string">"string value"</variable>"`,
400
+ );
401
+ });
402
+ });
403
+
404
+ describe("isContextAttachment", () => {
405
+ type Part = UIMessage["parts"][number];
406
+
407
+ it("is true for a file part tagged as context", () => {
408
+ const part = {
409
+ type: "file",
410
+ mediaType: "image/png",
411
+ url: "data:image/png;base64,abc",
412
+ providerMetadata: { marimo: { source: "context" } },
413
+ } as Part;
414
+ expect(isContextAttachment(part)).toBe(true);
415
+ });
416
+
417
+ it("is false for a user-uploaded file part (no marker)", () => {
418
+ const part = {
419
+ type: "file",
420
+ mediaType: "image/png",
421
+ url: "data:image/png;base64,abc",
422
+ } as Part;
423
+ expect(isContextAttachment(part)).toBe(false);
424
+ });
425
+
426
+ it("is false for a file part with unrelated provider metadata", () => {
427
+ const part = {
428
+ type: "file",
429
+ mediaType: "image/png",
430
+ url: "data:image/png;base64,abc",
431
+ providerMetadata: { openai: { foo: "bar" } },
432
+ } as Part;
433
+ expect(isContextAttachment(part)).toBe(false);
434
+ });
435
+
436
+ it("is false for non-file parts", () => {
437
+ expect(isContextAttachment({ type: "text", text: "hi" } as Part)).toBe(
438
+ false,
439
+ );
440
+ });
441
+ });
442
+
353
443
  describe("codeToCells", () => {
354
444
  it("should return empty array for empty string", () => {
355
445
  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
  );
@@ -7,7 +7,7 @@ import {
7
7
  startCompletion,
8
8
  } from "@codemirror/autocomplete";
9
9
  import type { ReactCodeMirrorRef } from "@uiw/react-codemirror";
10
- import type { FileUIPart } from "ai";
10
+ import type { DataUIPart, FileUIPart, UIMessage } from "ai";
11
11
  import { getAIContextRegistry } from "@/core/ai/context/context";
12
12
  import { getCodes } from "@/core/codemirror/copilot/getCodes";
13
13
  import type { LanguageAdapterType } from "@/core/codemirror/language/types";
@@ -50,6 +50,91 @@ export function getAICompletionBody({
50
50
  };
51
51
  }
52
52
 
53
+ export interface MarimoContextData {
54
+ plainText: string;
55
+ contextIds: string[];
56
+ }
57
+
58
+ export type MarimoContextUIPart = DataUIPart<{
59
+ "marimo-context": MarimoContextData;
60
+ }>;
61
+
62
+ /**
63
+ * Wire `type` of the @-context data part. Must match
64
+ * `MARIMO_CONTEXT_PART_TYPE` on the backend.
65
+ */
66
+ export const MARIMO_CONTEXT_PART_TYPE =
67
+ "data-marimo-context" as const satisfies MarimoContextUIPart["type"];
68
+
69
+ export interface ResolvedChatContext {
70
+ contextPart: MarimoContextUIPart | null;
71
+ attachments: FileUIPart[];
72
+ }
73
+
74
+ /**
75
+ * Marker stamped onto attachments derived from @-context (as opposed to files
76
+ * the user uploaded directly).
77
+ */
78
+ const CONTEXT_ATTACHMENT_METADATA = {
79
+ marimo: { source: "context" },
80
+ } as const;
81
+
82
+ /** Whether a part is an attachment that was derived from @-context. */
83
+ export function isContextAttachment(part: UIMessage["parts"][number]): boolean {
84
+ return (
85
+ part.type === "file" &&
86
+ part.providerMetadata?.marimo?.source ===
87
+ CONTEXT_ATTACHMENT_METADATA.marimo.source
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Resolve @-context for messages. They represent referenced
93
+ * datasets, variables, or other context from the user's prompt.
94
+ */
95
+ export async function resolveChatContext(
96
+ input: string,
97
+ ): Promise<ResolvedChatContext> {
98
+ if (!input.includes(CONTEXT_TRIGGER)) {
99
+ return { contextPart: null, attachments: [] };
100
+ }
101
+
102
+ const registry = getAIContextRegistry(store);
103
+ const contextIds = registry.parseAllContextIds(input);
104
+ if (contextIds.length === 0) {
105
+ return { contextPart: null, attachments: [] };
106
+ }
107
+
108
+ const plainText = registry.formatContextForAI(contextIds);
109
+
110
+ let attachments: FileUIPart[] = [];
111
+ try {
112
+ const resolved = await registry.getAttachmentsForContext(contextIds);
113
+ attachments = resolved.map((attachment) => ({
114
+ ...attachment,
115
+ providerMetadata: {
116
+ ...attachment.providerMetadata,
117
+ marimo: {
118
+ ...attachment.providerMetadata?.marimo,
119
+ ...CONTEXT_ATTACHMENT_METADATA.marimo,
120
+ },
121
+ },
122
+ }));
123
+ } catch (error) {
124
+ Logger.error("Error getting attachments:", error);
125
+ }
126
+
127
+ let contextPart: MarimoContextUIPart | null = null;
128
+ if (plainText.trim()) {
129
+ contextPart = {
130
+ type: MARIMO_CONTEXT_PART_TYPE,
131
+ data: { plainText, contextIds: contextIds.map(String) },
132
+ };
133
+ }
134
+
135
+ return { contextPart, attachments };
136
+ }
137
+
53
138
  /**
54
139
  * Gets the request body and attachments for the AI completion API.
55
140
  */
@@ -142,7 +142,7 @@ export const CreateCellButton = ({
142
142
  <DropdownMenuTrigger asChild={true}>
143
143
  <Button
144
144
  className={cn(
145
- "border-none hover-action shadow-none! bg-transparent! focus-visible:outline-none",
145
+ "border-none hover-action shadow-none! bg-transparent! focus-visible:outline-hidden",
146
146
  isAppInteractionDisabled(connectionState) && " inactive-button",
147
147
  )}
148
148
  onPointerDownCapture={handlePointerDownCapture}
@@ -20,7 +20,7 @@ export const PanelEmptyState = ({
20
20
  {icon &&
21
21
  // oxlint-disable-next-line react/no-clone-element
22
22
  React.cloneElement(icon, {
23
- className: "text-accent-foreground flex-shrink-0",
23
+ className: "text-accent-foreground shrink-0",
24
24
  })}
25
25
  <span className="mt-1 text-accent-foreground">{title}</span>
26
26
  </div>
@@ -38,7 +38,7 @@ export const FloatingOutline: React.FC = () => {
38
38
  <OutlineList
39
39
  className={cn(
40
40
  "-top-4 max-h-[70vh] bg-background rounded-lg shadow-lg absolute overflow-auto transition-all duration-300 w-[300px] border",
41
- isHovered ? "-left-[280px] opacity-100" : "left-[300px] opacity-0",
41
+ isHovered ? "left-[-280px] opacity-100" : "left-[300px] opacity-0",
42
42
  )}
43
43
  items={items}
44
44
  activeHeaderId={activeHeaderId}
@@ -74,7 +74,7 @@ export const PendingAICells: React.FC = () => {
74
74
  <Button variant="ghost" size="icon" onClick={() => clickNext("up")}>
75
75
  <ChevronUp className="h-3.5 w-3.5" />
76
76
  </Button>
77
- <span className="text-xs font-mono min-w-[3.5rem] text-center">
77
+ <span className="text-xs font-mono min-w-14 text-center">
78
78
  {currentIndex === null
79
79
  ? `${listStagedCells.length} pending`
80
80
  : `${currentIndex + 1} / ${listStagedCells.length}`}
@@ -81,7 +81,7 @@ const ResizableComponent = ({
81
81
  <div
82
82
  ref={ref}
83
83
  className={`w-[3px] cursor-col-resize transition-colors duration-200 z-100
84
- relative before:content-[''] before:absolute before:inset-y-0 before:-left-[3px]
84
+ relative before:content-[''] before:absolute before:inset-y-0 before:left-[-3px]
85
85
  before:right-[-3px] before:w-[9px] before:z-[-1]
86
86
  hover/column:bg-[var(--slate-3)] dark:hover/column:bg-[var(--slate-5)]
87
87
  hover/column:hover:bg-primary/60 dark:hover/column:hover:bg-primary/60`}
@@ -78,7 +78,7 @@ const SortableColumnInternal = React.forwardRef(
78
78
  };
79
79
 
80
80
  const dragHandle = (
81
- <div className="px-2 pb-0 group flex items-center overflow-hidden border-b border-[var(--slate-7)]">
81
+ <div className="px-2 pb-0 group flex items-center overflow-hidden border-b border-(--slate-7)">
82
82
  <Tooltip content="Move column left" side="top" delayDuration={300}>
83
83
  <Button
84
84
  variant="text"
@@ -161,7 +161,7 @@ const SortableColumnInternal = React.forwardRef(
161
161
  isOver && "bg-accent/20", // Add a background color when dragging over
162
162
  )}
163
163
  >
164
- <div className="border border-[var(--slate-7)]">
164
+ <div className="border border-(--slate-7)">
165
165
  {dragHandle}
166
166
  {props.children}
167
167
  </div>
@@ -722,7 +722,7 @@ export const MarimoErrorOutput = ({
722
722
  <Alert
723
723
  variant={alertVariant}
724
724
  className={cn(
725
- "border-none font-code text-sm text-[0.84375rem] p-0 text-muted-foreground normal [&:has(svg)]:pl-0 space-y-2",
725
+ "border-none font-code text-sm text-[0.84375rem] p-0 text-muted-foreground normal has-[svg]:pl-0 space-y-2",
726
726
  className,
727
727
  )}
728
728
  >
@@ -18,7 +18,7 @@ export const TextOutput = ({ text, channel, wrapText }: Props): JSX.Element => {
18
18
  return (
19
19
  <span
20
20
  className={
21
- wrapText ? "whitespace-pre-wrap break-words" : "whitespace-pre"
21
+ wrapText ? "whitespace-pre-wrap wrap-break-word" : "whitespace-pre"
22
22
  }
23
23
  >
24
24
  <RenderTextWithLinks text={text} />
@@ -30,7 +30,7 @@ export const TextOutput = ({ text, channel, wrapText }: Props): JSX.Element => {
30
30
  <span
31
31
  className={cn(
32
32
  !shouldRenderAnsi &&
33
- (wrapText ? "whitespace-pre-wrap break-words" : "whitespace-pre"),
33
+ (wrapText ? "whitespace-pre-wrap wrap-break-word" : "whitespace-pre"),
34
34
  channel === "output" && "font-prose",
35
35
  channel,
36
36
  )}