@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
@@ -8,21 +8,24 @@ import {
8
8
  screen,
9
9
  waitFor,
10
10
  } from "@testing-library/react";
11
- import { createStore, Provider, useAtomValue } from "jotai";
11
+ import { atom, createStore, Provider, useAtomValue } from "jotai";
12
12
  import type React from "react";
13
13
  import { beforeEach, describe, expect, it, vi } from "vitest";
14
14
  import { MockNotebook } from "@/__mocks__/notebook";
15
15
  import { cellId } from "@/__tests__/branded";
16
- import { PAIR_PREVIEW } from "@/__tests__/fixtures/pair-preview";
16
+ import { PAIR_CONFIG } from "@/__tests__/fixtures/pair-config";
17
17
  import { TooltipProvider } from "@/components/ui/tooltip";
18
18
  import { toast } from "@/components/ui/use-toast";
19
- import { type Attachment, attachmentsAtom } from "@/core/attachments/state";
19
+ import type { Attachment } from "@/core/attachments/state";
20
20
  import { exportedForTesting, notebookAtom } from "@/core/cells/cells";
21
- import type {
22
- CellOutputHandoff,
23
- ErrorHandoffOutput,
21
+ import {
22
+ type CellOutputMessage,
23
+ type ErrorMessageOutput,
24
+ ERROR_MESSAGE_INSTRUCTIONS,
24
25
  } from "@/core/cells/error-report";
25
- import { pairPreviewAtom } from "@/core/config/pair";
26
+ import { pairConfigAtom } from "@/core/config/pair";
27
+ import { notebookStateFromSession } from "@/core/cells/session";
28
+ import { createNotebookClient } from "@/core/host/notebook-client";
26
29
  import type { MarimoError, OutputMessage } from "@/core/kernel/messages";
27
30
  import {
28
31
  type StableSessionId,
@@ -30,7 +33,8 @@ import {
30
33
  } from "@/core/kernel/session";
31
34
  import { API } from "@/core/network/api";
32
35
  import { connectionAtom } from "@/core/network/connection";
33
- import { WebSocketClosedReason, ConnectionState } from "@/core/websocket/types";
36
+ import { notebookClientAtom } from "@/core/websocket/state";
37
+ import { ConnectionState } from "@/core/websocket/types";
34
38
  import { Deferred } from "@/utils/Deferred";
35
39
  import { HTTPError } from "@/utils/errors";
36
40
  import { MarimoErrorOutput } from "../../output/MarimoErrorOutput";
@@ -46,6 +50,8 @@ vi.mock("@/components/editor/chrome/wrapper/useOpenAiAssistant", () => ({
46
50
 
47
51
  const CELL_ID = cellId("test-cell");
48
52
  const STABLE_ID = "stable/test" as StableSessionId;
53
+ const attachmentsAtom = atom<Attachment[]>([]);
54
+
49
55
  const AGENT: Attachment = {
50
56
  id: "a1",
51
57
  kind: "agent",
@@ -57,11 +63,15 @@ const AGENT: Attachment = {
57
63
  const ROUTE = "/pair/notebooks/stable%2Ftest/messages";
58
64
  const MESSAGE = "df is not defined";
59
65
  const CODE = "df.head()";
60
- const REPORT: CellOutputHandoff<ErrorHandoffOutput> = {
66
+ const REPORT: CellOutputMessage<ErrorMessageOutput> = {
61
67
  type: "cell-output",
62
68
  cell_id: CELL_ID,
63
69
  code: CODE,
64
- output: { type: "error", message: MESSAGE },
70
+ output: {
71
+ type: "error",
72
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
73
+ message: MESSAGE,
74
+ },
65
75
  };
66
76
  const ERRORS: MarimoError[] = [
67
77
  {
@@ -86,7 +96,12 @@ function renderButton({
86
96
  } = {}) {
87
97
  const store = createStore();
88
98
  store.set(connectionAtom, { state: ConnectionState.OPEN });
89
- store.set(pairPreviewAtom, preview ? PAIR_PREVIEW : undefined);
99
+ store.set(pairConfigAtom, { ...PAIR_CONFIG, hostEnabled: preview });
100
+ store.set(notebookClientAtom, {
101
+ ...createNotebookClient(store).client,
102
+ connectionAtom: atom({ state: ConnectionState.OPEN }),
103
+ attachmentsAtom,
104
+ });
90
105
  store.set(attachmentsAtom, attachments);
91
106
  store.set(stableSessionIdAtom, stableId);
92
107
  const wrapper = ({ children }: { children: React.ReactNode }) => (
@@ -105,9 +120,15 @@ describe("SendErrorReportButton", () => {
105
120
  beforeEach(() => {
106
121
  vi.clearAllMocks();
107
122
  vi.mocked(API.post).mockResolvedValue("");
123
+ vi.mocked(toast).mockReturnValue({
124
+ id: "notice",
125
+ dismiss: vi.fn(),
126
+ update: vi.fn(),
127
+ upsert: vi.fn(),
128
+ });
108
129
  });
109
130
 
110
- it("hides outside the preview without building content", () => {
131
+ it("hides when host is disabled without building content", () => {
111
132
  const getContent = vi.fn(() => REPORT);
112
133
  expect(
113
134
  renderButton({
@@ -138,7 +159,10 @@ describe("SendErrorReportButton", () => {
138
159
  expect(getContent).not.toHaveBeenCalled();
139
160
  send();
140
161
  await waitFor(() =>
141
- expect(toast).toHaveBeenCalledWith({ title: "Sent to Pi, Claude Code" }),
162
+ expect(toast).toHaveBeenCalledWith({
163
+ title: "Sent to Pi, Claude Code",
164
+ duration: 3000,
165
+ }),
142
166
  );
143
167
  expect(API.post).toHaveBeenCalledWith(ROUTE, REPORT);
144
168
  expect(getContent).toHaveBeenCalledOnce();
@@ -162,26 +186,6 @@ describe("SendErrorReportButton", () => {
162
186
  expect(screen.getByRole("button")).toBeDisabled();
163
187
  });
164
188
 
165
- it.each([
166
- { state: ConnectionState.CONNECTING },
167
- { state: ConnectionState.CLOSING },
168
- { state: ConnectionState.NOT_STARTED },
169
- {
170
- state: ConnectionState.CLOSED,
171
- code: WebSocketClosedReason.KERNEL_DISCONNECTED,
172
- reason: "offline",
173
- },
174
- ])(
175
- "does not send from a stale snapshot while the browser is %j",
176
- (connection) => {
177
- const { store } = renderButton();
178
- act(() => store.set(connectionAtom, connection));
179
- expect(screen.getByRole("button")).toBeDisabled();
180
- send();
181
- expect(API.post).not.toHaveBeenCalled();
182
- },
183
- );
184
-
185
189
  it.each([
186
190
  [new HTTPError(409, "Conflict"), "No agent attached"],
187
191
  [new HTTPError(500, "Server error"), "Could not send error"],
@@ -190,12 +194,20 @@ describe("SendErrorReportButton", () => {
190
194
  renderButton();
191
195
  send();
192
196
  await waitFor(() =>
193
- expect(toast).toHaveBeenCalledWith({ variant: "danger", title }),
197
+ expect(toast).toHaveBeenCalledWith({
198
+ variant: "danger",
199
+ title,
200
+ description: error.message,
201
+ duration: 5000,
202
+ }),
194
203
  );
195
204
  expect(screen.getByRole("button")).toBeEnabled();
196
205
  send();
197
206
  await waitFor(() =>
198
- expect(toast).toHaveBeenCalledWith({ title: "Sent to Pi" }),
207
+ expect(toast).toHaveBeenCalledWith({
208
+ title: "Sent to Pi",
209
+ duration: 3000,
210
+ }),
199
211
  );
200
212
  expect(API.post).toHaveBeenCalledTimes(2);
201
213
  expect(API.post).toHaveBeenLastCalledWith(ROUTE, REPORT);
@@ -222,6 +234,7 @@ describe("SendErrorReportButton", () => {
222
234
  ...REPORT,
223
235
  output: {
224
236
  type: "error",
237
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
225
238
  message: MESSAGE,
226
239
  traceback: "Traceback\nNameError: df is not defined",
227
240
  },
@@ -264,6 +277,7 @@ describe("SendErrorReportButton", () => {
264
277
  ...REPORT,
265
278
  output: {
266
279
  type: "error",
280
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
267
281
  message: "ValueError: bad <value>",
268
282
  traceback: "Traceback\nValueError: bad <value>",
269
283
  },
@@ -271,6 +285,90 @@ describe("SendErrorReportButton", () => {
271
285
  );
272
286
  });
273
287
 
288
+ it("sends plain code-mode traceback text without parsing it as HTML", async () => {
289
+ const traceback =
290
+ ' File "<cell>", line 1\nValueError: bad <value> & <other>';
291
+ renderButton({
292
+ content: (
293
+ <OutputRenderer
294
+ cellId={CELL_ID}
295
+ message={{
296
+ channel: "stderr",
297
+ mimetype: "application/vnd.marimo+traceback",
298
+ traceback_format: "text",
299
+ code: CODE,
300
+ data: traceback,
301
+ }}
302
+ />
303
+ ),
304
+ });
305
+ expect(screen.getByText(/ValueError: bad <value>/)).toHaveTextContent(
306
+ "<cell>",
307
+ );
308
+ send();
309
+ await waitFor(() =>
310
+ expect(API.post).toHaveBeenCalledWith(ROUTE, {
311
+ ...REPORT,
312
+ output: {
313
+ type: "error",
314
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
315
+ message: "ValueError: bad <value> & <other>",
316
+ traceback,
317
+ },
318
+ }),
319
+ );
320
+ });
321
+
322
+ it("sends the restored exception type and complete traceback", async () => {
323
+ const state = notebookStateFromSession(
324
+ {
325
+ version: "1",
326
+ metadata: { marimo_version: "1", script_metadata_hash: null },
327
+ cells: [
328
+ {
329
+ id: CELL_ID,
330
+ code_hash: null,
331
+ console: [],
332
+ outputs: [
333
+ {
334
+ type: "error",
335
+ ename: "ValueError",
336
+ evalue: "cached failure",
337
+ traceback: [
338
+ "<pre>Traceback",
339
+ " failed()\n",
340
+ "ValueError: cached failure</pre>",
341
+ ],
342
+ code: CODE,
343
+ },
344
+ ],
345
+ },
346
+ ],
347
+ },
348
+ null,
349
+ );
350
+ const message = state?.cellRuntime[CELL_ID].output;
351
+ expect(message).toBeDefined();
352
+ renderButton({
353
+ content: message && <OutputRenderer cellId={CELL_ID} message={message} />,
354
+ });
355
+ send();
356
+ await waitFor(() =>
357
+ expect(API.post).toHaveBeenCalledWith(ROUTE, {
358
+ ...REPORT,
359
+ output: {
360
+ type: "error",
361
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
362
+ message: "cached failure",
363
+ traceback: "Traceback\n failed()\nValueError: cached failure",
364
+ },
365
+ }),
366
+ );
367
+ expect(message?.data).toEqual([
368
+ expect.objectContaining({ exception_type: "ValueError" }),
369
+ ]);
370
+ });
371
+
274
372
  it("sends updated content after the error changes", async () => {
275
373
  const { rerender } = renderButton();
276
374
  const updated = {
@@ -385,7 +483,11 @@ describe("SendErrorReportButton", () => {
385
483
  type: "cell-output",
386
484
  cell_id: CELL_ID,
387
485
  code: "x + 1",
388
- output: { type: "error", message: "Ancestor failed" },
486
+ output: {
487
+ type: "error",
488
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
489
+ message: "Ancestor failed",
490
+ },
389
491
  }),
390
492
  );
391
493
  });
@@ -408,6 +510,7 @@ describe("SendErrorReportButton", () => {
408
510
  code: "slow()",
409
511
  output: {
410
512
  type: "error",
513
+ instructions: ERROR_MESSAGE_INSTRUCTIONS,
411
514
  message: "This cell was interrupted and needs to be re-run",
412
515
  },
413
516
  }),
@@ -4,27 +4,27 @@ import { useAtomValue } from "jotai";
4
4
  import { SendIcon } from "lucide-react";
5
5
  import { useState } from "react";
6
6
  import { Button } from "@/components/ui/button";
7
- import { toast } from "@/components/ui/use-toast";
8
7
  import {
9
8
  agentAttachmentsAtom,
10
9
  attachmentLabel,
11
10
  } from "@/core/attachments/state";
12
- import { pairPreviewAtom } from "@/core/config/pair";
11
+ import { agentToast } from "@/core/attachments/toast";
12
+ import { pairConfigAtom } from "@/core/config/pair";
13
13
  import { stableSessionIdAtom } from "@/core/kernel/session";
14
14
  import { API } from "@/core/network/api";
15
- import { HTTPError } from "@/utils/errors";
15
+ import { HTTPError, prettyError } from "@/utils/errors";
16
16
 
17
17
  export function SendErrorReportButton({
18
18
  getContent,
19
19
  }: {
20
20
  getContent: () => unknown;
21
21
  }) {
22
- const pairPreview = useAtomValue(pairPreviewAtom);
22
+ const hostEnabled = useAtomValue(pairConfigAtom)?.hostEnabled;
23
23
  const agents = useAtomValue(agentAttachmentsAtom);
24
24
  const stableSessionId = useAtomValue(stableSessionIdAtom);
25
25
  const [sending, setSending] = useState(false);
26
26
 
27
- if (!pairPreview) {
27
+ if (!hostEnabled) {
28
28
  return null;
29
29
  }
30
30
 
@@ -38,14 +38,17 @@ export function SendErrorReportButton({
38
38
  `/pair/notebooks/${encodeURIComponent(stableSessionId)}/messages`,
39
39
  getContent(),
40
40
  );
41
- toast({ title: `Sent to ${agents.map(attachmentLabel).join(", ")}` });
41
+ agentToast({
42
+ title: `Sent to ${agents.map(attachmentLabel).join(", ")}`,
43
+ });
42
44
  } catch (error) {
43
- toast({
45
+ agentToast({
44
46
  variant: "danger",
45
47
  title:
46
48
  error instanceof HTTPError && error.status === 409
47
49
  ? "No agent attached"
48
50
  : "Could not send error",
51
+ description: prettyError(error),
49
52
  });
50
53
  } finally {
51
54
  setSending(false);
@@ -9,7 +9,7 @@ import {
9
9
  SaveIcon,
10
10
  } from "lucide-react";
11
11
  import type React from "react";
12
- import { useEffect, useRef, useState } from "react";
12
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
13
13
  import { renderShortcut } from "@/components/shortcuts/renderShortcut";
14
14
  import { Alert, AlertDescription } from "@/components/ui/alert";
15
15
  import { Button } from "@/components/ui/button";
@@ -82,7 +82,9 @@ export const FileViewer: React.FC<Props> = ({ file, onOpenNotebook }) => {
82
82
  // carry no draft to preserve.
83
83
  // We use a ref for internalValue so we don't call this effect on each keystroke
84
84
  const internalValueRef = useRef<string>(internalValue);
85
- internalValueRef.current = internalValue;
85
+ useLayoutEffect(() => {
86
+ internalValueRef.current = internalValue;
87
+ }, [internalValue]);
86
88
  useEffect(() => {
87
89
  return () => {
88
90
  if (
@@ -35,6 +35,7 @@ import { isWasm } from "@/core/wasm/utils";
35
35
  import { renderHTML } from "@/plugins/core/RenderHTML";
36
36
  import { sanitizeHtml } from "@/plugins/core/sanitize-html";
37
37
  import { copyToClipboard } from "@/utils/copy";
38
+ import { parseHtmlContent } from "@/utils/dom";
38
39
  import {
39
40
  containsMangledLocal,
40
41
  splitMangledLocals,
@@ -54,29 +55,38 @@ import type { OnRefactorWithAI } from "../Output";
54
55
  interface Props {
55
56
  cellId: CellId | undefined;
56
57
  traceback: string;
58
+ tracebackFormat?: "html" | "text";
57
59
  code?: string;
58
60
  onRefactorWithAI?: OnRefactorWithAI;
59
61
  }
60
62
 
61
- /**
62
- * List of errors due to violations of Marimo semantics.
63
- */
63
+ /** Render a traceback and actions for its captured execution. */
64
64
  export const MarimoTracebackOutput = ({
65
65
  onRefactorWithAI,
66
66
  traceback,
67
+ tracebackFormat = "html",
67
68
  cellId,
68
69
  code,
69
70
  }: Props): JSX.Element => {
70
- const htmlTraceback = renderHTML({
71
- html: traceback,
72
- additionalReplacements: [
73
- replaceTracebackFilenames,
74
- replaceTracebackPrefix,
75
- replaceMangledLocal,
76
- ],
77
- });
71
+ const htmlTraceback =
72
+ tracebackFormat === "text" ? (
73
+ <pre>{traceback}</pre>
74
+ ) : (
75
+ renderHTML({
76
+ html: traceback,
77
+ additionalReplacements: [
78
+ replaceTracebackFilenames,
79
+ replaceTracebackPrefix,
80
+ replaceMangledLocal,
81
+ ],
82
+ })
83
+ );
78
84
 
79
- const lastTracebackLine = lastLine(traceback);
85
+ const textTraceback =
86
+ tracebackFormat === "html"
87
+ ? parseHtmlContent(sanitizeHtml(traceback))
88
+ : traceback;
89
+ const lastTracebackLine = lastLine(textTraceback);
80
90
  const aiFeaturesEnabled = useAtomValue(aiFeaturesEnabledAtom);
81
91
  const openAiAssistant = useOpenAiAssistant();
82
92
 
@@ -137,7 +147,9 @@ export const MarimoTracebackOutput = ({
137
147
  <div className="flex gap-2">
138
148
  {cellId && lastTracebackLine && (
139
149
  <SendErrorReportButton
140
- getContent={() => buildErrorReport(cellId, { code, traceback })}
150
+ getContent={() =>
151
+ buildErrorReport(cellId, { code, traceback, tracebackFormat })
152
+ }
141
153
  />
142
154
  )}
143
155
  {showAIFix && (
@@ -195,11 +207,7 @@ export const MarimoTracebackOutput = ({
195
207
  </DropdownMenuItem>
196
208
  <DropdownMenuItem
197
209
  onClick={() => {
198
- // Strip HTML from the traceback (sanitize first to prevent XSS)
199
- const div = document.createElement("div");
200
- div.innerHTML = sanitizeHtml(traceback);
201
- const textContent = div.textContent || "";
202
- copyToClipboard(textContent);
210
+ copyToClipboard(textTraceback);
203
211
  }}
204
212
  >
205
213
  <CopyIcon className="h-4 w-4 mr-2" />
@@ -214,9 +222,7 @@ export const MarimoTracebackOutput = ({
214
222
  };
215
223
 
216
224
  function lastLine(text: string): string {
217
- const el = document.createElement("div");
218
- el.innerHTML = sanitizeHtml(text);
219
- const lines = el.textContent?.split("\n").filter(Boolean);
225
+ const lines = text.split("\n").filter(Boolean);
220
226
  return lines?.at(-1) || "";
221
227
  }
222
228
 
@@ -242,7 +248,7 @@ export const replaceTracebackFilenames = (domNode: DOMNode) => {
242
248
  <Tooltip content={tooltipContent}>
243
249
  <button
244
250
  type="button"
245
- className="ml-1 p-1 rounded-sm hover:bg-muted transition-all inline"
251
+ className="ml-1 p-1 rounded-sm hover:bg-muted transition-colors inline"
246
252
  >
247
253
  <BugPlayIcon
248
254
  onClick={() => {
@@ -6,7 +6,7 @@ import {
6
6
  ChevronsUpDownIcon,
7
7
  WrapTextIcon,
8
8
  } from "lucide-react";
9
- import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
9
+ import React, { useEffect, useLayoutEffect, useState } from "react";
10
10
  import { ToggleButton } from "react-aria-components";
11
11
  import { DebuggerControls } from "@/components/debugger/debugger-code";
12
12
  import { CopyClipboardIcon } from "@/components/icons/copy-icon";
@@ -49,31 +49,21 @@ export const CONSOLE_CLEAR_DEBOUNCE_MS = 200;
49
49
  */
50
50
  function useDebouncedConsoleOutputs<T>(outputs: T[]): T[] {
51
51
  const [debouncedOutputs, setDebouncedOutputs] = useState(outputs);
52
- const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
53
52
 
54
- // Non-empty outputs: apply immediately and cancel any pending clear
53
+ // Non-empty outputs: apply immediately. Effect cleanup cancels pending clears.
55
54
  if (outputs.length > 0 && debouncedOutputs !== outputs) {
56
- if (timerRef.current !== null) {
57
- clearTimeout(timerRef.current);
58
- timerRef.current = null;
59
- }
60
55
  setDebouncedOutputs(outputs);
61
56
  }
62
57
 
63
58
  // Empty outputs: delay the clear so new outputs can arrive first
64
59
  useEffect(() => {
65
- if (outputs.length === 0 && timerRef.current === null) {
66
- timerRef.current = setTimeout(() => {
67
- timerRef.current = null;
68
- setDebouncedOutputs([]);
69
- }, CONSOLE_CLEAR_DEBOUNCE_MS);
60
+ if (outputs.length > 0) {
61
+ return;
70
62
  }
71
- return () => {
72
- if (timerRef.current !== null) {
73
- clearTimeout(timerRef.current);
74
- timerRef.current = null;
75
- }
76
- };
63
+ const timer = setTimeout(() => {
64
+ setDebouncedOutputs([]);
65
+ }, CONSOLE_CLEAR_DEBOUNCE_MS);
66
+ return () => clearTimeout(timer);
77
67
  }, [outputs]);
78
68
 
79
69
  return debouncedOutputs;
@@ -514,7 +514,7 @@ const MarimoFileComponent = ({ file }: { file: MarimoFile }) => {
514
514
 
515
515
  return (
516
516
  <a
517
- className="py-1.5 px-4 hover:bg-(--blue-2) hover:text-primary transition-all duration-300 cursor-pointer group relative flex gap-4 items-center"
517
+ className="py-1.5 px-4 hover:bg-(--blue-2) hover:text-primary transition-colors duration-300 cursor-pointer group relative flex gap-4 items-center"
518
518
  key={file.path}
519
519
  href={href.toString()}
520
520
  target={tabTarget(file.initializationId || file.path)}
@@ -542,7 +542,7 @@ const MarimoFileComponent = ({ file }: { file: MarimoFile }) => {
542
542
  </div>
543
543
  <ExternalLinkIcon
544
544
  size={20}
545
- className="group-hover:opacity-100 opacity-0 transition-all duration-300 text-primary"
545
+ className="group-hover:opacity-100 opacity-0 transition-opacity duration-300 text-primary"
546
546
  />
547
547
  </div>
548
548
  {!!file.lastModified && (
@@ -617,14 +617,14 @@ const CreateNewNotebook: React.FC = () => {
617
617
  <a
618
618
  className="relative rounded-lg p-6 group
619
619
  text-primary hover:bg-(--blue-2) shadow-md-solid shadow-accent border bg-(--blue-1)
620
- transition-all duration-300 cursor-pointer
620
+ transition-colors duration-300 cursor-pointer
621
621
  "
622
622
  href={url}
623
623
  target="_blank"
624
624
  rel="noreferrer"
625
625
  >
626
626
  <h2 className="text-lg font-semibold">Create a new notebook</h2>
627
- <div className="group-hover:opacity-100 opacity-0 absolute right-5 top-0 bottom-0 rounded-lg flex items-center justify-center transition-all duration-300">
627
+ <div className="group-hover:opacity-100 opacity-0 absolute right-5 top-0 bottom-0 rounded-lg flex items-center justify-center transition-opacity duration-300">
628
628
  <ExternalLinkIcon size={24} />
629
629
  </div>
630
630
  </a>
@@ -117,7 +117,7 @@ export const SlideCellView = ({ cell }: { cell: RuntimeCell }) => {
117
117
  />
118
118
  <LanguageToggles
119
119
  code={cell.code}
120
- editorView={editorViewRef.current}
120
+ editorViewRef={editorViewRef}
121
121
  currentLanguageAdapter={languageAdapter}
122
122
  onAfterToggle={afterToggleLanguage}
123
123
  className="flex items-center gap-1"
@@ -145,11 +145,11 @@ const TraceBlock: React.FC<{
145
145
  isMostRecentRun: boolean;
146
146
  chartPosition: ChartPosition;
147
147
  theme: ResolvedTheme;
148
- }> = ({ run, isMostRecentRun, chartPosition, isExpanded, theme }) => {
148
+ }> = ({ run, isMostRecentRun, chartPosition, isExpanded: expanded, theme }) => {
149
149
  const setExpandedRuns = useSetAtom(expandedRunsAtom);
150
150
  // We prefer the user's last click, but if they haven't clicked on this run,
151
151
  // we expand the most recent run by default, otherwise we collapse it.
152
- isExpanded = isExpanded ?? isMostRecentRun;
152
+ const isExpanded = expanded ?? isMostRecentRun;
153
153
 
154
154
  const onToggleExpanded = () => {
155
155
  setExpandedRuns((prev) => {
@@ -0,0 +1,58 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ // @vitest-environment jsdom
3
+
4
+ import { act, render, renderHook } from "@testing-library/react";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { Toaster } from "@/components/ui/toaster";
7
+ import { clearToasts, toast, useToast } from "@/components/ui/use-toast";
8
+ import { agentToast } from "../toast";
9
+
10
+ describe("agent notifications", () => {
11
+ beforeEach(() => {
12
+ vi.useFakeTimers();
13
+ clearToasts();
14
+ });
15
+
16
+ afterEach(() => {
17
+ act(() => {
18
+ clearToasts();
19
+ vi.runOnlyPendingTimers();
20
+ });
21
+ vi.useRealTimers();
22
+ });
23
+
24
+ it.each([
25
+ { variant: undefined, duration: 3000 },
26
+ { variant: "danger" as const, duration: 5000 },
27
+ ])("expires while the window is blurred: %j", ({ variant, duration }) => {
28
+ const { result } = renderHook(useToast);
29
+ render(<Toaster />);
30
+ act(() => agentToast({ title: "Agent notice", variant }));
31
+ act(() => window.dispatchEvent(new Event("blur")));
32
+ act(() => vi.advanceTimersByTime(duration - 1));
33
+ expect(result.current.toasts[0].open).toBe(true);
34
+ act(() => vi.advanceTimersByTime(1));
35
+ expect(result.current.toasts[0].open).toBe(false);
36
+ });
37
+
38
+ it("preserves pause behavior for ordinary notifications", () => {
39
+ const { result } = renderHook(useToast);
40
+ render(<Toaster />);
41
+ act(() => toast({ title: "Ordinary notice", duration: 3000 }));
42
+ act(() => window.dispatchEvent(new Event("blur")));
43
+ act(() => vi.advanceTimersByTime(5000));
44
+ expect(result.current.toasts[0].open).toBe(true);
45
+ });
46
+
47
+ it("does not let an older timer dismiss a newer notification", () => {
48
+ const { result } = renderHook(useToast);
49
+ act(() => agentToast({ title: "First" }));
50
+ act(() => vi.advanceTimersByTime(1000));
51
+ act(() => agentToast({ title: "Second" }));
52
+ act(() => vi.advanceTimersByTime(2000));
53
+ expect(result.current.toasts[0].title).toBe("Second");
54
+ expect(result.current.toasts[0].open).toBe(true);
55
+ act(() => vi.advanceTimersByTime(1000));
56
+ expect(result.current.toasts[0].open).toBe(false);
57
+ });
58
+ });
@@ -1,17 +1,14 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import type { components } from "@marimo-team/marimo-api";
4
3
  import { atom } from "jotai";
5
- import { isConnectedAtom } from "@/core/network/connection";
4
+ import type { Attachment } from "@marimo-team/client/protocol";
5
+ import { notebookClientAtom } from "@/core/websocket/state";
6
6
 
7
- export type Attachment = components["schemas"]["Attachment"];
7
+ export type { Attachment } from "@marimo-team/client/protocol";
8
8
 
9
- export const attachmentsAtom = atom<Attachment[]>([]);
10
9
  export const agentAttachmentsAtom = atom((get) => {
11
- if (!get(isConnectedAtom)) {
12
- return [];
13
- }
14
- return get(attachmentsAtom).filter(
10
+ const notebook = get(notebookClientAtom);
11
+ return (notebook ? get(notebook.attachmentsAtom) : []).filter(
15
12
  (attachment) => attachment.kind === "agent",
16
13
  );
17
14
  });
@@ -0,0 +1,15 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { toast } from "@/components/ui/use-toast";
4
+
5
+ export function agentToast(
6
+ options: Pick<
7
+ Parameters<typeof toast>[0],
8
+ "title" | "variant" | "description"
9
+ >,
10
+ ) {
11
+ const duration = options.variant === "danger" ? 5000 : 3000;
12
+ const { dismiss } = toast({ ...options, duration });
13
+ // Agent notices must also expire while the user focuses the agent terminal.
14
+ window.setTimeout(dismiss, duration);
15
+ }