@marimo-team/islands 0.23.17-dev5 → 0.23.17-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 (194) hide show
  1. package/dist/{ConnectedDataExplorerComponent-f9vLjQiF.js → ConnectedDataExplorerComponent-BBZRR1-C.js} +16 -16
  2. package/dist/{ErrorBoundary-DhLqwSWA.js → ErrorBoundary-CptUP1_t.js} +3 -3
  3. package/dist/{ImageComparisonComponent-C3y7L3eJ.js → ImageComparisonComponent-fdY0LUuf.js} +38 -27
  4. package/dist/{any-language-editor-DifE-fl7.js → any-language-editor-BnIS9Mvg.js} +21 -21
  5. package/dist/assets/__vite-browser-external-DUz7vvG0.js +1 -0
  6. package/dist/assets/{worker-CDEKsBgX.js → worker-Cl1KJG2_.js} +2 -2
  7. package/dist/{button-BSm8IXRU.js → button-8aS1QcHr.js} +4 -2
  8. package/dist/{capabilities-C6OQ5YBO.js → capabilities-D23lgbH_.js} +1 -1
  9. package/dist/chat-ui-DH9M4L-6.js +8620 -0
  10. package/dist/{check-DP7U0H0G.js → check-D-BaEjEh.js} +1 -1
  11. package/dist/{common-uMQbwoo-.js → common-Cpr8ff_v.js} +787 -794
  12. package/dist/{copy-DNq8q3zw.js → copy-BkA3t7rJ.js} +2 -2
  13. package/dist/{defaultLocale-BpsHxBd7.js → defaultLocale-BsfI21d_.js} +26 -26
  14. package/dist/{diagram-PSM6KHXK-D4ftnaua.js → diagram-PSM6KHXK-D3GsWNcQ.js} +1 -1
  15. package/dist/{dist-BpeiYpG2.js → dist-3F1XDSYc.js} +2 -2
  16. package/dist/{dist-BP39XM8u.js → dist-B-upo54Y.js} +1 -1
  17. package/dist/{dist-BdqCKQ1O.js → dist-B1W12VzQ.js} +2 -2
  18. package/dist/{dist-3NCqaWjt.js → dist-B6au1PTB.js} +2 -2
  19. package/dist/{dist-BCX980mU.js → dist-BDwn6WIk.js} +2 -2
  20. package/dist/dist-BDzVdDo2.js +8 -0
  21. package/dist/dist-BH2Tq1S1.js +5 -0
  22. package/dist/dist-BIhBrzqg.js +5 -0
  23. package/dist/dist-BLCsvXTG.js +6 -0
  24. package/dist/dist-BNVtuFdd.js +5 -0
  25. package/dist/{dist-BOMbzyb3.js → dist-BNeJ8P-4.js} +3 -3
  26. package/dist/{dist-LpLXa9Lb.js → dist-BT96P4oR.js} +2 -2
  27. package/dist/{dist-CFSb_Frm.js → dist-BUzxp0Xf.js} +1 -1
  28. package/dist/{dist-JGehap7K.js → dist-Ba1WFESi.js} +5 -5
  29. package/dist/{dist-CRmSjkLP.js → dist-BrKNRKC8.js} +2 -2
  30. package/dist/{dist-BIEt-mlF.js → dist-BvvQUFUK.js} +1 -1
  31. package/dist/{dist-pqIwD8VD.js → dist-BxR-g1ig.js} +2 -2
  32. package/dist/{dist-DviUBLUp.js → dist-CBraAJlT.js} +4 -4
  33. package/dist/{dist-BwhXBwMd.js → dist-CDBV8n0I.js} +1 -1
  34. package/dist/dist-CEJ6Lowi.js +5 -0
  35. package/dist/{dist-CSlJrrAw.js → dist-CWYsUX_O.js} +1 -1
  36. package/dist/dist-CYoa69kj.js +5 -0
  37. package/dist/{dist-DwqBJ014.js → dist-CdKRSqjo.js} +2 -2
  38. package/dist/{dist-De-GlSAs.js → dist-Cds3J-EE.js} +15 -9
  39. package/dist/dist-CnZyXFq3.js +8 -0
  40. package/dist/dist-D-4HaJKc.js +8 -0
  41. package/dist/{dist-C18MUWTj.js → dist-D8SX5Qdg.js} +2 -2
  42. package/dist/{dist-D3bdsa1d.js → dist-DFQ91Mx1.js} +4 -4
  43. package/dist/{dist-BPF0gTRZ.js → dist-DGQmYLkh.js} +1 -1
  44. package/dist/dist-DIGyvbg3.js +5 -0
  45. package/dist/dist-DIhaoCL4.js +5 -0
  46. package/dist/{dist-C2_4pjuS.js → dist-DK0pFEmc.js} +1 -1
  47. package/dist/{dist-BTdrwBWP.js → dist-DQj6A7SE.js} +3 -3
  48. package/dist/{dist-DIlxLFXG.js → dist-DarwE0ZO.js} +11 -11
  49. package/dist/{dist-DaXJPX8F.js → dist-Ddb0VFS3.js} +2 -2
  50. package/dist/dist-DgQleHmb.js +8 -0
  51. package/dist/{dist-DZwp8lro.js → dist-DgkJegw4.js} +3 -3
  52. package/dist/{dist-BO5HRiTf.js → dist-DjQRRttD.js} +2 -2
  53. package/dist/dist-JaoLzZmt.js +5 -0
  54. package/dist/{dist-B2jmQKKx.js → dist-PQDZaPEo.js} +2 -2
  55. package/dist/{dist-4lVIWUT3.js → dist-Slyx4vXr.js} +1 -1
  56. package/dist/{dist-DgjbnqTg.js → dist-dJeh9ef4.js} +2 -2
  57. package/dist/dist-pEX0t8ZV.js +6 -0
  58. package/dist/{dist-w_gWE5Wf.js → dist-v-kAJfMq.js} +1 -1
  59. package/dist/{error-banner-B_GQfxxD.js → error-banner-dmkbm21G.js} +2 -2
  60. package/dist/{esm-DzZVaNH4.js → esm-D8vjxCyn.js} +3 -3
  61. package/dist/{esm-DKOv6P2M.js → esm-DEQU-LUO.js} +22 -22
  62. package/dist/{extends-w2_S44bV.js → extends-DDOVg_b8.js} +5 -5
  63. package/dist/{formats-Dwq1THhr.js → formats-xc-Msa0v.js} +3 -3
  64. package/dist/{ganttDiagram-JELNMOA3-CXXCN7LN.js → ganttDiagram-JELNMOA3-D9ywbC5g.js} +3 -3
  65. package/dist/{glide-data-editor-B9K6-MJe.js → glide-data-editor-CyTNN635.js} +9 -9
  66. package/dist/{html-to-image-CZ1kLKkq.js → html-to-image-DrEMf75G.js} +2219 -2209
  67. package/dist/{input-C5K4VBvB.js → input-B4ddy8Jy.js} +6 -6
  68. package/dist/{label-DjoYwqmj.js → label-CzHncDvS.js} +2 -2
  69. package/dist/{linear-H_vsH9ao.js → linear-BOGUO8Cd.js} +2 -2
  70. package/dist/{loader-DX-IDS7n.js → loader-C7SfUdOD.js} +10 -10
  71. package/dist/main.js +729 -733
  72. package/dist/{mermaid-hv6IckVw.js → mermaid-CpL8y5pw.js} +9 -9
  73. package/dist/{precisionRound-CnHPY_5v.js → precisionRound-Bq50n-bG.js} +1 -1
  74. package/dist/{process-output-bVfbcx5_.js → process-output-B9IHQuus.js} +3 -3
  75. package/dist/{quadrantDiagram-AYHSOK5B-BqDB9tUC.js → quadrantDiagram-AYHSOK5B-DD4QBdSv.js} +2 -2
  76. package/dist/{react-vega-BazDALhI.js → react-vega-Cg17A6rO.js} +8017 -4777
  77. package/dist/{reveal-component-CtEDksOP.js → reveal-component-8WsJKKhA.js} +19 -19
  78. package/dist/{spec-CQKL_D3i.js → spec-uMKNvMRZ.js} +4 -4
  79. package/dist/{strings-DIy9NAmg.js → strings-BUDhKc0s.js} +299 -299
  80. package/dist/style.css +1 -1
  81. package/dist/{swiper-component-CEdAYjb7.js → swiper-component-dl1H9WIW.js} +2 -2
  82. package/dist/{time-Bb4A7I6a.js → time-CQnohwNM.js} +2 -2
  83. package/dist/{toDate-CqhBWjvO.js → toDate-BiyuFdnT.js} +3 -3
  84. package/dist/{tooltip-f-qR0iUx.js → tooltip-CkCWlUOf.js} +3 -3
  85. package/dist/{types-DicH9Oi0.js → types-uENjXsiC.js} +1 -1
  86. package/dist/{useAsyncData-CYOQb1EW.js → useAsyncData-DqJLysjC.js} +1 -1
  87. package/dist/{useDateFormatter-Bt2qr_XS.js → useDateFormatter-BeN3fHMH.js} +2 -2
  88. package/dist/{useDeepCompareMemoize-CSHQeQEI.js → useDeepCompareMemoize-CvlmHAmy.js} +1 -1
  89. package/dist/{useIframeCapabilities-kA09ykNq.js → useIframeCapabilities-pLC-ceDP.js} +1 -1
  90. package/dist/{useLifecycle-CMJGqBaY.js → useLifecycle-D4N02q82.js} +3 -3
  91. package/dist/{useTheme-DYfby65G.js → useTheme-Dur2p3iC.js} +4 -3
  92. package/dist/{vega-component-C7nO5zcf.js → vega-component-Cft458Sr.js} +13 -13
  93. package/dist/{vega-embed-container-DAQAMjiO.js → vega-embed-container-S9gRu4TZ.js} +3 -3
  94. package/dist/{vega-loader.browser-DmpxCJqx.js → vega-loader.browser-CmLRI3wk.js} +104 -111
  95. package/dist/{xychartDiagram-PRI3JC2R-BSkNxYNw.js → xychartDiagram-PRI3JC2R-D9DgEtKn.js} +2 -2
  96. package/dist/{zod-BUcyXSdR.js → zod-yTHHzJiv.js} +37 -38
  97. package/package.json +7 -7
  98. package/src/__mocks__/requests.ts +5 -0
  99. package/src/components/ai/ai-model-dropdown.tsx +2 -2
  100. package/src/components/app-config/user-config-form.tsx +26 -0
  101. package/src/components/chat/__tests__/capabilities-popover.test.tsx +38 -0
  102. package/src/components/chat/__tests__/chat-abort.test.ts +51 -0
  103. package/src/components/chat/__tests__/use-chat-controller-id.test.tsx +39 -0
  104. package/src/components/chat/capabilities-popover.tsx +68 -0
  105. package/src/components/chat/chat-abort.ts +44 -0
  106. package/src/components/chat/chat-display.tsx +17 -0
  107. package/src/components/chat/chat-panel.tsx +107 -50
  108. package/src/components/chat/chat-utils.ts +1 -1
  109. package/src/components/chat/use-chat-controller-id.ts +24 -0
  110. package/src/components/data-table/charts/__tests__/schemas.test.ts +35 -0
  111. package/src/components/data-table/charts/schemas.ts +5 -2
  112. package/src/components/databases/icons/huggingface.svg +8 -0
  113. package/src/components/datasets/__tests__/icons.test.ts +21 -0
  114. package/src/components/datasets/icons.tsx +10 -0
  115. package/src/components/datasources/__tests__/components.test.tsx +29 -0
  116. package/src/components/datasources/components.tsx +8 -3
  117. package/src/components/editor/actions/export-dialog/__tests__/export-dialog.test.tsx +35 -0
  118. package/src/components/editor/actions/export-dialog/__tests__/state.test.ts +61 -0
  119. package/src/components/editor/actions/export-dialog/export-command.ts +4 -9
  120. package/src/components/editor/actions/export-dialog/export-dialog.tsx +27 -23
  121. package/src/components/editor/actions/export-dialog/format-notice.tsx +28 -0
  122. package/src/components/editor/actions/export-dialog/state.ts +23 -5
  123. package/src/components/editor/actions/export-dialog/use-export-dialog.ts +7 -15
  124. package/src/components/editor/actions/useConfigActions.tsx +24 -0
  125. package/src/components/editor/ai/add-cell-with-ai.tsx +2 -2
  126. package/src/components/editor/ai/ai-completion-editor.tsx +2 -2
  127. package/src/components/editor/chrome/panels/outline/__tests__/useActiveOutline.test.ts +26 -0
  128. package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +2 -2
  129. package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +114 -2
  130. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +14 -0
  131. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +20 -0
  132. package/src/components/editor/connections/storage/add-storage-form.tsx +10 -0
  133. package/src/components/editor/connections/storage/as-code.ts +19 -1
  134. package/src/components/editor/connections/storage/schemas.ts +19 -0
  135. package/src/components/storage/__tests__/storage-snippets.test.ts +88 -0
  136. package/src/components/storage/components.tsx +2 -0
  137. package/src/components/storage/storage-snippets.ts +58 -0
  138. package/src/core/__tests__/mode.test.ts +85 -0
  139. package/src/core/ai/__tests__/config.test.tsx +96 -0
  140. package/src/core/ai/config.ts +16 -22
  141. package/src/core/ai/constants.ts +7 -0
  142. package/src/core/ai/staged-cells.ts +3 -0
  143. package/src/core/ai/state.ts +10 -0
  144. package/src/core/codemirror/__tests__/setup.test.ts +1 -1
  145. package/src/core/codemirror/cm.ts +2 -2
  146. package/src/core/codemirror/code-lens/__tests__/extension.test.ts +4 -5
  147. package/src/core/codemirror/code-lens/extension.ts +3 -3
  148. package/src/core/codemirror/find-replace/search-highlight.ts +8 -3
  149. package/src/core/codemirror/language/languages/sql/__tests__/renderers.test.tsx +44 -0
  150. package/src/core/codemirror/language/languages/sql/renderers.tsx +3 -3
  151. package/src/core/codemirror/theme/__tests__/light.test.ts +59 -123
  152. package/src/core/codemirror/theme/dark.ts +6 -5
  153. package/src/core/codemirror/theme/light.ts +6 -4
  154. package/src/core/config/__tests__/config-schema.test.ts +2 -0
  155. package/src/core/config/config-schema.ts +1 -0
  156. package/src/core/config/feature-flag.tsx +0 -2
  157. package/src/core/dom/outline.ts +25 -2
  158. package/src/core/hotkeys/hotkeys.ts +2 -0
  159. package/src/core/mode.ts +11 -9
  160. package/src/core/network/__tests__/api.test.ts +132 -17
  161. package/src/core/network/__tests__/export-filename.test.ts +43 -0
  162. package/src/core/network/__tests__/requests-lazy.test.ts +1 -0
  163. package/src/core/network/api.ts +47 -12
  164. package/src/core/network/export-filename.ts +37 -0
  165. package/src/core/network/markdown-export.ts +19 -0
  166. package/src/core/network/requests-network.ts +29 -5
  167. package/src/core/network/types.ts +4 -0
  168. package/src/core/storage/types.ts +1 -0
  169. package/src/css/app/codemirror.css +11 -7
  170. package/src/css/globals.css +8 -0
  171. package/src/plugins/impl/DataEditorPlugin.tsx +2 -9
  172. package/src/plugins/impl/__tests__/DataEditorPlugin.test.ts +18 -0
  173. package/src/plugins/impl/chat/chat-ui.tsx +51 -3
  174. package/src/plugins/impl/data-frames/__tests__/schema.test.ts +30 -0
  175. package/src/plugins/impl/data-frames/schema.ts +4 -1
  176. package/src/plugins/impl/image-comparison/ImageComparisonComponent.tsx +20 -1
  177. package/src/plugins/impl/image-comparison/__tests__/ImageComparisonComponent.test.tsx +58 -0
  178. package/src/stories/theme.stories.tsx +71 -1
  179. package/dist/assets/__vite-browser-external-DltVXDv7.js +0 -1
  180. package/dist/chat-ui-DsZJj75A.js +0 -8369
  181. package/dist/dist-5o9uz19x.js +0 -5
  182. package/dist/dist-BpDH0DEP.js +0 -5
  183. package/dist/dist-BpXhbQXO.js +0 -8
  184. package/dist/dist-ByadAk_L.js +0 -5
  185. package/dist/dist-CK0g7JvP.js +0 -8
  186. package/dist/dist-CfzCtiq2.js +0 -5
  187. package/dist/dist-D8JtVFrW.js +0 -8
  188. package/dist/dist-DK2Oocpu.js +0 -6
  189. package/dist/dist-DnSu977r.js +0 -5
  190. package/dist/dist-UmxJm6sW.js +0 -8
  191. package/dist/dist-aYSBQSyM.js +0 -5
  192. package/dist/dist-kkZ1Ztlh.js +0 -6
  193. package/dist/dist-ngPczwNi.js +0 -5
  194. package/dist/dist-wQRyxyTV.js +0 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.17-dev5",
3
+ "version": "0.23.17-dev51",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -21,7 +21,7 @@
21
21
  "./unstable_internal/*": "./src/*"
22
22
  },
23
23
  "dependencies": {
24
- "@ai-sdk/react": "^3.0.134",
24
+ "@ai-sdk/react": "^4.0.40",
25
25
  "@anywidget/types": "^0.4.0",
26
26
  "@codemirror/autocomplete": "^6.20.1",
27
27
  "@codemirror/commands": "^6.10.2",
@@ -92,7 +92,7 @@
92
92
  "@xterm/addon-web-links": "^0.12.0",
93
93
  "@xterm/xterm": "^5.5.0",
94
94
  "@zed-industries/agent-client-protocol": "^0.4.5",
95
- "ai": "^6.0.129",
95
+ "ai": "^7.0.37",
96
96
  "ansi_up": "^6.0.6",
97
97
  "class-variance-authority": "^0.7.1",
98
98
  "clsx": "^2.1.1",
@@ -195,7 +195,7 @@
195
195
  "@babel/preset-typescript": "^7.25.9",
196
196
  "@codecov/vite-plugin": "^1.9.1",
197
197
  "@csstools/postcss-light-dark-function": "^2.0.11",
198
- "@playwright/test": "^1.59.1",
198
+ "@playwright/test": "^1.62.1",
199
199
  "@storybook/addon-docs": "^10.3.5",
200
200
  "@storybook/addon-links": "^10.3.5",
201
201
  "@storybook/react-vite": "^10.3.5",
@@ -211,7 +211,7 @@
211
211
  "@types/react-dom": "^19.2.3",
212
212
  "@types/timestring": "^6.0.5",
213
213
  "@vitejs/plugin-react": "^5.2.0",
214
- "@vitest/coverage-v8": "3.2.4",
214
+ "@vitest/coverage-v8": "3.2.7",
215
215
  "babel-plugin-react-compiler": "19.1.0-rc.3",
216
216
  "blob-polyfill": "^7.0.20220408",
217
217
  "cross-env": "^7.0.3",
@@ -228,11 +228,11 @@
228
228
  "storybook": "^10.3.5",
229
229
  "stylelint": "^16.26.1",
230
230
  "stylelint-config-standard": "^36.0.1",
231
- "tailwindcss": "^4.3.0",
231
+ "tailwindcss": "^4.3.3",
232
232
  "vega-typings": "^2.1.0",
233
233
  "vite": "npm:rolldown-vite@7.3.1",
234
234
  "vite-plugin-top-level-await": "^1.6.0",
235
235
  "vite-plugin-wasm": "^3.6.0",
236
- "vitest": "^3.2.4"
236
+ "vitest": "^3.2.7"
237
237
  }
238
238
  }
@@ -72,26 +72,31 @@ export const MockRequestClient = {
72
72
  format: "html",
73
73
  dependenciesAvailable: true,
74
74
  missingPackages: [],
75
+ missingSetup: [],
75
76
  },
76
77
  {
77
78
  format: "markdown",
78
79
  dependenciesAvailable: true,
79
80
  missingPackages: [],
81
+ missingSetup: [],
80
82
  },
81
83
  {
82
84
  format: "ipynb",
83
85
  dependenciesAvailable: true,
84
86
  missingPackages: [],
87
+ missingSetup: [],
85
88
  },
86
89
  {
87
90
  format: "pdf",
88
91
  dependenciesAvailable: true,
89
92
  missingPackages: [],
93
+ missingSetup: [],
90
94
  },
91
95
  {
92
96
  format: "script",
93
97
  dependenciesAvailable: true,
94
98
  missingPackages: [],
99
+ missingSetup: [],
95
100
  },
96
101
  ],
97
102
  }),
@@ -9,7 +9,7 @@ import {
9
9
  CircleHelpIcon,
10
10
  } from "lucide-react";
11
11
  import React from "react";
12
- import { type SupportedRole, useModelChange } from "@/core/ai/config";
12
+ import { type SupportedRole, useAIConfigActions } from "@/core/ai/config";
13
13
  import {
14
14
  AiModelId,
15
15
  isKnownAIProvider,
@@ -64,7 +64,7 @@ export const AIModelDropdown = ({
64
64
 
65
65
  const ai = useAtomValue(aiAtom);
66
66
  const completion = useAtomValue(completionAtom);
67
- const { saveModelChange } = useModelChange();
67
+ const { saveModelChange } = useAIConfigActions();
68
68
  const { handleClick } = useOpenSettingsToTab();
69
69
 
70
70
  // Only include autocompleteModel if copilot is set to "custom"
@@ -985,6 +985,32 @@ export const UserConfigForm: React.FC = () => {
985
985
  </div>
986
986
  )}
987
987
  />
988
+ <OverriddenFormField
989
+ control={form.control}
990
+ name="display.code_lens"
991
+ render={({ field, override }) => (
992
+ <div className="flex flex-col space-y-1">
993
+ <FormItem className={formItemClasses}>
994
+ <FormLabel>Code lens</FormLabel>
995
+ <FormControl>
996
+ <Checkbox
997
+ data-testid="code-lens-checkbox"
998
+ checked={override.value}
999
+ disabled={override.isOverridden}
1000
+ onCheckedChange={field.onChange}
1001
+ />
1002
+ </FormControl>
1003
+ <FormMessage />
1004
+ <IsOverridden override={override} />
1005
+ </FormItem>
1006
+
1007
+ <FormDescription>
1008
+ Shows inline icons in cell editors for datasources and
1009
+ storage buckets.
1010
+ </FormDescription>
1011
+ </div>
1012
+ )}
1013
+ />
988
1014
  </SettingGroup>
989
1015
  <SettingGroup title="Outputs">
990
1016
  <OverriddenFormField
@@ -0,0 +1,38 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+ import { createStore, Provider } from "jotai";
5
+ import { describe, expect, it } from "vitest";
6
+ import { chatOptionsAtom } from "@/core/ai/state";
7
+ import { CapabilitiesPopover } from "../capabilities-popover";
8
+
9
+ describe("CapabilitiesPopover", () => {
10
+ it("updates ephemeral chat options", () => {
11
+ const store = createStore();
12
+
13
+ render(
14
+ <Provider store={store}>
15
+ <CapabilitiesPopover />
16
+ </Provider>,
17
+ );
18
+
19
+ const trigger = screen.getByRole("button", { name: "Capabilities" });
20
+ expect(trigger).not.toHaveAttribute("data-active");
21
+
22
+ fireEvent.click(trigger);
23
+ const webSearch = screen.getByRole("switch", { name: "Web search" });
24
+ expect(webSearch).not.toBeChecked();
25
+
26
+ fireEvent.click(webSearch);
27
+
28
+ expect(webSearch).toBeChecked();
29
+ expect(trigger).toHaveAttribute("data-active", "true");
30
+ expect(trigger).toHaveClass("bg-primary/15", "text-primary", "ring-1");
31
+ expect(trigger).toHaveAttribute(
32
+ "title",
33
+ "Capabilities (web search enabled)",
34
+ );
35
+ expect(store.get(chatOptionsAtom)).toEqual({ webSearch: true });
36
+ expect(createStore().get(chatOptionsAtom)).toEqual({ webSearch: false });
37
+ });
38
+ });
@@ -0,0 +1,51 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { UIMessageChunk } from "ai";
4
+ import { describe, expect, it } from "vitest";
5
+ import {
6
+ describeChatAbortReason,
7
+ getAbortReasonFromChunk,
8
+ resolveChatAbortReason,
9
+ USER_CANCELLED_ABORT_REASON,
10
+ } from "../chat-abort";
11
+
12
+ describe("chat-abort", () => {
13
+ it("reads reason from abort chunks", () => {
14
+ const chunk: UIMessageChunk = {
15
+ type: "abort",
16
+ reason: USER_CANCELLED_ABORT_REASON,
17
+ };
18
+ expect(getAbortReasonFromChunk(chunk)).toBe(USER_CANCELLED_ABORT_REASON);
19
+ });
20
+
21
+ it("returns undefined for non-abort chunks", () => {
22
+ expect(
23
+ getAbortReasonFromChunk({ type: "text-delta", id: "1", delta: "hi" }),
24
+ ).toBeUndefined();
25
+ });
26
+
27
+ it("defaults client-side aborts to user_cancelled", () => {
28
+ expect(resolveChatAbortReason({ isAbort: true, streamReason: null })).toBe(
29
+ USER_CANCELLED_ABORT_REASON,
30
+ );
31
+ expect(resolveChatAbortReason({ isAbort: false })).toBeNull();
32
+ });
33
+
34
+ it("preserves stream abort reasons when present", () => {
35
+ expect(
36
+ resolveChatAbortReason({ isAbort: true, streamReason: "timeout" }),
37
+ ).toBe("timeout");
38
+ });
39
+
40
+ it("describes known and unknown abort reasons", () => {
41
+ expect(describeChatAbortReason(USER_CANCELLED_ABORT_REASON)).toBe(
42
+ "Generation stopped",
43
+ );
44
+ expect(describeChatAbortReason("timeout")).toBe(
45
+ "Generation stopped: timed out",
46
+ );
47
+ expect(describeChatAbortReason("provider_reset")).toBe(
48
+ "Generation stopped: provider_reset",
49
+ );
50
+ });
51
+ });
@@ -0,0 +1,39 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { act, renderHook } from "@testing-library/react";
4
+ import { describe, expect, it } from "vitest";
5
+ import type { ChatId } from "@/core/ai/state";
6
+ import { useChatControllerId } from "../use-chat-controller-id";
7
+
8
+ describe("useChatControllerId", () => {
9
+ it("creates a new ID when starting another unsaved chat", () => {
10
+ const activeChatId = "existing-chat" as ChatId;
11
+ const initialProps: { activeId: ChatId | undefined } = {
12
+ activeId: undefined,
13
+ };
14
+ const { result, rerender } = renderHook(
15
+ ({ activeId }: { activeId: ChatId | undefined }) =>
16
+ useChatControllerId(activeId),
17
+ { initialProps },
18
+ );
19
+
20
+ const initialDraftId = result.current.chatControllerId;
21
+ rerender({ activeId: activeChatId });
22
+
23
+ expect(result.current.chatControllerId).toBe(activeChatId);
24
+
25
+ act(() => result.current.renewDraftChatId());
26
+ rerender({ activeId: undefined });
27
+
28
+ expect(result.current.chatControllerId).not.toBe(initialDraftId);
29
+ });
30
+
31
+ it("renews the ID when the current chat is already unsaved", () => {
32
+ const { result } = renderHook(() => useChatControllerId(undefined));
33
+ const initialDraftId = result.current.chatControllerId;
34
+
35
+ act(() => result.current.renewDraftChatId());
36
+
37
+ expect(result.current.chatControllerId).not.toBe(initialDraftId);
38
+ });
39
+ });
@@ -0,0 +1,68 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { useAtom } from "jotai";
4
+ import { SlidersHorizontalIcon } from "lucide-react";
5
+ import type React from "react";
6
+ import { Button } from "@/components/ui/button";
7
+ import {
8
+ Popover,
9
+ PopoverContent,
10
+ PopoverTrigger,
11
+ } from "@/components/ui/popover";
12
+ import { ExternalLink } from "@/components/ui/links";
13
+ import { Switch } from "@/components/ui/switch";
14
+ import { chatOptionsAtom } from "@/core/ai/state";
15
+ import { cn } from "@/utils/cn";
16
+
17
+ export const CapabilitiesPopover: React.FC = () => {
18
+ const [chatOptions, setChatOptions] = useAtom(chatOptionsAtom);
19
+ const webSearchOn = chatOptions.webSearch;
20
+
21
+ return (
22
+ <Popover>
23
+ <PopoverTrigger asChild={true}>
24
+ <Button
25
+ aria-label="Capabilities"
26
+ title={
27
+ webSearchOn ? "Capabilities (web search enabled)" : "Capabilities"
28
+ }
29
+ data-active={webSearchOn || undefined}
30
+ variant="text"
31
+ size="icon"
32
+ className={cn(
33
+ "h-6 w-6 shrink-0 bg-muted hover:bg-muted/30",
34
+ webSearchOn &&
35
+ "bg-primary/15 text-primary ring-1 ring-primary/30 opacity-100 hover:bg-primary/20",
36
+ )}
37
+ >
38
+ <SlidersHorizontalIcon className="h-3.5 w-3.5" />
39
+ </Button>
40
+ </PopoverTrigger>
41
+ <PopoverContent className="w-76 p-3" align="start" side="top">
42
+ <label
43
+ htmlFor="web-search-toggle"
44
+ className="flex items-start justify-between gap-3 cursor-pointer"
45
+ >
46
+ <div className="flex flex-col gap-0.5">
47
+ <span className="text-sm font-semibold">Web search</span>
48
+ <span className="text-xs text-muted-foreground">
49
+ Search the web when the model supports it.{" "}
50
+ <ExternalLink href="https://docs.marimo.io/guides/editor_features/tools#web-search-and-fetch">
51
+ Learn more
52
+ </ExternalLink>
53
+ </span>
54
+ </div>
55
+ <Switch
56
+ id="web-search-toggle"
57
+ aria-label="Web search"
58
+ size="sm"
59
+ checked={webSearchOn}
60
+ onCheckedChange={(webSearch) =>
61
+ setChatOptions((options) => ({ ...options, webSearch }))
62
+ }
63
+ />
64
+ </label>
65
+ </PopoverContent>
66
+ </Popover>
67
+ );
68
+ };
@@ -0,0 +1,44 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { UIMessageChunk } from "ai";
4
+
5
+ /** Matches pydantic-ai / marimo AbortChunk.reason for user Stop. */
6
+ export const USER_CANCELLED_ABORT_REASON = "user_cancelled";
7
+
8
+ /**
9
+ * Read an abort reason from a UI message stream chunk, if present.
10
+ */
11
+ export function getAbortReasonFromChunk(
12
+ chunk: UIMessageChunk,
13
+ ): string | undefined {
14
+ if (chunk.type !== "abort") {
15
+ return undefined;
16
+ }
17
+ return chunk.reason;
18
+ }
19
+
20
+ /**
21
+ * Resolve the abort reason for logging/UI. Client-side Stop often never
22
+ * receives a server AbortChunk (the fetch is aborted first), so default to
23
+ * user_cancelled when the turn ended as an abort.
24
+ */
25
+ export function resolveChatAbortReason(opts: {
26
+ isAbort: boolean;
27
+ streamReason?: string | null;
28
+ }): string | null {
29
+ if (!opts.isAbort) {
30
+ return null;
31
+ }
32
+ return opts.streamReason || USER_CANCELLED_ABORT_REASON;
33
+ }
34
+
35
+ export function describeChatAbortReason(reason: string): string {
36
+ switch (reason) {
37
+ case USER_CANCELLED_ABORT_REASON:
38
+ return "Generation stopped";
39
+ case "timeout":
40
+ return "Generation stopped: timed out";
41
+ default:
42
+ return `Generation stopped: ${reason}`;
43
+ }
44
+ }
@@ -92,6 +92,17 @@ export const renderUIMessage = ({
92
92
  );
93
93
  case "file":
94
94
  return <AttachmentRenderer attachment={part} key={index} />;
95
+ case "reasoning-file":
96
+ return (
97
+ <AttachmentRenderer
98
+ key={index}
99
+ attachment={{
100
+ type: "file",
101
+ mediaType: part.mediaType,
102
+ url: part.url,
103
+ }}
104
+ />
105
+ );
95
106
  case "dynamic-tool":
96
107
  return (
97
108
  <ToolCallView
@@ -128,6 +139,12 @@ export const renderUIMessage = ({
128
139
  href={part.url}
129
140
  />
130
141
  );
142
+ case "custom":
143
+ return (
144
+ <div key={index} className="text-sm text-muted-foreground">
145
+ {part.kind}
146
+ </div>
147
+ );
131
148
  case "step-start":
132
149
  return null;
133
150
  default: