@marimo-team/frontend 0.24.1-dev42 → 0.24.1-dev44

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 (174) hide show
  1. package/dist/assets/{CellStatus-CfNQG_fZ.js → CellStatus-B6CWKORY.js} +1 -1
  2. package/dist/assets/ConnectedDataExplorerComponent-DZnvpjue.js +1 -0
  3. package/dist/assets/JsonOutput-BI2qFHVr.js +50 -0
  4. package/dist/assets/MarimoErrorOutput-BXh29HCp.js +6 -0
  5. package/dist/assets/RenderHTML-COTZYPO7.js +1 -0
  6. package/dist/assets/RunButton-DTUzITFm.js +1 -0
  7. package/dist/assets/VisuallyHidden-DurJ_JBW.js +1 -0
  8. package/dist/assets/{add-cell-with-ai-CGBEoX6T.js → add-cell-with-ai-B0RdHenk.js} +19 -19
  9. package/dist/assets/add-connection-dialog-CcnJ2LkX.js +166 -0
  10. package/dist/assets/agent-panel-PB0I1dIC.js +286 -0
  11. package/dist/assets/ai-model-dropdown-DjQy2bo-.js +3 -0
  12. package/dist/assets/app-config-button-COFLeJCU.js +1 -0
  13. package/dist/assets/{blob-ChElOQvm.js → blob-BBiZd3vX.js} +1 -1
  14. package/dist/assets/{cache-panel-CLfGxru6.js → cache-panel-MAXVmKkG.js} +1 -1
  15. package/dist/assets/cell-editor-5wcJHF0s.js +24 -0
  16. package/dist/assets/cell-link-BbAJokoX.js +1 -0
  17. package/dist/assets/cells-B1S_6FG3.js +227 -0
  18. package/dist/assets/chat-display-ClG9MRI_.js +1 -0
  19. package/dist/assets/chat-panel-BRoqCSCR.js +4 -0
  20. package/dist/assets/chat-ui-CNvkb287.js +4 -0
  21. package/dist/assets/circle--ysFeW9z.js +1 -0
  22. package/dist/assets/column-preview-D6cIOnWQ.js +1 -0
  23. package/dist/assets/command-palette-DtZy0Am-.js +1 -0
  24. package/dist/assets/common-BGLbVHCS.js +1 -0
  25. package/dist/assets/components-BAvTsivu.js +1 -0
  26. package/dist/assets/config-DcB2swfZ.js +1 -0
  27. package/dist/assets/context-Do8M9TDF.js +1 -0
  28. package/dist/assets/{datasource-C0_eJO-0.js → datasource-BrSYgi6C.js} +2 -2
  29. package/dist/assets/dependency-graph-panel-C13q-nga.js +5 -0
  30. package/dist/assets/{documentation-panel-CxjnCPq6.js → documentation-panel-CtQqNEQw.js} +1 -1
  31. package/dist/assets/{download-CxzOE8FX.js → download-CIdXYNHv.js} +1 -1
  32. package/dist/assets/dropdown-menu-CqNabM1f.js +1 -0
  33. package/dist/assets/edit-page-jAyDC21z.js +9 -0
  34. package/dist/assets/{error-entries-DCRQWi_N.js → error-entries-DEjvzkum.js} +1 -1
  35. package/dist/assets/{error-panel-Bx5cPw6N.js → error-panel-BvrDwUv8.js} +1 -1
  36. package/dist/assets/file-explorer-panel-DYujxxK-.js +1 -0
  37. package/dist/assets/{file-icons-DxXY84Jn.js → file-icons-Bo1Ewf1W.js} +1 -1
  38. package/dist/assets/file-name-input-BDGq0-ac.js +3 -0
  39. package/dist/assets/{floating-outline-gjfcwmyU.js → floating-outline-CNsG18LK.js} +1 -1
  40. package/dist/assets/{gallery-page-BI3aTl8u.js → gallery-page-4dZlIWMe.js} +1 -1
  41. package/dist/assets/{glide-data-editor-CrWGp7DP.js → glide-data-editor-BQNVc0zc.js} +2 -2
  42. package/dist/assets/home-page-CFAldshJ.js +4 -0
  43. package/dist/assets/html-to-image-DMn_7CCA.js +2 -0
  44. package/dist/assets/index-0VrRpta0.css +2 -0
  45. package/dist/assets/index-BBPHmHdW.js +29 -0
  46. package/dist/assets/input-DeyycFtq.js +7 -0
  47. package/dist/assets/{layout-BQjGHHu-.js → layout-Ddsz1eHh.js} +5 -5
  48. package/dist/assets/{logs-panel-D1RUMXL1.js → logs-panel-BSD1GETR.js} +1 -1
  49. package/dist/assets/markdown-renderer-DIXiKP4m.js +2 -0
  50. package/dist/assets/{mermaid-DtNubw9S.js → mermaid-CwvTqKGz.js} +2 -2
  51. package/dist/assets/{name-cell-input-DSKoNRk3.js → name-cell-input-zqypwqoZ.js} +1 -1
  52. package/dist/assets/{outline-panel-UIygpaO3.js → outline-panel-C5GAyqII.js} +1 -1
  53. package/dist/assets/packages-panel-CYLyGihe.js +1 -0
  54. package/dist/assets/{pair-with-agent-modal-Pl5cv7Gv.js → pair-with-agent-modal-CBIL9_6g.js} +2 -2
  55. package/dist/assets/panel-accordion-state-DmV3OgfX.js +1 -0
  56. package/dist/assets/panels-D53vWR00.js +4 -0
  57. package/dist/assets/{process-output-BWDSbk5d.js → process-output-DNhz9LLW.js} +1 -1
  58. package/dist/assets/{readonly-code-display-Bbnq-khe.js → readonly-code-display-CKT-gv1m.js} +1 -1
  59. package/dist/assets/readonly-python-code-De0vvuWh.js +1 -0
  60. package/dist/assets/{renderShortcut-Dj3NbkoC.js → renderShortcut-D9XYVNe3.js} +1 -1
  61. package/dist/assets/{reveal-component-Bgmfon3f.js → reveal-component-C0L5MU2W.js} +4 -4
  62. package/dist/assets/run-page-DXZapRtY.js +1 -0
  63. package/dist/assets/scratchpad-panel-ja6doGoa.js +1 -0
  64. package/dist/assets/secrets-panel-B7Rcwc2I.js +1 -0
  65. package/dist/assets/select-BUEX2Vfl.js +1 -0
  66. package/dist/assets/session-panel-ftyqfsYy.js +1 -0
  67. package/dist/assets/{skeleton-DGs-LrlZ.js → skeleton-CxFczTPo.js} +1 -1
  68. package/dist/assets/snippets-panel-BZ254lxN.js +1 -0
  69. package/dist/assets/{state-WVnIPIsw.js → state-MlBpc9qi.js} +3 -3
  70. package/dist/assets/{state-N_csMEH1.js → state-llVv0lIh.js} +1 -1
  71. package/dist/assets/switch-CBR-ykv3.js +1 -0
  72. package/dist/assets/tabs-c6mpccpH.js +1 -0
  73. package/dist/assets/{terminal-aLrVlOf6.js → terminal-OC_FnEas.js} +3 -3
  74. package/dist/assets/tracing-C0FGWieO.js +1 -0
  75. package/dist/assets/tracing-panel-LeIOi1Tv.js +2 -0
  76. package/dist/assets/{tree-actions-Ctf5W0DR.js → tree-actions-CrWpuExP.js} +1 -1
  77. package/dist/assets/useAddCell-DIdbaU70.js +1 -0
  78. package/dist/assets/useCellActionButton-DXyUqFdJ.js +1 -0
  79. package/dist/assets/{useDateFormatter-CfPcCT-J.js → useDateFormatter-BiqAQ-zI.js} +1 -1
  80. package/dist/assets/{useDeleteCell-6wcjnAMK.js → useDeleteCell-CSCMg7Up.js} +1 -1
  81. package/dist/assets/useDependencyPanelTab-Bt7aBDpS.js +1 -0
  82. package/dist/assets/useNotebookActions-ClT4GmnB.js +10 -0
  83. package/dist/assets/useNumberFormatter-DHJ9s6Vk.js +1 -0
  84. package/dist/assets/useRunCells-DHnbXALT.js +1 -0
  85. package/dist/assets/{useSplitCell-oIRu7Qlj.js → useSplitCell-CxlsGCB_.js} +1 -1
  86. package/dist/assets/{vega-component-D4OKyVdb.js → vega-component-CzgE5HEs.js} +1 -1
  87. package/dist/assets/write-secret-modal-BG_B6y1P.js +2 -0
  88. package/dist/index.html +57 -71
  89. package/package.json +1 -1
  90. package/src/components/datasources/datasources.tsx +16 -17
  91. package/src/components/editor/actions/useNotebookActions.tsx +50 -0
  92. package/src/components/editor/chrome/panels/components.tsx +1 -1
  93. package/src/components/editor/connections/__tests__/add-connection-button.test.tsx +129 -0
  94. package/src/components/editor/connections/add-connection-button.tsx +132 -0
  95. package/src/components/editor/connections/components.tsx +11 -0
  96. package/src/components/editor/connections/quick-add-data-sources.tsx +11 -11
  97. package/src/components/editor/controls/__tests__/notebook-menu-dropdown.test.tsx +109 -1
  98. package/src/components/storage/storage-inspector.tsx +15 -17
  99. package/src/core/codemirror/language/panel/sql.test.tsx +69 -0
  100. package/src/core/codemirror/language/panel/sql.tsx +81 -4
  101. package/dist/assets/ConnectedDataExplorerComponent-88Ojqyhx.js +0 -1
  102. package/dist/assets/DeferredRequestRegistry-C-SpvRCH.js +0 -1
  103. package/dist/assets/ImperativeModal-68zj0lxy.js +0 -1
  104. package/dist/assets/JsonOutput-pCdkYz4X.js +0 -50
  105. package/dist/assets/MarimoErrorOutput-b7bFhjgD.js +0 -6
  106. package/dist/assets/RSPContexts-D2JcYZo2.js +0 -1
  107. package/dist/assets/RenderHTML-BUuDW4yZ.js +0 -1
  108. package/dist/assets/RunButton-4MKgVYXU.js +0 -1
  109. package/dist/assets/SSRProvider-C_WMmqnS.js +0 -1
  110. package/dist/assets/add-connection-dialog-BcLKuMHt.js +0 -166
  111. package/dist/assets/agent-panel-BVyaNGyG.js +0 -286
  112. package/dist/assets/ai-model-dropdown-C_IoSmhR.js +0 -3
  113. package/dist/assets/app-config-button-BvpunAmJ.js +0 -1
  114. package/dist/assets/cell-editor-pKuytlrm.js +0 -24
  115. package/dist/assets/cell-link-D9d1y67_.js +0 -1
  116. package/dist/assets/cells-BdoPd1mK.js +0 -226
  117. package/dist/assets/chat-display-CbsB82vm.js +0 -1
  118. package/dist/assets/chat-panel-DCPKH1gP.js +0 -4
  119. package/dist/assets/chat-ui-BwLG6iSS.js +0 -4
  120. package/dist/assets/circle-plus-cm-FRu9O.js +0 -1
  121. package/dist/assets/column-preview-fku2dKeP.js +0 -1
  122. package/dist/assets/command-Cvmdzx0n.js +0 -1
  123. package/dist/assets/command-palette-BJFR8wKv.js +0 -1
  124. package/dist/assets/common-CQ9OqrFS.js +0 -1
  125. package/dist/assets/components-CaOG3LOY.js +0 -1
  126. package/dist/assets/config-xlrIxOZJ.js +0 -1
  127. package/dist/assets/context-Bf2wPdef.js +0 -1
  128. package/dist/assets/data-source-discovery-BsrDlL04.js +0 -1
  129. package/dist/assets/dependency-graph-panel-CxZJ5e7D.js +0 -5
  130. package/dist/assets/dialog-EjSc4yoH.js +0 -1
  131. package/dist/assets/dropdown-menu-BpI5klSq.js +0 -1
  132. package/dist/assets/edit-page-DwKN7vIP.js +0 -9
  133. package/dist/assets/field-YoG_gQAY.js +0 -1
  134. package/dist/assets/file-explorer-panel-CRrPqu3s.js +0 -1
  135. package/dist/assets/file-name-input-uFNWUWCc.js +0 -3
  136. package/dist/assets/focus-DgNLvj0o.js +0 -1
  137. package/dist/assets/form-CD2uATFu.js +0 -2
  138. package/dist/assets/form-GIq5QCAI.js +0 -1
  139. package/dist/assets/home-page-CYg3rQCB.js +0 -4
  140. package/dist/assets/html-to-image-Bt7Ld74W.js +0 -2
  141. package/dist/assets/index-CrsPwq0f.js +0 -29
  142. package/dist/assets/index-H2TMW127.css +0 -2
  143. package/dist/assets/input-BuOxQ3Wy.js +0 -1
  144. package/dist/assets/label-B67m1YH7.js +0 -1
  145. package/dist/assets/links-C7oaxPJK.js +0 -1
  146. package/dist/assets/markdown-renderer-C8IbrNw3.js +0 -2
  147. package/dist/assets/packages-panel-CIwyYf45.js +0 -1
  148. package/dist/assets/panel-accordion-state-DAAi8MPY.js +0 -1
  149. package/dist/assets/panels-DcguQKjE.js +0 -4
  150. package/dist/assets/popover-CgOo22ko.js +0 -1
  151. package/dist/assets/radio-group-DZoEgN7k.js +0 -1
  152. package/dist/assets/readonly-python-code-CyfabMFb.js +0 -1
  153. package/dist/assets/refresh-ccw-DrVG5f2L.js +0 -1
  154. package/dist/assets/request-registry-mNu7sXOU.js +0 -1
  155. package/dist/assets/run-page-Buh6gERG.js +0 -1
  156. package/dist/assets/scratchpad-panel-soCf5igY.js +0 -1
  157. package/dist/assets/secrets-panel-CCb8F6PC.js +0 -1
  158. package/dist/assets/select-DwqnFsyg.js +0 -1
  159. package/dist/assets/session-panel-BO4lOJc-.js +0 -1
  160. package/dist/assets/snippets-panel-CulFiQ0V.js +0 -1
  161. package/dist/assets/sparkles-30wHhAF5.js +0 -1
  162. package/dist/assets/state-B-K0k6lb.js +0 -1
  163. package/dist/assets/switch-VzdUoXkf.js +0 -1
  164. package/dist/assets/textarea-mD3C73jw.js +0 -1
  165. package/dist/assets/tracing-B_Qlv38-.js +0 -1
  166. package/dist/assets/tracing-panel--KVMPYqh.js +0 -2
  167. package/dist/assets/useCellActionButton-BDVM-sW-.js +0 -1
  168. package/dist/assets/useDependencyPanelTab-D4TnSeLa.js +0 -1
  169. package/dist/assets/useNotebookActions-gEpi1TfO.js +0 -10
  170. package/dist/assets/useNumberFormatter-BmUQe2Pf.js +0 -1
  171. package/dist/assets/usePress-CwOqeEX3.js +0 -7
  172. package/dist/assets/useRunCells-DP6cAHq0.js +0 -1
  173. package/dist/assets/write-secret-modal-dplU5PG8.js +0 -2
  174. package/dist/assets/x-BgkoSHM-.js +0 -1
@@ -21,6 +21,7 @@ import { maybeAddMarimoImport } from "@/core/cells/add-missing-import";
21
21
  import { useCellActions } from "@/core/cells/cells";
22
22
  import { useLastFocusedCellId } from "@/core/cells/focus";
23
23
  import { autoInstantiateAtom } from "@/core/config/config";
24
+ import type { DetectedDataSource } from "@/core/datasets/data-source-discovery";
24
25
  import {
25
26
  ENV_RENDERER,
26
27
  SECRET_TEXTAREA_RENDERER,
@@ -134,6 +135,16 @@ export function useInsertCode() {
134
135
  };
135
136
  }
136
137
 
138
+ /** Insert the configured cell supplied by a detected data source. */
139
+ export function useAddDetectedDataSource(onAdd?: () => void) {
140
+ const insertCode = useInsertCode();
141
+
142
+ return (source: DetectedDataSource) => {
143
+ insertCode(source.code);
144
+ onAdd?.();
145
+ };
146
+ }
147
+
137
148
  /**
138
149
  * Generic connection form: Zod-driven form with secrets support, a library
139
150
  * picker, and Back/Add buttons. Used by both database and storage forms.
@@ -1,13 +1,14 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { PlusIcon, SparklesIcon } from "lucide-react";
4
+ import { useId } from "react";
4
5
  import { Tooltip, TooltipProvider } from "@/components/ui/tooltip";
5
6
  import type {
6
7
  DataSourceDiscoveryGroup,
7
8
  DetectedDataSource,
8
9
  } from "@/core/datasets/data-source-discovery";
9
10
  import { useDetectedDataSources } from "@/hooks/useDataSourceDiscovery";
10
- import { useInsertCode } from "./components";
11
+ import { useAddDetectedDataSource } from "./components";
11
12
  import { cn } from "@/utils/cn";
12
13
 
13
14
  export const QuickAddDataSources: React.FC<{
@@ -15,22 +16,24 @@ export const QuickAddDataSources: React.FC<{
15
16
  sources: DetectedDataSource[];
16
17
  onAdd: (source: DetectedDataSource) => void;
17
18
  }> = ({ className, sources, onAdd }) => {
19
+ const titleId = useId();
20
+
18
21
  if (sources.length === 0) {
19
22
  return null;
20
23
  }
21
24
 
22
25
  return (
23
26
  <section
24
- aria-labelledby="quick-add-data-sources-title"
27
+ aria-labelledby={titleId}
25
28
  className={cn(
26
- "rounded-full bg-[linear-gradient(135deg,var(--blue-2),var(--purple-3))] px-3 py-2",
29
+ "rounded-full bg-[linear-gradient(135deg,var(--blue-2),var(--purple-3))] px-3 py-2 dark:bg-[linear-gradient(135deg,var(--blue-2),var(--purple-2))]",
27
30
  className,
28
31
  )}
29
32
  >
30
33
  <div className="flex flex-wrap items-center gap-2">
31
34
  <div className="mr-1 flex items-center gap-1.5">
32
- <SparklesIcon className="h-3.5 w-3.5 text-(--blue-9)" />
33
- <h3 id="quick-add-data-sources-title" className="text-sm">
35
+ <SparklesIcon className="h-3.5 w-3.5 text-(--blue-9) dark:text-(--blue-10)" />
36
+ <h3 id={titleId} className="text-sm">
34
37
  Quick add
35
38
  </h3>
36
39
  </div>
@@ -44,7 +47,7 @@ export const QuickAddDataSources: React.FC<{
44
47
  <button
45
48
  type="button"
46
49
  aria-label={`Add ${source.displayName} connection`}
47
- className="inline-flex items-center gap-1 rounded-full border border-border/60 bg-background px-2.5 py-1 text-xs font-medium text-foreground/90 transition-colors hover:border-(--blue-7) focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2"
50
+ className="inline-flex items-center gap-1 rounded-full border border-border/60 bg-background px-2.5 py-1 text-xs font-medium text-foreground/90 transition-colors hover:border-(--blue-7) focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 dark:border-border dark:bg-background/60 dark:hover:bg-accent/80"
48
51
  onClick={() => onAdd(source)}
49
52
  >
50
53
  <PlusIcon className="h-3 w-3 text-muted-foreground" />
@@ -101,17 +104,14 @@ export const AutoDiscoveredDataSources: React.FC<{
101
104
  className?: string;
102
105
  group?: DataSourceDiscoveryGroup;
103
106
  }> = ({ onSubmit, className, group }) => {
104
- const insertCode = useInsertCode();
107
+ const addDetectedDataSource = useAddDetectedDataSource(onSubmit);
105
108
  const sources = useDetectedDataSources(group);
106
109
 
107
110
  return (
108
111
  <QuickAddDataSources
109
112
  className={className}
110
113
  sources={sources}
111
- onAdd={(source) => {
112
- insertCode(source.code);
113
- onSubmit?.();
114
- }}
114
+ onAdd={addDetectedDataSource}
115
115
  />
116
116
  );
117
117
  };
@@ -1,18 +1,31 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { fireEvent, render, screen, waitFor } from "@testing-library/react";
3
+ import {
4
+ act,
5
+ fireEvent,
6
+ render,
7
+ screen,
8
+ waitFor,
9
+ } from "@testing-library/react";
4
10
  import { Provider } from "jotai";
5
11
  import type React from "react";
6
12
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
13
  import { MockRequestClient } from "@/__mocks__/requests";
8
14
  import { ModalProvider } from "@/components/modal/ImperativeModal";
9
15
  import { TooltipProvider } from "@/components/ui/tooltip";
16
+ import {
17
+ type DetectedDataSource,
18
+ invalidateDataSourceDiscovery,
19
+ } from "@/core/datasets/data-source-discovery";
20
+ import { DiscoverDataSources } from "@/core/datasets/request-registry";
10
21
  import { layoutStateAtom } from "@/core/layout/layout";
11
22
  import { kioskModeAtom, viewStateAtom } from "@/core/mode";
23
+ import { connectionAtom } from "@/core/network/connection";
12
24
  import { requestClientAtom } from "@/core/network/requests";
13
25
  import { filenameAtom } from "@/core/saving/file-state";
14
26
  import { store } from "@/core/state/jotai";
15
27
  import { isWasm } from "@/core/wasm/utils";
28
+ import { WebSocketState } from "@/core/websocket/types";
16
29
  import {
17
30
  DEFAULT_EXPORT_OPTIONS,
18
31
  exportOptionsAtom,
@@ -20,6 +33,34 @@ import {
20
33
  } from "../../actions/export-dialog/state";
21
34
  import { NotebookMenuDropdown } from "../notebook-menu-dropdown";
22
35
 
36
+ const POSTGRES_SOURCE: DetectedDataSource = {
37
+ id: "postgres-libpq-environment",
38
+ integration: "postgres",
39
+ category: "database",
40
+ displayName: "PostgreSQL",
41
+ confidence: "high",
42
+ origins: [{ type: "environment", label: "Kernel environment" }],
43
+ configuration: [],
44
+ code: "engine = create_engine()",
45
+ hidesWhen: { kind: "dialect", substrings: ["postgres"] },
46
+ };
47
+
48
+ const S3_SOURCE: DetectedDataSource = {
49
+ id: "aws-s3-environment",
50
+ integration: "aws",
51
+ category: "object-storage",
52
+ displayName: "AWS S3",
53
+ confidence: "high",
54
+ origins: [{ type: "environment", label: "Kernel environment" }],
55
+ configuration: [],
56
+ code: "storage = create_s3_storage()",
57
+ hidesWhen: {
58
+ kind: "storage",
59
+ protocols: ["s3"],
60
+ backendTypes: ["s3"],
61
+ },
62
+ };
63
+
23
64
  vi.mock("@/core/wasm/utils", async (importOriginal) => {
24
65
  const actual = await importOriginal<typeof import("@/core/wasm/utils")>();
25
66
  return { ...actual, isWasm: vi.fn(() => false) };
@@ -59,6 +100,7 @@ describe("NotebookMenuDropdown", () => {
59
100
  store.set(requestClientAtom, MockRequestClient.create());
60
101
  store.set(filenameAtom, "/project/notebook.py");
61
102
  store.set(viewStateAtom, { mode: "edit", cellAnchor: null });
103
+ store.set(connectionAtom, { state: WebSocketState.OPEN });
62
104
  store.set(kioskModeAtom, false);
63
105
  store.set(layoutStateAtom, {
64
106
  selectedLayout: "vertical",
@@ -73,11 +115,77 @@ describe("NotebookMenuDropdown", () => {
73
115
  addEventListener: vi.fn(),
74
116
  removeEventListener: vi.fn(),
75
117
  }));
118
+ invalidateDataSourceDiscovery();
119
+ vi.spyOn(DiscoverDataSources, "request").mockResolvedValue({
120
+ request_id: "request-id",
121
+ sources: [],
122
+ });
76
123
  });
77
124
 
78
125
  afterEach(() => {
79
126
  document.title = "";
80
127
  vi.unstubAllGlobals();
128
+ vi.restoreAllMocks();
129
+ });
130
+
131
+ it("shows detected databases in the add connection submenu", async () => {
132
+ vi.mocked(DiscoverDataSources.request).mockResolvedValue({
133
+ request_id: "request-id",
134
+ sources: [POSTGRES_SOURCE],
135
+ });
136
+
137
+ render(<NotebookMenuDropdown />, { wrapper });
138
+ await waitFor(() =>
139
+ expect(DiscoverDataSources.request).toHaveBeenCalledOnce(),
140
+ );
141
+ await act(async () => Promise.resolve());
142
+ openNotebookMenu();
143
+ fireEvent.click(
144
+ await screen.findByRole("menuitem", { name: "Add database connection" }),
145
+ );
146
+
147
+ const suggestion = await screen.findByRole("menuitem", {
148
+ name: "Add PostgreSQL",
149
+ });
150
+ const browseAll = screen.getByRole("menuitem", {
151
+ name: "Browse all connections",
152
+ });
153
+ expect(suggestion).toBeVisible();
154
+ expect(browseAll).toBeVisible();
155
+ expect(
156
+ suggestion.compareDocumentPosition(browseAll) &
157
+ Node.DOCUMENT_POSITION_FOLLOWING,
158
+ ).toBeTruthy();
159
+ });
160
+
161
+ it("shows detected storage in the add remote storage submenu", async () => {
162
+ vi.mocked(DiscoverDataSources.request).mockResolvedValue({
163
+ request_id: "request-id",
164
+ sources: [S3_SOURCE],
165
+ });
166
+
167
+ render(<NotebookMenuDropdown />, { wrapper });
168
+ await waitFor(() =>
169
+ expect(DiscoverDataSources.request).toHaveBeenCalledOnce(),
170
+ );
171
+ await act(async () => Promise.resolve());
172
+ openNotebookMenu();
173
+ fireEvent.click(
174
+ await screen.findByRole("menuitem", { name: "Add remote storage" }),
175
+ );
176
+
177
+ const suggestion = await screen.findByRole("menuitem", {
178
+ name: "Add AWS S3",
179
+ });
180
+ const browseAll = screen.getByRole("menuitem", {
181
+ name: "Browse all connections",
182
+ });
183
+ expect(suggestion).toBeVisible();
184
+ expect(browseAll).toBeVisible();
185
+ expect(
186
+ suggestion.compareDocumentPosition(browseAll) &
187
+ Node.DOCUMENT_POSITION_FOLLOWING,
188
+ ).toBeTruthy();
81
189
  });
82
190
 
83
191
  it("returns focus to the notebook menu when the dialog closes", async () => {
@@ -8,7 +8,6 @@ import {
8
8
  HardDriveIcon,
9
9
  HelpCircleIcon,
10
10
  LoaderCircle,
11
- PlusIcon,
12
11
  ViewIcon,
13
12
  XIcon,
14
13
  } from "lucide-react";
@@ -17,7 +16,7 @@ import { useLocale } from "react-aria";
17
16
  import { EngineVariable } from "@/components/databases/engine-variable";
18
17
  import { useAddCodeToNewCell } from "@/components/editor/cell/useAddCell";
19
18
  import { PanelEmptyState } from "@/components/editor/chrome/panels/empty-state";
20
- import { AddConnectionDialog } from "@/components/editor/connections/add-connection-dialog";
19
+ import { AddConnectionButton } from "@/components/editor/connections/add-connection-button";
21
20
  import {
22
21
  FILE_ICON_COLOR,
23
22
  renderFileIcon,
@@ -1011,12 +1010,12 @@ export const StorageInspector: React.FC = () => {
1011
1010
  </span>
1012
1011
  }
1013
1012
  action={
1014
- <AddConnectionDialog defaultTab="storage">
1015
- <Button variant="outline" size="sm">
1016
- Add remote storage
1017
- <PlusIcon className="h-4 w-4 ml-2" />
1018
- </Button>
1019
- </AddConnectionDialog>
1013
+ <AddConnectionButton
1014
+ group="storage"
1015
+ label="Add remote storage"
1016
+ variant="outline"
1017
+ size="sm"
1018
+ />
1020
1019
  }
1021
1020
  icon={<HardDriveIcon className="h-8 w-8" />}
1022
1021
  />
@@ -1076,15 +1075,14 @@ export const StorageInspector: React.FC = () => {
1076
1075
  >
1077
1076
  <HelpCircleIcon className="h-3.5 w-3.5 shrink-0 cursor-help text-muted-foreground hover:text-foreground mr-2" />
1078
1077
  </Tooltip>
1079
- <AddConnectionDialog defaultTab="storage">
1080
- <Button
1081
- variant="ghost"
1082
- size="sm"
1083
- className="px-2 border-0 border-l border-muted-background rounded-none focus-visible:ring-0 focus-visible:ring-offset-0"
1084
- >
1085
- <PlusIcon className="h-4 w-4" />
1086
- </Button>
1087
- </AddConnectionDialog>
1078
+ <AddConnectionButton
1079
+ group="storage"
1080
+ label="Add remote storage"
1081
+ compact={true}
1082
+ variant="ghost"
1083
+ size="sm"
1084
+ className="px-2 border-0 border-l border-muted-background rounded-none focus-visible:ring-0 focus-visible:ring-offset-0"
1085
+ />
1088
1086
  </div>
1089
1087
  <CommandList className="flex flex-col">
1090
1088
  {namespaces.map((ns) => {
@@ -0,0 +1,69 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+ import { Provider } from "jotai";
5
+ import { beforeAll, describe, expect, it, vi } from "vitest";
6
+ import { TooltipProvider } from "@/components/ui/tooltip";
7
+ import type { CellId } from "@/core/cells/ids";
8
+ import type { DetectedDataSource } from "@/core/datasets/data-source-discovery";
9
+ import type { ConnectionName } from "@/core/datasets/engines";
10
+ import { store } from "@/core/state/jotai";
11
+ import { SQLEngineSelect } from "./sql";
12
+
13
+ const { addDetectedDataSource } = vi.hoisted(() => ({
14
+ addDetectedDataSource: vi.fn(),
15
+ }));
16
+
17
+ const POSTGRES_SOURCE: DetectedDataSource = {
18
+ id: "postgres-libpq-environment",
19
+ integration: "postgres",
20
+ category: "database",
21
+ displayName: "PostgreSQL",
22
+ confidence: "high",
23
+ origins: [{ type: "environment", label: "Kernel environment" }],
24
+ configuration: [],
25
+ code: "engine = create_engine()",
26
+ hidesWhen: { kind: "dialect", substrings: ["postgres"] },
27
+ };
28
+
29
+ vi.mock("@/hooks/useDataSourceDiscovery", () => ({
30
+ useDetectedDataSources: () => [POSTGRES_SOURCE],
31
+ }));
32
+
33
+ vi.mock("@/components/editor/connections/components", () => ({
34
+ useAddDetectedDataSource: () => addDetectedDataSource,
35
+ }));
36
+
37
+ describe("SQLEngineSelect", () => {
38
+ beforeAll(() => {
39
+ HTMLElement.prototype.hasPointerCapture = vi.fn(() => false);
40
+ HTMLElement.prototype.setPointerCapture = vi.fn();
41
+ HTMLElement.prototype.releasePointerCapture = vi.fn();
42
+ HTMLElement.prototype.scrollIntoView = vi.fn();
43
+ });
44
+
45
+ it("adds a database detected in the environment", async () => {
46
+ render(
47
+ <Provider store={store}>
48
+ <TooltipProvider>
49
+ <SQLEngineSelect
50
+ selectedEngine={"" as ConnectionName}
51
+ onChange={vi.fn()}
52
+ cellId={"cell-1" as CellId}
53
+ />
54
+ </TooltipProvider>
55
+ </Provider>,
56
+ );
57
+
58
+ fireEvent.keyDown(screen.getByRole("combobox"), { key: "ArrowDown" });
59
+
60
+ expect(
61
+ await screen.findByText("Detected in your environment"),
62
+ ).toBeVisible();
63
+ fireEvent.click(
64
+ screen.getByRole("option", { name: "Add PostgreSQL connection" }),
65
+ );
66
+
67
+ expect(addDetectedDataSource).toHaveBeenCalledWith(POSTGRES_SOURCE);
68
+ });
69
+ });
@@ -10,10 +10,12 @@ import {
10
10
  CircleHelpIcon,
11
11
  DatabaseZap,
12
12
  SearchCheck,
13
+ SparklesIcon,
13
14
  } from "lucide-react";
14
15
  import { getCellForDomProps } from "@/components/data-table/cell-utils";
15
16
  import { transformDisplayName } from "@/components/databases/display";
16
17
  import { DatabaseLogo } from "@/components/databases/icon";
18
+ import { useAddDetectedDataSource } from "@/components/editor/connections/components";
17
19
  import { Button } from "@/components/ui/button";
18
20
  import {
19
21
  Select,
@@ -31,11 +33,13 @@ import {
31
33
  dataConnectionsMapAtom,
32
34
  setLatestEngineSelected,
33
35
  } from "@/core/datasets/data-source-connections";
36
+ import type { DetectedDataSource } from "@/core/datasets/data-source-discovery";
34
37
  import {
35
38
  type ConnectionName,
36
39
  INTERNAL_SQL_ENGINES,
37
40
  } from "@/core/datasets/engines";
38
41
  import type { DataSourceConnection } from "@/core/kernel/messages";
42
+ import { useDetectedDataSources } from "@/hooks/useDataSourceDiscovery";
39
43
  import { useNonce } from "@/hooks/useNonce";
40
44
  import { clearAllSqlValidationErrors } from "../languages/sql/banner-validation-errors";
41
45
  import { type SQLMode, useSQLMode } from "../languages/sql/sql-mode";
@@ -46,19 +50,61 @@ interface SelectProps {
46
50
  cellId: CellId;
47
51
  }
48
52
 
49
- export const SQLEngineSelect: React.FC<SelectProps> = ({
53
+ export const SQLEngineSelect: React.FC<SelectProps> = (props) => {
54
+ const detectedDataSources = useDetectedDataSources("database");
55
+
56
+ if (detectedDataSources.length === 0) {
57
+ return (
58
+ <SQLEngineSelectImpl
59
+ {...props}
60
+ detectedDataSources={detectedDataSources}
61
+ />
62
+ );
63
+ }
64
+
65
+ return (
66
+ <SQLEngineSelectWithDetectedSources
67
+ {...props}
68
+ detectedDataSources={detectedDataSources}
69
+ />
70
+ );
71
+ };
72
+
73
+ const SQLEngineSelectWithDetectedSources: React.FC<
74
+ SelectProps & { detectedDataSources: DetectedDataSource[] }
75
+ > = ({ detectedDataSources, ...props }) => {
76
+ const addDetectedDataSource = useAddDetectedDataSource();
77
+ return (
78
+ <SQLEngineSelectImpl
79
+ {...props}
80
+ detectedDataSources={detectedDataSources}
81
+ addDetectedDataSource={addDetectedDataSource}
82
+ />
83
+ );
84
+ };
85
+
86
+ const SQLEngineSelectImpl: React.FC<
87
+ SelectProps & {
88
+ detectedDataSources: DetectedDataSource[];
89
+ addDetectedDataSource?: (source: DetectedDataSource) => void;
90
+ }
91
+ > = ({
50
92
  selectedEngine,
51
93
  onChange,
52
94
  cellId,
95
+ detectedDataSources,
96
+ addDetectedDataSource,
53
97
  }) => {
54
98
  const connectionsMap = useAtomValue(dataConnectionsMapAtom);
55
99
 
56
100
  const internalEngineConnections: DataSourceConnection[] = [];
57
101
  const userDefinedConnections: DataSourceConnection[] = [];
58
102
  for (const [connName, connection] of connectionsMap.entries()) {
59
- INTERNAL_SQL_ENGINES.has(connName)
60
- ? internalEngineConnections.push(connection)
61
- : userDefinedConnections.push(connection);
103
+ if (INTERNAL_SQL_ENGINES.has(connName)) {
104
+ internalEngineConnections.push(connection);
105
+ } else {
106
+ userDefinedConnections.push(connection);
107
+ }
62
108
  }
63
109
 
64
110
  // Use nonce to force re-render as languageAdapter.engine may not trigger change
@@ -74,6 +120,17 @@ export const SQLEngineSelect: React.FC<SelectProps> = ({
74
120
  return;
75
121
  }
76
122
 
123
+ if (value.startsWith(DETECTED_SOURCE_PREFIX)) {
124
+ const sourceId = value.slice(DETECTED_SOURCE_PREFIX.length);
125
+ const source = detectedDataSources.find(
126
+ (source) => source.id === sourceId,
127
+ );
128
+ if (source && addDetectedDataSource) {
129
+ addDetectedDataSource(source);
130
+ }
131
+ return;
132
+ }
133
+
77
134
  const nextEngine = connectionsMap.get(value as ConnectionName);
78
135
  if (nextEngine) {
79
136
  rerender();
@@ -125,6 +182,25 @@ export const SQLEngineSelect: React.FC<SelectProps> = ({
125
182
  {renderConnections(userDefinedConnections)}
126
183
  {userDefinedConnections.length > 0 && <SelectSeparator />}
127
184
  {renderConnections(internalEngineConnections)}
185
+ {detectedDataSources.length > 0 && (
186
+ <>
187
+ <SelectSeparator />
188
+ <SelectLabel>Detected in your environment</SelectLabel>
189
+ {detectedDataSources.map((source) => (
190
+ <SelectItem
191
+ key={source.id}
192
+ value={`${DETECTED_SOURCE_PREFIX}${source.id}`}
193
+ >
194
+ <div className="flex items-center gap-1">
195
+ <SparklesIcon className="h-3 w-3 text-(--blue-9)" />
196
+ <span className="truncate ml-0.5">
197
+ Add {source.displayName} connection
198
+ </span>
199
+ </div>
200
+ </SelectItem>
201
+ ))}
202
+ </>
203
+ )}
128
204
  <SelectSeparator />
129
205
  <SelectItem className="text-muted-foreground" value={HELP_KEY}>
130
206
  <a
@@ -145,6 +221,7 @@ export const SQLEngineSelect: React.FC<SelectProps> = ({
145
221
  };
146
222
 
147
223
  const HELP_KEY = "__help__";
224
+ const DETECTED_SOURCE_PREFIX = "__detected_source__:";
148
225
  const HELP_URL =
149
226
  "http://docs.marimo.io/guides/working_with_data/sql/#connecting-to-a-custom-database";
150
227