@marimo-team/islands 0.25.3-dev5 → 0.25.3-dev51

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 (220) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CNMliVzw.js → ConnectedDataExplorerComponent-Da5OoEUL.js} +14 -14
  2. package/dist/{ErrorBoundary-BiZWOrv4.js → ErrorBoundary-CbxCC2kk.js} +8 -8
  3. package/dist/{abnfDiagram-VCTEODGH-D-Ez91gV.js → abnfDiagram-VCTEODGH-Bz3mrprX.js} +2 -2
  4. package/dist/any-language-editor-k2B8YI3Y.js +75 -0
  5. package/dist/{architectureDiagram-5GKGNRK7-CTpcNtO8.js → architectureDiagram-5GKGNRK7-Cs5ujb9K.js} +1 -1
  6. package/dist/{arrays-CN1djC4k.js → arrays-R6JsImiT.js} +8 -0
  7. package/dist/{button-CvjkwdSk.js → button-BxB3RDjN.js} +1 -1
  8. package/dist/{capabilities-CQ5IjhzL.js → capabilities-BgR0osfN.js} +2 -2
  9. package/dist/{chat-ui-0lKdOoQ3.js → chat-ui-C1Gjgokv.js} +1103 -1103
  10. package/dist/{check-EDwSwP89.js → check-P35dc2dH.js} +1 -1
  11. package/dist/{chunk-IMKFNOWR-DGxi-zeo.js → chunk-IMKFNOWR-CcTSaxZt.js} +1 -1
  12. package/dist/{chunk-SHT3W25Y-CV9J3rAB.js → chunk-SHT3W25Y-DcJcjOaO.js} +1 -1
  13. package/dist/{chunk-SVP7TREG-BL-9Fqgg.js → chunk-SVP7TREG-BVtDTjaF.js} +1 -1
  14. package/dist/{chunk-TICWLB2K-Cu_XMjDM.js → chunk-TICWLB2K-BkDFkh8q.js} +1 -1
  15. package/dist/{chunk-YOKDWASO-DPZk8vhS.js → chunk-YOKDWASO-DKplTrBY.js} +20 -20
  16. package/dist/{classDiagram-ZZMXUADV-BXXV13Hq.js → classDiagram-ZZMXUADV-CfAt7HT5.js} +2 -2
  17. package/dist/{classDiagram-v2-VYDZK3BY-B7FXJ1Cp.js → classDiagram-v2-VYDZK3BY-CGEjNYa2.js} +2 -2
  18. package/dist/{code-visibility-CW5_8ZOI.js → code-visibility-DoZ9lxzx.js} +6 -6
  19. package/dist/{common-mzWw4FdI.js → common-D1PacyXO.js} +3112 -3015
  20. package/dist/{copy-icon-Ft7t9bqz.js → copy-icon-CPoxUG7G.js} +3 -3
  21. package/dist/{cynefinDiagram-5FMLGOSQ-CH96rwba.js → cynefinDiagram-5FMLGOSQ-BW6NmfJi.js} +1 -1
  22. package/dist/{dagre-GXQ25YYZ-BD3PmpFY.js → dagre-GXQ25YYZ-CWg3CTuF.js} +1 -1
  23. package/dist/{data-grid-overlay-editor-CJmehSdc.js → data-grid-overlay-editor-B50UpkXd.js} +5 -5
  24. package/dist/{data-grid-types-BH_r7g7l.js → data-grid-types-0PJ-9N5n.js} +1 -1
  25. package/dist/{diagram-S7CK7UJ4-BsSJJldB.js → diagram-S7CK7UJ4-BPxtoS-z.js} +1 -1
  26. package/dist/{diagram-UQ7AKVKN-muQtNZc4.js → diagram-UQ7AKVKN-DA-vJdFL.js} +1 -1
  27. package/dist/{diagram-VX7I27RA-CdhuZAqR.js → diagram-VX7I27RA-D6bkR84S.js} +1 -1
  28. package/dist/{diagram-Z3DM3KII-BD8mXjOj.js → diagram-Z3DM3KII-m60g4cAT.js} +1 -1
  29. package/dist/{dialog-CEBFZNyc.js → dialog-C1PcpCqX.js} +3 -3
  30. package/dist/{dist-DpBpA_n1.js → dist-Lm-D-Vr6.js} +3 -3
  31. package/dist/{dropdown-menu-CP_qCYHC.js → dropdown-menu-MG5hiCIa.js} +76 -76
  32. package/dist/{ebnfDiagram-PWID7BFC-BIN1sweE.js → ebnfDiagram-PWID7BFC-CK99sho2.js} +2 -2
  33. package/dist/{erDiagram-RLTQ6QDP-Ctp5cfoN.js → erDiagram-RLTQ6QDP-Bryq0tdE.js} +1 -1
  34. package/dist/{error-banner-D-WCIiMt.js → error-banner-Sl2FlSJ4.js} +2 -2
  35. package/dist/{error-entries-DSvFWT_M.js → error-entries-CMNtA8NK.js} +959 -959
  36. package/dist/{feedback-button-BcT1FBud.js → feedback-button-DqPqwryB.js} +29 -29
  37. package/dist/{flowDiagram-HODETNUW-3EfLrlFb.js → flowDiagram-HODETNUW-D-3aRpfS.js} +2 -2
  38. package/dist/{formats-CeOmU1MU.js → formats-6pLWWrY2.js} +3 -3
  39. package/dist/{ganttDiagram-EL5Y4UJY-BQT16JSc.js → ganttDiagram-EL5Y4UJY-Dh_4cBJZ.js} +2 -2
  40. package/dist/{glide-data-editor-XckatD0j.js → glide-data-editor-BxMhUd3K.js} +370 -368
  41. package/dist/{glide-utils-DJyqh0EM.js → glide-utils-Bt8_pBa2.js} +16 -16
  42. package/dist/{globals-jRrLCkrP.js → globals-DTJa_doJ.js} +3 -3
  43. package/dist/{highlighted-body-KPVGNVTW-zwjq21tf.js → highlighted-body-KPVGNVTW-3Ziwfz-V.js} +2 -2
  44. package/dist/{html-to-image-B8s5SgGm.js → html-to-image-By72ALyI.js} +29 -29
  45. package/dist/{infoDiagram-27XIBGKW-DipQoHxl.js → infoDiagram-27XIBGKW-s1C1kgfU.js} +1 -1
  46. package/dist/{ishikawaDiagram-5VMMS53U-DgFMjSGz.js → ishikawaDiagram-5VMMS53U-CCsRgolY.js} +1 -1
  47. package/dist/{kanban-definition-UXKFOSKX-w1ACVjgl.js → kanban-definition-UXKFOSKX-BrmxDlcc.js} +1 -1
  48. package/dist/{label-BDKF3Afc.js → label-BRnUC0NX.js} +2 -2
  49. package/dist/{linear-Cnaq3Tfb.js → linear-DWERh7eI.js} +121 -73
  50. package/dist/{loader-BQOfjTIJ.js → loader-Bm8cy3e8.js} +12 -12
  51. package/dist/main.js +2358 -2221
  52. package/dist/{mermaid-CHo49Yv-.js → mermaid-DKI0869I.js} +38 -38
  53. package/dist/mermaid-HWGCJPDP-Du3-HPQA.js +2 -0
  54. package/dist/{mindmap-definition-YA3MSWOX-CrT0KSfX.js → mindmap-definition-YA3MSWOX-641xJysF.js} +1 -1
  55. package/dist/{number-overlay-editor-pZx8dUDV.js → number-overlay-editor-Z_H6x5b-.js} +3 -3
  56. package/dist/{objects-CzUgoMIF.js → objects-DCmMvZtn.js} +1 -3
  57. package/dist/{once-BtE-wKRL.js → once-eG8Vmvjd.js} +1 -1
  58. package/dist/{pegDiagram-XKGWAZYB-BqvywNJA.js → pegDiagram-XKGWAZYB-keVUAUeS.js} +2 -2
  59. package/dist/{pieDiagram-E7YTZNPT-BfxJ_XCc.js → pieDiagram-E7YTZNPT-DHktXPO_.js} +1 -1
  60. package/dist/precisionRound-DhAzeRcm.js +49 -0
  61. package/dist/{process-output-CKRXyszb.js → process-output-D-OMW5sV.js} +4 -4
  62. package/dist/{quadrantDiagram-AXDQQJYC-Ccw0xMjN.js → quadrantDiagram-AXDQQJYC-Blrm7V7_.js} +1 -1
  63. package/dist/{railroadDiagram-O6MQD6OU-8vFLVTPu.js → railroadDiagram-O6MQD6OU-BVEF4OV_.js} +2 -2
  64. package/dist/{react-vega-BED0DjiB.js → react-vega-C4T9Rb_w.js} +12284 -12270
  65. package/dist/{requirementDiagram-BXWQKSXE-a_MxGvxe.js → requirementDiagram-BXWQKSXE-CWo-UAKV.js} +1 -1
  66. package/dist/{reveal-component-79ez8-D5.js → reveal-component-tAB5K3QE.js} +10 -10
  67. package/dist/{select-BButFi-d.js → select-B0LRDH0g.js} +4 -4
  68. package/dist/{sequenceDiagram-WJ2MYXX4-CXFMnxGh.js → sequenceDiagram-WJ2MYXX4-CHQvLnq4.js} +1 -1
  69. package/dist/{spec-BwarBpUJ.js → spec-C-LRKGtT.js} +4 -4
  70. package/dist/{stateDiagram-D77RDMKH-DQeA_CwI.js → stateDiagram-D77RDMKH-BOJZgbUJ.js} +2 -2
  71. package/dist/{stateDiagram-v2-MP3YSRHH-BxxQucm1.js → stateDiagram-v2-MP3YSRHH-C53XXdYH.js} +2 -2
  72. package/dist/style.css +1 -1
  73. package/dist/{swimlanes-42K2YHIH-Do6L-cYg.js → swimlanes-42K2YHIH-D2o1gOtw.js} +1 -1
  74. package/dist/{swimlanesDiagram-VR7AAH4N-DUzPvSVU.js → swimlanesDiagram-VR7AAH4N-BHFzN1Wk.js} +2 -2
  75. package/dist/{swiper-component-DKntyn8p.js → swiper-component-DfhRvAFi.js} +2 -2
  76. package/dist/{time-BieKwdYS.js → time-BaMbJs4z.js} +18 -18
  77. package/dist/{timeline-definition-24CTP7MA-D3c46WM5.js → timeline-definition-24CTP7MA-rv7QUa46.js} +1 -1
  78. package/dist/{toDate-vIXljnsr.js → toDate-CjPuEPou.js} +21 -20
  79. package/dist/{toast-B9UMEi8u.js → toast-CFWpihJU.js} +4 -4
  80. package/dist/{tooltip-BO1REbqs.js → tooltip-CKO1pHJo.js} +2 -2
  81. package/dist/{use-restore-focus-BEM7sFWl.js → use-restore-focus-DBCITLeF.js} +1 -1
  82. package/dist/{useAsyncData-Deh5kszT.js → useAsyncData-ChgNtdmE.js} +15 -14
  83. package/dist/{useDateFormatter-D8XruNGG.js → useDateFormatter-c_yJtkAe.js} +2 -2
  84. package/dist/{useDeepCompareMemoize-DXwzRINI.js → useDeepCompareMemoize-Bet7kSsI.js} +1 -1
  85. package/dist/{useIframeCapabilities-DBETvvPl.js → useIframeCapabilities-BkdeVdXC.js} +1 -1
  86. package/dist/{useLifecycle-j7D9uD1o.js → useLifecycle-VVmz_24o.js} +3 -3
  87. package/dist/{useTheme-CRnjkku0.js → useTheme-DympCsQg.js} +5 -5
  88. package/dist/{vega-component-DTG6zxaY.js → vega-component-DnfQeoyt.js} +147 -147
  89. package/dist/{vega-embed-container-BNHE18G9.js → vega-embed-container--LYZqguO.js} +2 -2
  90. package/dist/{vega-loader.browser-DNST6uVf.js → vega-loader.browser-B9Pz0EGP.js} +298 -507
  91. package/dist/{vennDiagram-4TSXK5OY-BSZD8JcZ.js → vennDiagram-4TSXK5OY-D36Zk7t-.js} +1 -1
  92. package/dist/{wardleyDiagram-VM6X3IG4-48cUWkfA.js → wardleyDiagram-VM6X3IG4-BLBolvyQ.js} +1 -1
  93. package/dist/{xychartDiagram-S5SC5T6Z-CswYgYeu.js → xychartDiagram-S5SC5T6Z-CfH57l0v.js} +2 -2
  94. package/package.json +4 -4
  95. package/src/__tests__/fixtures/{pair-preview.ts → pair-config.ts} +3 -2
  96. package/src/__tests__/host-imports.test.ts +1 -0
  97. package/src/__tests__/mount.test.ts +8 -8
  98. package/src/components/app-config/ai-config.tsx +5 -1
  99. package/src/components/chat/__tests__/message-queue.test.tsx +17 -0
  100. package/src/components/chat/chat-panel.tsx +3 -7
  101. package/src/components/chat/chat-utils.ts +4 -8
  102. package/src/components/data-table/__tests__/context-menu-interaction.test.tsx +130 -0
  103. package/src/components/data-table/__tests__/date-popover.test.tsx +72 -0
  104. package/src/components/data-table/__tests__/export-actions.test.tsx +417 -6
  105. package/src/components/data-table/column-visibility-dropdown.tsx +8 -13
  106. package/src/components/data-table/columns.tsx +2 -1
  107. package/src/components/data-table/context-menu.tsx +36 -20
  108. package/src/components/data-table/date-popover.tsx +6 -4
  109. package/src/components/data-table/export-actions.tsx +263 -161
  110. package/src/components/data-table/row-viewer-panel/row-viewer.tsx +2 -2
  111. package/src/components/data-table/schemas.ts +14 -36
  112. package/src/components/editor/Output.tsx +5 -1
  113. package/src/components/editor/__tests__/dynamic-favicon.test.tsx +111 -18
  114. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +80 -252
  115. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +186 -34
  116. package/src/components/editor/actions/pair-with-agent-commands.ts +42 -51
  117. package/src/components/editor/actions/pair-with-agent-modal.tsx +96 -36
  118. package/src/components/editor/ai/add-cell-with-ai.tsx +3 -3
  119. package/src/components/editor/cell/CreateCellButton.tsx +8 -10
  120. package/src/components/editor/cell/code/__tests__/language-toggle.test.tsx +54 -0
  121. package/src/components/editor/cell/code/cell-editor.tsx +1 -1
  122. package/src/components/editor/cell/code/language-toggle.tsx +8 -7
  123. package/src/components/editor/chrome/wrapper/__tests__/attachment-status.test.tsx +277 -78
  124. package/src/components/editor/chrome/wrapper/footer-items/attachment-status.tsx +196 -41
  125. package/src/components/editor/connections/components.tsx +1 -1
  126. package/src/components/editor/controls/__tests__/command-palette.test.tsx +53 -0
  127. package/src/components/editor/controls/command-palette.tsx +22 -10
  128. package/src/components/editor/dynamic-favicon.tsx +21 -40
  129. package/src/components/editor/errors/__tests__/send-error-report-button.test.tsx +139 -36
  130. package/src/components/editor/errors/send-error-report-button.tsx +10 -7
  131. package/src/components/editor/file-tree/file-viewer.tsx +4 -2
  132. package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -22
  133. package/src/components/editor/output/console/ConsoleOutput.tsx +8 -18
  134. package/src/components/pages/home-page.tsx +4 -4
  135. package/src/components/slides/slide-cell-view.tsx +1 -1
  136. package/src/components/tracing/tracing.tsx +2 -2
  137. package/src/core/attachments/__tests__/toast.test.tsx +58 -0
  138. package/src/core/attachments/state.ts +5 -8
  139. package/src/core/attachments/toast.ts +15 -0
  140. package/src/core/cells/__tests__/error-report.test.ts +53 -3
  141. package/src/core/cells/__tests__/notebook-outline.test.ts +113 -0
  142. package/src/core/cells/__tests__/session.test.ts +18 -3
  143. package/src/core/cells/cells.ts +2 -1
  144. package/src/core/cells/error-report.ts +14 -6
  145. package/src/core/cells/pending-delete-service.ts +2 -6
  146. package/src/core/cells/session.ts +19 -1
  147. package/src/core/config/pair.ts +5 -3
  148. package/src/core/datasets/__tests__/data-source-discovery.test.ts +4 -4
  149. package/src/core/datasets/data-source-discovery.ts +4 -6
  150. package/src/core/edit-app.tsx +2 -2
  151. package/src/core/host/AGENTS.md +11 -2
  152. package/src/core/host/__tests__/attachments.test.ts +56 -0
  153. package/src/core/host/__tests__/document.test.ts +126 -0
  154. package/src/core/host/__tests__/events.test.ts +49 -0
  155. package/src/core/host/__tests__/fixtures.ts +41 -0
  156. package/src/core/host/__tests__/notebook-client.test.ts +496 -0
  157. package/src/core/host/connection.ts +81 -0
  158. package/src/core/host/events.ts +68 -0
  159. package/src/core/host/notebook-client.ts +316 -0
  160. package/src/core/host/resources/attachments.ts +19 -0
  161. package/src/core/host/resources/document.ts +101 -0
  162. package/src/core/hotkeys/actions.ts +2 -4
  163. package/src/core/islands/__tests__/bootstrap.test.ts +0 -26
  164. package/src/core/islands/bootstrap.ts +0 -1
  165. package/src/core/network/connection.ts +4 -2
  166. package/src/core/run-app.tsx +2 -2
  167. package/src/core/runtime/__tests__/runtime.test.ts +5 -12
  168. package/src/core/runtime/runtime.ts +5 -5
  169. package/src/core/static/files.ts +2 -0
  170. package/src/core/websocket/__tests__/{useMarimoKernelConnection.test.ts → connection-state.test.ts} +15 -17
  171. package/src/core/websocket/__tests__/{useWebSocket.test.ts → createConnectionTransport.test.ts} +1 -1
  172. package/src/core/websocket/__tests__/useNotebookClient.hook.test.tsx +149 -0
  173. package/src/core/websocket/__tests__/{useMarimoKernelConnection.hook.test.tsx → useNotebookSession.hook.test.tsx} +168 -253
  174. package/src/core/websocket/connection-state.ts +70 -0
  175. package/src/core/websocket/state.ts +7 -0
  176. package/src/core/websocket/transports/__tests__/sse.test.ts +18 -4
  177. package/src/core/websocket/transports/create.ts +31 -0
  178. package/src/core/websocket/transports/sse.ts +14 -2
  179. package/src/core/websocket/transports/transport.ts +12 -23
  180. package/src/core/websocket/types.ts +9 -39
  181. package/src/core/websocket/useNotebookClient.ts +78 -0
  182. package/src/core/websocket/{useMarimoKernelConnection.tsx → useNotebookSession.tsx} +53 -233
  183. package/src/hooks/__tests__/useAsyncData.test.tsx +148 -0
  184. package/src/hooks/useAsyncData.ts +3 -3
  185. package/src/hooks/useDataSourceDiscovery.ts +6 -4
  186. package/src/hooks/useElapsedTime.ts +3 -3
  187. package/src/hooks/useInputHistory.ts +4 -7
  188. package/src/hooks/useResizeObserver.ts +4 -7
  189. package/src/mount.tsx +3 -3
  190. package/src/plugins/core/registerReactComponent.tsx +1 -1
  191. package/src/plugins/impl/DropdownPlugin.tsx +2 -6
  192. package/src/plugins/impl/FormPlugin.tsx +1 -1
  193. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +84 -0
  194. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +2 -0
  195. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +241 -2
  196. package/src/plugins/impl/anywidget/runtime.ts +21 -9
  197. package/src/plugins/impl/anywidget/widget-binding.ts +160 -82
  198. package/src/plugins/impl/code/any-language-editor.tsx +2 -2
  199. package/src/plugins/impl/data-editor/glide-data-editor.tsx +8 -5
  200. package/src/plugins/impl/data-editor/glide-utils.ts +3 -1
  201. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +5 -3
  202. package/src/plugins/impl/panel/PanelPlugin.tsx +15 -16
  203. package/src/plugins/impl/panel/__tests__/utils.test.ts +39 -1
  204. package/src/plugins/impl/panel/utils.ts +17 -0
  205. package/src/plugins/impl/vega/make-selectable.ts +3 -6
  206. package/src/stories/__tests__/editor-story.test.tsx +33 -0
  207. package/src/stories/editor.stories.tsx +24 -17
  208. package/src/utils/__tests__/arrays.test.ts +35 -0
  209. package/src/utils/__tests__/numbers.test.ts +48 -1
  210. package/src/utils/__tests__/objects.test.ts +21 -0
  211. package/src/utils/arrays.ts +10 -0
  212. package/src/utils/numbers.ts +28 -17
  213. package/src/utils/objects.ts +3 -7
  214. package/src/utils/once.ts +19 -19
  215. package/dist/any-language-editor-DZ_YTVus.js +0 -76
  216. package/dist/mermaid-HWGCJPDP-ByCsZnYF.js +0 -2
  217. package/dist/precisionRound-DMIVQrt2.js +0 -97
  218. package/src/core/host/__tests__/protocol.test.ts +0 -214
  219. package/src/core/host/protocol.ts +0 -1943
  220. package/src/core/websocket/useWebSocket.tsx +0 -88
@@ -1,17 +1,41 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { render, waitFor } from "@testing-library/react";
3
+ import {
4
+ act,
5
+ render as renderComponent,
6
+ waitFor,
7
+ } from "@testing-library/react";
4
8
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
- import { useCellErrors } from "@/core/cells/cells";
9
+ import { Provider, createStore } from "jotai";
10
+ import type { ReactNode } from "react";
11
+ import { MockNotebook } from "@/__mocks__/notebook";
12
+ import { cellId } from "@/__tests__/branded";
13
+ import { notebookAtom } from "@/core/cells/cells";
6
14
  import { DynamicFavicon } from "../dynamic-favicon";
7
15
 
8
- // Mock useCellErrors hook
9
- vi.mock("@/core/cells/cells", () => ({
10
- useCellErrors: vi.fn(),
11
- }));
12
-
13
16
  describe("DynamicFavicon", () => {
14
17
  let favicon: HTMLLinkElement;
18
+ let store: ReturnType<typeof createStore>;
19
+ const id = cellId("cell");
20
+
21
+ function render(ui: ReactNode) {
22
+ return renderComponent(ui, {
23
+ wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
24
+ });
25
+ }
26
+
27
+ function setErrors() {
28
+ store.set(
29
+ notebookAtom,
30
+ MockNotebook.notebookStateWithErrors([
31
+ {
32
+ cellId: id,
33
+ cellName: "cell",
34
+ errorData: [MockNotebook.errors.syntax("Invalid syntax")],
35
+ },
36
+ ]),
37
+ );
38
+ }
15
39
 
16
40
  beforeEach(() => {
17
41
  document
@@ -27,15 +51,20 @@ describe("DynamicFavicon", () => {
27
51
  // Mock document.hasFocus
28
52
  vi.spyOn(document, "hasFocus").mockReturnValue(true);
29
53
 
30
- // Mock useCellErrors to return no errors by default
31
- (useCellErrors as ReturnType<typeof vi.fn>).mockReturnValue([]);
54
+ store = createStore();
55
+ store.set(
56
+ notebookAtom,
57
+ MockNotebook.notebookState({
58
+ cellData: { [id]: { code: "x = 1" } },
59
+ }),
60
+ );
32
61
  });
33
62
 
34
63
  afterEach(() => {
35
64
  document
36
65
  .querySelectorAll("link[rel~='icon']")
37
66
  .forEach((element) => element.remove());
38
- vi.clearAllMocks();
67
+ vi.restoreAllMocks();
39
68
  vi.useRealTimers();
40
69
  });
41
70
 
@@ -67,9 +96,7 @@ describe("DynamicFavicon", () => {
67
96
  });
68
97
 
69
98
  it("should show error favicon when run completes with errors", async () => {
70
- (useCellErrors as ReturnType<typeof vi.fn>).mockReturnValue([
71
- { error: "mock error" },
72
- ]);
99
+ setErrors();
73
100
 
74
101
  const { rerender } = render(<DynamicFavicon isRunning={true} />);
75
102
 
@@ -147,6 +174,72 @@ describe("DynamicFavicon", () => {
147
174
  });
148
175
  });
149
176
 
177
+ it("does not look up or rewrite the favicon when code changes", async () => {
178
+ const { rerender } = render(<DynamicFavicon isRunning={true} />);
179
+ await waitFor(() => expect(favicon.href).toContain("circle-play.ico"));
180
+ rerender(<DynamicFavicon isRunning={false} />);
181
+ await waitFor(() => expect(favicon.href).toContain("circle-check.ico"));
182
+ const lookup = vi.spyOn(document, "querySelector");
183
+ const write = vi.spyOn(favicon, "href", "set");
184
+
185
+ await act(async () => {
186
+ store.set(notebookAtom, (state) => ({
187
+ ...state,
188
+ cellData: {
189
+ ...state.cellData,
190
+ [id]: { ...state.cellData[id], code: "x = 2" },
191
+ },
192
+ }));
193
+ });
194
+
195
+ expect(
196
+ lookup.mock.calls.filter(
197
+ ([selector]) => selector === "link[rel~='icon']",
198
+ ),
199
+ ).toEqual([]);
200
+ expect(write).not.toHaveBeenCalled();
201
+ lookup.mockRestore();
202
+ write.mockRestore();
203
+ });
204
+
205
+ it("updates the favicon when the error count changes", async () => {
206
+ const { rerender } = render(<DynamicFavicon isRunning={true} />);
207
+ await waitFor(() => expect(favicon.href).toContain("circle-play.ico"));
208
+ rerender(<DynamicFavicon isRunning={false} />);
209
+ await waitFor(() => expect(favicon.href).toContain("circle-check.ico"));
210
+
211
+ act(() => setErrors());
212
+ await waitFor(() => expect(favicon.href).toContain("circle-x.ico"));
213
+
214
+ act(() =>
215
+ store.set(
216
+ notebookAtom,
217
+ MockNotebook.notebookState({ cellData: { [id]: {} } }),
218
+ ),
219
+ );
220
+ await waitFor(() => expect(favicon.href).toContain("circle-check.ico"));
221
+ });
222
+
223
+ it("keeps the completion icon until its own reset timer expires", async () => {
224
+ vi.useFakeTimers();
225
+ const { rerender } = render(<DynamicFavicon isRunning={true} />);
226
+ await act(() => vi.advanceTimersByTimeAsync(1000));
227
+ rerender(<DynamicFavicon isRunning={false} />);
228
+ await act(() => vi.advanceTimersByTimeAsync(2000));
229
+ expect(favicon.href).toContain("circle-check.ico");
230
+ await act(() => vi.advanceTimersByTimeAsync(1000));
231
+ expect(favicon.href).toContain("favicon.ico");
232
+ });
233
+
234
+ it("cancels the reset timer when unmounted", async () => {
235
+ vi.useFakeTimers();
236
+ const { unmount } = render(<DynamicFavicon isRunning={true} />);
237
+ await act(() => vi.advanceTimersByTimeAsync(0));
238
+ expect(vi.getTimerCount()).toBe(1);
239
+ unmount();
240
+ expect(vi.getTimerCount()).toBe(0);
241
+ });
242
+
150
243
  describe("notifications", () => {
151
244
  beforeEach(() => {
152
245
  vi.spyOn(document, "visibilityState", "get").mockReturnValue("hidden");
@@ -156,7 +249,7 @@ describe("DynamicFavicon", () => {
156
249
  global.Notification.permission = "granted";
157
250
  });
158
251
 
159
- it("should send success notification when run completes without errors", () => {
252
+ it("should send success notification when run completes without errors", async () => {
160
253
  // @ts-expect-error ok in tests
161
254
  global.Notification = vi
162
255
  .fn()
@@ -172,12 +265,11 @@ describe("DynamicFavicon", () => {
172
265
 
173
266
  const { rerender } = render(<DynamicFavicon isRunning={true} />);
174
267
  rerender(<DynamicFavicon isRunning={false} />);
268
+ await waitFor(() => expect(Notification).toHaveBeenCalledOnce());
175
269
  });
176
270
 
177
- it("should send error notification when run completes with errors", () => {
178
- (useCellErrors as ReturnType<typeof vi.fn>).mockReturnValue([
179
- { error: "mock error" },
180
- ]);
271
+ it("should send error notification when run completes with errors", async () => {
272
+ setErrors();
181
273
 
182
274
  // @ts-expect-error ok in tests
183
275
  global.Notification = vi
@@ -194,6 +286,7 @@ describe("DynamicFavicon", () => {
194
286
 
195
287
  const { rerender } = render(<DynamicFavicon isRunning={true} />);
196
288
  rerender(<DynamicFavicon isRunning={false} />);
289
+ await waitFor(() => expect(Notification).toHaveBeenCalledOnce());
197
290
  });
198
291
 
199
292
  it("should not send notification when document is visible", () => {
@@ -1,50 +1,34 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import { PAIR_CONFIG } from "@/__tests__/fixtures/pair-config";
5
+ import { shellQuote } from "@/utils/shell";
4
6
  import {
5
7
  type ConnectionInfo,
6
- getFileFromURL,
7
8
  getMarimoCommand,
8
9
  getRawPrompt,
9
10
  getTerminalCommand,
10
11
  maskToken,
11
12
  } from "../pair-with-agent-commands";
12
- import { shellQuote } from "@/utils/shell";
13
- import { PAIR_PREVIEW } from "@/__tests__/fixtures/pair-preview";
14
13
 
15
14
  const CONNECTION: ConnectionInfo = {
16
15
  url: "http://localhost:8000",
17
- file: "notebooks/example.py",
18
- session: "s_ab12cd",
16
+ notebook: "nb-one",
19
17
  };
20
-
21
- const CONNECTION_WITHOUT_FILE: ConnectionInfo = {
18
+ const CONNECTION_WITHOUT_TARGET: ConnectionInfo = {
22
19
  url: "http://localhost:8000",
23
20
  };
24
21
 
25
22
  describe("shellQuote", () => {
26
- it("quotes an empty string", () => {
23
+ it("quotes empty and unsafe arguments", () => {
27
24
  expect(shellQuote("")).toBe("''");
28
- });
29
-
30
- it("leaves shell-safe values untouched", () => {
31
25
  expect(shellQuote("http://localhost:8000")).toBe("http://localhost:8000");
32
- expect(shellQuote("notebooks/example.py")).toBe("notebooks/example.py");
33
- });
34
-
35
- it("quotes values with shell metacharacters", () => {
36
26
  expect(shellQuote("http://host:8000?a=1&b=2")).toBe(
37
27
  "'http://host:8000?a=1&b=2'",
38
28
  );
39
- expect(shellQuote("has space")).toBe("'has space'");
40
29
  expect(shellQuote("$(rm -rf /)")).toBe("'$(rm -rf /)'");
41
- });
42
-
43
- it("escapes embedded single quotes without breaking out", () => {
44
- // Closes the quote, emits a literal ' via "'", then reopens: '"'"'
45
30
  expect(shellQuote("a'b")).toBe(`'a'"'"'b'`);
46
31
  });
47
-
48
32
  it.each([
49
33
  ["/tmp/my notebook.py", "'/tmp/my notebook.py'"],
50
34
  [
@@ -55,267 +39,115 @@ describe("shellQuote", () => {
55
39
  String.raw`\\server\share\my notebook.py`,
56
40
  String.raw`'\\server\share\my notebook.py'`,
57
41
  ],
58
- ])("quotes non-portable path %s as one argument", (path, expected) => {
42
+ ])("quotes path %s as one argument", (path, expected) => {
59
43
  expect(shellQuote(path)).toBe(expected);
60
44
  });
61
45
  });
62
46
 
63
- describe("getFileFromURL", () => {
64
- it("returns undefined when the file query parameter is absent or empty", () => {
65
- expect(getFileFromURL("http://localhost:8000")).toBeUndefined();
66
- expect(getFileFromURL("http://localhost:8000?file=")).toBeUndefined();
67
- });
68
-
69
- it("decodes spaces and literal plus signs", () => {
70
- expect(
71
- getFileFromURL(
72
- "http://localhost:8000?file=relative%2Fmy%20notebook%2Bdata.py",
73
- ),
74
- ).toBe("relative/my notebook+data.py");
75
- expect(getFileFromURL("http://localhost:8000?file=my+notebook.py")).toBe(
76
- "my notebook.py",
77
- );
78
- });
79
-
80
- it("preserves decoded Windows paths and uses the first duplicate value", () => {
81
- expect(
82
- getFileFromURL(
83
- "http://localhost:8000?file=C%3A%5CUsers%5CJane%20Doe%5Cnotebook.py",
84
- ),
85
- ).toBe(String.raw`C:\Users\Jane Doe\notebook.py`);
86
- expect(
87
- getFileFromURL("http://localhost:8000?file=first.py&file=second.py"),
88
- ).toBe("first.py");
89
- });
90
- });
91
-
92
47
  describe("getMarimoCommand", () => {
93
- afterEach(() => {
94
- vi.unstubAllEnvs();
95
- });
96
-
97
- it("uses the local checkout in dev", () => {
48
+ afterEach(() => vi.unstubAllEnvs());
49
+ it("uses the checkout command in development", () => {
98
50
  vi.stubEnv("DEV", true);
99
51
  expect(getMarimoCommand()).toBe("uv run marimo");
100
52
  });
101
-
102
- it("uses uvx outside of dev", () => {
53
+ it("uses uvx outside development", () => {
103
54
  vi.stubEnv("DEV", false);
104
55
  expect(getMarimoCommand()).toBe("uvx marimo@latest");
105
56
  });
106
57
  });
107
58
 
108
59
  describe("getTerminalCommand", () => {
109
- it("uses the preview launcher in production and quotes session-only targets", () => {
110
- vi.stubEnv("DEV", false);
111
- try {
112
- expect(
113
- getTerminalCommand(
114
- "claude",
115
- { url: "http://host:8000?a=1&b=2", session: "s_' {file}" },
116
- false,
117
- { ...PAIR_PREVIEW, command: "uvx marimo@latest" },
118
- ),
119
- ).toBe(
120
- String.raw`claude "$(MARIMO_PAIR_NEXT=1 uvx marimo@latest pair prompt \
121
- --url 'http://host:8000?a=1&b=2' \
122
- --session 's_'"'"' {file}')"`,
123
- );
124
- expect(
125
- getTerminalCommand(
126
- "codex",
127
- CONNECTION_WITHOUT_FILE,
128
- false,
129
- PAIR_PREVIEW,
130
- ),
131
- ).toBe(
132
- String.raw`codex "$(MARIMO_PAIR_NEXT=1 uvx marimo@latest pair prompt \
133
- --url http://localhost:8000)"`,
134
- );
135
- } finally {
136
- vi.unstubAllEnvs();
137
- }
138
- });
139
-
140
60
  it.each([
141
61
  ["claude", "claude"],
142
62
  ["codex", "codex"],
143
63
  ["opencode", "opencode --prompt"],
144
- ] as const)(
145
- "enables the preview inside the %s prompt command",
146
- (agent, cli) => {
147
- expect(
148
- getTerminalCommand(
149
- agent,
150
- { ...CONNECTION, session: "s_ab12cd" },
151
- true,
152
- PAIR_PREVIEW,
153
- ),
154
- ).toBe(
155
- String.raw`${cli} "$(MARIMO_PAIR_NEXT=1 uvx marimo@latest pair prompt \
156
- --url http://localhost:8000 \
157
- --session s_ab12cd \
158
- --with-token)"`,
159
- );
160
- },
161
- );
162
-
163
- it("includes the url and agent flag for each agent", () => {
164
- expect(getTerminalCommand("claude", CONNECTION, false)).toBe(
165
- `claude "$(uv run marimo pair prompt --url http://localhost:8000 --claude)"`,
166
- );
167
- expect(getTerminalCommand("codex", CONNECTION, false)).toBe(
168
- `codex "$(uv run marimo pair prompt --url http://localhost:8000 --codex)"`,
169
- );
170
- expect(getTerminalCommand("opencode", CONNECTION, false)).toBe(
171
- `opencode --prompt "$(uv run marimo pair prompt --url http://localhost:8000 --opencode)"`,
64
+ ] as const)("launches %s through the public CLI", (agent, cli) => {
65
+ expect(getTerminalCommand(agent, CONNECTION, true, PAIR_CONFIG)).toBe(
66
+ `${cli} "$(uvx marimo@latest pair prompt \\\n --url http://localhost:8000 \\\n --notebook nb-one \\\n --with-token)"`,
172
67
  );
173
68
  });
174
-
175
- it("never passes a file to marimo pair prompt", () => {
176
- const command = getTerminalCommand("claude", CONNECTION, false);
177
- expect(command).not.toContain("--file");
178
- expect(command).not.toContain("notebooks/example.py");
179
- });
180
-
181
- it("omits the session outside the preview", () => {
182
- expect(getTerminalCommand("claude", CONNECTION, false)).not.toContain(
183
- "--session",
184
- );
185
- });
186
-
187
- it("omits the session when it is unknown", () => {
188
- expect(
189
- getTerminalCommand(
190
- "claude",
191
- CONNECTION_WITHOUT_FILE,
192
- false,
193
- PAIR_PREVIEW,
194
- ),
195
- ).not.toContain("--session");
196
- });
197
-
198
- it("shell-escapes a url containing metacharacters", () => {
69
+ it("uses a stable Session ID for editor execution", () => {
199
70
  const command = getTerminalCommand(
200
71
  "claude",
201
- { url: "http://host:8000?auth=a&b" },
72
+ { url: "http://host:8000?a=1&b=2", session: "stable_' {file}" },
202
73
  false,
74
+ PAIR_CONFIG,
203
75
  );
204
- expect(command).toContain("--url 'http://host:8000?auth=a&b'");
205
- });
206
-
207
- it("adds --with-token before the agent flag when requested", () => {
208
- const command = getTerminalCommand("claude", CONNECTION, true);
209
- expect(command).toContain("--with-token --claude");
76
+ expect(command).toContain("--url 'http://host:8000?a=1&b=2'");
77
+ expect(command).toContain(`--session 'stable_'"'"' {file}'`);
78
+ expect(command).not.toContain("--notebook");
210
79
  });
211
-
212
- it("omits --with-token when not requested", () => {
213
- expect(getTerminalCommand("claude", CONNECTION, false)).not.toContain(
214
- "--with-token",
80
+ it("prefers the verified Notebook ID to a Session ID", () => {
81
+ const command = getTerminalCommand(
82
+ "codex",
83
+ { ...CONNECTION, session: "stable-one" },
84
+ false,
85
+ PAIR_CONFIG,
215
86
  );
87
+ expect(command).toContain("--notebook nb-one");
88
+ expect(command).not.toContain("stable-one");
89
+ expect(command).not.toContain("--with-token");
90
+ });
91
+ it("uses the CLI without injected configuration", () => {
92
+ vi.stubEnv("DEV", true);
93
+ try {
94
+ const command = getTerminalCommand(
95
+ "claude",
96
+ CONNECTION_WITHOUT_TARGET,
97
+ false,
98
+ );
99
+ expect(command).toContain("uv run marimo pair prompt");
100
+ expect(command).not.toMatch(
101
+ /--claude|MARIMO_PAIR_NEXT|--file|--session|--notebook/,
102
+ );
103
+ } finally {
104
+ vi.unstubAllEnvs();
105
+ }
216
106
  });
217
107
  });
218
108
 
219
109
  describe("getRawPrompt", () => {
220
- it("uses supplied templates without reinterpreting inserted values", () => {
221
- expect(
222
- getRawPrompt(
223
- {
224
- url: "http://host/{session}",
225
- session: "{command}/it's",
226
- },
227
- "tok'en {session} $&",
228
- {
229
- ...PAIR_PREVIEW,
230
- command: "custom marimo",
231
- templates: {
232
- ...PAIR_PREVIEW.templates,
233
- prompt: "{command}\n{url}\n{session}{authentication}",
234
- },
110
+ it("does not interpret placeholders in inserted values", () => {
111
+ const prompt = getRawPrompt(
112
+ { url: "http://host/{session}", notebook: "{command}/it's" },
113
+ "tok'en {session} $&",
114
+ {
115
+ ...PAIR_CONFIG,
116
+ command: "custom marimo",
117
+ templates: {
118
+ ...PAIR_CONFIG.templates,
119
+ prompt: "{command}\n{url}\n{notebook}{authentication}",
235
120
  },
236
- ),
237
- ).toBe(
238
- "custom marimo\nhttp://host/{session}\nSession: {command}/it's\n" +
239
- `\n\nFor authenticated Pair commands, set \`export MARIMO_TOKEN='tok'"'"'en {session} $&'\` in the shell that runs marimo.`,
121
+ },
122
+ );
123
+ expect(prompt).toBe(
124
+ "custom marimo\nhttp://host/{session}\nNotebook: {command}/it's\n" +
125
+ `\n\nFor authenticated notebook discovery, set \`export MARIMO_TOKEN='tok'"'"'en {session} $&'\` in the shell that runs marimo.`,
240
126
  );
241
127
  });
242
-
243
128
  it.each([
244
- [CONNECTION_WITHOUT_FILE, ""],
129
+ [CONNECTION_WITHOUT_TARGET, ""],
130
+ [CONNECTION, "Notebook: nb-one\n"],
245
131
  [
246
- { ...CONNECTION_WITHOUT_FILE, session: "s_ab12cd" },
247
- "Session: s_ab12cd\n",
132
+ { ...CONNECTION_WITHOUT_TARGET, session: "stable-one" },
133
+ "Session: stable-one\n",
248
134
  ],
249
- [{ ...CONNECTION_WITHOUT_FILE, file: "notebook.py" }, ""],
250
- ])("omits absent preview fields for %j", (connection, fields) => {
251
- expect(getRawPrompt(connection, null, PAIR_PREVIEW)).toBe(
252
- "Pair with me on this running marimo notebook.\n\n" +
253
- `URL: http://localhost:8000\n${fields}\n` +
254
- "Run `uvx marimo@latest pair --help` first.\n" +
255
- "Use `uvx marimo@latest` for all marimo commands.\n\n" +
256
- "Once connected, send a fun toast using `mo.status.toast(...)` (`import marimo as mo`).",
257
- );
258
- });
259
-
260
- it("renders the shared preview template with the session and no file", () => {
135
+ ])("renders the shared CLI prompt for %j", (connection, target) => {
136
+ const expected = `Pair with me on this running marimo notebook.\n\nURL: http://localhost:8000\n${target}\nRun \`uvx marimo@latest pair --help\` first.\nUse \`uvx marimo@latest\` for all marimo commands.`;
137
+ expect(getRawPrompt(connection, null, PAIR_CONFIG)).toBe(expected);
261
138
  expect(
262
- getRawPrompt({ ...CONNECTION, session: "s_ab12cd" }, null, PAIR_PREVIEW),
263
- ).toBe(
264
- "Pair with me on this running marimo notebook.\n\n" +
265
- "URL: http://localhost:8000\n" +
266
- "Session: s_ab12cd\n\n" +
267
- "Run `uvx marimo@latest pair --help` first.\n" +
268
- "Use `uvx marimo@latest` for all marimo commands.\n\n" +
269
- "Once connected, send a fun toast using `mo.status.toast(...)` (`import marimo as mo`).",
270
- );
139
+ getRawPrompt(connection, null, { ...PAIR_CONFIG, hostEnabled: false }),
140
+ ).toBe(expected);
271
141
  });
272
-
273
- it("references the file-scoped execute-code command", () => {
274
- const prompt = getRawPrompt(CONNECTION, null);
275
- expect(prompt).toContain(
276
- "execute-code.sh --url http://localhost:8000 --file notebooks/example.py",
277
- );
278
- expect(prompt).toContain(
279
- "Connect to the notebook at: http://localhost:8000 (file notebooks/example.py)",
280
- );
281
- });
282
-
283
- it("omits file targeting when the page URL has no file", () => {
284
- const prompt = getRawPrompt(CONNECTION_WITHOUT_FILE, null);
285
- expect(prompt).toContain("execute-code.sh --url http://localhost:8000");
286
- expect(prompt).not.toContain("--file");
287
- expect(prompt).not.toContain("--session");
288
- });
289
-
290
- it("omits the token hint when there is no token", () => {
291
- const prompt = getRawPrompt(CONNECTION, null);
292
- expect(prompt).not.toContain("--token");
293
- expect(prompt).not.toContain("auth token");
294
- });
295
-
296
- it("includes a file-scoped token hint when a token is present", () => {
297
- const prompt = getRawPrompt(CONNECTION, "secret-token");
298
- expect(prompt).toContain(
299
- "execute-code.sh --url http://localhost:8000 --file notebooks/example.py --token secret-token",
300
- );
301
- });
302
-
303
- it("shell-escapes a token containing a single quote", () => {
304
- const prompt = getRawPrompt(CONNECTION, "tok'en");
305
- expect(prompt).toContain(`--token 'tok'"'"'en'`);
306
- });
307
-
308
- it("matches the CLI prompt shape", () => {
309
- const prompt = getRawPrompt(CONNECTION, null);
310
- expect(prompt).toMatchInlineSnapshot(`
311
- "Use the /marimo-pair skill to pair-program on a running marimo notebook.
312
-
313
- Connect to the notebook at: http://localhost:8000 (file notebooks/example.py)
314
-
315
- Use \`execute-code.sh --url http://localhost:8000 --file notebooks/example.py\` from the marimo-pair skill to execute code in the notebook.
316
-
317
- Once you are connected, send a fun toast (mo.status.toast(...)) to the user inside marimo letting them know you're ready to pair."
318
- `);
142
+ it("matches the shared prompt without injected templates", () => {
143
+ vi.stubEnv("DEV", false);
144
+ try {
145
+ expect(getRawPrompt(CONNECTION, "secret-token")).toBe(
146
+ getRawPrompt(CONNECTION, "secret-token", PAIR_CONFIG),
147
+ );
148
+ } finally {
149
+ vi.unstubAllEnvs();
150
+ }
319
151
  });
320
152
  });
321
153
 
@@ -324,12 +156,8 @@ describe("maskToken", () => {
324
156
  expect(maskToken("ab")).toBe("****");
325
157
  expect(maskToken("abcd")).toBe("****");
326
158
  });
327
-
328
- it("reveals only the last four characters", () => {
329
- expect(maskToken("abcdefgh")).toBe("****efgh");
330
- });
331
-
332
- it("caps the number of mask characters at eight", () => {
333
- expect(maskToken("0123456789abcdef")).toBe("********cdef");
334
- });
159
+ it("reveals only the last four characters", () =>
160
+ expect(maskToken("abcdefgh")).toBe("****efgh"));
161
+ it("caps mask characters at eight", () =>
162
+ expect(maskToken("0123456789abcdef")).toBe("********cdef"));
335
163
  });