@marimo-team/islands 0.15.2 → 0.15.3

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 (189) hide show
  1. package/dist/{ConnectedDataExplorerComponent-C39nQwtD.js → ConnectedDataExplorerComponent-DfvW3rBn.js} +323 -328
  2. package/dist/{ImageComparisonComponent-BhkiyswP.js → ImageComparisonComponent-XaJshw7d.js} +13 -13
  3. package/dist/{_baseUniq-DdHL34FO.js → _baseUniq-dN9WKF9m.js} +67 -67
  4. package/dist/any-language-editor-CpFniVi-.js +27 -0
  5. package/dist/{arc-BXrety1g.js → arc-BOhn-m2C.js} +1 -1
  6. package/dist/{architectureDiagram-KFL7JDKH-BMy6ywCF.js → architectureDiagram-W76B3OCA-Bpg85ZKv.js} +144 -144
  7. package/dist/assets/{worker-COGufAQn.js → worker-Y-Q4G-N2.js} +30 -26
  8. package/dist/asterisk-DS281yxp.js +271 -0
  9. package/dist/{blockDiagram-ZYB65J3Q-DYT2-nlI.js → blockDiagram-QIGZ2CNN-DS1kOHlW.js} +10 -10
  10. package/dist/{c4Diagram-AAMF2YG6-ZiQzioe6.js → c4Diagram-FPNF74CW-CyRVKssw.js} +8 -8
  11. package/dist/{channel-CeuXqUAU.js → channel-BilGXox7.js} +1 -1
  12. package/dist/{chunk-ANTBXLJU-BvYnIrdq.js → chunk-4BX2VUAB-CZR39zCO.js} +1 -1
  13. package/dist/{chunk-WVR4S24B-DXj8yaUk.js → chunk-55IACEB6-BIH-MYov.js} +1 -1
  14. package/dist/{chunk-GLLZNHP4-CyFsosAe.js → chunk-FMBD7UC4-4PZXFZE8.js} +1 -1
  15. package/dist/{chunk-JBRWN2VN-DA_EEhy2.js → chunk-K7UQS3LO-CEvWKznk.js} +117 -117
  16. package/dist/{chunk-NRVI72HA-BYx2jMlI.js → chunk-QN33PNHL-D5LO5Jq_.js} +1 -1
  17. package/dist/{chunk-FHKO5MBM-DfCztBk8.js → chunk-QZHKN3VN-6gwUonWI.js} +1 -1
  18. package/dist/{chunk-LXBSTHXV-Se7vdY6J.js → chunk-TVAH2DTR-3gm06QdU.js} +7 -7
  19. package/dist/{chunk-OMD6QJNC-CqgcPMgL.js → chunk-TZMSLE5B-Cm8Iy9bO.js} +1 -1
  20. package/dist/{classDiagram-v2-QTMF73CY-B19A3G1l.js → classDiagram-KNZD7YFC-DC529O_z.js} +2 -2
  21. package/dist/{classDiagram-3BZAVTQC-B19A3G1l.js → classDiagram-v2-RKCZMP56-DC529O_z.js} +2 -2
  22. package/dist/{clone-78au0tn1.js → clone-CLoRX3j6.js} +1 -1
  23. package/dist/cose-bilkent-S5V4N54A-qf5DlS6Y.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-Ceocls0m.js} +6 -6
  26. package/dist/{data-grid-overlay-editor-CH_qLkV2.js → data-grid-overlay-editor-AqDS_UKe.js} +11 -11
  27. package/dist/{diagram-4IRLE6MV-CL8xidnG.js → diagram-N5W7TBWH-CP66oSiv.js} +59 -60
  28. package/dist/{diagram-RP2FKANI-B1BPcUew.js → diagram-QEK2KX5R-_YD4kxxi.js} +15 -15
  29. package/dist/{diagram-GUPCWM2R-CZ5cfqlq.js → diagram-S2PKOQOG-Cnj8T-OP.js} +10 -10
  30. package/dist/dockerfile-Cm8cRYCN.js +194 -0
  31. package/dist/ebnf-DUPDuY4r.js +78 -0
  32. package/dist/{erDiagram-HZWUO2LU-BEAIww50.js → erDiagram-AWTI2OKA-CGnvoHx6.js} +8 -8
  33. package/dist/fcl-CPC2WYrI.js +103 -0
  34. package/dist/{flowDiagram-THRYKUMA-Czs2UAI2.js → flowDiagram-PVAE7QVJ-DG-pr9R9.js} +9 -9
  35. package/dist/{ganttDiagram-WV7ZQ7D5-ByYIAVFO.js → ganttDiagram-OWAHRB6G-JmChtxvn.js} +34 -34
  36. package/dist/{gitGraphDiagram-OJR772UL-BcpDsiyB.js → gitGraphDiagram-NY62KEGX-D8wLfOPd.js} +4 -4
  37. package/dist/{glide-data-editor-CmN6FVyi.js → glide-data-editor-9nC3iCIZ.js} +33 -33
  38. package/dist/{graph-77W6heli.js → graph-CoRe7vAN.js} +3 -3
  39. package/dist/http-D9LttvKF.js +44 -0
  40. package/dist/{index-Bfk9dnyS.js → index-6qYeHHjQ.js} +33090 -32892
  41. package/dist/{index-BOojn38D.js → index-BpzLh4Qe.js} +7711 -7711
  42. package/dist/{index-CmozKMxx.js → index-BthgsgYX.js} +6 -6
  43. package/dist/{index-pBmAzQJl.js → index-MCx5v1x0.js} +2 -2
  44. package/dist/index-jkm77Jrz.js +98 -0
  45. package/dist/{infoDiagram-6WOFNB3A-CfzLHHVP.js → infoDiagram-STP46IZ2-BlXxvOrR.js} +2 -2
  46. package/dist/{journeyDiagram-FFXJYRFH-ndAcpkGn.js → journeyDiagram-BIP6EPQ6-CNRYs_Fc.js} +24 -26
  47. package/dist/{kanban-definition-KOZQBZVT-DcQYzNvc.js → kanban-definition-6OIFK2YF-B9HeMAuP.js} +14 -14
  48. package/dist/{layout-XySVHJgD.js → layout-m2vOUiW1.js} +81 -81
  49. package/dist/{linear-PbooOqg7.js → linear-DU6Q5CX3.js} +35 -35
  50. package/dist/{main-B5yML0bw.js → main-BD2KGFpU.js} +74594 -68034
  51. package/dist/main.js +1 -1
  52. package/dist/{mermaid-Cg5IX6Nv.js → mermaid-HVCtvbyx.js} +6160 -7493
  53. package/dist/min-DcGMA4e_.js +80 -0
  54. package/dist/mindmap-definition-Q6HEUPPD-BW8UmIDQ.js +785 -0
  55. package/dist/nginx-zDPm3Z74.js +89 -0
  56. package/dist/{number-overlay-editor-DUhfZqtP.js → number-overlay-editor-D8Hl0Syo.js} +19 -19
  57. package/dist/{pieDiagram-DBDJKBY4-DTOlNsja.js → pieDiagram-ADFJNKIX-Bg-3zg5U.js} +17 -17
  58. package/dist/{quadrantDiagram-YPSRARAO-BX2d8VS-.js → quadrantDiagram-LMRXKWRM-BO4IG6Yz.js} +6 -6
  59. package/dist/{react-plotly-Dcyw-3Sa.js → react-plotly-dkvHVuRb.js} +3577 -3577
  60. package/dist/{requirementDiagram-EGVEC5DT-D1T5u-wG.js → requirementDiagram-4UW4RH46-5sdTguSM.js} +7 -7
  61. package/dist/{sankeyDiagram-HRAUVNP4-G6xDfnp-.js → sankeyDiagram-GR3RE2ED-Buhlv9OI.js} +5 -5
  62. package/dist/sequenceDiagram-C3RYC4MD-C3qsM2UP.js +2519 -0
  63. package/dist/{slides-component-BJLlPJSr.js → slides-component-D209A0-s.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-CopJ7G6P.js} +5 -5
  67. package/dist/{stateDiagram-v2-EYPG3UTE-Br1HYKT6.js → stateDiagram-v2-UMBNRL4Z-CejL8AKf.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-BwSBitlN.js} +58 -58
  72. package/dist/{timeline-definition-3HZDQTIS-DeK_ZRD0.js → timeline-definition-XQNQX7LJ-DzMNTX-C.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-zeJG07dk.js} +14 -14
  75. package/dist/{vega-component-CpgdqX2d.js → vega-component-CUkiTayd.js} +30 -30
  76. package/dist/{xychartDiagram-FDP5SA34-AMEPsx_R.js → xychartDiagram-6GGTOJPD-DiENNXMS.js} +7 -7
  77. package/package.json +39 -39
  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/app-config/ai-config.tsx +7 -0
  84. package/src/components/chat/chat-components.tsx +71 -0
  85. package/src/components/chat/chat-panel.tsx +481 -291
  86. package/src/components/chat/chat-utils.ts +50 -0
  87. package/src/components/chat/markdown-renderer.tsx +3 -7
  88. package/src/components/chat/tool-call-accordion.tsx +5 -5
  89. package/src/components/datasources/__tests__/utils.test.ts +6 -0
  90. package/src/components/datasources/column-preview.tsx +1 -3
  91. package/src/components/editor/actions/useNotebookActions.tsx +1 -1
  92. package/src/components/editor/ai/add-cell-with-ai.tsx +20 -15
  93. package/src/components/editor/ai/ai-completion-editor.tsx +22 -3
  94. package/src/components/editor/ai/completion-handlers.tsx +2 -4
  95. package/src/components/editor/ai/completion-utils.ts +85 -11
  96. package/src/components/editor/alerts/startup-logs-alert.tsx +72 -0
  97. package/src/components/editor/chrome/panels/datasources-panel.tsx +3 -1
  98. package/src/components/editor/chrome/panels/dependency-graph-panel.tsx +3 -1
  99. package/src/components/editor/chrome/panels/documentation-panel.tsx +3 -1
  100. package/src/components/editor/chrome/panels/error-panel.tsx +3 -1
  101. package/src/components/editor/chrome/panels/file-explorer-panel.tsx +3 -1
  102. package/src/components/editor/chrome/panels/logs-panel.tsx +3 -1
  103. package/src/components/editor/chrome/panels/outline-panel.tsx +3 -1
  104. package/src/components/editor/chrome/panels/packages-panel.tsx +4 -2
  105. package/src/components/editor/chrome/panels/scratchpad-panel.tsx +3 -1
  106. package/src/components/editor/chrome/panels/secrets-panel.tsx +3 -1
  107. package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -1
  108. package/src/components/editor/chrome/panels/tracing-panel.tsx +3 -1
  109. package/src/components/editor/chrome/panels/variable-panel.tsx +3 -1
  110. package/src/components/editor/chrome/wrapper/app-chrome.tsx +38 -28
  111. package/src/components/editor/controls/command-palette-button.tsx +1 -1
  112. package/src/components/editor/controls/command-palette.tsx +5 -4
  113. package/src/components/editor/controls/state.ts +4 -0
  114. package/src/components/editor/package-alert.tsx +108 -58
  115. package/src/components/editor/renderers/CellArray.tsx +2 -0
  116. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +0 -1
  117. package/src/components/pages/edit-page.tsx +7 -3
  118. package/src/core/ai/chat-utils.ts +26 -43
  119. package/src/core/ai/config.ts +1 -1
  120. package/src/core/ai/context/__tests__/registry.test.ts +277 -3
  121. package/src/core/ai/context/context.ts +11 -1
  122. package/src/core/ai/context/providers/__tests__/cell-output.test.ts +378 -0
  123. package/src/core/ai/context/providers/__tests__/error.test.ts +3 -2
  124. package/src/core/ai/context/providers/__tests__/file.test.ts +119 -0
  125. package/src/core/ai/context/providers/cell-output.ts +349 -0
  126. package/src/core/ai/context/providers/common.ts +5 -1
  127. package/src/core/ai/context/providers/file.ts +287 -0
  128. package/src/core/ai/context/registry.ts +79 -0
  129. package/src/core/ai/state.ts +22 -5
  130. package/src/core/alerts/state.ts +71 -3
  131. package/src/core/cells/cell.ts +2 -2
  132. package/src/core/cells/cells.ts +1 -1
  133. package/src/core/cells/logs.ts +1 -1
  134. package/src/core/cells/runs.ts +6 -5
  135. package/src/core/codemirror/ai/resources.ts +47 -5
  136. package/src/core/codemirror/ai/state.ts +12 -0
  137. package/src/core/codemirror/language/__tests__/sql.test.ts +45 -0
  138. package/src/core/codemirror/markdown/__tests__/commands.test.ts +1 -0
  139. package/src/core/codemirror/theme/dark.ts +1 -1
  140. package/src/core/config/capabilities.ts +1 -1
  141. package/src/core/datasets/__tests__/data-source.test.ts +24 -0
  142. package/src/core/errors/__tests__/errors.test.ts +2 -0
  143. package/src/core/islands/bridge.ts +1 -0
  144. package/src/core/islands/main.ts +1 -0
  145. package/src/core/kernel/messages.ts +12 -6
  146. package/src/core/layout/layout.ts +3 -3
  147. package/src/core/network/requests-network.ts +8 -0
  148. package/src/core/network/requests-static.ts +1 -0
  149. package/src/core/network/requests-toasting.ts +1 -0
  150. package/src/core/network/types.ts +4 -1
  151. package/src/core/wasm/bridge.ts +18 -2
  152. package/src/core/wasm/worker/bootstrap.ts +3 -1
  153. package/src/core/wasm/worker/getMarimoWheel.ts +3 -8
  154. package/src/core/wasm/worker/types.ts +3 -0
  155. package/src/core/websocket/useMarimoWebSocket.tsx +7 -1
  156. package/src/css/app/Cell.css +42 -21
  157. package/src/css/app/codemirror.css +5 -1
  158. package/src/css/globals.css +3 -0
  159. package/src/css/md.css +1 -1
  160. package/src/plugins/impl/MicrophonePlugin.tsx +2 -2
  161. package/src/plugins/impl/chat/ChatPlugin.tsx +2 -9
  162. package/src/plugins/impl/chat/chat-ui.tsx +129 -110
  163. package/src/plugins/impl/chat/types.ts +5 -8
  164. package/src/plugins/impl/code/__tests__/language.test.ts +15 -0
  165. package/src/plugins/impl/code/any-language-editor.tsx +11 -8
  166. package/src/plugins/layout/MimeRenderPlugin.tsx +3 -6
  167. package/src/stories/cell.stories.tsx +6 -0
  168. package/src/stories/layout/vertical/one-column.stories.tsx +215 -0
  169. package/src/theme/useTheme.ts +11 -6
  170. package/src/utils/__tests__/blob.test.ts +37 -0
  171. package/src/utils/arrays.ts +13 -0
  172. package/src/utils/fileToBase64.ts +21 -6
  173. package/src/utils/json/base64.ts +5 -2
  174. package/src/utils/numbers.ts +9 -7
  175. package/dist/any-language-editor-DC5170DQ.js +0 -45
  176. package/dist/asn1-jKiBa2Ya.js +0 -95
  177. package/dist/clojure-CCKyeQKf.js +0 -800
  178. package/dist/css-BkF-NPzE.js +0 -1553
  179. package/dist/index-5ZH_qS8j.js +0 -288
  180. package/dist/index-U4yn89qO.js +0 -341
  181. package/dist/javascript-C2yteZeJ.js +0 -691
  182. package/dist/min-DS5Jz-hg.js +0 -80
  183. package/dist/mindmap-definition-LNHGMQRG-0aOVaMR8.js +0 -3234
  184. package/dist/mllike-BSnXJBGA.js +0 -272
  185. package/dist/pug-CwAQJzGR.js +0 -248
  186. package/dist/python-BkR3uSy8.js +0 -313
  187. package/dist/rpm-IznJm2Xc.js +0 -57
  188. package/dist/sequenceDiagram-WFGC7UMF-DMhHzllb.js +0 -2284
  189. package/dist/ttcn-cfg-Bac_acMi.js +0 -88
@@ -0,0 +1,349 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import type { Completion } from "@codemirror/autocomplete";
4
+ import type { FileUIPart } from "ai";
5
+ import { toPng } from "html-to-image";
6
+ import { notebookAtom } from "@/core/cells/cells";
7
+ import { type CellId, CellOutputId } from "@/core/cells/ids";
8
+ import { displayCellName } from "@/core/cells/names";
9
+ import { isOutputEmpty } from "@/core/cells/outputs";
10
+ import type { OutputMessage } from "@/core/kernel/messages";
11
+ import type { JotaiStore } from "@/core/state/jotai";
12
+ import { Logger } from "@/utils/Logger";
13
+ import { type AIContextItem, AIContextProvider } from "../registry";
14
+ import { contextToXml } from "../utils";
15
+ import { Boosts } from "./common";
16
+
17
+ export interface CellOutputContextItem extends AIContextItem {
18
+ type: "cell-output";
19
+ data: {
20
+ cellId: CellId;
21
+ cellName: string;
22
+ cellCode: string;
23
+ output: OutputMessage;
24
+ outputType: "text" | "media";
25
+ processedContent?: string;
26
+ imageUrl?: string;
27
+ shouldDownloadImage?: boolean;
28
+ };
29
+ }
30
+
31
+ function isMediaMimetype(
32
+ mimetype: OutputMessage["mimetype"] | undefined,
33
+ htmlString: string,
34
+ ): boolean {
35
+ if (!mimetype) {
36
+ return false;
37
+ }
38
+
39
+ const mediaPrefixes = ["image/", "video/", "audio/", "application/pdf"];
40
+ if (mediaPrefixes.some((prefix) => mimetype.startsWith(prefix))) {
41
+ return true;
42
+ }
43
+ const mediaIncludes = ["svg", "vega"];
44
+ if (mediaIncludes.some((include) => mimetype.includes(include))) {
45
+ return true;
46
+ }
47
+
48
+ // If it is HTML, we need to check if it contains a media tag
49
+ if (mimetype === "text/html") {
50
+ const mediaTags = [
51
+ "<img",
52
+ "<video",
53
+ "<audio",
54
+ "<iframe",
55
+ "<canvas",
56
+ "<svg",
57
+ "<marimo-ui-element",
58
+ ];
59
+ if (mediaTags.some((tag) => htmlString.includes(tag))) {
60
+ return true;
61
+ }
62
+ }
63
+
64
+ return false;
65
+ }
66
+
67
+ function parseHtmlContent(htmlString: string): string {
68
+ try {
69
+ // Create a temporary DOM element to parse HTML
70
+ const tempDiv = document.createElement("div");
71
+ tempDiv.innerHTML = htmlString;
72
+
73
+ // Extract text content, removing HTML tags
74
+ const textContent = tempDiv.textContent || tempDiv.innerText || "";
75
+
76
+ // Clean up extra whitespace
77
+ return textContent.replaceAll(/\s+/g, " ").trim();
78
+ } catch (error) {
79
+ Logger.error("Error parsing HTML content:", error);
80
+ // If parsing fails, return the original string
81
+ return htmlString;
82
+ }
83
+ }
84
+
85
+ export class CellOutputContextProvider extends AIContextProvider<CellOutputContextItem> {
86
+ readonly title = "Cell Outputs";
87
+ readonly mentionPrefix = "@";
88
+ readonly contextType = "cell-output";
89
+
90
+ constructor(private store: JotaiStore) {
91
+ super();
92
+ }
93
+
94
+ getItems(): CellOutputContextItem[] {
95
+ const notebook = this.store.get(notebookAtom);
96
+ const items: CellOutputContextItem[] = [];
97
+
98
+ for (const [cellIndex, cellId] of notebook.cellIds.inOrderIds.entries()) {
99
+ const cellRuntime = notebook.cellRuntime[cellId];
100
+
101
+ // Filter to only cells with output
102
+ if (!cellRuntime?.output || isOutputEmpty(cellRuntime.output)) {
103
+ continue;
104
+ }
105
+
106
+ const cellData = notebook.cellData[cellId];
107
+ const output = cellRuntime.output;
108
+ const mimetype = output.mimetype;
109
+
110
+ // Determine output type
111
+ const isMedia = isMediaMimetype(mimetype, String(output.data));
112
+ const outputType = isMedia ? "media" : "text";
113
+
114
+ const cellName = displayCellName(cellData.name, cellIndex);
115
+
116
+ let processedContent: string | undefined;
117
+ let imageUrl: string | undefined;
118
+ let shouldDownloadImage = false;
119
+
120
+ // Process text content
121
+ if (outputType === "text" && typeof output.data === "string") {
122
+ processedContent =
123
+ mimetype === "text/html"
124
+ ? parseHtmlContent(output.data)
125
+ : output.data;
126
+ }
127
+
128
+ // Process media content - for now, we'll just note that it's media
129
+ if (outputType === "media") {
130
+ if (
131
+ typeof output.data === "string" &&
132
+ output.data.startsWith("data:")
133
+ ) {
134
+ imageUrl = output.data; // Data URL
135
+ } else if (
136
+ typeof output.data === "string" &&
137
+ output.data.startsWith("http")
138
+ ) {
139
+ imageUrl = output.data; // External URL
140
+ } else {
141
+ // Download the image from the DOM element
142
+ shouldDownloadImage = true;
143
+ }
144
+ }
145
+
146
+ items.push({
147
+ uri: this.asURI(cellId),
148
+ name: cellName,
149
+ type: this.contextType,
150
+ description: `Cell output (${mimetype || "unknown"})`,
151
+ data: {
152
+ cellId,
153
+ cellName,
154
+ cellCode: cellData?.code || "",
155
+ output,
156
+ outputType,
157
+ processedContent,
158
+ imageUrl,
159
+ shouldDownloadImage,
160
+ },
161
+ });
162
+ }
163
+
164
+ return items;
165
+ }
166
+
167
+ formatCompletion(item: CellOutputContextItem): Completion {
168
+ const { data } = item;
169
+ return {
170
+ label: `@${data.cellName}`,
171
+ displayLabel: data.cellName,
172
+ detail: `${data.outputType} output`,
173
+ boost: Boosts.CELL_OUTPUT,
174
+ type: this.contextType,
175
+ section: "Cell Output",
176
+ apply: `@${data.cellName}`,
177
+ info: () => {
178
+ const infoContainer = document.createElement("div");
179
+ infoContainer.classList.add(
180
+ "mo-cm-tooltip",
181
+ "docs-documentation",
182
+ "min-w-[300px]",
183
+ "max-w-[500px]",
184
+ "flex",
185
+ "flex-col",
186
+ "gap-2",
187
+ );
188
+
189
+ const headerDiv = document.createElement("div");
190
+ headerDiv.classList.add("flex", "flex-col", "gap-1");
191
+
192
+ const nameDiv = document.createElement("div");
193
+ nameDiv.classList.add("font-bold", "text-base");
194
+ nameDiv.textContent = data.cellName;
195
+ headerDiv.append(nameDiv);
196
+
197
+ const descriptionDiv = document.createElement("div");
198
+ descriptionDiv.classList.add("text-sm", "text-muted-foreground");
199
+ headerDiv.append(descriptionDiv);
200
+
201
+ infoContainer.append(headerDiv);
202
+
203
+ // Show cell code preview
204
+ if (data.cellCode) {
205
+ const codeHeaderDiv = document.createElement("div");
206
+ codeHeaderDiv.classList.add(
207
+ "text-xs",
208
+ "font-medium",
209
+ "text-muted-foreground",
210
+ );
211
+ codeHeaderDiv.textContent = "Code:";
212
+ infoContainer.append(codeHeaderDiv);
213
+
214
+ const codeDiv = document.createElement("div");
215
+ codeDiv.classList.add(
216
+ "text-xs",
217
+ "font-mono",
218
+ "bg-muted",
219
+ "p-2",
220
+ "rounded",
221
+ "max-h-20",
222
+ "overflow-y-auto",
223
+ );
224
+ codeDiv.textContent =
225
+ data.cellCode.slice(0, 200) +
226
+ (data.cellCode.length > 200 ? "..." : "");
227
+ infoContainer.append(codeDiv);
228
+ }
229
+
230
+ // Show output preview
231
+ if (data.processedContent) {
232
+ const outputHeaderDiv = document.createElement("div");
233
+ outputHeaderDiv.classList.add(
234
+ "text-xs",
235
+ "font-medium",
236
+ "text-muted-foreground",
237
+ "mt-2",
238
+ );
239
+ outputHeaderDiv.textContent = "Output Preview:";
240
+ infoContainer.append(outputHeaderDiv);
241
+
242
+ const outputDiv = document.createElement("div");
243
+ outputDiv.classList.add(
244
+ "text-xs",
245
+ "bg-muted",
246
+ "p-2",
247
+ "rounded",
248
+ "max-h-24",
249
+ "overflow-y-auto",
250
+ "mb-2",
251
+ );
252
+ outputDiv.textContent =
253
+ data.processedContent.slice(0, 300) +
254
+ (data.processedContent.length > 300 ? "..." : "");
255
+ infoContainer.append(outputDiv);
256
+ }
257
+
258
+ if (data.outputType === "media") {
259
+ const mediaDiv = document.createElement("div");
260
+ mediaDiv.classList.add(
261
+ "text-xs",
262
+ "text-muted-foreground",
263
+ "italic",
264
+ "mb-2",
265
+ );
266
+ mediaDiv.textContent =
267
+ "Contains media content (image, SVG, or canvas)";
268
+ infoContainer.append(mediaDiv);
269
+ }
270
+
271
+ return infoContainer;
272
+ },
273
+ };
274
+ }
275
+
276
+ formatContext(item: CellOutputContextItem): string {
277
+ const { data } = item;
278
+
279
+ const contextData = {
280
+ name: data.cellName,
281
+ cellId: data.cellId,
282
+ outputType: data.outputType,
283
+ mimetype: data.output.mimetype,
284
+ } as const;
285
+
286
+ let details = `Cell Code:\n${data.cellCode}\n\n`;
287
+
288
+ if (data.outputType === "text" && data.processedContent) {
289
+ details += `Output:\n${data.processedContent}`;
290
+ } else if (data.outputType === "media") {
291
+ details += `Media Output: Contains ${data.output.mimetype} content`;
292
+ if (data.imageUrl) {
293
+ details += `\nImage URL: ${data.imageUrl}`;
294
+ }
295
+ }
296
+
297
+ return contextToXml({
298
+ type: this.contextType,
299
+ data: contextData,
300
+ details,
301
+ });
302
+ }
303
+
304
+ /** Get attachments for cell output items that have shouldDownloadImage=true */
305
+ override async getAttachments(
306
+ items: CellOutputContextItem[],
307
+ ): Promise<FileUIPart[]> {
308
+ // Filter items that need image downloading
309
+ const itemsNeedingDownload = items.filter(
310
+ (item) =>
311
+ item.data.shouldDownloadImage && item.data.outputType === "media",
312
+ );
313
+
314
+ if (itemsNeedingDownload.length === 0) {
315
+ return [];
316
+ }
317
+
318
+ // Prepare download requests
319
+ const downloadRequests = itemsNeedingDownload.flatMap((item) => {
320
+ const outputElement = document.getElementById(
321
+ CellOutputId.create(item.data.cellId),
322
+ );
323
+ if (!outputElement) {
324
+ Logger.warn(`Output element not found for cell ${item.data.cellId}`);
325
+ return [];
326
+ }
327
+ return {
328
+ cellId: item.data.cellId,
329
+ cellName: item.data.cellName,
330
+ mimetype: item.data.output.mimetype,
331
+ element: outputElement,
332
+ };
333
+ });
334
+
335
+ try {
336
+ return await Promise.all(
337
+ downloadRequests.map(async (item) => ({
338
+ type: "file",
339
+ filename: `${item.cellName}-output-screenshot`,
340
+ mediaType: "image/png",
341
+ url: await toPng(item.element),
342
+ })),
343
+ );
344
+ } catch (error) {
345
+ Logger.error("Error downloading cell output images:", error);
346
+ return [];
347
+ }
348
+ }
349
+ }
@@ -3,6 +3,10 @@
3
3
  export const Boosts = {
4
4
  LOCAL_TABLE: 5,
5
5
  REMOTE_TABLE: 4,
6
+ HIGH: 4,
6
7
  VARIABLE: 3,
7
- ERROR: 2,
8
+ MEDIUM: 3,
9
+ CELL_OUTPUT: 2,
10
+ LOW: 2,
11
+ ERROR: 1,
8
12
  } as const;
@@ -0,0 +1,287 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import {
4
+ type Completion,
5
+ type CompletionContext,
6
+ type CompletionResult,
7
+ type CompletionSource,
8
+ closeCompletion,
9
+ } from "@codemirror/autocomplete";
10
+ import { toast } from "@/components/ui/use-toast";
11
+ import { contextCallbacks } from "@/core/codemirror/ai/state";
12
+ import type { EditRequests, FileInfo, RunRequests } from "@/core/network/types";
13
+ import { deserializeBlob } from "@/utils/blob";
14
+ import { type Base64String, base64ToDataURL } from "@/utils/json/base64";
15
+ import { Logger } from "@/utils/Logger";
16
+ import { type AIContextItem, AIContextProvider } from "../registry";
17
+ import { contextToXml } from "../utils";
18
+ import { Boosts } from "./common";
19
+ export interface FileContextItem extends AIContextItem {
20
+ type: "file";
21
+ data: {
22
+ path: string;
23
+ isDirectory: boolean;
24
+ };
25
+ }
26
+
27
+ export interface FileSearchConfig {
28
+ maxDepth: number;
29
+ maxResults: number;
30
+ defaultResultsLimit: number;
31
+ includeDirectories: boolean;
32
+ }
33
+
34
+ const DEFAULT_FILE_SEARCH_CONFIG: FileSearchConfig = {
35
+ maxDepth: 3,
36
+ maxResults: 20,
37
+ defaultResultsLimit: 5,
38
+ includeDirectories: false,
39
+ };
40
+
41
+ export class FileContextProvider extends AIContextProvider<FileContextItem> {
42
+ readonly title = "Files";
43
+ readonly mentionPrefix = "#";
44
+ readonly contextType = "file";
45
+
46
+ constructor(
47
+ private apiRequests: EditRequests & RunRequests,
48
+ private config: FileSearchConfig = DEFAULT_FILE_SEARCH_CONFIG,
49
+ ) {
50
+ super();
51
+ }
52
+
53
+ /**
54
+ * Create a dynamic completion source for file mentions
55
+ * This bypasses the standard registry system to enable dynamic searching
56
+ */
57
+ createCompletionSource(): CompletionSource {
58
+ return async (
59
+ context: CompletionContext,
60
+ ): Promise<CompletionResult | null> => {
61
+ // Look for # followed by any characters (including dots, slashes, etc.)
62
+ const match = context.matchBefore(/#[^\s#]*/);
63
+ if (!match) {
64
+ return null;
65
+ }
66
+
67
+ const matchText = match.text;
68
+ if (!matchText.startsWith("#")) {
69
+ return null;
70
+ }
71
+
72
+ const searchQuery = matchText.slice(1); // Remove the #
73
+ if (searchQuery.length === 0) {
74
+ // Show some popular files/directories even with no query
75
+ return this.getDefaultCompletions(match);
76
+ }
77
+
78
+ try {
79
+ const files = (await this.searchFiles(searchQuery)) || [];
80
+ const completions = files.map((file: FileInfo) => {
81
+ const item: FileContextItem = {
82
+ uri: this.asURI(file.path),
83
+ name: file.name,
84
+ type: this.contextType,
85
+ description: file.isDirectory ? "Directory" : "File",
86
+ data: {
87
+ path: file.path,
88
+ isDirectory: file.isDirectory,
89
+ },
90
+ };
91
+ return this.formatCompletion(item);
92
+ });
93
+
94
+ return {
95
+ from: match.from,
96
+ options: completions,
97
+ };
98
+ } catch (error) {
99
+ Logger.error("Failed to search files:", error);
100
+ return null;
101
+ }
102
+ };
103
+ }
104
+
105
+ private searchFiles = async (
106
+ query: string,
107
+ options: Partial<FileSearchConfig> = {},
108
+ ): Promise<FileInfo[]> => {
109
+ const { maxDepth, maxResults, includeDirectories } = {
110
+ ...this.config,
111
+ ...options,
112
+ };
113
+ const response = await this.apiRequests.sendSearchFiles({
114
+ query,
115
+ includeFiles: true,
116
+ includeDirectories: includeDirectories,
117
+ depth: maxDepth,
118
+ limit: maxResults,
119
+ });
120
+ return response.files;
121
+ };
122
+
123
+ private async getDefaultCompletions(match: {
124
+ from: number;
125
+ }): Promise<CompletionResult | null> {
126
+ try {
127
+ // Show common file types when no specific query is given
128
+ // Use broad searches for common file types
129
+ const searches = ["py", "md", "csv"];
130
+
131
+ // Try the first search that returns results
132
+ for (const search of searches) {
133
+ try {
134
+ const files = await this.searchFiles(search, {
135
+ maxDepth: 1,
136
+ maxResults: 5,
137
+ });
138
+
139
+ if (files && files.length > 0) {
140
+ const completions = files.map((file) => {
141
+ const item: FileContextItem = {
142
+ uri: this.asURI(file.path),
143
+ name: file.name,
144
+ type: this.contextType,
145
+ description: file.isDirectory ? "Directory" : "File",
146
+ data: {
147
+ path: file.path,
148
+ isDirectory: file.isDirectory,
149
+ },
150
+ };
151
+ return this.formatCompletion(item);
152
+ });
153
+
154
+ return {
155
+ from: match.from,
156
+ options: completions,
157
+ };
158
+ }
159
+ } catch (error) {
160
+ Logger.error("Failed to get default file completions:", error);
161
+ }
162
+ }
163
+
164
+ // If no searches return results, return empty
165
+ return {
166
+ from: match.from,
167
+ options: [],
168
+ };
169
+ } catch (error) {
170
+ Logger.error("Failed to get default file completions:", error);
171
+ return null;
172
+ }
173
+ }
174
+
175
+ getItems(): FileContextItem[] {
176
+ // Files are fetched dynamically, so return empty array
177
+ // This provider relies on dynamic fetching via createCompletionSource()
178
+ return [];
179
+ }
180
+
181
+ formatCompletion(item: FileContextItem): Completion {
182
+ const { data, name } = item;
183
+ const icon = data.isDirectory ? "📁" : "📄";
184
+
185
+ return {
186
+ ...this.createBasicCompletion(item),
187
+ type: "file",
188
+ section: "File",
189
+ boost: data.isDirectory ? Boosts.MEDIUM : Boosts.LOW,
190
+ detail: data.path,
191
+ displayLabel: `${icon} ${name}`,
192
+ apply: async (view, completion, from, to) => {
193
+ // First try to add the file as an attachment, if the callback is provided
194
+ // otherwise add it to the prompt
195
+ const addAttachment = view.state.facet(contextCallbacks)?.addAttachment;
196
+ if (!addAttachment) {
197
+ Logger.warn("No addAttachment callback provided");
198
+ return;
199
+ }
200
+
201
+ const fileDetails = await this.apiRequests
202
+ .sendFileDetails({ path: data.path })
203
+ .catch((error) => {
204
+ toast({
205
+ title: "Failed to get file details",
206
+ description: error.message,
207
+ });
208
+ return null;
209
+ });
210
+
211
+ if (!fileDetails) {
212
+ return;
213
+ }
214
+
215
+ const mimeType = fileDetails.mimeType || "text/plain";
216
+
217
+ // Handle binary vs text files
218
+ let blob: Blob;
219
+ if (
220
+ mimeType.startsWith("text/") ||
221
+ mimeType.includes("json") ||
222
+ mimeType.includes("xml")
223
+ ) {
224
+ // Text files - create blob directly from contents
225
+ blob = new Blob([fileDetails.contents || ""], { type: mimeType });
226
+ } else {
227
+ // Binary files - use blob utility to decode base64
228
+ if (fileDetails.contents) {
229
+ try {
230
+ // Create data URL using utility and deserialize blob
231
+ const dataURL = base64ToDataURL(
232
+ fileDetails.contents as Base64String,
233
+ mimeType,
234
+ );
235
+ blob = await deserializeBlob(dataURL);
236
+ } catch {
237
+ // Fallback to treating as text
238
+ blob = new Blob([fileDetails.contents], { type: mimeType });
239
+ }
240
+ } else {
241
+ blob = new Blob([""], { type: mimeType });
242
+ }
243
+ }
244
+
245
+ const file = new File([blob], name, { type: mimeType });
246
+ addAttachment(file);
247
+
248
+ // Close completion and delete the entire mention text (from # to cursor)
249
+ view.dispatch({
250
+ changes: { from, to, insert: "" },
251
+ });
252
+
253
+ closeCompletion(view);
254
+ },
255
+ info: () => {
256
+ const element = document.createElement("div");
257
+ element.classList.add("flex", "flex-col", "gap-1", "p-2");
258
+
259
+ const title = document.createElement("div");
260
+ title.classList.add("font-bold");
261
+ title.textContent = name;
262
+ element.append(title);
263
+
264
+ const path = document.createElement("div");
265
+ path.classList.add("text-xs", "text-muted-foreground");
266
+ path.textContent = data.path;
267
+ element.append(path);
268
+ return element;
269
+ },
270
+ };
271
+ }
272
+
273
+ formatContext(item: FileContextItem): string {
274
+ const { data, name } = item;
275
+ return contextToXml({
276
+ type: this.contextType,
277
+ data: {
278
+ name: name,
279
+ path: data.path,
280
+ isDirectory: data.isDirectory,
281
+ },
282
+ details: data.isDirectory
283
+ ? "Directory containing files and subdirectories"
284
+ : "File",
285
+ });
286
+ }
287
+ }