@marimo-team/islands 0.23.12-dev1 → 0.23.12-dev3

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.
@@ -35996,7 +35996,7 @@ ${d}`,
35996
35996
  return Logger.warn("Failed to get version from mount config"), null;
35997
35997
  }
35998
35998
  }
35999
- marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.12-dev1");
35999
+ marimoVersionAtom = atom(getVersionFromMountConfig() || "0.23.12-dev3");
36000
36000
  showCodeInRunModeAtom = atom(true);
36001
36001
  atom(null);
36002
36002
  var import_compiler_runtime = require_compiler_runtime();
package/dist/main.js CHANGED
@@ -26,7 +26,7 @@ import { $ as reducer, B as safeExtractSetUIElementMessageBuffers, Bn as CircleA
26
26
  import { __tla as __tla_1 } from "./chunk-5FQGJX7Z-BbqSm5gU.js";
27
27
  import { o as useSize, s as Root$1, u as createLucideIcon } from "./dist-DYGLrbYQ.js";
28
28
  import { A as SquareFunction, C as DEFAULT_COLOR_SCHEME, D as SCALE_TYPE_DESCRIPTIONS, E as EMPTY_VALUE$1, O as TIME_UNIT_DESCRIPTIONS, S as DEFAULT_AGGREGATION, T as DEFAULT_TIME_UNIT, _ as AGGREGATION_TYPE_DESCRIPTIONS, a as AGGREGATION_FNS$1, b as COLOR_SCHEMES, c as COLOR_BY_FIELDS, d as NONE_VALUE, f as SELECTABLE_DATA_TYPES, g as TIME_UNITS, h as STRING_AGGREGATION_FNS, i as convertDataTypeToSelectable, j as ChartColumn, k as escapeFieldName, l as COMBINED_TIME_UNITS, m as SORT_TYPES, n as createSpecWithoutData, o as BIN_AGGREGATION, p as SINGLE_TIME_UNITS, r as isFieldSet, s as CHART_TYPES, t as augmentSpecWithData, u as ChartType, v as AGGREGATION_TYPE_ICON, w as DEFAULT_MAX_BINS_FACET, x as COUNT_FIELD, y as CHART_TYPE_ICON } from "./spec-B45_YCNI.js";
29
- import { $ as contextAwarePanelOpen, $t as $fae977aafc393c5c$export$6b862160d295c8e, A as prettifyRowCount, At as SELECT_COLUMN_ID, B as DatePicker, Bt as TabsContent, C as downloadSizeLimitAtom, Ct as DelayMount, D as ErrorState, Dt as loadTableAndRawData, E as EmptyState, Et as getPageIndexForRow, F as ContextMenuSeparator, Ft as Maps, G as CommandEmpty, Gt as ChartLoadingState, H as Combobox, Ht as TabsTrigger, I as ContextMenuTrigger, It as dateToLocalISODate, J as CommandList, Jt as RenderTextWithLinks, K as CommandInput, Kt as LazyVegaEmbed, L as useInternalStateWithSync, Lt as dateToLocalISODateTime, M as ContextMenu, Mt as toFieldTypes, N as ContextMenuContent, Nt as getMimeValues, O as LoadingState, Ot as loadTableData, P as ContextMenuItem, Pt as isNullishFilter, Q as PANEL_TYPES, Qt as $fae977aafc393c5c$export$588937bcd60ade55, R as useSelectList, Rt as dateToLocalISOTime, S as Filenames, St as ColumnChartSpecModel, T as ColumnPreviewContainer, Tt as usePrevious$1, U as ComboboxItem, Ut as ChartErrorState, V as DateRangePicker, Vt as TabsList, W as Command, Wt as ChartInfoState, X as smartMatch, Xt as HtmlOutput, Y as CommandSeparator, Yt as Kbd, Z as ContextAwarePanelItem, Zt as EmotionCacheProvider, _ as ADD_PRINTING_CLASS, _t as NAMELESS_COLUMN_PREFIX, an as Ellipsis, at as Toggle, b as downloadHTMLAsImage, bt as renderCellValue, c as Slide, cn as ChevronsUpDown, d as RadioGroupItem, dn as ChevronsDownUp, dt as Table, en as TextWrap, et as contextAwarePanelOwner, f as JsonOutput, fn as ChevronLeft, ft as TableBody, g as InstallPackageButton, gt as TableRow, h as DataTable, ht as TableHeader, it as slotsController, j as getColumnCountForDisplay, jt as TOO_MANY_ROWS, k as prettifyRowColumnCount, kt as INDEX_COLUMN_NAME, l as Switch, ln as ChevronsRight, lt as Fill, m as OutputRenderer, mt as TableHead, n as marimoVersionAtom, nn as Funnel, nt as isCellAwareAtom, o as SLIDE_TYPE_OPTIONS_BY_VALUE, on as Download, p as OutputArea, pn as ArrowDownWideNarrow, pt as TableCell, q as CommandItem, qt as useOverflowDetection, r as showCodeInRunModeAtom, rn as EyeOff, rt as SlotNames, sn as Code, t as useNotebookCodeAvailable, tn as GripHorizontal, tt as contextAwarePanelType, u as RadioGroup, un as ChevronsLeft, ut as Provider$1, v as downloadBlob, vt as generateColumns, w as ColumnName, wt as useIntersectionObserver, x as Progress, xt as ColumnChartContext, y as downloadByURL, yt as inferFieldTypes, z as CompactChipRow, zt as Tabs, __tla as __tla_2 } from "./code-visibility-CqnbA4Ex.js";
29
+ import { $ as contextAwarePanelOpen, $t as $fae977aafc393c5c$export$6b862160d295c8e, A as prettifyRowCount, At as SELECT_COLUMN_ID, B as DatePicker, Bt as TabsContent, C as downloadSizeLimitAtom, Ct as DelayMount, D as ErrorState, Dt as loadTableAndRawData, E as EmptyState, Et as getPageIndexForRow, F as ContextMenuSeparator, Ft as Maps, G as CommandEmpty, Gt as ChartLoadingState, H as Combobox, Ht as TabsTrigger, I as ContextMenuTrigger, It as dateToLocalISODate, J as CommandList, Jt as RenderTextWithLinks, K as CommandInput, Kt as LazyVegaEmbed, L as useInternalStateWithSync, Lt as dateToLocalISODateTime, M as ContextMenu, Mt as toFieldTypes, N as ContextMenuContent, Nt as getMimeValues, O as LoadingState, Ot as loadTableData, P as ContextMenuItem, Pt as isNullishFilter, Q as PANEL_TYPES, Qt as $fae977aafc393c5c$export$588937bcd60ade55, R as useSelectList, Rt as dateToLocalISOTime, S as Filenames, St as ColumnChartSpecModel, T as ColumnPreviewContainer, Tt as usePrevious$1, U as ComboboxItem, Ut as ChartErrorState, V as DateRangePicker, Vt as TabsList, W as Command, Wt as ChartInfoState, X as smartMatch, Xt as HtmlOutput, Y as CommandSeparator, Yt as Kbd, Z as ContextAwarePanelItem, Zt as EmotionCacheProvider, _ as ADD_PRINTING_CLASS, _t as NAMELESS_COLUMN_PREFIX, an as Ellipsis, at as Toggle, b as downloadHTMLAsImage, bt as renderCellValue, c as Slide, cn as ChevronsUpDown, d as RadioGroupItem, dn as ChevronsDownUp, dt as Table, en as TextWrap, et as contextAwarePanelOwner, f as JsonOutput, fn as ChevronLeft, ft as TableBody, g as InstallPackageButton, gt as TableRow, h as DataTable, ht as TableHeader, it as slotsController, j as getColumnCountForDisplay, jt as TOO_MANY_ROWS, k as prettifyRowColumnCount, kt as INDEX_COLUMN_NAME, l as Switch, ln as ChevronsRight, lt as Fill, m as OutputRenderer, mt as TableHead, n as marimoVersionAtom, nn as Funnel, nt as isCellAwareAtom, o as SLIDE_TYPE_OPTIONS_BY_VALUE, on as Download, p as OutputArea, pn as ArrowDownWideNarrow, pt as TableCell, q as CommandItem, qt as useOverflowDetection, r as showCodeInRunModeAtom, rn as EyeOff, rt as SlotNames, sn as Code, t as useNotebookCodeAvailable, tn as GripHorizontal, tt as contextAwarePanelType, u as RadioGroup, un as ChevronsLeft, ut as Provider$1, v as downloadBlob, vt as generateColumns, w as ColumnName, wt as useIntersectionObserver, x as Progress, xt as ColumnChartContext, y as downloadByURL, yt as inferFieldTypes, z as CompactChipRow, zt as Tabs, __tla as __tla_2 } from "./code-visibility-BCupU198.js";
30
30
  import { c as Calendar, i as createReducerAndAtoms, n as useOnUnmount, o as ToggleLeft, t as useOnMount } from "./useLifecycle-C3Ec71q0.js";
31
31
  import { t as Check } from "./check-nrzHDi45.js";
32
32
  import { A as Icon, C as logNever, D as $18f2051aff69b9bf$export$a54013f0d02a8f82, E as $18f2051aff69b9bf$export$43bb16f9c6d9e3f7, F as createCollection, I as X, M as clamp$2, N as usePrevious$2, P as useDirection, R as ChevronDown, S as assertNever, a as SelectGroup, c as SelectSeparator, d as NativeSelect, i as SelectContent, j as Trigger$1, l as SelectTrigger, n as capitalize, o as SelectItem, r as Select, s as SelectLabel, t as Strings, u as SelectValue, w as $a916eb452884faea$export$b7a616150fdb9f44 } from "./strings-Cq2s9_EQ.js";
@@ -36190,7 +36190,7 @@ ${c}
36190
36190
  function _temp2$2(e) {
36191
36191
  e.target === e.currentTarget && e.key === "Enter" && (e.preventDefault(), e.stopPropagation(), e.currentTarget.click());
36192
36192
  }
36193
- var LazySlidesComponent = import_react.lazy(() => import("./reveal-component-CKXMgI4_.js"));
36193
+ var LazySlidesComponent = import_react.lazy(() => import("./reveal-component-6yT3rma2.js"));
36194
36194
  const SlidesLayoutRenderer = ({ layout: e, setLayout: r, cells: c, mode: l }) => {
36195
36195
  var _a3;
36196
36196
  let u = useAtomValue(kioskModeAtom), d = l === "read" || u, f = useAtomValue(numColumnsAtom) > 1, [p, m] = (0, import_react.useState)(null), { slideCells: h, skippedIds: g, noOutputIds: _, slideTypes: v, startCellIndex: y } = (0, import_react.useMemo)(() => computeSlideCellsInfo(c, e), [
@@ -9,7 +9,7 @@ import { t as require_compiler_runtime } from "./compiler-runtime-CEbnTgxf.js";
9
9
  import { lt as kioskModeAtom } from "./html-to-image-C86pQALH.js";
10
10
  import "./chunk-5FQGJX7Z-BbqSm5gU.js";
11
11
  import { u as createLucideIcon } from "./dist-DYGLrbYQ.js";
12
- import { a as DEFAULT_SLIDE_TYPE, c as Slide, ct as PanelResizeHandle, i as DEFAULT_DECK_TRANSITION, in as Expand, ot as Panel, rn as EyeOff, s as SlideSidebar, sn as Code, st as PanelGroup, t as useNotebookCodeAvailable } from "./code-visibility-CqnbA4Ex.js";
12
+ import { a as DEFAULT_SLIDE_TYPE, c as Slide, ct as PanelResizeHandle, i as DEFAULT_DECK_TRANSITION, in as Expand, ot as Panel, rn as EyeOff, s as SlideSidebar, sn as Code, st as PanelGroup, t as useNotebookCodeAvailable } from "./code-visibility-BCupU198.js";
13
13
  import { X as useDebouncedCallback } from "./input-AKkGXdyV.js";
14
14
  import "./toDate-Do1xRzAo.js";
15
15
  import "./react-dom-BTJzcVJ9.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.12-dev1",
3
+ "version": "0.23.12-dev3",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -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();
@@ -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 = "";
@@ -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
  */