@marimo-team/islands 0.24.1-dev3 → 0.24.1-dev31

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 (72) hide show
  1. package/dist/{ConnectedDataExplorerComponent-BUC1MrsF.js → ConnectedDataExplorerComponent-yEsovh7C.js} +4 -4
  2. package/dist/arrays-Ct6XviUd.js +59 -0
  3. package/dist/{capabilities-BilXeZ7p.js → capabilities-DNKeBoT5.js} +1 -1
  4. package/dist/{chat-ui-ChBRy1iT.js → chat-ui-ChbH2I73.js} +3 -3
  5. package/dist/{common-BvTQu-LJ.js → common-ZoATHjIZ.js} +17 -11
  6. package/dist/{formats-Dz0QNUEa.js → formats-BZtMpeQe.js} +1 -1
  7. package/dist/{glide-data-editor-BI2i_IEE.js → glide-data-editor-aimC_hok.js} +3 -1
  8. package/dist/{html-to-image-BtNr4zrO.js → html-to-image-BzT-fb14.js} +2357 -2325
  9. package/dist/{input-BRE2a7Ay.js → input-CqV2ZPgL.js} +1 -1
  10. package/dist/{loader-DTnbNSVZ.js → loader-Chm6nOqy.js} +1 -1
  11. package/dist/main.js +1196 -1088
  12. package/dist/{once-Boutk_9X.js → once-D1_qYl2e.js} +1 -1
  13. package/dist/{process-output-Dch8KCi8.js → process-output-C8Q8G6-g.js} +1 -1
  14. package/dist/{react-vega-zyfTF9SY.js → react-vega-BE2Uv1cq.js} +1 -1
  15. package/dist/{reveal-component-qNkzuPp3.js → reveal-component-CBpmFHpU.js} +5 -5
  16. package/dist/{spec-BL1GSUUF.js → spec-DR1CyTOo.js} +2 -0
  17. package/dist/style.css +1 -1
  18. package/dist/{swiper-component-Cx0cRWLk.js → swiper-component-UYicacis.js} +1 -1
  19. package/dist/{useIframeCapabilities-CxuXXwjj.js → useIframeCapabilities-BbnoLKxG.js} +1 -1
  20. package/dist/{vega-component-CioKuDut.js → vega-component-xrHju6oo.js} +5 -5
  21. package/dist/{vega-embed-container-BRjjqRGz.js → vega-embed-container-B1tc-DoN.js} +2 -2
  22. package/dist/{vega-loader.browser-CGb-2Kwi.js → vega-loader.browser-VFQfHaoV.js} +1 -1
  23. package/package.json +1 -1
  24. package/src/components/app-config/optional-features.test.ts +78 -0
  25. package/src/components/app-config/optional-features.tsx +33 -6
  26. package/src/components/data-table/__tests__/column_formatting.test.ts +7 -0
  27. package/src/components/data-table/charts/__tests__/schemas.test.ts +17 -2
  28. package/src/components/data-table/charts/chart-spec/types.ts +2 -0
  29. package/src/components/data-table/column-formatting/feature.ts +1 -0
  30. package/src/components/data-table/column-formatting/types.ts +1 -0
  31. package/src/components/data-table/column-summary/chart-spec-model.tsx +1 -0
  32. package/src/components/data-table/column-summary/column-summary.tsx +1 -0
  33. package/src/components/data-table/columns.tsx +4 -3
  34. package/src/components/datasets/__tests__/icons.test.ts +10 -2
  35. package/src/components/datasets/icons.tsx +4 -0
  36. package/src/components/datasources/__tests__/components.test.tsx +14 -1
  37. package/src/components/datasources/datasources.tsx +2 -40
  38. package/src/components/editor/chrome/panels/components.tsx +35 -0
  39. package/src/components/editor/chrome/panels/file-explorer-panel.tsx +10 -0
  40. package/src/components/editor/chrome/panels/session-panel.tsx +12 -4
  41. package/src/components/editor/connections/__tests__/quick-add-data-sources.test.tsx +2 -0
  42. package/src/components/editor/connections/add-connection-dialog.tsx +5 -1
  43. package/src/components/editor/connections/quick-add-data-sources.tsx +11 -7
  44. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +43 -0
  45. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +127 -0
  46. package/src/components/editor/connections/storage/add-storage-form.tsx +11 -1
  47. package/src/components/editor/connections/storage/as-code.ts +30 -1
  48. package/src/components/editor/connections/storage/schemas.ts +63 -0
  49. package/src/core/codemirror/code-lens/__tests__/popover.test.ts +13 -0
  50. package/src/core/codemirror/code-lens/popover.tsx +11 -5
  51. package/src/core/codemirror/language/languages/sql/__tests__/renderers.test.tsx +15 -4
  52. package/src/core/codemirror/language/languages/sql/renderers.tsx +1 -0
  53. package/src/core/datasets/__tests__/data-source-discovery.test.ts +277 -0
  54. package/src/core/datasets/data-source-connections.ts +36 -0
  55. package/src/core/datasets/data-source-discovery.ts +92 -0
  56. package/src/core/kernel/messages.ts +1 -0
  57. package/src/core/websocket/useMarimoKernelConnection.tsx +11 -3
  58. package/src/hooks/useDataSourceDiscovery.ts +50 -9
  59. package/src/plugins/impl/MatrixPlugin.tsx +256 -35
  60. package/src/plugins/impl/__tests__/DataEditorPlugin.test.ts +13 -1
  61. package/src/plugins/impl/__tests__/MatrixPlugin.test.tsx +540 -3
  62. package/src/plugins/impl/data-editor/glide-data-editor.tsx +1 -0
  63. package/src/plugins/impl/data-editor/glide-utils.ts +1 -0
  64. package/src/plugins/impl/data-frames/__tests__/schema.test.ts +9 -1
  65. package/src/plugins/impl/data-frames/utils/__tests__/operators.test.ts +8 -0
  66. package/src/plugins/impl/data-frames/utils/operators.ts +1 -1
  67. package/src/plugins/impl/matplotlib/__tests__/matplotlib-renderer.test.ts +184 -24
  68. package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +40 -7
  69. package/src/plugins/impl/vega/__tests__/vega.test.ts +22 -4
  70. package/src/plugins/impl/vega/utils.ts +3 -0
  71. package/dist/arrays-C56grfjL.js +0 -55
  72. package/src/hooks/__tests__/useDataSourceDiscovery.test.ts +0 -80
@@ -0,0 +1,277 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { connectionAtom } from "@/core/network/connection";
4
+ import { store } from "@/core/state/jotai";
5
+ import { WebSocketState } from "@/core/websocket/types";
6
+ import {
7
+ fetchDataSourceDiscovery,
8
+ invalidateDataSourceDiscovery,
9
+ isDetectedSourceConnected,
10
+ matchesDiscoveryGroup,
11
+ type DetectedDataSource,
12
+ } from "../data-source-discovery";
13
+ import { DiscoverDataSources } from "../request-registry";
14
+
15
+ function source(
16
+ overrides: Partial<DetectedDataSource> &
17
+ Pick<DetectedDataSource, "id" | "integration" | "category" | "displayName">,
18
+ ): DetectedDataSource {
19
+ return {
20
+ confidence: "high",
21
+ origins: [],
22
+ configuration: [],
23
+ code: "",
24
+ hidesWhen: { kind: "dialect", substrings: [overrides.integration] },
25
+ ...overrides,
26
+ };
27
+ }
28
+
29
+ describe("fetchDataSourceDiscovery", () => {
30
+ beforeEach(() => {
31
+ // Discovery waits for the session's websocket connection to be open
32
+ // before requesting, so it doesn't race a still-connecting session.
33
+ store.set(connectionAtom, { state: WebSocketState.OPEN });
34
+ });
35
+
36
+ afterEach(() => {
37
+ vi.restoreAllMocks();
38
+ invalidateDataSourceDiscovery();
39
+ });
40
+
41
+ it("shares a single in-flight request across concurrent callers", async () => {
42
+ const request = vi
43
+ .spyOn(DiscoverDataSources, "request")
44
+ .mockResolvedValue({ request_id: "request-id", sources: [] });
45
+
46
+ await Promise.all([
47
+ fetchDataSourceDiscovery(),
48
+ fetchDataSourceDiscovery(),
49
+ fetchDataSourceDiscovery(),
50
+ ]);
51
+
52
+ expect(request).toHaveBeenCalledTimes(1);
53
+ });
54
+
55
+ it("re-runs discovery after the cache is invalidated", async () => {
56
+ const request = vi
57
+ .spyOn(DiscoverDataSources, "request")
58
+ .mockResolvedValue({ request_id: "request-id", sources: [] });
59
+
60
+ await fetchDataSourceDiscovery();
61
+ invalidateDataSourceDiscovery();
62
+ await fetchDataSourceDiscovery();
63
+
64
+ expect(request).toHaveBeenCalledTimes(2);
65
+ });
66
+
67
+ it("clears the cache on failure so the next call retries", async () => {
68
+ const request = vi
69
+ .spyOn(DiscoverDataSources, "request")
70
+ .mockRejectedValueOnce(new Error("boom"))
71
+ .mockResolvedValueOnce({ request_id: "request-id", sources: [] });
72
+
73
+ await expect(fetchDataSourceDiscovery()).rejects.toThrow("boom");
74
+ await expect(fetchDataSourceDiscovery()).resolves.toEqual([]);
75
+ expect(request).toHaveBeenCalledTimes(2);
76
+ });
77
+
78
+ it("does not clear a newer cache entry when an invalidated request fails", async () => {
79
+ let rejectFirstRequest: ((error: Error) => void) | undefined;
80
+ const firstRequest = new Promise<{
81
+ request_id: string;
82
+ sources: DetectedDataSource[];
83
+ }>((_resolve, reject) => {
84
+ rejectFirstRequest = reject;
85
+ });
86
+ const request = vi
87
+ .spyOn(DiscoverDataSources, "request")
88
+ .mockReturnValueOnce(firstRequest)
89
+ .mockResolvedValueOnce({ request_id: "request-id", sources: [] });
90
+
91
+ const staleResult = fetchDataSourceDiscovery().catch((error) => error);
92
+ await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(1));
93
+ invalidateDataSourceDiscovery();
94
+ await fetchDataSourceDiscovery();
95
+
96
+ rejectFirstRequest?.(new Error("stale request failed"));
97
+ await staleResult;
98
+ await fetchDataSourceDiscovery();
99
+
100
+ expect(request).toHaveBeenCalledTimes(2);
101
+ });
102
+
103
+ it("returns sources from the kernel", async () => {
104
+ const sources: DetectedDataSource[] = [
105
+ source({
106
+ id: "postgres",
107
+ integration: "postgres",
108
+ category: "database",
109
+ displayName: "PostgreSQL",
110
+ }),
111
+ source({
112
+ id: "mysql",
113
+ integration: "mysql",
114
+ category: "database",
115
+ displayName: "MySQL",
116
+ }),
117
+ ];
118
+ vi.spyOn(DiscoverDataSources, "request").mockResolvedValue({
119
+ request_id: "request-id",
120
+ sources,
121
+ });
122
+
123
+ await expect(fetchDataSourceDiscovery()).resolves.toEqual(sources);
124
+ });
125
+ });
126
+
127
+ describe("matchesDiscoveryGroup", () => {
128
+ const postgres = source({
129
+ id: "postgres",
130
+ integration: "postgres",
131
+ category: "database",
132
+ displayName: "PostgreSQL",
133
+ });
134
+ const iceberg = source({
135
+ id: "iceberg",
136
+ integration: "pyiceberg",
137
+ category: "catalog",
138
+ displayName: "PyIceberg",
139
+ });
140
+ const s3 = source({
141
+ id: "s3",
142
+ integration: "aws",
143
+ category: "object-storage",
144
+ displayName: "S3",
145
+ hidesWhen: {
146
+ kind: "storage",
147
+ protocols: ["s3"],
148
+ backendTypes: [],
149
+ },
150
+ });
151
+
152
+ it("keeps every source when no group is given", () => {
153
+ expect(matchesDiscoveryGroup(postgres)).toBe(true);
154
+ expect(matchesDiscoveryGroup(iceberg)).toBe(true);
155
+ expect(matchesDiscoveryGroup(s3)).toBe(true);
156
+ });
157
+
158
+ it("groups catalogs with databases", () => {
159
+ expect(matchesDiscoveryGroup(postgres, "database")).toBe(true);
160
+ expect(matchesDiscoveryGroup(iceberg, "database")).toBe(true);
161
+ expect(matchesDiscoveryGroup(s3, "database")).toBe(false);
162
+ });
163
+
164
+ it("narrows storage to object-storage sources", () => {
165
+ expect(matchesDiscoveryGroup(postgres, "storage")).toBe(false);
166
+ expect(matchesDiscoveryGroup(iceberg, "storage")).toBe(false);
167
+ expect(matchesDiscoveryGroup(s3, "storage")).toBe(true);
168
+ });
169
+ });
170
+
171
+ describe("isDetectedSourceConnected", () => {
172
+ const postgres = source({
173
+ id: "postgres",
174
+ integration: "postgres",
175
+ category: "database",
176
+ displayName: "PostgreSQL",
177
+ hidesWhen: { kind: "dialect", substrings: ["postgres"] },
178
+ });
179
+ const emptySnapshot = {
180
+ dialects: [] as string[],
181
+ storageProtocols: [] as string[],
182
+ storageBackendTypes: [] as string[],
183
+ };
184
+
185
+ it("does not hide a source when nothing is connected", () => {
186
+ expect(isDetectedSourceConnected(postgres, emptySnapshot)).toBe(false);
187
+ });
188
+
189
+ it("hides a database suggestion when a matching dialect is live", () => {
190
+ expect(
191
+ isDetectedSourceConnected(postgres, {
192
+ ...emptySnapshot,
193
+ dialects: ["postgresql"],
194
+ }),
195
+ ).toBe(true);
196
+ });
197
+
198
+ it("does not hide a database suggestion for a different dialect", () => {
199
+ expect(
200
+ isDetectedSourceConnected(postgres, {
201
+ ...emptySnapshot,
202
+ dialects: ["mysql", "duckdb"],
203
+ }),
204
+ ).toBe(false);
205
+ });
206
+
207
+ it("hides spark suggestions for either pyspark or spark dialects", () => {
208
+ const spark = source({
209
+ id: "spark",
210
+ integration: "pyspark",
211
+ category: "catalog",
212
+ displayName: "Spark",
213
+ hidesWhen: { kind: "dialect", substrings: ["pyspark", "spark"] },
214
+ });
215
+ expect(
216
+ isDetectedSourceConnected(spark, {
217
+ ...emptySnapshot,
218
+ dialects: ["spark"],
219
+ }),
220
+ ).toBe(true);
221
+ });
222
+
223
+ it("hides object storage when a matching protocol is live", () => {
224
+ const s3 = source({
225
+ id: "s3",
226
+ integration: "aws",
227
+ category: "object-storage",
228
+ displayName: "S3",
229
+ hidesWhen: {
230
+ kind: "storage",
231
+ protocols: ["s3", "cloudflare", "coreweave"],
232
+ backendTypes: [],
233
+ },
234
+ });
235
+ expect(
236
+ isDetectedSourceConnected(s3, {
237
+ ...emptySnapshot,
238
+ storageProtocols: ["s3"],
239
+ }),
240
+ ).toBe(true);
241
+ });
242
+
243
+ it("hides huggingface when the backend type matches", () => {
244
+ const hf = source({
245
+ id: "hf",
246
+ integration: "huggingface",
247
+ category: "object-storage",
248
+ displayName: "Hugging Face",
249
+ hidesWhen: {
250
+ kind: "storage",
251
+ protocols: ["hf"],
252
+ backendTypes: ["huggingface"],
253
+ },
254
+ });
255
+ expect(
256
+ isDetectedSourceConnected(hf, {
257
+ ...emptySnapshot,
258
+ storageBackendTypes: ["huggingface"],
259
+ }),
260
+ ).toBe(true);
261
+ });
262
+
263
+ it("does not hide when hidesWhen does not match live connections", () => {
264
+ expect(
265
+ isDetectedSourceConnected(
266
+ source({
267
+ id: "custom",
268
+ integration: "custom",
269
+ category: "database",
270
+ displayName: "Custom",
271
+ hidesWhen: { kind: "dialect", substrings: ["custom"] },
272
+ }),
273
+ { ...emptySnapshot, dialects: ["postgres"] },
274
+ ),
275
+ ).toBe(false);
276
+ });
277
+ });
@@ -4,6 +4,7 @@ import { atom } from "jotai";
4
4
  import { isSchemaless } from "@/components/datasources/utils";
5
5
  import { createReducerAndAtoms } from "@/utils/createReducer";
6
6
  import { Logger } from "@/utils/Logger";
7
+ import { sortBy } from "@/utils/arrays";
7
8
  import type {
8
9
  DatabaseSchema,
9
10
  DataSourceConnection as DataSourceConnectionType,
@@ -13,6 +14,7 @@ import { store } from "../state/jotai";
13
14
  import type { VariableName } from "../variables/types";
14
15
  import {
15
16
  type ConnectionName,
17
+ DEFAULT_DUCKDB_DATABASE,
16
18
  DUCKDB_ENGINE,
17
19
  INTERNAL_SQL_ENGINES,
18
20
  } from "./engines";
@@ -341,6 +343,40 @@ export const dataConnectionsMapAtom = atom(
341
343
  (get) => get(dataSourceConnectionsAtom).connectionsMap,
342
344
  );
343
345
 
346
+ /**
347
+ * Get the data connections that are available to the user.
348
+ * Filters out internal engines if it has no databases or if it has only the in-memory database and no schemas.
349
+ */
350
+ export const connectionsAtom = atom((get) => {
351
+ const dataConnections = new Map(get(dataConnectionsMapAtom));
352
+
353
+ // Filter out the internal engines if it has no databases
354
+ // Or if it has only the in-memory database and no schemas
355
+ for (const engine of INTERNAL_SQL_ENGINES) {
356
+ const connection = dataConnections.get(engine);
357
+ if (!connection) {
358
+ continue;
359
+ }
360
+
361
+ if (connection.databases.length === 0) {
362
+ dataConnections.delete(engine);
363
+ }
364
+
365
+ if (
366
+ connection.databases.length === 1 &&
367
+ connection.databases[0].name === DEFAULT_DUCKDB_DATABASE &&
368
+ connection.databases[0].schemas.length === 0
369
+ ) {
370
+ dataConnections.delete(engine);
371
+ }
372
+ }
373
+
374
+ // Put internal engines last to prioritize user-defined connections
375
+ return sortBy([...dataConnections.values()], (connection) =>
376
+ INTERNAL_SQL_ENGINES.has(connection.name) ? 1 : 0,
377
+ );
378
+ });
379
+
344
380
  export function setLatestEngineSelected(engine: ConnectionName) {
345
381
  const existing = store.get(dataSourceConnectionsAtom);
346
382
  // Don't update the map if the engine is not in the map
@@ -1,5 +1,97 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import { atom } from "jotai";
3
4
  import type { components } from "@marimo-team/marimo-api";
5
+ import { waitForConnectionOpenIfNotebook } from "../network/connection";
6
+ import { store } from "../state/jotai";
7
+ import { DiscoverDataSources } from "./request-registry";
4
8
 
5
9
  export type DetectedDataSource = components["schemas"]["DetectedDataSource"];
10
+
11
+ export type DataSourceDiscoveryGroup = "database" | "storage";
12
+
13
+ /** Category filter for UI surfaces. Catalogs ride with databases. */
14
+ export function matchesDiscoveryGroup(
15
+ source: DetectedDataSource,
16
+ group?: DataSourceDiscoveryGroup,
17
+ ): boolean {
18
+ if (!group) {
19
+ return true;
20
+ }
21
+ const isStorage = source.category === "object-storage";
22
+ return isStorage === (group === "storage");
23
+ }
24
+
25
+ /**
26
+ * Live engines and storage used to hide suggestions of the same type.
27
+ */
28
+ export interface LiveConnectionSnapshot {
29
+ dialects: readonly string[];
30
+ storageProtocols: readonly string[];
31
+ storageBackendTypes: readonly string[];
32
+ }
33
+
34
+ /**
35
+ * Whether a detected source already has a live connection of the same type.
36
+ * Match rules come from the kernel on each suggestion.
37
+ */
38
+ export function isDetectedSourceConnected(
39
+ source: DetectedDataSource,
40
+ snapshot: LiveConnectionSnapshot,
41
+ ): boolean {
42
+ const match = source.hidesWhen;
43
+ if (match.kind === "dialect") {
44
+ return match.substrings.some((alias) =>
45
+ snapshot.dialects.some((dialect) => dialect.includes(alias)),
46
+ );
47
+ }
48
+
49
+ if (
50
+ match.protocols.some((protocol) =>
51
+ snapshot.storageProtocols.includes(protocol),
52
+ )
53
+ ) {
54
+ return true;
55
+ }
56
+
57
+ return match.backendTypes.some((backendType) =>
58
+ snapshot.storageBackendTypes.includes(backendType),
59
+ );
60
+ }
61
+
62
+ /**
63
+ * Bumped when discovery should be re-fetched (kernel restart).
64
+ */
65
+ export const dataSourceDiscoveryEpochAtom = atom(0);
66
+
67
+ /**
68
+ * Kernel-managed datasource discovery scans the environment once per epoch.
69
+ * Every consumer shares a single in-flight request.
70
+ */
71
+ let discoveryPromise: Promise<DetectedDataSource[]> | undefined;
72
+ let discoveryEpoch = 0;
73
+
74
+ export function fetchDataSourceDiscovery(): Promise<DetectedDataSource[]> {
75
+ const epoch = store.get(dataSourceDiscoveryEpochAtom);
76
+ if (!discoveryPromise || discoveryEpoch !== epoch) {
77
+ discoveryEpoch = epoch;
78
+ discoveryPromise = waitForConnectionOpenIfNotebook()
79
+ .then(() => DiscoverDataSources.request({}))
80
+ .then((result) => result.sources)
81
+ .catch((error: unknown) => {
82
+ if (discoveryEpoch === epoch) {
83
+ discoveryPromise = undefined;
84
+ }
85
+ throw error;
86
+ });
87
+ }
88
+ return discoveryPromise;
89
+ }
90
+
91
+ /**
92
+ * Clears the shared discovery cache and triggers consumers to refetch.
93
+ */
94
+ export function invalidateDataSourceDiscovery(): void {
95
+ discoveryPromise = undefined;
96
+ store.set(dataSourceDiscoveryEpochAtom, (value) => value + 1);
97
+ }
@@ -11,6 +11,7 @@ export const DATA_TYPES = [
11
11
  "date",
12
12
  "datetime",
13
13
  "time",
14
+ "geometry",
14
15
  "unknown",
15
16
  ] as const;
16
17
  export type ModelLifecycle = NotificationMessageData<"model-lifecycle">;
@@ -40,6 +40,7 @@ import type { CellData } from "../cells/types";
40
40
  import { capabilitiesAtom } from "../config/capabilities";
41
41
  import { connectionTransportTypeAtom, useSetAppConfig } from "../config/config";
42
42
  import { useDataSourceActions } from "../datasets/data-source-connections";
43
+ import { invalidateDataSourceDiscovery } from "../datasets/data-source-discovery";
43
44
  import type { ConnectionName } from "../datasets/engines";
44
45
  import {
45
46
  DiscoverDataSources,
@@ -251,6 +252,12 @@ export function useMarimoKernelConnection(opts: {
251
252
  onError: showBoundary,
252
253
  existingCells,
253
254
  });
255
+ if (!msg.data.resumed) {
256
+ // A freshly started kernel may expose a different environment
257
+ // (e.g. new env vars); re-run discovery instead of reusing stale
258
+ // suggestions from the previous kernel instance.
259
+ invalidateDataSourceDiscovery();
260
+ }
254
261
  setKioskMode(msg.data.kiosk);
255
262
  // A freshly started kernel has no breakpoints of its own; re-push
256
263
  // the client's set so they still apply, and clear the stale
@@ -311,6 +318,7 @@ export function useMarimoKernelConnection(opts: {
311
318
  }
312
319
 
313
320
  case "variables":
321
+ const variableNames = msg.data.variables.map((v) => v.name);
314
322
  setVariables(
315
323
  msg.data.variables.map((v) => ({
316
324
  name: v.name,
@@ -318,9 +326,9 @@ export function useMarimoKernelConnection(opts: {
318
326
  usedBy: v.used_by,
319
327
  })),
320
328
  );
321
- filterDatasetsFromVariables(msg.data.variables.map((v) => v.name));
322
- filterDataSourcesFromVariables(msg.data.variables.map((v) => v.name));
323
- filterStorageFromVariables(msg.data.variables.map((v) => v.name));
329
+ filterDatasetsFromVariables(variableNames);
330
+ filterDataSourcesFromVariables(variableNames);
331
+ filterStorageFromVariables(variableNames);
324
332
  return;
325
333
  case "variable-values":
326
334
  setMetadata(
@@ -1,18 +1,59 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import type { DetectedDataSource } from "@/core/datasets/data-source-discovery";
4
- import { DiscoverDataSources } from "@/core/datasets/request-registry";
3
+ import { useAtomValue } from "jotai";
4
+ import { useMemo } from "react";
5
+ import { dataConnectionsMapAtom } from "@/core/datasets/data-source-connections";
6
+ import {
7
+ dataSourceDiscoveryEpochAtom,
8
+ type DataSourceDiscoveryGroup,
9
+ type DetectedDataSource,
10
+ fetchDataSourceDiscovery,
11
+ isDetectedSourceConnected,
12
+ matchesDiscoveryGroup,
13
+ } from "@/core/datasets/data-source-discovery";
14
+ import { storageNamespacesAtom } from "@/core/storage/state";
5
15
  import { useAsyncData } from "./useAsyncData";
6
16
 
7
- export async function loadDataSourceDiscovery(): Promise<DetectedDataSource[]> {
8
- const result = await DiscoverDataSources.request({});
9
- return result.sources;
10
- }
11
-
12
17
  /**
13
18
  * Reusable UI-facing hook for kernel-managed datasource discovery.
14
- * Consumers decide how to render, filter, or act on the detected model.
15
19
  */
16
20
  export function useDataSourceDiscovery() {
17
- return useAsyncData(loadDataSourceDiscovery, []);
21
+ const epoch = useAtomValue(dataSourceDiscoveryEpochAtom);
22
+ return useAsyncData(fetchDataSourceDiscovery, [epoch]);
23
+ }
24
+
25
+ /**
26
+ * Detected sources from the kernel environment that are not already backed
27
+ * by a live connection, optionally narrowed to "database" or "storage".
28
+ */
29
+ export function useDetectedDataSources(
30
+ group?: DataSourceDiscoveryGroup,
31
+ ): DetectedDataSource[] {
32
+ const { data } = useDataSourceDiscovery();
33
+ const connections = useAtomValue(dataConnectionsMapAtom);
34
+ const storageNamespaces = useAtomValue(storageNamespacesAtom);
35
+
36
+ return useMemo(() => {
37
+ if (!data) {
38
+ return [];
39
+ }
40
+
41
+ const snapshot = {
42
+ dialects: [...connections.values()].map((connection) =>
43
+ connection.dialect.toLowerCase(),
44
+ ),
45
+ storageProtocols: storageNamespaces.map((namespace) =>
46
+ namespace.protocol.toLowerCase(),
47
+ ),
48
+ storageBackendTypes: storageNamespaces.map((namespace) =>
49
+ namespace.backendType.toLowerCase(),
50
+ ),
51
+ };
52
+
53
+ return data.filter(
54
+ (source) =>
55
+ matchesDiscoveryGroup(source, group) &&
56
+ !isDetectedSourceConnected(source, snapshot),
57
+ );
58
+ }, [connections, data, group, storageNamespaces]);
18
59
  }