@marimo-team/frontend 0.15.5 → 0.16.0

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 (223) hide show
  1. package/dist/assets/{ConnectedDataExplorerComponent-Cn5-l2X1.js → ConnectedDataExplorerComponent-BErMbWvG.js} +1 -1
  2. package/dist/assets/{ImageComparisonComponent-CEXMKKA4.js → ImageComparisonComponent-fTHv1Ih0.js} +1 -1
  3. package/dist/assets/{VegaLite-Bt14Ds9k.js → VegaLite-Bdi-TyfY.js} +6 -6
  4. package/dist/assets/_baseEach-CNBxBxvS.js +1 -0
  5. package/dist/assets/_baseMap-D1WHjKrd.js +1 -0
  6. package/dist/assets/_baseUniq-CCgDNtZb.js +1 -0
  7. package/dist/assets/_createAggregator-DcD0kTA5.js +1 -0
  8. package/dist/assets/agent-panel-Crv430aI.js +268 -0
  9. package/dist/assets/agent-panel-D92Mfy1i.css +1 -0
  10. package/dist/assets/{any-language-editor-DiwNT6zp.js → any-language-editor-CQh552Wu.js} +1 -1
  11. package/dist/assets/architectureDiagram-W76B3OCA-BAJeBxzt.js +36 -0
  12. package/dist/assets/{between-horizontal-start-FyewyCGn.js → between-horizontal-start-Boxgxbt_.js} +1 -1
  13. package/dist/assets/{blockDiagram-QIGZ2CNN-BrOkAf_c.js → blockDiagram-QIGZ2CNN-CL-1svEK.js} +1 -1
  14. package/dist/assets/{c4Diagram-FPNF74CW-BHPzDxE2.js → c4Diagram-FPNF74CW-BbEqbCTl.js} +5 -5
  15. package/dist/assets/channel-_2eNSz0n.js +1 -0
  16. package/dist/assets/chat-panel-CXh5Wl6C.js +3 -0
  17. package/dist/assets/{chunk-4BX2VUAB-DLxaCNYh.js → chunk-4BX2VUAB-C--8TXeE.js} +1 -1
  18. package/dist/assets/{chunk-55IACEB6-DdzvO3HR.js → chunk-55IACEB6-Bj00HDqq.js} +1 -1
  19. package/dist/assets/{chunk-FMBD7UC4-R5o-nSiG.js → chunk-FMBD7UC4-C-lhB6hN.js} +1 -1
  20. package/dist/assets/{chunk-K7UQS3LO-DxaMrGgG.js → chunk-K7UQS3LO-B-pGTXPt.js} +1 -1
  21. package/dist/assets/{chunk-QN33PNHL-DqS9-FYm.js → chunk-QN33PNHL-DqUzGhvm.js} +1 -1
  22. package/dist/assets/{chunk-QZHKN3VN-BZ-TzajS.js → chunk-QZHKN3VN-TntJHfSk.js} +1 -1
  23. package/dist/assets/{chunk-TVAH2DTR-BsgP2dyv.js → chunk-TVAH2DTR-HUJb1psV.js} +1 -1
  24. package/dist/assets/{chunk-TZMSLE5B-D-h3ahXI.js → chunk-TZMSLE5B-BK3C__t3.js} +1 -1
  25. package/dist/assets/{circle-play-CQtRZ-rT.js → circle-play-DBLOv1Yu.js} +1 -1
  26. package/dist/assets/classDiagram-KNZD7YFC-BGmh9POF.js +1 -0
  27. package/dist/assets/classDiagram-v2-RKCZMP56-BGmh9POF.js +1 -0
  28. package/dist/assets/{clear-button-BY6Z_ViL.js → clear-button-BeoFbEKH.js} +1 -1
  29. package/dist/assets/clone-BFDSPAj3.js +1 -0
  30. package/dist/assets/command-palette-CXZiSv0I.js +1 -0
  31. package/dist/assets/common-C7oJcmCT.js +1 -0
  32. package/dist/assets/{compile-Ct_jzdKr.js → compile-7L0MwhyI.js} +1 -1
  33. package/dist/assets/cose-bilkent-S5V4N54A-BMkGLcVC.js +1 -0
  34. package/dist/assets/dagre-5GWH7T2D-BJtRienS.js +4 -0
  35. package/dist/assets/{data-grid-overlay-editor-BN_wulc3.js → data-grid-overlay-editor-DBkmGtNs.js} +1 -1
  36. package/dist/assets/datasources-panel-B7FbYLiy.js +1 -0
  37. package/dist/assets/{dependency-graph-panel-BOmSCZf7.js → dependency-graph-panel-DEdOxp2X.js} +4 -4
  38. package/dist/assets/diagram-N5W7TBWH-CmECY3nb.js +24 -0
  39. package/dist/assets/diagram-QEK2KX5R-DMOVSNKD.js +43 -0
  40. package/dist/assets/diagram-S2PKOQOG-BiJ96PNQ.js +24 -0
  41. package/dist/assets/{documentation-panel-BxjJO_Gw.js → documentation-panel-xULhaEv3.js} +1 -1
  42. package/dist/assets/edit-page-BrYda9VE.js +129 -0
  43. package/dist/assets/{ellipsis-vertical-UHbmjI2n.js → ellipsis-vertical-BBqXIlc2.js} +1 -1
  44. package/dist/assets/{empty-state-BIBXzY_0.js → empty-state-B3dA3G5P.js} +1 -1
  45. package/dist/assets/{erDiagram-AWTI2OKA-E84mAle_.js → erDiagram-AWTI2OKA-MP1DiFRo.js} +1 -1
  46. package/dist/assets/{error-panel-MEvQ6K7h.js → error-panel-Cc1sv-Ag.js} +1 -1
  47. package/dist/assets/file-explorer-panel-Bw59Kva1.js +1 -0
  48. package/dist/assets/{flowDiagram-PVAE7QVJ-DfbIRSAW.js → flowDiagram-PVAE7QVJ-BX7caPp7.js} +1 -1
  49. package/dist/assets/{ganttDiagram-OWAHRB6G-DR4HZ1z_.js → ganttDiagram-OWAHRB6G-B462g4Yf.js} +3 -3
  50. package/dist/assets/gitGraphDiagram-NY62KEGX-CGgvZ9-9.js +65 -0
  51. package/dist/assets/{glide-data-editor-nNmo1lPq.js → glide-data-editor-C0gUFZON.js} +4 -4
  52. package/dist/assets/graph-CHRVBzY5.js +1 -0
  53. package/dist/assets/{home-page-9eW6qida.js → home-page-Fb2osjys.js} +3 -3
  54. package/dist/assets/{index-DMomwMcN.js → index-BVgAenPd.js} +1 -1
  55. package/dist/assets/{index-B8llrTSo.js → index-BY93Ejhl.js} +1 -1
  56. package/dist/assets/{index-BFSnz7iM.js → index-C-8WADat.js} +1 -1
  57. package/dist/assets/{index-CPN7TRA1.js → index-C-GhZ7ti.js} +1 -1
  58. package/dist/assets/{index-DyLSuOH1.js → index-C1v_Z9et.js} +1 -1
  59. package/dist/assets/{index-VPWqq2Pg.js → index-C4Tn5NvJ.js} +1 -1
  60. package/dist/assets/{index-BAH034Ue.js → index-C77h_TXN.js} +1 -1
  61. package/dist/assets/{index-Dt9UWeWn.js → index-CQDrxQ0j.js} +1 -1
  62. package/dist/assets/{index-DWOaniGT.js → index-CWMgowgL.js} +1 -1
  63. package/dist/assets/{index-B1_GXGaP.js → index-Clbi_Yaq.js} +1 -1
  64. package/dist/assets/{index-B7yXbrLa.js → index-CpTPJo4k.js} +1 -1
  65. package/dist/assets/{index-CknhX2Vy.css → index-Cx0bsY1w.css} +1 -1
  66. package/dist/assets/{index-DqzMPAC8.js → index-D1vmG6DS.js} +2 -2
  67. package/dist/assets/{index-c6If577Q.js → index-D9UKkrr2.js} +1 -1
  68. package/dist/assets/{index-CB2pnVQG.js → index-DEQvTChO.js} +1 -1
  69. package/dist/assets/{index-OC46250R.js → index-DKEudB02.js} +205 -197
  70. package/dist/assets/{index-CSgxTUzD.js → index-DRMm6SNo.js} +1 -1
  71. package/dist/assets/{index-Bq516OmX.js → index-DoRmcrKM.js} +1 -1
  72. package/dist/assets/{index-DSU75csX.js → index-lYa_leQE.js} +1 -1
  73. package/dist/assets/{index-BLu5CX6z.js → index-vmICa5KN.js} +1 -1
  74. package/dist/assets/{index-uacyUula.js → index-z9bohSQJ.js} +1 -1
  75. package/dist/assets/infoDiagram-STP46IZ2-CVyrdLc8.js +2 -0
  76. package/dist/assets/isEmpty-DU_ogP_D.js +1 -0
  77. package/dist/assets/{journeyDiagram-BIP6EPQ6-BBiFyygf.js → journeyDiagram-BIP6EPQ6-C6EgLP_Q.js} +1 -1
  78. package/dist/assets/{kanban-definition-6OIFK2YF-DhgA6Nt6.js → kanban-definition-6OIFK2YF-BXzYO1yj.js} +4 -4
  79. package/dist/assets/layout-jihVw5-i.js +1 -0
  80. package/dist/assets/linear-C4blANlC.js +1 -0
  81. package/dist/assets/{links-CbvGxbsJ.js → links-D59GIweI.js} +3 -3
  82. package/dist/assets/{logs-panel-B9SmTZAW.js → logs-panel-D401qzZh.js} +1 -1
  83. package/dist/assets/markdown-renderer-Cd9eYyaL.js +263 -0
  84. package/dist/assets/{agent-panel-DpQ6muj-.css → markdown-renderer-ClyzDMmG.css} +1 -1
  85. package/dist/assets/mermaid-BEVuRz_O.js +1 -0
  86. package/dist/assets/{mermaid.core-4nVOEVX3.js → mermaid.core-CaSnaLH0.js} +41 -41
  87. package/dist/assets/min-DUMu_zeK.js +1 -0
  88. package/dist/assets/{mindmap-definition-Q6HEUPPD-CVLQNn1q.js → mindmap-definition-Q6HEUPPD-BXUM5MT2.js} +2 -2
  89. package/dist/assets/{number-overlay-editor-CzRzXLcd.js → number-overlay-editor-4uWXGlPG.js} +1 -1
  90. package/dist/assets/{outline-panel-uvsS-YEQ.js → outline-panel-DIzkvm2I.js} +1 -1
  91. package/dist/assets/packages-panel-CJL0MVlj.js +1 -0
  92. package/dist/assets/{pieDiagram-ADFJNKIX-C5IQ5DBZ.js → pieDiagram-ADFJNKIX-Dxt5PVNo.js} +3 -3
  93. package/dist/assets/{quadrantDiagram-LMRXKWRM-CFXFnQxx.js → quadrantDiagram-LMRXKWRM-D4pUaA31.js} +1 -1
  94. package/dist/assets/{react-plotly-mzdv02_Y.js → react-plotly-cJZ0VWBq.js} +1 -1
  95. package/dist/assets/{requirementDiagram-4UW4RH46-D9bPC89T.js → requirementDiagram-4UW4RH46-DVRTjgas.js} +1 -1
  96. package/dist/assets/run-page-BUEnMC9w.js +1 -0
  97. package/dist/assets/sankeyDiagram-GR3RE2ED-CVFnD9C-.js +10 -0
  98. package/dist/assets/scratchpad-panel-BIgRENkI.js +1 -0
  99. package/dist/assets/secrets-panel-xY5-V_BD.js +1 -0
  100. package/dist/assets/{sequenceDiagram-C3RYC4MD-6N7_hY4k.js → sequenceDiagram-C3RYC4MD-_lY4ZN_S.js} +4 -4
  101. package/dist/assets/{slides-component-EcjC8sDK.js → slides-component-Xjymwj7X.js} +1 -1
  102. package/dist/assets/snippets-panel-CTPYW41n.js +1 -0
  103. package/dist/assets/sortBy-BNZKwiq_.js +1 -0
  104. package/dist/assets/state-C4NiC9tO.js +1 -0
  105. package/dist/assets/stateDiagram-KXAO66HF-Da0JQWCn.js +1 -0
  106. package/dist/assets/stateDiagram-v2-UMBNRL4Z-D5lYZOOt.js +1 -0
  107. package/dist/assets/storage-CMdLzB_c.js +26 -0
  108. package/dist/assets/terminal-BPwTkXae.js +10 -0
  109. package/dist/assets/time-Dv5_Ouz_.js +1 -0
  110. package/dist/assets/{timeline-definition-XQNQX7LJ-BEaynAiY.js → timeline-definition-XQNQX7LJ-Dxh5Zu2e.js} +1 -1
  111. package/dist/assets/tracing-BCIurUfa.js +2 -0
  112. package/dist/assets/{tracing-panel-BmuHLPrY.js → tracing-panel-DAzrzNmm.js} +2 -2
  113. package/dist/assets/{trash-UBqfK4mR.js → trash-Dc6DSjz_.js} +1 -1
  114. package/dist/assets/{tree-XiEycetl.js → tree-jheoerAX.js} +1 -1
  115. package/dist/assets/{treemap-75Q7IDZK-CnuVFbBG.js → treemap-75Q7IDZK-IgpxeGaf.js} +21 -21
  116. package/dist/assets/{ts-tags-CloPe9IY.js → ts-tags-DxCDHihD.js} +1 -1
  117. package/dist/assets/variable-panel-DYAiLBmF.js +1 -0
  118. package/dist/assets/{vega-component-DsTH4tuX.js → vega-component-BpfpiPKI.js} +1 -1
  119. package/dist/assets/{xychartDiagram-6GGTOJPD-Dcz3O-A3.js → xychartDiagram-6GGTOJPD-CmNigJ31.js} +1 -1
  120. package/dist/index.html +2 -2
  121. package/package.json +8 -4
  122. package/src/__tests__/mocks.ts +43 -0
  123. package/src/components/app-config/user-config-form.tsx +32 -0
  124. package/src/components/chat/acp/__tests__/__snapshots__/prompt.test.ts.snap +55 -23
  125. package/src/components/chat/acp/__tests__/context-utils.test.ts +222 -0
  126. package/src/components/chat/acp/__tests__/prompt.test.ts +1 -1
  127. package/src/components/chat/acp/__tests__/state.test.ts +2 -6
  128. package/src/components/chat/acp/agent-docs.tsx +33 -6
  129. package/src/components/chat/acp/agent-panel.css +0 -18
  130. package/src/components/chat/acp/agent-panel.tsx +397 -72
  131. package/src/components/chat/acp/agent-selector.tsx +7 -1
  132. package/src/components/chat/acp/blocks.tsx +40 -10
  133. package/src/components/chat/acp/common.tsx +10 -2
  134. package/src/components/chat/acp/context-utils.ts +127 -0
  135. package/src/components/chat/acp/prompt.ts +34 -10
  136. package/src/components/chat/acp/state.ts +1 -1
  137. package/src/components/chat/acp/types.ts +8 -0
  138. package/src/components/chat/chat-panel.tsx +23 -88
  139. package/src/components/chat/chat-utils.ts +127 -1
  140. package/src/components/chat/markdown-renderer.css +39 -0
  141. package/src/components/chat/markdown-renderer.tsx +7 -38
  142. package/src/components/chat/tool-call-accordion.tsx +113 -23
  143. package/src/components/editor/Cell.tsx +6 -0
  144. package/src/components/editor/actions/name-cell-input.tsx +6 -1
  145. package/src/components/editor/actions/useCellActionButton.tsx +3 -1
  146. package/src/components/editor/ai/__tests__/completion-utils.test.ts +178 -1
  147. package/src/components/editor/ai/add-cell-with-ai.tsx +68 -66
  148. package/src/components/editor/ai/ai-completion-editor.tsx +29 -26
  149. package/src/components/editor/ai/completion-handlers.tsx +44 -6
  150. package/src/components/editor/ai/completion-utils.ts +92 -0
  151. package/src/components/editor/ai/transport/chat-transport.tsx +36 -0
  152. package/src/components/editor/cell/StagedAICell.tsx +51 -0
  153. package/src/components/editor/cell/cell-actions.tsx +2 -1
  154. package/src/components/terminal/__tests__/state.test.ts +207 -0
  155. package/src/components/terminal/hooks.ts +41 -0
  156. package/src/components/terminal/state.ts +75 -0
  157. package/src/components/terminal/terminal.tsx +334 -13
  158. package/src/components/terminal/theme.tsx +56 -0
  159. package/src/core/ai/__tests__/staged-cells.test.ts +356 -0
  160. package/src/core/ai/staged-cells.ts +208 -0
  161. package/src/core/cells/cells.ts +1 -1
  162. package/src/core/codemirror/lsp/federated-lsp.ts +1 -1
  163. package/src/core/islands/main.ts +2 -2
  164. package/src/core/kernel/messages.ts +8 -12
  165. package/src/core/saving/__tests__/filename.test.ts +37 -0
  166. package/src/core/static/__tests__/download-html.test.ts +43 -1
  167. package/src/core/websocket/useMarimoWebSocket.tsx +2 -2
  168. package/src/css/app/Cell.css +11 -0
  169. package/src/plugins/core/RenderHTML.tsx +36 -2
  170. package/src/plugins/core/__test__/RenderHTML.test.ts +72 -0
  171. package/src/plugins/core/registerReactComponent.tsx +28 -0
  172. package/src/plugins/impl/FileBrowserPlugin.tsx +8 -2
  173. package/src/stories/cell.stories.tsx +1 -1
  174. package/src/stories/layout/vertical/one-column.stories.tsx +1 -1
  175. package/src/utils/__tests__/cell-urls.test.ts +29 -0
  176. package/src/utils/__tests__/filenames.test.ts +18 -0
  177. package/src/utils/__tests__/path.test.ts +38 -0
  178. package/src/utils/__tests__/urls.test.ts +56 -1
  179. package/src/utils/errors.ts +9 -0
  180. package/dist/assets/_baseEach-C1FLm7WW.js +0 -1
  181. package/dist/assets/_baseMap-DBVArUYD.js +0 -1
  182. package/dist/assets/_baseUniq-Dk7ZPJ3N.js +0 -1
  183. package/dist/assets/_createAggregator-Bn38fDd3.js +0 -1
  184. package/dist/assets/agent-panel-COUYnuIK.js +0 -475
  185. package/dist/assets/architectureDiagram-W76B3OCA-DBzWQKKu.js +0 -36
  186. package/dist/assets/channel-CjhbjOv4.js +0 -1
  187. package/dist/assets/chat-panel-BPXKoTnZ.js +0 -7
  188. package/dist/assets/chat-panel-Brrs_eeH.css +0 -1
  189. package/dist/assets/classDiagram-KNZD7YFC-DHs5cFzy.js +0 -1
  190. package/dist/assets/classDiagram-v2-RKCZMP56-DHs5cFzy.js +0 -1
  191. package/dist/assets/clone-DM1YNjEn.js +0 -1
  192. package/dist/assets/command-palette-S0bzQp7v.js +0 -1
  193. package/dist/assets/common-B8U9k2Ly.js +0 -1
  194. package/dist/assets/cose-bilkent-S5V4N54A-wz1Sfx7j.js +0 -1
  195. package/dist/assets/dagre-5GWH7T2D-BfpcVBgq.js +0 -4
  196. package/dist/assets/datasources-panel-DfuURLJw.js +0 -1
  197. package/dist/assets/diagram-N5W7TBWH-Bf0oqqQh.js +0 -24
  198. package/dist/assets/diagram-QEK2KX5R-ZTc3qikh.js +0 -43
  199. package/dist/assets/diagram-S2PKOQOG-tLScBy7Z.js +0 -24
  200. package/dist/assets/edit-page-DJ8kJZ9w.js +0 -129
  201. package/dist/assets/file-explorer-panel-CzNUJ63G.js +0 -1
  202. package/dist/assets/gitGraphDiagram-NY62KEGX-C1t6QtVa.js +0 -65
  203. package/dist/assets/graph-CssCVWIq.js +0 -1
  204. package/dist/assets/index-DcCIe7np.js +0 -28
  205. package/dist/assets/infoDiagram-STP46IZ2-CwiAoz9f.js +0 -2
  206. package/dist/assets/layout-DpQrxGW-.js +0 -1
  207. package/dist/assets/linear-NsreOeBF.js +0 -1
  208. package/dist/assets/mermaid-DSt0r6IQ.js +0 -1
  209. package/dist/assets/min-D259kI3t.js +0 -1
  210. package/dist/assets/packages-panel-xMz9W2hW.js +0 -1
  211. package/dist/assets/run-page-Bb68qdhQ.js +0 -1
  212. package/dist/assets/sankeyDiagram-GR3RE2ED-BSJOau8E.js +0 -10
  213. package/dist/assets/scratchpad-panel-BF4BO-U4.js +0 -1
  214. package/dist/assets/secrets-panel-CdIX44dQ.js +0 -1
  215. package/dist/assets/snippets-panel-Dco9h0rb.js +0 -1
  216. package/dist/assets/sortBy-aLGA-PGK.js +0 -1
  217. package/dist/assets/stateDiagram-KXAO66HF-Bd68WT3b.js +0 -1
  218. package/dist/assets/stateDiagram-v2-UMBNRL4Z-BXz_GSwb.js +0 -1
  219. package/dist/assets/storage-CGlP4lCF.js +0 -26
  220. package/dist/assets/terminal-CxkHubcu.js +0 -9
  221. package/dist/assets/time-D2nr1UgQ.js +0 -1
  222. package/dist/assets/tracing-kTqHxa7q.js +0 -2
  223. package/dist/assets/variable-panel-noTnH-AQ.js +0 -1
@@ -0,0 +1,356 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import { renderHook } from "@testing-library/react";
4
+ import { getDefaultStore } from "jotai";
5
+ import { beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { CellId } from "@/core/cells/ids";
7
+ import { updateEditorCodeFromPython } from "../../codemirror/language/utils";
8
+ import {
9
+ stagedAICellsAtom,
10
+ useStagedCells,
11
+ visibleForTesting,
12
+ } from "../staged-cells";
13
+
14
+ const { createActions, reducer, initialState } = visibleForTesting;
15
+
16
+ // Mock the dependencies
17
+ const mockCreateNewCell = vi.fn();
18
+ const mockUpdateCellEditor = vi.fn();
19
+ const mockDeleteCellCallback = vi.fn();
20
+
21
+ // Mock cell handle with editor view
22
+ const mockCellHandle = {
23
+ current: {
24
+ editorViewOrNull: {
25
+ dispatch: vi.fn(),
26
+ },
27
+ },
28
+ };
29
+
30
+ vi.mock("../../cells/cells", () => ({
31
+ useCellActions: () => ({
32
+ createNewCell: mockCreateNewCell,
33
+ updateCellEditor: mockUpdateCellEditor,
34
+ }),
35
+ cellHandleAtom: vi.fn(() => ({
36
+ read: vi.fn(() => mockCellHandle),
37
+ })),
38
+ }));
39
+
40
+ vi.mock("@/components/editor/cell/useDeleteCell", () => ({
41
+ useDeleteCellCallback: () => mockDeleteCellCallback,
42
+ }));
43
+
44
+ vi.mock("../../codemirror/language/utils", () => ({
45
+ updateEditorCodeFromPython: vi.fn(),
46
+ }));
47
+
48
+ // Mock CellId.create
49
+ vi.mock("@/core/cells/ids", () => ({
50
+ CellId: {
51
+ create: vi.fn(),
52
+ },
53
+ }));
54
+
55
+ describe("staged-cells", () => {
56
+ let store: ReturnType<typeof getDefaultStore>;
57
+ let cellId1: CellId;
58
+ let cellId2: CellId;
59
+
60
+ beforeEach(() => {
61
+ store = getDefaultStore();
62
+ cellId1 = "cell-1" as CellId;
63
+ cellId2 = "cell-2" as CellId;
64
+
65
+ // Reset mocks
66
+ vi.clearAllMocks();
67
+
68
+ // Reset the atom state
69
+ store.set(stagedAICellsAtom, new Set<CellId>());
70
+ });
71
+
72
+ describe("reducer and actions", () => {
73
+ it("should initialize with empty map", () => {
74
+ const state = initialState();
75
+ expect(state).toEqual(new Set());
76
+ });
77
+
78
+ it("should add cell IDs", () => {
79
+ let state = initialState();
80
+ state = reducer(state, {
81
+ type: "addStagedCell",
82
+ payload: { cellId: cellId1 },
83
+ });
84
+ state = reducer(state, {
85
+ type: "addStagedCell",
86
+ payload: { cellId: cellId2 },
87
+ });
88
+
89
+ expect(state.has(cellId1)).toBe(true);
90
+ expect(state.has(cellId2)).toBe(true);
91
+ });
92
+
93
+ it("should remove cell IDs", () => {
94
+ const state = new Set([cellId1, cellId2]);
95
+ const newState = reducer(state, {
96
+ type: "removeStagedCell",
97
+ payload: cellId1,
98
+ });
99
+
100
+ expect(newState.has(cellId1)).toBe(false);
101
+ expect(newState.has(cellId2)).toBe(true);
102
+ });
103
+
104
+ it("should clear all cell IDs", () => {
105
+ const state = new Set([cellId1, cellId2]);
106
+ const newState = reducer(state, {
107
+ type: "clearStagedCells",
108
+ payload: undefined,
109
+ });
110
+
111
+ expect(newState).toEqual(new Set());
112
+ });
113
+
114
+ it("should not mutate original state", () => {
115
+ const state = new Set([cellId1]);
116
+ const originalSize = state.size;
117
+
118
+ reducer(state, {
119
+ type: "addStagedCell",
120
+ payload: { cellId: cellId2 },
121
+ });
122
+
123
+ expect(state.size).toBe(originalSize);
124
+ expect(state.has(cellId1)).toBe(true);
125
+ expect(state.has(cellId2)).toBe(false);
126
+ });
127
+
128
+ it("should create action functions", () => {
129
+ const mockDispatch = vi.fn();
130
+ const actions = createActions(mockDispatch);
131
+
132
+ expect(typeof actions.addStagedCell).toBe("function");
133
+ expect(typeof actions.removeStagedCell).toBe("function");
134
+ expect(typeof actions.clearStagedCells).toBe("function");
135
+ });
136
+
137
+ it("should initialize atom with empty map", () => {
138
+ const state = store.get(stagedAICellsAtom);
139
+ expect(state).toEqual(new Set());
140
+ });
141
+ });
142
+
143
+ describe("useStagedCells hook", () => {
144
+ it("should create a staged cell with code", () => {
145
+ const { result } = renderHook(() => useStagedCells(store));
146
+ const testCode = "print('hello world')";
147
+
148
+ // Mock CellId.create to return a predictable ID
149
+ const mockCellId = "mock-cell-id" as CellId;
150
+ vi.mocked(CellId.create).mockReturnValue(mockCellId);
151
+
152
+ const returnedCellId = result.current.createStagedCell(testCode);
153
+
154
+ expect(returnedCellId).toBe(mockCellId);
155
+ expect(mockCreateNewCell).toHaveBeenCalledWith({
156
+ cellId: "__end__",
157
+ code: testCode,
158
+ before: false,
159
+ newCellId: mockCellId,
160
+ });
161
+ });
162
+
163
+ it("should delete a staged cell", () => {
164
+ const { result } = renderHook(() => useStagedCells(store));
165
+ const testCellId = "test-cell-id" as CellId;
166
+
167
+ result.current.deleteStagedCell(testCellId);
168
+
169
+ expect(mockDeleteCellCallback).toHaveBeenCalledWith({
170
+ cellId: testCellId,
171
+ });
172
+ });
173
+
174
+ it("should delete all staged cells when none exist", () => {
175
+ const { result } = renderHook(() => useStagedCells(store));
176
+
177
+ // Should not throw when no cells exist
178
+ expect(() => result.current.deleteAllStagedCells()).not.toThrow();
179
+ expect(mockDeleteCellCallback).not.toHaveBeenCalled();
180
+ });
181
+
182
+ it("should delete all staged cells when cells exist", () => {
183
+ // First set the atom state before rendering the hook
184
+ store.set(stagedAICellsAtom, new Set([cellId1, cellId2]));
185
+
186
+ const { result } = renderHook(() => useStagedCells(store));
187
+ result.current.deleteAllStagedCells();
188
+
189
+ expect(mockDeleteCellCallback).toHaveBeenCalledTimes(2);
190
+ expect(mockDeleteCellCallback).toHaveBeenCalledWith({ cellId: cellId1 });
191
+ expect(mockDeleteCellCallback).toHaveBeenCalledWith({ cellId: cellId2 });
192
+
193
+ // Verify cells were cleared from the atom
194
+ const state = store.get(stagedAICellsAtom);
195
+ expect(state).toEqual(new Set());
196
+ });
197
+ });
198
+
199
+ it("should add staged cell", () => {
200
+ const { result } = renderHook(() => useStagedCells(store));
201
+
202
+ result.current.addStagedCell({ cellId: cellId1 });
203
+
204
+ // Check that the cell was added to the atom
205
+ const state = store.get(stagedAICellsAtom);
206
+ expect(state.has(cellId1)).toBe(true);
207
+ });
208
+
209
+ it("should remove staged cell", () => {
210
+ const { result } = renderHook(() => useStagedCells(store));
211
+
212
+ // First add cells
213
+ result.current.addStagedCell({ cellId: cellId1 });
214
+ result.current.addStagedCell({ cellId: cellId2 });
215
+
216
+ // Then remove one
217
+ result.current.removeStagedCell(cellId1);
218
+
219
+ // Check that only the remaining cell is in the map
220
+ const state = store.get(stagedAICellsAtom);
221
+ expect(state.has(cellId1)).toBe(false);
222
+ expect(state.has(cellId2)).toBe(true);
223
+ });
224
+
225
+ it("should clear all staged cells", () => {
226
+ const { result } = renderHook(() => useStagedCells(store));
227
+
228
+ // First add some cells
229
+ result.current.addStagedCell({ cellId: cellId1 });
230
+ result.current.addStagedCell({ cellId: cellId2 });
231
+
232
+ // Then clear all
233
+ result.current.clearStagedCells();
234
+
235
+ // Check that no cells remain
236
+ const state = store.get(stagedAICellsAtom);
237
+ expect(state).toEqual(new Set());
238
+ });
239
+
240
+ it("should handle multiple operations correctly", () => {
241
+ const { result } = renderHook(() => useStagedCells(store));
242
+
243
+ // Create a staged cell
244
+ const mockCellId = "mock-cell-id" as CellId;
245
+ vi.mocked(CellId.create).mockReturnValue(mockCellId);
246
+
247
+ const createdCellId = result.current.createStagedCell("test code");
248
+
249
+ // Verify it was created and added
250
+ expect(createdCellId).toBe(mockCellId);
251
+ expect(mockCreateNewCell).toHaveBeenCalled();
252
+
253
+ let state = store.get(stagedAICellsAtom);
254
+ expect(state.has(mockCellId)).toBe(true);
255
+
256
+ // Delete the staged cell
257
+ result.current.deleteStagedCell(mockCellId);
258
+ expect(mockDeleteCellCallback).toHaveBeenCalledWith({
259
+ cellId: mockCellId,
260
+ });
261
+
262
+ // Verify it was removed from staged cells
263
+ state = store.get(stagedAICellsAtom);
264
+ expect(state.has(mockCellId)).toBe(false);
265
+ });
266
+ });
267
+
268
+ describe("onStream", () => {
269
+ let store: ReturnType<typeof getDefaultStore>;
270
+ beforeEach(() => {
271
+ store = getDefaultStore();
272
+ });
273
+
274
+ it("should create a cell creation stream", () => {
275
+ const { result } = renderHook(() => useStagedCells(store));
276
+ result.current.onStream({ type: "text-start", id: "test-id" });
277
+
278
+ // No cell or cell update should have been called
279
+ expect(mockCreateNewCell).not.toHaveBeenCalled();
280
+ expect(mockUpdateCellEditor).not.toHaveBeenCalled();
281
+ });
282
+
283
+ it("should not create cells when text-delta is received and no stream has been created", () => {
284
+ const { result } = renderHook(() => useStagedCells(store));
285
+ result.current.onStream({
286
+ type: "text-delta",
287
+ id: "test-id",
288
+ delta: "test-delta",
289
+ });
290
+
291
+ // No cell or cell update should have been called
292
+ expect(mockCreateNewCell).not.toHaveBeenCalled();
293
+ expect(mockUpdateCellEditor).not.toHaveBeenCalled();
294
+ });
295
+
296
+ it("should create cells when text-delta is received and a stream has been created", () => {
297
+ const { result } = renderHook(() => useStagedCells(store));
298
+ result.current.onStream({ type: "text-start", id: "test-id" });
299
+
300
+ // Mock CellId.create to return a predictable ID
301
+ const mockCellId = "mock-cell-id" as CellId;
302
+ vi.mocked(CellId.create).mockReturnValue(mockCellId);
303
+
304
+ result.current.onStream({
305
+ type: "text-delta",
306
+ id: "test-id",
307
+ delta: "some code",
308
+ });
309
+
310
+ expect(mockCreateNewCell).toHaveBeenCalledWith({
311
+ cellId: "__end__",
312
+ code: "some code",
313
+ before: false,
314
+ newCellId: "mock-cell-id",
315
+ });
316
+ });
317
+
318
+ it("should handle delta chunks", () => {
319
+ const { result } = renderHook(() => useStagedCells(store));
320
+ result.current.onStream({ type: "text-start", id: "test-id" });
321
+
322
+ const mockCellId = "mock-cell-id" as CellId;
323
+ vi.mocked(CellId.create).mockReturnValue(mockCellId);
324
+
325
+ result.current.onStream({
326
+ type: "text-delta",
327
+ id: "test-id",
328
+ delta: "``",
329
+ });
330
+
331
+ expect(mockCreateNewCell).toHaveBeenCalledWith({
332
+ cellId: "__end__",
333
+ code: "``",
334
+ before: false,
335
+ newCellId: "mock-cell-id",
336
+ });
337
+
338
+ result.current.onStream({
339
+ type: "text-delta",
340
+ id: "test-id",
341
+ delta: "```python\nsome code",
342
+ });
343
+
344
+ // Now the cell is recognized and only some code is seen
345
+ expect(vi.mocked(updateEditorCodeFromPython)).toHaveBeenCalledWith(
346
+ mockCellHandle.current.editorViewOrNull,
347
+ "some code",
348
+ );
349
+
350
+ result.current.onStream({
351
+ type: "text-delta",
352
+ id: "test-id",
353
+ delta: "\n```",
354
+ });
355
+ });
356
+ });
@@ -0,0 +1,208 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import type { UIMessageChunk } from "ai";
4
+ import { useRef } from "react";
5
+ import {
6
+ type AiCompletion,
7
+ codeToCells,
8
+ } from "@/components/editor/ai/completion-utils";
9
+ import { useDeleteCellCallback } from "@/components/editor/cell/useDeleteCell";
10
+ import { CellId } from "@/core/cells/ids";
11
+ import { createReducerAndAtoms } from "@/utils/createReducer";
12
+ import { Logger } from "@/utils/Logger";
13
+ import { cellHandleAtom, useCellActions } from "../cells/cells";
14
+ import type { LanguageAdapterType } from "../codemirror/language/types";
15
+ import { updateEditorCodeFromPython } from "../codemirror/language/utils";
16
+ import type { JotaiStore } from "../state/jotai";
17
+
18
+ /**
19
+ * Cells that are staged for AI completion
20
+ * They function similarly to cells in the notebook, but they can be deleted or accepted by the user.
21
+ * We only track one set of staged cells at a time.
22
+ */
23
+
24
+ const initialState = (): Set<CellId> => {
25
+ return new Set();
26
+ };
27
+
28
+ const {
29
+ valueAtom: stagedAICellsAtom,
30
+ useActions: useStagedAICellsActions,
31
+ createActions,
32
+ reducer,
33
+ } = createReducerAndAtoms(initialState, {
34
+ addStagedCell: (state, action: { cellId: CellId }) => {
35
+ const { cellId } = action;
36
+ return new Set([...state, cellId]);
37
+ },
38
+ removeStagedCell: (state, cellId: CellId) => {
39
+ return new Set([...state].filter((id) => id !== cellId));
40
+ },
41
+ clearStagedCells: () => {
42
+ return initialState();
43
+ },
44
+ });
45
+
46
+ interface UpdateStagedCellAction {
47
+ cellId: CellId;
48
+ code: string;
49
+ language?: LanguageAdapterType;
50
+ }
51
+
52
+ /**
53
+ * Helper functions to create and delete staged cells.
54
+ */
55
+ export function useStagedCells(store: JotaiStore) {
56
+ const { addStagedCell, removeStagedCell, clearStagedCells } =
57
+ useStagedAICellsActions();
58
+ const { createNewCell } = useCellActions();
59
+ const deleteCellCallback = useDeleteCellCallback();
60
+
61
+ const cellCreationStream = useRef<CellCreationStream | null>(null);
62
+
63
+ const createStagedCell = (code: string): CellId => {
64
+ const newCellId = CellId.create();
65
+ addStagedCell({ cellId: newCellId });
66
+ createNewCell({
67
+ cellId: "__end__",
68
+ code,
69
+ before: false,
70
+ newCellId: newCellId,
71
+ });
72
+ return newCellId;
73
+ };
74
+
75
+ const updateStagedCell = (opts: UpdateStagedCellAction) => {
76
+ const { cellId, code } = opts;
77
+ const stagedAICells = store.get(stagedAICellsAtom);
78
+
79
+ if (!stagedAICells.has(cellId)) {
80
+ Logger.error("Staged cell not found", { cellId });
81
+ return;
82
+ }
83
+
84
+ const cellHandle = store.get(cellHandleAtom(cellId));
85
+ const editorView = cellHandle?.current?.editorViewOrNull;
86
+ if (!editorView) {
87
+ Logger.error("Editor for this cell not found", { cellId });
88
+ return;
89
+ }
90
+ // TODO: Update the language
91
+ updateEditorCodeFromPython(editorView, code);
92
+ };
93
+
94
+ // Delete a staged cell and the corresponding cell in the notebook.
95
+ const deleteStagedCell = (cellId: CellId) => {
96
+ removeStagedCell(cellId);
97
+ deleteCellCallback({ cellId });
98
+ };
99
+
100
+ // Delete all staged cells and the corresponding cells in the notebook.
101
+ const deleteAllStagedCells = () => {
102
+ const stagedAICells = store.get(stagedAICellsAtom);
103
+ for (const cellId of stagedAICells) {
104
+ deleteCellCallback({ cellId });
105
+ }
106
+ clearStagedCells();
107
+ };
108
+
109
+ const onStream = (chunk: UIMessageChunk) => {
110
+ switch (chunk.type) {
111
+ case "text-start":
112
+ // Create stream
113
+ cellCreationStream.current = new CellCreationStream(
114
+ createStagedCell,
115
+ updateStagedCell,
116
+ );
117
+ break;
118
+ case "text-delta":
119
+ if (!cellCreationStream.current) {
120
+ Logger.error("Cell creation stream not found");
121
+ return;
122
+ }
123
+ cellCreationStream.current.stream(chunk);
124
+ break;
125
+ case "text-end":
126
+ case "finish":
127
+ if (!cellCreationStream.current) {
128
+ Logger.error("Cell creation stream not found");
129
+ return;
130
+ }
131
+ cellCreationStream.current.stop();
132
+ break;
133
+ default:
134
+ Logger.error("Unknown chunk type", { chunk });
135
+ }
136
+ };
137
+
138
+ return {
139
+ createStagedCell,
140
+ updateStagedCell,
141
+ addStagedCell,
142
+ removeStagedCell,
143
+ clearStagedCells,
144
+ deleteStagedCell,
145
+ deleteAllStagedCells,
146
+ onStream,
147
+ };
148
+ }
149
+
150
+ export { stagedAICellsAtom };
151
+ export const visibleForTesting = {
152
+ createActions,
153
+ reducer,
154
+ initialState,
155
+ useStagedAICellsActions,
156
+ };
157
+
158
+ type TextDeltaChunk = Extract<UIMessageChunk, { type: "text-delta" }>;
159
+
160
+ interface CreatedCell {
161
+ cellId: CellId;
162
+ cell: AiCompletion;
163
+ }
164
+
165
+ class CellCreationStream {
166
+ private createdCells: CreatedCell[] = [];
167
+ private buffer = "";
168
+
169
+ private onCreateCell: (code: string) => CellId;
170
+ private onUpdateCell: (opts: UpdateStagedCellAction) => void;
171
+
172
+ constructor(
173
+ onCreateCell: (code: string) => CellId,
174
+ onUpdateCell: (opts: UpdateStagedCellAction) => void,
175
+ ) {
176
+ this.onCreateCell = onCreateCell;
177
+ this.onUpdateCell = onUpdateCell;
178
+ }
179
+
180
+ stream(chunk: TextDeltaChunk) {
181
+ const delta = chunk.delta;
182
+ this.buffer += delta;
183
+ const completionCells = codeToCells(this.buffer);
184
+
185
+ // As incoming chunks are appended to the buffer,
186
+ // we parse the buffer into cells and determine which parts correspond to which cell.
187
+ // For each parsed cell, we either update an existing staged cell or create a new one.
188
+ for (const [idx, cell] of completionCells.entries()) {
189
+ if (idx < this.createdCells.length) {
190
+ const existingCell = this.createdCells[idx];
191
+ this.createdCells[idx] = { ...existingCell, cell };
192
+ this.onUpdateCell({
193
+ cellId: existingCell.cellId,
194
+ code: cell.code,
195
+ language: cell.language,
196
+ });
197
+ } else {
198
+ const newCellId = this.onCreateCell(cell.code);
199
+ this.createdCells.push({ cellId: newCellId, cell });
200
+ }
201
+ }
202
+ }
203
+
204
+ stop() {
205
+ // Clear all state
206
+ this.buffer = "";
207
+ }
208
+ }
@@ -1623,7 +1623,7 @@ const cellDataAtom = atomFamily((cellId: CellId) =>
1623
1623
  const cellRuntimeAtom = atomFamily((cellId: CellId) =>
1624
1624
  atom((get) => get(notebookAtom).cellRuntime[cellId]),
1625
1625
  );
1626
- const cellHandleAtom = atomFamily((cellId: CellId) =>
1626
+ export const cellHandleAtom = atomFamily((cellId: CellId) =>
1627
1627
  atom((get) => get(notebookAtom).cellHandles[cellId]),
1628
1628
  );
1629
1629
  /**
@@ -4,7 +4,7 @@ import type { LanguageServerPlugin } from "@marimo-team/codemirror-languageserve
4
4
  import type * as LSP from "vscode-languageserver-protocol";
5
5
  import { Objects } from "@/utils/objects";
6
6
  import type { ILanguageServerClient } from "./types";
7
- import { getLSPDocument } from "./utils.ts";
7
+ import { getLSPDocument } from "./utils";
8
8
 
9
9
  function removeFalseyValues<T extends object>(obj: T): T {
10
10
  return Objects.filter(obj, (value) => value !== false && value !== null) as T;
@@ -103,7 +103,7 @@ export async function initialize() {
103
103
  // Consume messages from the kernel
104
104
  IslandsPyodideBridge.INSTANCE.consumeMessages((message) => {
105
105
  const msg = jsonParseWithSpecialChar(message);
106
- switch (msg.op) {
106
+ switch (msg.data.op) {
107
107
  case "banner":
108
108
  case "missing-package-alert":
109
109
  case "installing-package-alert":
@@ -185,7 +185,7 @@ export async function initialize() {
185
185
  case "reconnected":
186
186
  return;
187
187
  default:
188
- logNever(msg);
188
+ logNever(msg.data);
189
189
  }
190
190
  });
191
191
 
@@ -40,21 +40,17 @@ export type SQLTableListPreview =
40
40
  OperationMessageData<"sql-table-list-preview">;
41
41
  export type SecretKeysResult = OperationMessageData<"secret-keys-result">;
42
42
  export type StartupLogs = OperationMessageData<"startup-logs">;
43
- export type MessageOperation = schemas["KnownUnions"]["operation"];
43
+ export type CellMessage = OperationMessageData<"cell-op">;
44
+ export type Capabilities = OperationMessageData<"kernel-ready">["capabilities"];
44
45
 
45
- export type OperationMessageType = MessageOperation["op"];
46
- export type OperationMessage = {
47
- [Type in OperationMessageType]: {
48
- op: Type;
49
- data: Omit<Extract<MessageOperation, { op: Type }>, "op">;
50
- };
51
- }[OperationMessageType];
46
+ export type MessageOperationUnion = schemas["KnownUnions"]["operation"];
52
47
 
53
- export type CellMessage = OperationMessageData<"cell-op">;
48
+ export type OperationMessageType = MessageOperationUnion["op"];
49
+ export type OperationMessage = {
50
+ data: MessageOperationUnion;
51
+ };
54
52
 
55
53
  export type OperationMessageData<T extends OperationMessageType> = Omit<
56
- Extract<MessageOperation, { op: T }>,
54
+ Extract<MessageOperationUnion, { op: T }>,
57
55
  "op"
58
56
  >;
59
-
60
- export type Capabilities = OperationMessageData<"kernel-ready">["capabilities"];
@@ -0,0 +1,37 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import { EDGE_CASE_FILENAMES } from "../../../__tests__/mocks";
5
+ import { Paths } from "../../../utils/paths";
6
+
7
+ describe("filename handling logic", () => {
8
+ it.each(EDGE_CASE_FILENAMES)(
9
+ "should extract basename correctly for document title: %s",
10
+ (filename) => {
11
+ const basename = Paths.basename(filename);
12
+ expect(basename).toBe(filename); // Since no path separator
13
+ },
14
+ );
15
+
16
+ it("should handle full paths with unicode filenames", () => {
17
+ EDGE_CASE_FILENAMES.forEach((filename) => {
18
+ const fullPath = `/path/to/${filename}`;
19
+
20
+ const basename = Paths.basename(fullPath);
21
+ expect(basename).toBe(filename);
22
+ });
23
+ });
24
+
25
+ it("should handle document title setting with unicode", () => {
26
+ EDGE_CASE_FILENAMES.forEach((filename) => {
27
+ const originalTitle = document.title;
28
+
29
+ // In case this does any conversions, we want to simulate reading/writing the title
30
+ document.title = filename;
31
+ expect(document.title).toBe(filename);
32
+
33
+ // Restore
34
+ document.title = originalTitle;
35
+ });
36
+ });
37
+ });