@marimo-team/islands 0.15.2 → 0.15.4

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 (211) hide show
  1. package/dist/{ConnectedDataExplorerComponent-C39nQwtD.js → ConnectedDataExplorerComponent-B68gXlbY.js} +323 -328
  2. package/dist/{ImageComparisonComponent-BhkiyswP.js → ImageComparisonComponent-Cw1oA8Tn.js} +13 -13
  3. package/dist/{_baseUniq-DdHL34FO.js → _baseUniq-CQrhBg_9.js} +67 -67
  4. package/dist/any-language-editor-pzUl6lxp.js +27 -0
  5. package/dist/{arc-BXrety1g.js → arc-BOhn-m2C.js} +1 -1
  6. package/dist/{architectureDiagram-KFL7JDKH-BMy6ywCF.js → architectureDiagram-W76B3OCA-DdYf2VnU.js} +144 -144
  7. package/dist/assets/{worker-COGufAQn.js → worker-BcG8m3h5.js} +33 -29
  8. package/dist/asterisk-DS281yxp.js +271 -0
  9. package/dist/{blockDiagram-ZYB65J3Q-DYT2-nlI.js → blockDiagram-QIGZ2CNN-DmmYotkP.js} +10 -10
  10. package/dist/{c4Diagram-AAMF2YG6-ZiQzioe6.js → c4Diagram-FPNF74CW-Dyz4zMHJ.js} +8 -8
  11. package/dist/{channel-CeuXqUAU.js → channel-CCL8jXAe.js} +1 -1
  12. package/dist/{chunk-ANTBXLJU-BvYnIrdq.js → chunk-4BX2VUAB-BfKwWLfJ.js} +1 -1
  13. package/dist/{chunk-WVR4S24B-DXj8yaUk.js → chunk-55IACEB6-CFQU7zSp.js} +1 -1
  14. package/dist/{chunk-GLLZNHP4-CyFsosAe.js → chunk-FMBD7UC4-DDjZzUcl.js} +1 -1
  15. package/dist/{chunk-JBRWN2VN-DA_EEhy2.js → chunk-K7UQS3LO-nBRjBU1H.js} +117 -117
  16. package/dist/{chunk-NRVI72HA-BYx2jMlI.js → chunk-QN33PNHL-B-g8sJzl.js} +1 -1
  17. package/dist/{chunk-FHKO5MBM-DfCztBk8.js → chunk-QZHKN3VN-B7QSJS3J.js} +1 -1
  18. package/dist/{chunk-LXBSTHXV-Se7vdY6J.js → chunk-TVAH2DTR-pGXll4d1.js} +7 -7
  19. package/dist/{chunk-OMD6QJNC-CqgcPMgL.js → chunk-TZMSLE5B-Dx9h-1mv.js} +1 -1
  20. package/dist/{classDiagram-v2-QTMF73CY-B19A3G1l.js → classDiagram-KNZD7YFC-BXryI7DY.js} +2 -2
  21. package/dist/{classDiagram-3BZAVTQC-B19A3G1l.js → classDiagram-v2-RKCZMP56-BXryI7DY.js} +2 -2
  22. package/dist/{clone-78au0tn1.js → clone-DqwV7ges.js} +1 -1
  23. package/dist/cose-bilkent-S5V4N54A-Di6FNMXz.js +2609 -0
  24. package/dist/{cytoscape.esm-BYnVVhJX.js → cytoscape.esm-DfdJODL8.js} +34 -34
  25. package/dist/{dagre-2BBEFEWP-BfEn3ZUV.js → dagre-5GWH7T2D-BTZPMTey.js} +6 -6
  26. package/dist/{data-grid-overlay-editor-CH_qLkV2.js → data-grid-overlay-editor-ryatXXby.js} +11 -11
  27. package/dist/{diagram-4IRLE6MV-CL8xidnG.js → diagram-N5W7TBWH-D79_zdOu.js} +59 -60
  28. package/dist/{diagram-RP2FKANI-B1BPcUew.js → diagram-QEK2KX5R-DX2A_SD0.js} +15 -15
  29. package/dist/{diagram-GUPCWM2R-CZ5cfqlq.js → diagram-S2PKOQOG-DM6VMTrJ.js} +10 -10
  30. package/dist/dockerfile-BoowzQlp.js +194 -0
  31. package/dist/ebnf-DUPDuY4r.js +78 -0
  32. package/dist/{erDiagram-HZWUO2LU-BEAIww50.js → erDiagram-AWTI2OKA-BBirxtlI.js} +8 -8
  33. package/dist/fcl-CPC2WYrI.js +103 -0
  34. package/dist/{flowDiagram-THRYKUMA-Czs2UAI2.js → flowDiagram-PVAE7QVJ-DyVweEMs.js} +9 -9
  35. package/dist/{ganttDiagram-WV7ZQ7D5-ByYIAVFO.js → ganttDiagram-OWAHRB6G-DTB7FX7r.js} +34 -34
  36. package/dist/{gitGraphDiagram-OJR772UL-BcpDsiyB.js → gitGraphDiagram-NY62KEGX-BrbIb5pD.js} +4 -4
  37. package/dist/{glide-data-editor-CmN6FVyi.js → glide-data-editor-DhMX4nmM.js} +33 -33
  38. package/dist/{graph-77W6heli.js → graph-CuLSrclI.js} +3 -3
  39. package/dist/http-D9LttvKF.js +44 -0
  40. package/dist/{index-BOojn38D.js → index-BNgdUQ2e.js} +7711 -7711
  41. package/dist/index-DIy6LHLJ.js +98 -0
  42. package/dist/{index-CmozKMxx.js → index-Df2dsx1t.js} +6 -6
  43. package/dist/{index-pBmAzQJl.js → index-MCx5v1x0.js} +2 -2
  44. package/dist/{index-Bfk9dnyS.js → index-cz_xaKvT.js} +33090 -32892
  45. package/dist/{infoDiagram-6WOFNB3A-CfzLHHVP.js → infoDiagram-STP46IZ2-CCBHc7-K.js} +2 -2
  46. package/dist/{journeyDiagram-FFXJYRFH-ndAcpkGn.js → journeyDiagram-BIP6EPQ6-LhGSj54j.js} +24 -26
  47. package/dist/{kanban-definition-KOZQBZVT-DcQYzNvc.js → kanban-definition-6OIFK2YF-aegTMFS6.js} +14 -14
  48. package/dist/{layout-XySVHJgD.js → layout-BEARWMhl.js} +81 -81
  49. package/dist/{linear-PbooOqg7.js → linear-fbJq6cdO.js} +35 -35
  50. package/dist/{main-B5yML0bw.js → main-HerZgEhd.js} +76533 -69945
  51. package/dist/main.js +1 -1
  52. package/dist/{mermaid-Cg5IX6Nv.js → mermaid-DxPYK0KX.js} +6160 -7493
  53. package/dist/min-DBJkhObB.js +80 -0
  54. package/dist/mindmap-definition-Q6HEUPPD-A3Fh5XDZ.js +785 -0
  55. package/dist/nginx-zDPm3Z74.js +89 -0
  56. package/dist/{number-overlay-editor-DUhfZqtP.js → number-overlay-editor-USMrY6k3.js} +19 -19
  57. package/dist/{pieDiagram-DBDJKBY4-DTOlNsja.js → pieDiagram-ADFJNKIX-Q9uFlCV0.js} +17 -17
  58. package/dist/{quadrantDiagram-YPSRARAO-BX2d8VS-.js → quadrantDiagram-LMRXKWRM-BuPh-qpK.js} +6 -6
  59. package/dist/{react-plotly-Dcyw-3Sa.js → react-plotly-HSqJPRfa.js} +3577 -3577
  60. package/dist/{requirementDiagram-EGVEC5DT-D1T5u-wG.js → requirementDiagram-4UW4RH46-CHROYNU_.js} +7 -7
  61. package/dist/{sankeyDiagram-HRAUVNP4-G6xDfnp-.js → sankeyDiagram-GR3RE2ED-DkUqHP2d.js} +5 -5
  62. package/dist/sequenceDiagram-C3RYC4MD-YoPTMplP.js +2519 -0
  63. package/dist/{slides-component-BJLlPJSr.js → slides-component-D7CHSR00.js} +66 -66
  64. package/dist/solr-BNlsLglM.js +41 -0
  65. package/dist/spreadsheet-C-cy4P5N.js +49 -0
  66. package/dist/{stateDiagram-UUKSUZ4H-CYXbjaom.js → stateDiagram-KXAO66HF-DEN00mVU.js} +5 -5
  67. package/dist/{stateDiagram-v2-EYPG3UTE-Br1HYKT6.js → stateDiagram-v2-UMBNRL4Z-DlQqSUAa.js} +2 -2
  68. package/dist/style.css +1 -1
  69. package/dist/tiddlywiki-5wqsXtSk.js +155 -0
  70. package/dist/tiki-__Kn3CeS.js +181 -0
  71. package/dist/{time-B9SZnSen.js → time-BtVcKqeD.js} +58 -58
  72. package/dist/{timeline-definition-3HZDQTIS-DeK_ZRD0.js → timeline-definition-XQNQX7LJ-DEteLt8D.js} +10 -12
  73. package/dist/{timer-BYwnU4DF.js → timer-B0-z63CM.js} +16 -16
  74. package/dist/{treemap-75Q7IDZK-CKP4vV_0.js → treemap-75Q7IDZK-8S6podme.js} +14 -14
  75. package/dist/{vega-component-CpgdqX2d.js → vega-component-D35L45kI.js} +30 -30
  76. package/dist/{xychartDiagram-FDP5SA34-AMEPsx_R.js → xychartDiagram-6GGTOJPD-DKwGThyy.js} +7 -7
  77. package/package.json +44 -41
  78. package/src/__mocks__/notebook.ts +3 -0
  79. package/src/__mocks__/requests.ts +3 -0
  80. package/src/__tests__/__snapshots__/CellStatus.test.tsx.snap +12 -12
  81. package/src/__tests__/chat-utils.test.ts +26 -211
  82. package/src/components/ai/ai-model-dropdown.tsx +25 -9
  83. package/src/components/ai/ai-provider-icon.tsx +5 -1
  84. package/src/components/app-config/ai-config.tsx +7 -0
  85. package/src/components/chat/acp/__tests__/__snapshots__/prompt.test.ts.snap +304 -0
  86. package/src/components/chat/acp/__tests__/atoms.test.ts +56 -0
  87. package/src/components/chat/acp/__tests__/prompt.test.ts +12 -0
  88. package/src/components/chat/acp/__tests__/state.test.ts +621 -0
  89. package/src/components/chat/acp/agent-docs.tsx +78 -0
  90. package/src/components/chat/acp/agent-panel.css +23 -0
  91. package/src/components/chat/acp/agent-panel.tsx +715 -0
  92. package/src/components/chat/acp/agent-selector.tsx +138 -0
  93. package/src/components/chat/acp/blocks.tsx +664 -0
  94. package/src/components/chat/acp/common.tsx +198 -0
  95. package/src/components/chat/acp/prompt.ts +284 -0
  96. package/src/components/chat/acp/scroll-to-bottom-button.tsx +50 -0
  97. package/src/components/chat/acp/session-tabs.tsx +138 -0
  98. package/src/components/chat/acp/state.ts +263 -0
  99. package/src/components/chat/acp/thread.tsx +121 -0
  100. package/src/components/chat/acp/types.ts +63 -0
  101. package/src/components/chat/acp/utils.ts +45 -0
  102. package/src/components/chat/chat-components.tsx +71 -0
  103. package/src/components/chat/chat-panel.tsx +481 -291
  104. package/src/components/chat/chat-utils.ts +50 -0
  105. package/src/components/chat/markdown-renderer.tsx +3 -7
  106. package/src/components/chat/tool-call-accordion.tsx +6 -6
  107. package/src/components/datasources/__tests__/utils.test.ts +6 -0
  108. package/src/components/datasources/column-preview.tsx +1 -3
  109. package/src/components/editor/actions/useNotebookActions.tsx +1 -1
  110. package/src/components/editor/ai/add-cell-with-ai.tsx +20 -15
  111. package/src/components/editor/ai/ai-completion-editor.tsx +22 -3
  112. package/src/components/editor/ai/completion-handlers.tsx +2 -4
  113. package/src/components/editor/ai/completion-utils.ts +85 -11
  114. package/src/components/editor/alerts/startup-logs-alert.tsx +72 -0
  115. package/src/components/editor/chrome/panels/datasources-panel.tsx +3 -1
  116. package/src/components/editor/chrome/panels/dependency-graph-panel.tsx +3 -1
  117. package/src/components/editor/chrome/panels/documentation-panel.tsx +3 -1
  118. package/src/components/editor/chrome/panels/error-panel.tsx +3 -1
  119. package/src/components/editor/chrome/panels/file-explorer-panel.tsx +3 -1
  120. package/src/components/editor/chrome/panels/logs-panel.tsx +3 -1
  121. package/src/components/editor/chrome/panels/outline-panel.tsx +3 -1
  122. package/src/components/editor/chrome/panels/packages-panel.tsx +4 -2
  123. package/src/components/editor/chrome/panels/scratchpad-panel.tsx +3 -1
  124. package/src/components/editor/chrome/panels/secrets-panel.tsx +3 -1
  125. package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -1
  126. package/src/components/editor/chrome/panels/tracing-panel.tsx +3 -1
  127. package/src/components/editor/chrome/panels/variable-panel.tsx +3 -1
  128. package/src/components/editor/chrome/types.ts +10 -0
  129. package/src/components/editor/chrome/wrapper/app-chrome.tsx +55 -31
  130. package/src/components/editor/controls/command-palette-button.tsx +1 -1
  131. package/src/components/editor/controls/command-palette.tsx +5 -4
  132. package/src/components/editor/controls/state.ts +4 -0
  133. package/src/components/editor/package-alert.tsx +108 -58
  134. package/src/components/editor/renderers/CellArray.tsx +2 -0
  135. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +0 -1
  136. package/src/components/pages/edit-page.tsx +7 -3
  137. package/src/core/ai/chat-utils.ts +26 -43
  138. package/src/core/ai/config.ts +1 -1
  139. package/src/core/ai/context/__tests__/registry.test.ts +277 -3
  140. package/src/core/ai/context/context.ts +11 -1
  141. package/src/core/ai/context/providers/__tests__/cell-output.test.ts +378 -0
  142. package/src/core/ai/context/providers/__tests__/error.test.ts +3 -2
  143. package/src/core/ai/context/providers/__tests__/file.test.ts +119 -0
  144. package/src/core/ai/context/providers/cell-output.ts +349 -0
  145. package/src/core/ai/context/providers/common.ts +5 -1
  146. package/src/core/ai/context/providers/file.ts +287 -0
  147. package/src/core/ai/context/registry.ts +79 -0
  148. package/src/core/ai/state.ts +22 -5
  149. package/src/core/alerts/state.ts +71 -3
  150. package/src/core/cells/cell.ts +2 -2
  151. package/src/core/cells/cells.ts +1 -1
  152. package/src/core/cells/logs.ts +1 -1
  153. package/src/core/cells/runs.ts +6 -5
  154. package/src/core/codemirror/ai/resources.ts +47 -5
  155. package/src/core/codemirror/ai/state.ts +12 -0
  156. package/src/core/codemirror/language/__tests__/sql.test.ts +45 -0
  157. package/src/core/codemirror/markdown/__tests__/commands.test.ts +1 -0
  158. package/src/core/codemirror/theme/dark.ts +1 -1
  159. package/src/core/config/capabilities.ts +1 -1
  160. package/src/core/config/feature-flag.tsx +2 -0
  161. package/src/core/datasets/__tests__/data-source.test.ts +24 -0
  162. package/src/core/errors/__tests__/errors.test.ts +2 -0
  163. package/src/core/islands/bridge.ts +1 -0
  164. package/src/core/islands/main.ts +1 -0
  165. package/src/core/kernel/messages.ts +12 -6
  166. package/src/core/layout/layout.ts +3 -3
  167. package/src/core/network/requests-network.ts +8 -0
  168. package/src/core/network/requests-static.ts +1 -0
  169. package/src/core/network/requests-toasting.ts +1 -0
  170. package/src/core/network/types.ts +4 -1
  171. package/src/core/wasm/bridge.ts +18 -2
  172. package/src/core/wasm/worker/bootstrap.ts +3 -1
  173. package/src/core/wasm/worker/getMarimoWheel.ts +3 -8
  174. package/src/core/wasm/worker/types.ts +3 -0
  175. package/src/core/websocket/useMarimoWebSocket.tsx +7 -1
  176. package/src/css/app/Cell.css +42 -21
  177. package/src/css/app/codemirror.css +5 -1
  178. package/src/css/globals.css +3 -0
  179. package/src/css/md.css +1 -1
  180. package/src/plugins/impl/MicrophonePlugin.tsx +2 -2
  181. package/src/plugins/impl/chat/ChatPlugin.tsx +2 -9
  182. package/src/plugins/impl/chat/chat-ui.tsx +129 -110
  183. package/src/plugins/impl/chat/types.ts +5 -8
  184. package/src/plugins/impl/code/__tests__/language.test.ts +15 -0
  185. package/src/plugins/impl/code/any-language-editor.tsx +11 -8
  186. package/src/plugins/impl/vega/vega.css +121 -0
  187. package/src/plugins/layout/MimeRenderPlugin.tsx +3 -6
  188. package/src/stories/cell.stories.tsx +6 -0
  189. package/src/stories/layout/vertical/one-column.stories.tsx +215 -0
  190. package/src/theme/useTheme.ts +11 -6
  191. package/src/utils/Logger.ts +5 -6
  192. package/src/utils/__tests__/blob.test.ts +37 -0
  193. package/src/utils/arrays.ts +13 -0
  194. package/src/utils/fileToBase64.ts +21 -6
  195. package/src/utils/json/base64.ts +5 -2
  196. package/src/utils/numbers.ts +9 -7
  197. package/dist/any-language-editor-DC5170DQ.js +0 -45
  198. package/dist/asn1-jKiBa2Ya.js +0 -95
  199. package/dist/clojure-CCKyeQKf.js +0 -800
  200. package/dist/css-BkF-NPzE.js +0 -1553
  201. package/dist/index-5ZH_qS8j.js +0 -288
  202. package/dist/index-U4yn89qO.js +0 -341
  203. package/dist/javascript-C2yteZeJ.js +0 -691
  204. package/dist/min-DS5Jz-hg.js +0 -80
  205. package/dist/mindmap-definition-LNHGMQRG-0aOVaMR8.js +0 -3234
  206. package/dist/mllike-BSnXJBGA.js +0 -272
  207. package/dist/pug-CwAQJzGR.js +0 -248
  208. package/dist/python-BkR3uSy8.js +0 -313
  209. package/dist/rpm-IznJm2Xc.js +0 -57
  210. package/dist/sequenceDiagram-WFGC7UMF-DMhHzllb.js +0 -2284
  211. package/dist/ttcn-cfg-Bac_acMi.js +0 -88
@@ -1,17 +1,29 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
2
 
3
- import type { Message as AIMessage } from "@ai-sdk/react";
3
+ import type { UIMessage } from "@ai-sdk/react";
4
4
  import { Logger } from "@/utils/Logger";
5
5
  import type { ChatId, ChatState } from "./state";
6
6
 
7
- export const addMessageToChat = (
8
- chatState: ChatState,
9
- chatId: ChatId | null,
10
- messageId: string,
11
- role: "user" | "assistant",
12
- content: string,
13
- parts?: AIMessage["parts"],
14
- ): ChatState => {
7
+ interface ReplaceMessagesInChatParams {
8
+ /**
9
+ * The state of the chats
10
+ */
11
+ chatState: ChatState;
12
+ /**
13
+ * The chat to replace the messages in
14
+ */
15
+ chatId: ChatId | null;
16
+ /**
17
+ * The messages to replace in the chat
18
+ */
19
+ messages: UIMessage[];
20
+ }
21
+
22
+ export const replaceMessagesInChat = ({
23
+ chatState,
24
+ chatId,
25
+ messages,
26
+ }: ReplaceMessagesInChatParams): ChatState => {
15
27
  if (!chatId) {
16
28
  Logger.warn("No active chat");
17
29
  return chatState;
@@ -22,44 +34,15 @@ export const addMessageToChat = (
22
34
  return chatState;
23
35
  }
24
36
 
25
- const messageIndex = chat.messages.findIndex(
26
- (message) => message.id === messageId,
27
- );
28
-
29
37
  // Create copy of chats to modify
30
38
  const newChats = new Map(chatState.chats);
31
39
  const timestamp = Date.now();
32
40
 
33
- if (messageIndex === -1) {
34
- // Handle new message
35
- newChats.set(chatId, {
36
- ...chat,
37
- messages: [
38
- ...chat.messages,
39
- {
40
- id: messageId,
41
- role,
42
- content,
43
- timestamp: timestamp,
44
- parts,
45
- },
46
- ],
47
- updatedAt: timestamp,
48
- });
49
- } else {
50
- // Handle update message
51
- const newMessages = [...chat.messages];
52
- newMessages[messageIndex] = {
53
- ...newMessages[messageIndex],
54
- content,
55
- parts,
56
- };
57
- newChats.set(chat.id, {
58
- ...chat,
59
- messages: newMessages,
60
- updatedAt: timestamp,
61
- });
62
- }
41
+ newChats.set(chat.id, {
42
+ ...chat,
43
+ messages: messages,
44
+ updatedAt: timestamp,
45
+ });
63
46
 
64
47
  return {
65
48
  ...chatState,
@@ -8,7 +8,7 @@ import type { AIModelKey, UserConfig } from "@/core/config/config-schema";
8
8
  import { useRequestClient } from "@/core/network/requests";
9
9
 
10
10
  // Extract only the supported roles from the Role type
11
- type SupportedRole = Extract<Role, "chat" | "autocomplete" | "edit">;
11
+ export type SupportedRole = Extract<Role, "chat" | "autocomplete" | "edit">;
12
12
 
13
13
  const getModelKeyForRole = (forRole: SupportedRole): AIModelKey | null => {
14
14
  switch (forRole) {
@@ -1,8 +1,9 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
2
 
3
3
  import type { Completion } from "@codemirror/autocomplete";
4
+ import type { FileUIPart } from "ai";
4
5
  import { createStore } from "jotai";
5
- import { beforeEach, describe, expect, it } from "vitest";
6
+ import { beforeEach, describe, expect, it, vi } from "vitest";
6
7
  import { MockNotebook } from "@/__mocks__/notebook";
7
8
  import { notebookAtom } from "@/core/cells/cells";
8
9
  import { CellId as CellIdClass } from "@/core/cells/ids";
@@ -19,10 +20,28 @@ import {
19
20
 
20
21
  // Mock context item for testing
21
22
  interface MockContextItem extends AIContextItem {
22
- type: "mock";
23
- data: { value: string };
23
+ type: "mock" | "attachment";
24
+ data: {
25
+ value: string;
26
+ needsAttachment?: boolean;
27
+ };
24
28
  }
25
29
 
30
+ // Mock attachment data for testing
31
+ const mockAttachment1: FileUIPart = {
32
+ type: "file",
33
+ filename: "test-image-1.png",
34
+ mediaType: "image/png",
35
+ url: "data:image/png;base64,mockdata1",
36
+ };
37
+
38
+ const mockAttachment2: FileUIPart = {
39
+ type: "file",
40
+ filename: "test-image-2.jpg",
41
+ mediaType: "image/jpeg",
42
+ url: "data:image/jpeg;base64,mockdata2",
43
+ };
44
+
26
45
  // Concrete implementation of AIContextProvider for testing
27
46
  class MockContextProvider extends AIContextProvider<MockContextItem> {
28
47
  readonly title = "Mock Items";
@@ -76,6 +95,44 @@ class MockContextProvider extends AIContextProvider<MockContextItem> {
76
95
  }
77
96
  }
78
97
 
98
+ // Test provider that supports attachments
99
+ class AttachmentContextProvider extends AIContextProvider<MockContextItem> {
100
+ readonly title = "Attachment Items";
101
+ readonly mentionPrefix = "@";
102
+ readonly contextType = "attachment";
103
+
104
+ constructor(
105
+ private items: MockContextItem[] = [],
106
+ private attachments: FileUIPart[] = [],
107
+ ) {
108
+ super();
109
+ }
110
+
111
+ getItems(): MockContextItem[] {
112
+ return this.items;
113
+ }
114
+
115
+ formatContext(item: MockContextItem): string {
116
+ return `Attachment: ${item.name} (${item.data.value})`;
117
+ }
118
+
119
+ formatCompletion(item: MockContextItem): Completion {
120
+ return this.createBasicCompletion(item);
121
+ }
122
+
123
+ override async getAttachments(
124
+ items: MockContextItem[],
125
+ ): Promise<FileUIPart[]> {
126
+ // Return attachments for items that need them
127
+ const itemsNeedingAttachments = items.filter(
128
+ (item) => item.data.needsAttachment,
129
+ );
130
+ return itemsNeedingAttachments
131
+ .map((_, index) => this.attachments[index % this.attachments.length])
132
+ .filter(Boolean);
133
+ }
134
+ }
135
+
79
136
  interface FileContextItem extends AIContextItem {
80
137
  type: "file";
81
138
  data: { value: string };
@@ -667,4 +724,221 @@ describe("AIContextRegistry", () => {
667
724
  expect(formattedContext).toContain("Item 1");
668
725
  });
669
726
  });
727
+
728
+ describe("attachment functionality", () => {
729
+ let attachmentProvider: AttachmentContextProvider;
730
+ let attachmentRegistry: AIContextRegistry<MockContextItem>;
731
+
732
+ beforeEach(() => {
733
+ const itemWithAttachment: MockContextItem = {
734
+ uri: "attachment://item1" as ContextLocatorId,
735
+ name: "item1",
736
+ type: "attachment",
737
+ description: "Item with attachment",
738
+ data: { needsAttachment: true, value: "test1" },
739
+ };
740
+
741
+ const itemWithoutAttachment: MockContextItem = {
742
+ uri: "attachment://item2" as ContextLocatorId,
743
+ name: "item2",
744
+ type: "attachment",
745
+ description: "Item without attachment",
746
+ data: { needsAttachment: false, value: "test2" },
747
+ };
748
+
749
+ attachmentProvider = new AttachmentContextProvider(
750
+ [itemWithAttachment, itemWithoutAttachment],
751
+ [mockAttachment1, mockAttachment2],
752
+ );
753
+
754
+ attachmentRegistry = new AIContextRegistry<MockContextItem>().register(
755
+ attachmentProvider,
756
+ );
757
+ });
758
+
759
+ describe("getAttachmentsForContext", () => {
760
+ it("should return empty array for empty context IDs", async () => {
761
+ const attachments = await attachmentRegistry.getAttachmentsForContext(
762
+ [],
763
+ );
764
+ expect(attachments).toEqual([]);
765
+ });
766
+
767
+ it("should return empty array for non-existent context IDs", async () => {
768
+ const nonExistentIds = ["attachment://nonexistent" as ContextLocatorId];
769
+ const attachments =
770
+ await attachmentRegistry.getAttachmentsForContext(nonExistentIds);
771
+ expect(attachments).toEqual([]);
772
+ });
773
+
774
+ it("should get attachments from provider that supports them", async () => {
775
+ const contextIds = ["attachment://item1" as ContextLocatorId];
776
+ const attachments =
777
+ await attachmentRegistry.getAttachmentsForContext(contextIds);
778
+
779
+ expect(attachments).toHaveLength(1);
780
+ expect(attachments[0]).toEqual(mockAttachment1);
781
+ });
782
+
783
+ it("should not get attachments for items that don't need them", async () => {
784
+ const contextIds = ["attachment://item2" as ContextLocatorId];
785
+ const attachments =
786
+ await attachmentRegistry.getAttachmentsForContext(contextIds);
787
+
788
+ expect(attachments).toHaveLength(0);
789
+ });
790
+
791
+ it("should not get attachments from providers with default implementation", async () => {
792
+ const simpleRegistry =
793
+ new AIContextRegistry<MockContextItem>().register(mockProvider);
794
+
795
+ const contextIds = ["mock://item1" as ContextLocatorId];
796
+ const attachments =
797
+ await simpleRegistry.getAttachmentsForContext(contextIds);
798
+
799
+ expect(attachments).toHaveLength(0);
800
+ });
801
+
802
+ it("should handle multiple context IDs from different providers", async () => {
803
+ const mixedRegistry = new AIContextRegistry<MockContextItem>()
804
+ .register(attachmentProvider)
805
+ .register(mockProvider);
806
+
807
+ const contextIds = [
808
+ "attachment://item1" as ContextLocatorId, // should have attachment
809
+ "attachment://item2" as ContextLocatorId, // should not have attachment
810
+ "mock://item1" as ContextLocatorId, // provider doesn't support attachments
811
+ ];
812
+
813
+ const attachments =
814
+ await mixedRegistry.getAttachmentsForContext(contextIds);
815
+
816
+ // Only attachment://item1 should contribute an attachment
817
+ expect(attachments).toHaveLength(1);
818
+ expect(attachments[0]).toEqual(mockAttachment1);
819
+ });
820
+
821
+ it("should handle multiple items needing attachments", async () => {
822
+ const multiItem1: MockContextItem = {
823
+ uri: "attachment://multi1" as ContextLocatorId,
824
+ name: "multi1",
825
+ type: "attachment",
826
+ description: "Multi item 1",
827
+ data: { needsAttachment: true, value: "multi1" },
828
+ };
829
+
830
+ const multiItem2: MockContextItem = {
831
+ uri: "attachment://multi2" as ContextLocatorId,
832
+ name: "multi2",
833
+ type: "attachment",
834
+ description: "Multi item 2",
835
+ data: { needsAttachment: true, value: "multi2" },
836
+ };
837
+
838
+ const multiProvider = new AttachmentContextProvider(
839
+ [multiItem1, multiItem2],
840
+ [mockAttachment1, mockAttachment2],
841
+ );
842
+
843
+ const multiRegistry = new AIContextRegistry<MockContextItem>().register(
844
+ multiProvider,
845
+ );
846
+
847
+ const contextIds = [
848
+ "attachment://multi1" as ContextLocatorId,
849
+ "attachment://multi2" as ContextLocatorId,
850
+ ];
851
+
852
+ const attachments =
853
+ await multiRegistry.getAttachmentsForContext(contextIds);
854
+
855
+ expect(attachments).toHaveLength(2);
856
+ expect(attachments).toContainEqual(mockAttachment1);
857
+ expect(attachments).toContainEqual(mockAttachment2);
858
+ });
859
+
860
+ it("should handle provider errors gracefully", async () => {
861
+ const errorProvider = new AttachmentContextProvider([
862
+ {
863
+ uri: "attachment://error-item" as ContextLocatorId,
864
+ name: "error-item",
865
+ type: "attachment",
866
+ description: "Item that causes error",
867
+ data: { needsAttachment: true, value: "error" },
868
+ },
869
+ ]);
870
+
871
+ // Override getAttachments to throw an error
872
+ errorProvider.getAttachments = vi
873
+ .fn()
874
+ .mockRejectedValue(new Error("Attachment error"));
875
+
876
+ const errorRegistry = new AIContextRegistry<MockContextItem>()
877
+ .register(errorProvider)
878
+ .register(mockProvider);
879
+
880
+ const contextIds = [
881
+ "attachment://error-item" as ContextLocatorId,
882
+ "mock://item1" as ContextLocatorId,
883
+ ];
884
+
885
+ // Should not throw and should handle the error gracefully
886
+ const attachments =
887
+ await errorRegistry.getAttachmentsForContext(contextIds);
888
+ expect(attachments).toEqual([]);
889
+ });
890
+ });
891
+
892
+ describe("provider batching for attachments", () => {
893
+ it("should batch multiple items to the same provider", async () => {
894
+ const getAttachmentsSpy = vi.spyOn(
895
+ attachmentProvider,
896
+ "getAttachments",
897
+ );
898
+
899
+ const contextIds = [
900
+ "attachment://item1" as ContextLocatorId,
901
+ "attachment://item2" as ContextLocatorId,
902
+ ];
903
+
904
+ await attachmentRegistry.getAttachmentsForContext(contextIds);
905
+
906
+ // Should call getAttachments once with both items
907
+ expect(getAttachmentsSpy).toHaveBeenCalledTimes(1);
908
+ expect(getAttachmentsSpy).toHaveBeenCalledWith(
909
+ expect.arrayContaining([
910
+ expect.objectContaining({ uri: "attachment://item1" }),
911
+ expect.objectContaining({ uri: "attachment://item2" }),
912
+ ]),
913
+ );
914
+ });
915
+
916
+ it("should call different providers separately", async () => {
917
+ const mixedRegistry = new AIContextRegistry<MockContextItem>()
918
+ .register(attachmentProvider)
919
+ .register(mockProvider);
920
+
921
+ const attachmentSpy = vi.spyOn(attachmentProvider, "getAttachments");
922
+ const mockSpy = vi.spyOn(mockProvider, "getAttachments");
923
+
924
+ const contextIds = [
925
+ "attachment://item1" as ContextLocatorId,
926
+ "mock://item1" as ContextLocatorId,
927
+ ];
928
+
929
+ await mixedRegistry.getAttachmentsForContext(contextIds);
930
+
931
+ // Should call each provider once
932
+ expect(attachmentSpy).toHaveBeenCalledTimes(1);
933
+ expect(mockSpy).toHaveBeenCalledTimes(1);
934
+
935
+ expect(attachmentSpy).toHaveBeenCalledWith([
936
+ expect.objectContaining({ uri: "attachment://item1" }),
937
+ ]);
938
+ expect(mockSpy).toHaveBeenCalledWith([
939
+ expect.objectContaining({ uri: "mock://item1" }),
940
+ ]);
941
+ });
942
+ });
943
+ });
670
944
  });
@@ -1,9 +1,12 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
2
 
3
3
  import { allTablesAtom } from "@/core/datasets/data-source-connections";
4
+ import { getRequestClient } from "@/core/network/requests";
4
5
  import type { JotaiStore } from "@/core/state/jotai";
5
6
  import { variablesAtom } from "@/core/variables/state";
7
+ import { CellOutputContextProvider } from "./providers/cell-output";
6
8
  import { ErrorContextProvider } from "./providers/error";
9
+ import { FileContextProvider } from "./providers/file";
7
10
  import { TableContextProvider } from "./providers/tables";
8
11
  import { VariableContextProvider } from "./providers/variable";
9
12
  import { AIContextRegistry } from "./registry";
@@ -11,8 +14,15 @@ import { AIContextRegistry } from "./registry";
11
14
  export function getAIContextRegistry(store: JotaiStore) {
12
15
  const tablesMap = store.get(allTablesAtom);
13
16
  const variables = store.get(variablesAtom);
17
+
14
18
  return new AIContextRegistry()
15
19
  .register(new TableContextProvider(tablesMap))
16
20
  .register(new VariableContextProvider(variables, tablesMap))
17
- .register(new ErrorContextProvider(store));
21
+ .register(new ErrorContextProvider(store))
22
+ .register(new CellOutputContextProvider(store));
23
+ }
24
+
25
+ export function getFileContextProvider(): FileContextProvider {
26
+ const apiRequests = getRequestClient();
27
+ return new FileContextProvider(apiRequests);
18
28
  }