@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,8 +1,15 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { useAtomValue } from "jotai";
4
- import { BotIcon } from "lucide-react";
5
- import { useState } from "react";
4
+ import {
5
+ BotIcon,
6
+ CheckIcon,
7
+ Loader2Icon,
8
+ UnplugIcon,
9
+ XIcon,
10
+ } from "lucide-react";
11
+ import { useCallback, useEffect, useRef, useState } from "react";
12
+ import { Button } from "@/components/ui/button";
6
13
  import {
7
14
  Popover,
8
15
  PopoverContent,
@@ -13,43 +20,86 @@ import {
13
20
  agentAttachmentsAtom,
14
21
  attachmentLabel,
15
22
  } from "@/core/attachments/state";
16
- import { pairPreviewAtom } from "@/core/config/pair";
23
+ import { agentToast } from "@/core/attachments/toast";
24
+ import { pairConfigAtom } from "@/core/config/pair";
25
+ import { stableSessionIdAtom } from "@/core/kernel/session";
26
+ import { API } from "@/core/network/api";
17
27
  import { useTimeAgo } from "@/hooks/useFormatting";
18
- import { Objects } from "@/utils/objects";
19
28
  import { FooterItem } from "../footer-item";
20
29
 
21
30
  export function AttachmentStatus() {
22
- const pairPreview = useAtomValue(pairPreviewAtom);
31
+ const hostEnabled = useAtomValue(pairConfigAtom)?.hostEnabled;
23
32
  const agents = useAtomValue(agentAttachmentsAtom);
24
- if (!pairPreview) {
33
+ const [open, setOpen] = useState(false);
34
+ const previous = useRef(new Set<string>());
35
+ const automatic = useRef(false);
36
+ const pointerInside = useRef(false);
37
+ const timeout = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
38
+
39
+ const stopTimer = useCallback(() => clearTimeout(timeout.current), []);
40
+ const startTimer = useCallback(() => {
41
+ stopTimer();
42
+ timeout.current = setTimeout(() => setOpen(false), 3000);
43
+ }, [stopTimer]);
44
+ const preserveEditorFocus = (event: Event) => {
45
+ if (automatic.current) {
46
+ event.preventDefault();
47
+ }
48
+ };
49
+
50
+ useEffect(() => {
51
+ if (!open) {
52
+ pointerInside.current = false;
53
+ }
54
+ const current = new Set(
55
+ hostEnabled ? agents.map((agent) => agent.identity ?? agent.id) : [],
56
+ );
57
+ const connected = [...current].some((id) => !previous.current.has(id));
58
+ for (const id of current) {
59
+ previous.current.add(id);
60
+ }
61
+ if (current.size === 0) {
62
+ stopTimer();
63
+ setOpen(false);
64
+ } else if (connected) {
65
+ if (open && !automatic.current) {
66
+ return;
67
+ }
68
+ automatic.current = true;
69
+ setOpen(true);
70
+ if (!pointerInside.current) {
71
+ startTimer();
72
+ }
73
+ }
74
+ }, [agents, hostEnabled, open, startTimer, stopTimer]);
75
+
76
+ useEffect(() => stopTimer, [stopTimer]);
77
+
78
+ if (!hostEnabled || agents.length === 0) {
25
79
  return null;
26
80
  }
27
- const groups = Objects.groupBy(
28
- agents,
29
- (attachment) =>
30
- attachment.identity
31
- ? `identity:${attachment.identity}`
32
- : `attachment:${attachment.id}`,
33
- (attachment) => attachment,
34
- );
35
- return Object.entries(groups).map(([identity, attachments]) => (
36
- <AttachmentStatusItem key={identity} attachments={attachments} />
37
- ));
38
- }
81
+ const identities = new Set(agents.map((agent) => agent.identity ?? agent.id));
82
+ const label =
83
+ identities.size === 1
84
+ ? `${attachmentLabel(agents[0])} connected`
85
+ : `${identities.size} agents connected`;
39
86
 
40
- function AttachmentStatusItem({ attachments }: { attachments: Attachment[] }) {
41
- const [open, setOpen] = useState(false);
42
- const timeAgo = useTimeAgo();
43
- const label = attachmentLabel(attachments[0]);
44
87
  return (
45
- <Popover open={open} onOpenChange={setOpen}>
88
+ <Popover
89
+ open={open}
90
+ onOpenChange={(nextOpen) => {
91
+ automatic.current = false;
92
+ stopTimer();
93
+ setOpen(nextOpen);
94
+ }}
95
+ >
46
96
  <PopoverTrigger asChild={true}>
47
97
  <FooterItem
48
98
  asChild={true}
49
- tooltip={`${label}: connected`}
99
+ tooltip={label}
50
100
  selected={open}
51
101
  data-testid="footer-attachment-status"
52
- aria-label={`${label}: Connected`}
102
+ aria-label={label}
53
103
  >
54
104
  <button type="button">
55
105
  <span className="flex items-center gap-1.5">
@@ -59,24 +109,129 @@ function AttachmentStatusItem({ attachments }: { attachments: Attachment[] }) {
59
109
  </button>
60
110
  </FooterItem>
61
111
  </PopoverTrigger>
62
- <PopoverContent className="w-64 text-sm">
63
- <h4 className="font-medium">{label}</h4>
64
- <ul className="space-y-2">
65
- {attachments.map((attachment) => (
66
- <li key={attachment.id}>
67
- <p
68
- className="truncate text-xs text-muted-foreground"
69
- title={attachment.id}
70
- >
71
- {attachment.id}
72
- </p>
73
- <p className="text-muted-foreground">
74
- Connected {timeAgo(attachment.since)}
75
- </p>
76
- </li>
112
+ <PopoverContent
113
+ className="w-80 p-0 text-sm"
114
+ side="top"
115
+ align="start"
116
+ onOpenAutoFocus={preserveEditorFocus}
117
+ onCloseAutoFocus={preserveEditorFocus}
118
+ onPointerEnter={() => {
119
+ pointerInside.current = true;
120
+ stopTimer();
121
+ }}
122
+ onPointerLeave={() => {
123
+ pointerInside.current = false;
124
+ if (automatic.current) {
125
+ startTimer();
126
+ }
127
+ }}
128
+ onFocusCapture={() => {
129
+ automatic.current = false;
130
+ stopTimer();
131
+ }}
132
+ >
133
+ <h4 className="font-medium px-4 py-3 border-b">{label}</h4>
134
+ <div className="max-h-72 overflow-auto divide-y">
135
+ {agents.map((attachment) => (
136
+ <AttachmentStatusItem key={attachment.id} attachment={attachment} />
77
137
  ))}
78
- </ul>
138
+ </div>
139
+ <p className="px-4 py-3 border-t text-xs text-muted-foreground">
140
+ Disconnect removes the notebook connection. The agent session keeps
141
+ running.
142
+ </p>
79
143
  </PopoverContent>
80
144
  </Popover>
81
145
  );
82
146
  }
147
+
148
+ function AttachmentStatusItem({ attachment }: { attachment: Attachment }) {
149
+ const stableSessionId = useAtomValue(stableSessionIdAtom);
150
+ const [state, setState] = useState<"idle" | "confirming" | "disconnecting">(
151
+ "idle",
152
+ );
153
+ const confirming = state !== "idle";
154
+ const disconnecting = state === "disconnecting";
155
+ const disconnectButton = useRef<HTMLButtonElement>(null);
156
+ const timeAgo = useTimeAgo();
157
+ const label = attachmentLabel(attachment);
158
+ const disconnectLabel = disconnecting
159
+ ? `Disconnecting ${label}`
160
+ : confirming
161
+ ? `Confirm disconnect ${label}`
162
+ : `Disconnect ${label}`;
163
+
164
+ const disconnect = async () => {
165
+ if (disconnecting || !stableSessionId) {
166
+ return;
167
+ }
168
+ setState("disconnecting");
169
+ try {
170
+ await API.post(
171
+ `/pair/notebooks/${encodeURIComponent(stableSessionId)}/attachments/detach`,
172
+ { attachment_id: attachment.id },
173
+ );
174
+ } catch {
175
+ agentToast({ variant: "danger", title: `Could not disconnect ${label}` });
176
+ } finally {
177
+ setState("idle");
178
+ }
179
+ };
180
+
181
+ return (
182
+ <div className="flex items-start gap-3 px-4 py-3">
183
+ <div className="min-w-0 flex-1">
184
+ <h5 className="font-medium">{label}</h5>
185
+ <p className="text-xs text-muted-foreground mt-1">
186
+ Connected {timeAgo(attachment.since)}
187
+ </p>
188
+ <p
189
+ className="text-xs font-mono text-muted-foreground truncate mt-1"
190
+ title={attachment.id}
191
+ >
192
+ {attachment.id}
193
+ </p>
194
+ </div>
195
+ <div className="flex shrink-0 items-center gap-1">
196
+ {confirming && (
197
+ <span className="text-xs text-muted-foreground">
198
+ {disconnecting ? "Disconnecting…" : "Disconnect?"}
199
+ </span>
200
+ )}
201
+ <Button
202
+ ref={disconnectButton}
203
+ size="icon"
204
+ variant="outlineDestructive"
205
+ disabled={disconnecting || !stableSessionId}
206
+ onClick={confirming ? disconnect : () => setState("confirming")}
207
+ aria-label={disconnectLabel}
208
+ title={disconnectLabel}
209
+ aria-busy={disconnecting}
210
+ >
211
+ {disconnecting ? (
212
+ <Loader2Icon className="size-3.5 animate-spin" aria-hidden={true} />
213
+ ) : confirming ? (
214
+ <CheckIcon className="size-3.5" aria-hidden={true} />
215
+ ) : (
216
+ <UnplugIcon className="size-3.5" aria-hidden={true} />
217
+ )}
218
+ </Button>
219
+ {confirming && (
220
+ <Button
221
+ size="icon"
222
+ variant="outline"
223
+ disabled={disconnecting}
224
+ onClick={() => {
225
+ setState("idle");
226
+ disconnectButton.current?.focus();
227
+ }}
228
+ aria-label={`Cancel disconnect ${label}`}
229
+ title={`Cancel disconnect ${label}`}
230
+ >
231
+ <XIcon className="size-3.5" aria-hidden={true} />
232
+ </Button>
233
+ )}
234
+ </div>
235
+ </div>
236
+ );
237
+ }
@@ -48,7 +48,7 @@ export const SelectorButton: React.FC<{
48
48
  }> = ({ name, color, icon, onSelect }) => (
49
49
  <button
50
50
  type="button"
51
- className="py-3 flex flex-col items-center justify-center gap-1 transition-all hover:scale-105 hover:brightness-110 rounded shadow-sm-solid hover:shadow-md-solid"
51
+ className="py-3 flex flex-col items-center justify-center gap-1 transition-[scale,filter,box-shadow] hover:scale-105 hover:brightness-110 rounded shadow-sm-solid hover:shadow-md-solid"
52
52
  style={{ backgroundColor: color }}
53
53
  onClick={onSelect}
54
54
  >
@@ -0,0 +1,53 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
4
+ import { createStore, Provider } from "jotai";
5
+ import { expect, it, vi } from "vitest";
6
+ import { useNotebookActions } from "../../actions/useNotebookActions";
7
+ import CommandPalette from "../command-palette";
8
+
9
+ vi.mock("../../actions/useNotebookActions", () => ({
10
+ useNotebookActions: vi.fn(),
11
+ }));
12
+ vi.mock("../../actions/useCellActionButton", () => ({
13
+ useCellActionButtons: () => [],
14
+ }));
15
+ vi.mock("../../actions/useConfigActions", () => ({
16
+ useConfigActions: () => [],
17
+ }));
18
+
19
+ it("builds current actions only while open and keeps the opening shortcut active", async () => {
20
+ HTMLElement.prototype.scrollIntoView = vi.fn();
21
+ const firstAction = vi.fn();
22
+ const nextAction = vi.fn();
23
+ vi.mocked(useNotebookActions).mockReturnValue([
24
+ { label: "First action", handle: firstAction },
25
+ ]);
26
+ render(
27
+ <Provider store={createStore()}>
28
+ <CommandPalette />
29
+ </Provider>,
30
+ );
31
+ expect(useNotebookActions).not.toHaveBeenCalled();
32
+
33
+ fireEvent.keyDown(document, { key: "k", ctrlKey: true });
34
+ await screen.findByPlaceholderText("Type to search...");
35
+ fireEvent.click(screen.getByText("First action"));
36
+ await waitFor(() => expect(firstAction).toHaveBeenCalledOnce());
37
+ await waitFor(() =>
38
+ expect(screen.queryByPlaceholderText("Type to search...")).toBeNull(),
39
+ );
40
+
41
+ vi.mocked(useNotebookActions)
42
+ .mockClear()
43
+ .mockReturnValue([{ label: "Updated action", handle: nextAction }]);
44
+ fireEvent.keyDown(document, { key: "x" });
45
+ expect(useNotebookActions).not.toHaveBeenCalled();
46
+
47
+ fireEvent.keyDown(document, { key: "k", ctrlKey: true });
48
+ await screen.findByPlaceholderText("Type to search...");
49
+ expect(screen.queryByText("First action")).toBeNull();
50
+ fireEvent.click(screen.getByText("Updated action"));
51
+ await waitFor(() => expect(nextAction).toHaveBeenCalledOnce());
52
+ expect(firstAction).toHaveBeenCalledOnce();
53
+ });
@@ -1,6 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { useAtom, useAtomValue } from "jotai";
3
+ import { useAtom, useAtomValue, useSetAtom } from "jotai";
4
4
  import {
5
5
  CommandDialog,
6
6
  CommandEmpty,
@@ -32,6 +32,25 @@ import { commandPaletteAtom } from "./state";
32
32
 
33
33
  const CommandPalette = () => {
34
34
  const [open, setOpen] = useAtom(commandPaletteAtom);
35
+ const hotkeys = useAtomValue(hotkeysAtom);
36
+
37
+ useEventListener(document, "keydown", (e) => {
38
+ if (parseShortcut(hotkeys.getHotkey("global.commandPalette").key)(e)) {
39
+ e.preventDefault();
40
+ setOpen((open) => !open);
41
+ }
42
+ });
43
+
44
+ return (
45
+ <CommandDialog open={open} onOpenChange={setOpen}>
46
+ <CommandPaletteContent />
47
+ </CommandDialog>
48
+ );
49
+ };
50
+
51
+ // The dialog mounts these subscriptions only while its content is present.
52
+ const CommandPaletteContent = () => {
53
+ const setOpen = useSetAtom(commandPaletteAtom);
35
54
  const registeredActions = useRegisteredActions();
36
55
  const lastFocusedCell = useAtomValue(lastFocusedCellAtom);
37
56
  const hotkeys = useAtomValue(hotkeysAtom);
@@ -55,13 +74,6 @@ const CommandPalette = () => {
55
74
  const { recentCommands, addRecentCommand } = useRecentCommands();
56
75
  const recentCommandsSet = new Set(recentCommands);
57
76
 
58
- useEventListener(document, "keydown", (e) => {
59
- if (parseShortcut(hotkeys.getHotkey("global.commandPalette").key)(e)) {
60
- e.preventDefault();
61
- setOpen((open) => !open);
62
- }
63
- });
64
-
65
77
  const renderShortcutCommandItem = (
66
78
  shortcut: HotkeyAction,
67
79
  props: {
@@ -142,7 +154,7 @@ const CommandPalette = () => {
142
154
  };
143
155
 
144
156
  return (
145
- <CommandDialog open={open} onOpenChange={setOpen}>
157
+ <>
146
158
  <CommandInput placeholder="Type to search..." />
147
159
  <CommandList>
148
160
  <CommandEmpty>No results found.</CommandEmpty>
@@ -211,7 +223,7 @@ const CommandPalette = () => {
211
223
  })}
212
224
  </CommandGroup>
213
225
  </CommandList>
214
- </CommandDialog>
226
+ </>
215
227
  );
216
228
  };
217
229
 
@@ -1,8 +1,9 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { usePrevious } from "@dnd-kit/utilities";
4
+ import { useAtomValue } from "jotai";
4
5
  import { useEffect } from "react";
5
- import { useCellErrors } from "@/core/cells/cells";
6
+ import { cellErrorCount } from "@/core/cells/cells";
6
7
  import { useEventListener } from "@/hooks/useEventListener";
7
8
  import successFaviconUrl from "../../assets/circle-check.ico";
8
9
  import runningFaviconUrl from "../../assets/circle-play.ico";
@@ -15,13 +16,6 @@ const FAVICON_PATHS = {
15
16
  error: errorFaviconUrl,
16
17
  } as const;
17
18
 
18
- // Cache favicon object URLs lazily
19
- type FaviconKey = keyof typeof FAVICON_PATHS;
20
-
21
- async function getFaviconUrl(key: FaviconKey): Promise<string> {
22
- return FAVICON_PATHS[key];
23
- }
24
-
25
19
  function getOrCreateFavicon(): HTMLLinkElement {
26
20
  const existingFavicon =
27
21
  document.querySelector<HTMLLinkElement>("link[rel~='icon']");
@@ -44,16 +38,16 @@ function maybeSendNotification(numErrors: number) {
44
38
  return;
45
39
  }
46
40
 
47
- const sendNotification = async () => {
41
+ const sendNotification = () => {
48
42
  if (numErrors === 0) {
49
43
  new Notification("Execution completed", {
50
44
  body: "Your notebook run completed successfully.",
51
- icon: await getFaviconUrl("success"),
45
+ icon: FAVICON_PATHS.success,
52
46
  });
53
47
  } else {
54
48
  new Notification("Execution failed", {
55
49
  body: `Your notebook run encountered ${numErrors} error(s).`,
56
- icon: await getFaviconUrl("error"),
50
+ icon: FAVICON_PATHS.error,
57
51
  });
58
52
  }
59
53
  };
@@ -75,7 +69,7 @@ function maybeSendNotification(numErrors: number) {
75
69
 
76
70
  export const DynamicFavicon = (props: Props) => {
77
71
  const { isRunning } = props;
78
- const errors = useCellErrors();
72
+ const numErrors = useAtomValue(cellErrorCount);
79
73
 
80
74
  useEffect(() => {
81
75
  const favicon = getOrCreateFavicon();
@@ -86,46 +80,33 @@ export const DynamicFavicon = (props: Props) => {
86
80
  return;
87
81
  }
88
82
 
89
- const updateFavicon = async () => {
90
- let key: FaviconKey;
91
- // When notebook is running, display running favicon
92
- if (isRunning) {
93
- key = "running";
94
- } else {
95
- // When run is complete, display success or error favicon
96
- key = errors.length === 0 ? "success" : "error";
97
- }
98
- favicon.href = await getFaviconUrl(key);
83
+ const key = isRunning ? "running" : numErrors === 0 ? "success" : "error";
84
+ favicon.href = FAVICON_PATHS[key];
99
85
 
100
- // If notebook is in focus, reset favicon after 3 seconds
101
- // If not in focus, the focus event listener handles it
102
- if (!document.hasFocus()) {
103
- return;
104
- }
105
-
106
- const timeoutId = setTimeout(async () => {
107
- favicon.href = await getFaviconUrl("idle");
108
- }, 3000);
109
-
110
- return () => clearTimeout(timeoutId);
111
- };
86
+ if (!document.hasFocus()) {
87
+ // The focus listener resets the favicon when the user returns.
88
+ return;
89
+ }
112
90
 
113
- updateFavicon();
114
- }, [isRunning, errors]);
91
+ const timeoutId = setTimeout(() => {
92
+ favicon.href = FAVICON_PATHS.idle;
93
+ }, 3000);
94
+ return () => clearTimeout(timeoutId);
95
+ }, [isRunning, numErrors]);
115
96
 
116
97
  // Send user notification when run has completed
117
98
  const prevRunning = usePrevious(isRunning) ?? isRunning;
118
99
  useEffect(() => {
119
100
  if (prevRunning && !isRunning) {
120
- maybeSendNotification(errors.length);
101
+ maybeSendNotification(numErrors);
121
102
  }
122
- }, [errors, prevRunning, isRunning]);
103
+ }, [numErrors, prevRunning, isRunning]);
123
104
 
124
105
  // When notebook comes back in focus, reset favicon
125
- useEventListener(window, "focus", async (_) => {
106
+ useEventListener(window, "focus", () => {
126
107
  if (!isRunning) {
127
108
  const favicon = getOrCreateFavicon();
128
- favicon.href = await getFaviconUrl("idle");
109
+ favicon.href = FAVICON_PATHS.idle;
129
110
  }
130
111
  });
131
112