@marimo-team/frontend 0.23.16-dev51 → 0.23.16-dev53

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 (101) hide show
  1. package/dist/assets/{CellStatus-DciB5rbr.js → CellStatus-BfPw99P2.js} +1 -1
  2. package/dist/assets/{JsonOutput-BM3vve9F.js → JsonOutput-E3o1Uk0e.js} +12 -12
  3. package/dist/assets/{MarimoErrorOutput-Bh0y53Du.js → MarimoErrorOutput-D9Dypu7Q.js} +4 -4
  4. package/dist/assets/{RenderHTML-B9jx7Dsm.js → RenderHTML-DRJxD5FQ.js} +1 -1
  5. package/dist/assets/{RunButton-DMtHbFuA.js → RunButton-y7Y9rjmF.js} +1 -1
  6. package/dist/assets/{add-cell-with-ai-BcRYriMM.js → add-cell-with-ai-ByfOWv0Z.js} +12 -12
  7. package/dist/assets/add-connection-dialog-DVSFAYXK.js +178 -0
  8. package/dist/assets/{agent-panel-B31W_VwU.js → agent-panel-B2OKzocQ.js} +4 -4
  9. package/dist/assets/{ai-model-dropdown-B3D05oQh.js → ai-model-dropdown-Dr2QbDlZ.js} +3 -3
  10. package/dist/assets/{app-config-button-D_mjovwU.js → app-config-button-DScmpqcV.js} +1 -1
  11. package/dist/assets/{blob-BJREJiQp.js → blob-BXcsY70H.js} +1 -1
  12. package/dist/assets/{cell-editor-BOhZHoLm.js → cell-editor-BuGvBHQI.js} +11 -11
  13. package/dist/assets/cell-link-DGv7Q4Mc.js +1 -0
  14. package/dist/assets/{cells-Jr-5Pc9f.js → cells-CMc2NsXm.js} +61 -61
  15. package/dist/assets/{chat-display-Djy1Jt6b.js → chat-display-CeQftKrl.js} +1 -1
  16. package/dist/assets/{chat-panel-B2nYSE_l.js → chat-panel-BP5hfvS7.js} +4 -4
  17. package/dist/assets/{chat-ui-DlG0En7w.js → chat-ui-DoA0lexh.js} +4 -4
  18. package/dist/assets/{column-preview-BUHlI5va.js → column-preview-BeR1SI6Z.js} +1 -1
  19. package/dist/assets/{command-palette-rEJmXCZM.js → command-palette-DZSYeVCO.js} +1 -1
  20. package/dist/assets/{common-CZc3fQI4.js → common-NWgkp5L1.js} +1 -1
  21. package/dist/assets/{components-GAYCj6Mx.js → components-Mbf0MEIC.js} +1 -1
  22. package/dist/assets/{datasource-AWkkAeiS.js → datasource-Wt8yUynT.js} +2 -2
  23. package/dist/assets/{dependency-graph-panel-wcqTPq6b.js → dependency-graph-panel-B7TXWKYs.js} +4 -4
  24. package/dist/assets/{documentation-panel-Eu8MIkoU.js → documentation-panel-D9MZUKaD.js} +1 -1
  25. package/dist/assets/{download-CQ7iZFJL.js → download-U_wgQ8mC.js} +2 -2
  26. package/dist/assets/{edit-page-Cw7nN6HC.js → edit-page-BBc9BGYT.js} +6 -6
  27. package/dist/assets/error-entries-Bs30Z6uG.js +8 -0
  28. package/dist/assets/{error-panel-BZzr0PA1.js → error-panel-KY679tDx.js} +1 -1
  29. package/dist/assets/{file-explorer-panel-C1W7g_Wp.js → file-explorer-panel-9y0hzNAa.js} +4 -4
  30. package/dist/assets/{file-icons-BAqZR08W.js → file-icons-D5l3kSYq.js} +1 -1
  31. package/dist/assets/{file-name-input-0MiZVw5r.js → file-name-input-D_xhjKFp.js} +1 -1
  32. package/dist/assets/{floating-outline-C6g75zrP.js → floating-outline-BdgJT2B9.js} +1 -1
  33. package/dist/assets/{focus-C0HXXg3V.js → focus-D64hL1tZ.js} +1 -1
  34. package/dist/assets/{form-DT44Rmhp.js → form-CoqwGbkO.js} +1 -1
  35. package/dist/assets/{form-C051zAev.js → form-b4jQBV3T.js} +1 -1
  36. package/dist/assets/{home-page-CJz9IHZ2.js → home-page-tVWY9YdF.js} +2 -2
  37. package/dist/assets/{html-to-image-CfjhKLA4.js → html-to-image-Dm7huiQj.js} +2 -2
  38. package/dist/assets/index-8Nlyq3mV.css +2 -0
  39. package/dist/assets/{index-CrEgp1mR.js → index-Dz8dLbnI.js} +6 -6
  40. package/dist/assets/{layout-D4bpsgVD.js → layout-DKUs0ozP.js} +5 -5
  41. package/dist/assets/{logs-panel-CsaY3O1m.js → logs-panel-BzS7VSh0.js} +1 -1
  42. package/dist/assets/{markdown-renderer-BgqeO4JR.js → markdown-renderer-uWTrQId1.js} +2 -2
  43. package/dist/assets/{name-cell-input-DcU8Rfrz.js → name-cell-input-BgxcWTfV.js} +1 -1
  44. package/dist/assets/{outline-panel-D-CHPZQM.js → outline-panel-BT3zzwfa.js} +1 -1
  45. package/dist/assets/{packages-panel-dq7QVZl1.js → packages-panel-BKfeGuve.js} +1 -1
  46. package/dist/assets/panels-4jSJxEYp.js +4 -0
  47. package/dist/assets/{process-output-NbwnUu05.js → process-output-DE8TTXPC.js} +1 -1
  48. package/dist/assets/{radio-group-rhQpGwU-.js → radio-group-Da80KtoT.js} +1 -1
  49. package/dist/assets/readonly-code-display-B8zhuSov.js +1 -0
  50. package/dist/assets/{readonly-python-code-Q_1Y3VyZ.js → readonly-python-code-Bmo6cMSq.js} +1 -1
  51. package/dist/assets/{reveal-component-CedpDwM8.js → reveal-component-Cv7WZ4r6.js} +2 -2
  52. package/dist/assets/{run-page-CGoux5cj.js → run-page-CA21zsME.js} +1 -1
  53. package/dist/assets/scratchpad-panel-Br7RFJkK.js +1 -0
  54. package/dist/assets/{session-panel-OIot3eyB.js → session-panel-BiUht4zM.js} +1 -1
  55. package/dist/assets/{skeleton-BRvjFlRu.js → skeleton-CY-B1asm.js} +1 -1
  56. package/dist/assets/{snippets-panel-YFyH2hUM.js → snippets-panel-CjiTlCpD.js} +1 -1
  57. package/dist/assets/{state-bHyw5u3R.js → state-BJ7IdtXW.js} +1 -1
  58. package/dist/assets/state-TgAqts79.js +3 -0
  59. package/dist/assets/{tracing-CDMM1BB2.js → tracing-DmQ7KQ3A.js} +1 -1
  60. package/dist/assets/{tracing-panel-BH5Zbvl9.js → tracing-panel-C1TOV9kj.js} +2 -2
  61. package/dist/assets/{tree-actions-DDveUIHK.js → tree-actions-enwITSy7.js} +1 -1
  62. package/dist/assets/useCellActionButton-CXvU9GmI.js +1 -0
  63. package/dist/assets/{useDeleteCell-BIopCngo.js → useDeleteCell-Bp3S6pL8.js} +1 -1
  64. package/dist/assets/{useDependencyPanelTab-snM337f9.js → useDependencyPanelTab-CewYv8cP.js} +1 -1
  65. package/dist/assets/{useNotebookActions-CsZfWFKy.js → useNotebookActions-l9NAqPKm.js} +4 -4
  66. package/dist/assets/useRunCells-B-KIwrVZ.js +1 -0
  67. package/dist/assets/{useSplitCell-BTmKY1CU.js → useSplitCell-B0jvSFsl.js} +1 -1
  68. package/dist/index.html +24 -24
  69. package/package.json +1 -1
  70. package/src/__mocks__/requests.ts +1 -0
  71. package/src/components/editor/connections/__tests__/quick-add-data-sources.test.tsx +113 -0
  72. package/src/components/editor/connections/add-connection-dialog.tsx +2 -0
  73. package/src/components/editor/connections/quick-add-data-sources.tsx +106 -0
  74. package/src/core/codemirror/language/__tests__/sql.test.ts +191 -7
  75. package/src/core/codemirror/language/languages/sql/sql.ts +41 -13
  76. package/src/core/datasets/data-source-discovery.ts +5 -0
  77. package/src/core/datasets/request-registry.ts +11 -0
  78. package/src/core/islands/bootstrap.ts +1 -0
  79. package/src/core/islands/bridge.ts +1 -0
  80. package/src/core/kernel/messages.ts +2 -0
  81. package/src/core/network/__tests__/requests-lazy.test.ts +15 -0
  82. package/src/core/network/__tests__/requests-network.test.ts +14 -0
  83. package/src/core/network/requests-lazy.ts +1 -0
  84. package/src/core/network/requests-network.ts +8 -0
  85. package/src/core/network/requests-static.ts +1 -0
  86. package/src/core/network/requests-toasting.tsx +1 -0
  87. package/src/core/network/types.ts +2 -0
  88. package/src/core/wasm/bridge.ts +10 -0
  89. package/src/core/websocket/useMarimoKernelConnection.tsx +4 -0
  90. package/src/hooks/__tests__/useDataSourceDiscovery.test.ts +80 -0
  91. package/src/hooks/useDataSourceDiscovery.ts +18 -0
  92. package/dist/assets/add-connection-dialog-syjRSNPT.js +0 -178
  93. package/dist/assets/cell-link-DqNEv3f9.js +0 -1
  94. package/dist/assets/error-entries-Cr8uVukN.js +0 -8
  95. package/dist/assets/index-D6Dp_PFj.css +0 -2
  96. package/dist/assets/panels-DWoniKhE.js +0 -4
  97. package/dist/assets/readonly-code-display-CVY2f7EZ.js +0 -1
  98. package/dist/assets/scratchpad-panel-BLuF25yR.js +0 -1
  99. package/dist/assets/state-DB_fxbhn.js +0 -3
  100. package/dist/assets/useCellActionButton-D5ix_g2a.js +0 -1
  101. package/dist/assets/useRunCells-CF_V8X2K.js +0 -1
@@ -66,7 +66,7 @@ import { getSQLMode, type SQLMode } from "./sql-mode";
66
66
  import { isKnownDialect } from "./utils";
67
67
 
68
68
  const DEFAULT_DIALECT = DuckDBDialect;
69
- const DEFAULT_PARSER_DIALECT = "DuckDB";
69
+ const DEFAULT_PARSER_DIALECT: ParserDialects = "DuckDB";
70
70
 
71
71
  // A compartment for the SQL config, so we can update the config of codemirror
72
72
  const sqlConfigCompartment = new Compartment();
@@ -372,6 +372,8 @@ class DialectAwareSqlStructureAnalyzer extends SqlStructureAnalyzer {
372
372
 
373
373
  class CustomSqlParser extends NodeSqlParser {
374
374
  private validationTimeout: number | null = null;
375
+ private pendingValidationResolve: ((errors: SqlParseError[]) => void) | null =
376
+ null;
375
377
  private readonly VALIDATION_DELAY_MS = 300; // Wait 300ms after user stops typing
376
378
  private isFocused = false; // Only validate if the editor is focused
377
379
 
@@ -379,37 +381,51 @@ class CustomSqlParser extends NodeSqlParser {
379
381
  this.isFocused = focused;
380
382
  }
381
383
 
384
+ private resolvePendingValidation(errors: SqlParseError[]): void {
385
+ this.pendingValidationResolve?.(errors);
386
+ this.pendingValidationResolve = null;
387
+ }
388
+
382
389
  private async validateWithDelay(
383
390
  sql: string,
384
- engine: string,
391
+ engine: ConnectionName,
385
392
  dialect: ParserDialects | null,
386
393
  ): Promise<SqlParseError[]> {
387
- // Clear any existing delay call
394
+ // Clear any existing delay call, resolving its promise so a superseded
395
+ // request doesn't hang forever.
388
396
  if (this.validationTimeout) {
389
397
  window.clearTimeout(this.validationTimeout);
398
+ this.resolvePendingValidation([]);
390
399
  }
391
400
 
392
401
  // Set up a new request to be called after the delay
393
402
  return new Promise((resolve) => {
403
+ this.pendingValidationResolve = resolve;
394
404
  this.validationTimeout = window.setTimeout(async () => {
405
+ this.validationTimeout = null;
406
+
395
407
  // Only validate if the editor is still focused
396
408
  if (!this.isFocused) {
397
- resolve([]);
409
+ this.resolvePendingValidation([]);
398
410
  return;
399
411
  }
400
412
 
401
413
  try {
402
- const sqlMode = getSQLMode();
414
+ // For validate mode, we run EXPLAIN queries on the engine, which can be
415
+ // expensive for remote databases. So, we only run for internal engines.
416
+ const sqlMode = INTERNAL_SQL_ENGINES.has(engine)
417
+ ? getSQLMode()
418
+ : "default";
403
419
  const result = await validateSQL(sql, engine, dialect, sqlMode);
404
420
  if (result.error) {
405
421
  Logger.error("Failed to validate SQL", { error: result.error });
406
- resolve([]);
422
+ this.resolvePendingValidation([]);
407
423
  return;
408
424
  }
409
- resolve(result.parse_result?.errors ?? []);
425
+ this.resolvePendingValidation(result.parse_result?.errors ?? []);
410
426
  } catch (error) {
411
427
  Logger.error("Failed to validate SQL", { error });
412
- resolve([]);
428
+ this.resolvePendingValidation([]);
413
429
  }
414
430
  }, this.VALIDATION_DELAY_MS);
415
431
  });
@@ -420,19 +436,23 @@ class CustomSqlParser extends NodeSqlParser {
420
436
  opts: { state: EditorState },
421
437
  ): Promise<SqlParseError[]> {
422
438
  const metadata = getSQLMetadata(opts.state);
439
+ const dialect = connectionNameToParserDialect(metadata.engine);
423
440
 
424
441
  // Only validate if the editor is focused
425
442
  if (!this.isFocused) {
426
443
  return [];
427
444
  }
428
445
 
429
- // Only perform custom validation for DuckDB
430
- if (!INTERNAL_SQL_ENGINES.has(metadata.engine)) {
446
+ // Only perform custom validation for DuckDB as we have a custom validation endpoint for it.
447
+ if (!isDuckDBConnection(metadata.engine, dialect)) {
431
448
  return super.validateSql(sql, opts);
432
449
  }
433
450
 
434
- const dialect = guessParserDialect(opts.state);
435
- return this.validateWithDelay(sql, metadata.engine, dialect);
451
+ return this.validateWithDelay(
452
+ sql,
453
+ metadata.engine,
454
+ dialect ?? DEFAULT_PARSER_DIALECT,
455
+ );
436
456
  }
437
457
 
438
458
  override async parse(
@@ -441,9 +461,10 @@ class CustomSqlParser extends NodeSqlParser {
441
461
  ): Promise<NodeSqlParserResult> {
442
462
  const metadata = getSQLMetadata(opts.state);
443
463
  const engine = metadata.engine;
464
+ const dialect = connectionNameToParserDialect(engine);
444
465
 
445
466
  // For now, always return success for DuckDB
446
- if (engine === DUCKDB_ENGINE) {
467
+ if (isDuckDBConnection(engine, dialect)) {
447
468
  return { success: true, errors: [] };
448
469
  }
449
470
 
@@ -451,6 +472,13 @@ class CustomSqlParser extends NodeSqlParser {
451
472
  }
452
473
  }
453
474
 
475
+ function isDuckDBConnection(
476
+ engine: ConnectionName,
477
+ dialect: ParserDialects | null,
478
+ ): boolean {
479
+ return engine === DUCKDB_ENGINE || dialect === "DuckDB";
480
+ }
481
+
454
482
  /**
455
483
  * Update the SQL dialect in the editor view.
456
484
  */
@@ -0,0 +1,5 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { components } from "@marimo-team/marimo-api";
4
+
5
+ export type DetectedDataSource = components["schemas"]["DetectedDataSource"];
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import type {
3
+ DataSourceDiscoveryResult,
3
4
  SQLSchemaListPreview,
4
5
  SQLTableListPreview,
5
6
  SQLTablePreview,
@@ -19,6 +20,16 @@ import type {
19
20
  // The backend returns data tables, which could also exist in other engines, dbs, schemas
20
21
  // Thus, we use the request ID pattern to match the response to the request
21
22
 
23
+ export const DiscoverDataSources = new DeferredRequestRegistry<
24
+ {},
25
+ DataSourceDiscoveryResult
26
+ >("data-source-discovery-result", async (requestId) => {
27
+ const client = getRequestClient();
28
+ await client.discoverDataSources({
29
+ requestId,
30
+ });
31
+ });
32
+
22
33
  export const PreviewSQLTable = new DeferredRequestRegistry<
23
34
  Omit<PreviewSQLTableRequest, "requestId">,
24
35
  SQLTablePreview
@@ -194,6 +194,7 @@ function handleMessage(
194
194
  case "sql-schema-list-preview":
195
195
  case "datasets":
196
196
  case "data-source-connections":
197
+ case "data-source-discovery-result":
197
198
  case "validate-sql-result":
198
199
  case "storage-namespaces":
199
200
  case "storage-entries":
@@ -341,6 +341,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
341
341
  previewSQLTableList = throwNotImplemented;
342
342
  previewSQLSchemaList = throwNotImplemented;
343
343
  previewDataSourceConnection = throwNotImplemented;
344
+ discoverDataSources = throwNotImplemented;
344
345
  validateSQL = throwNotImplemented;
345
346
  openFile = throwNotImplemented;
346
347
  sendListFiles = throwNotImplemented;
@@ -44,6 +44,8 @@ export type SQLTableListPreview =
44
44
  export type SQLSchemaListPreview =
45
45
  NotificationMessageData<"sql-schema-list-preview">;
46
46
  export type ValidateSQLResult = NotificationMessageData<"validate-sql-result">;
47
+ export type DataSourceDiscoveryResult =
48
+ NotificationMessageData<"data-source-discovery-result">;
47
49
  export type SecretKeysResult = NotificationMessageData<"secret-keys-result">;
48
50
  export type StartupLogs = NotificationMessageData<"startup-logs">;
49
51
  export type CellMessage = NotificationMessageData<"cell-op">;
@@ -114,6 +114,21 @@ describe("createLazyRequests", () => {
114
114
  expect(mockInit).toHaveBeenCalledTimes(1);
115
115
  });
116
116
 
117
+ it("starts the kernel for datasource discovery", async () => {
118
+ mockDelegate.discoverDataSources = vi.fn().mockResolvedValue(null);
119
+ const lazyRequests = createLazyRequests(
120
+ mockDelegate,
121
+ mockGetRuntimeManager,
122
+ );
123
+
124
+ await lazyRequests.discoverDataSources({
125
+ requestId: requestId("discovery"),
126
+ });
127
+
128
+ expect(mockInit).toHaveBeenCalledOnce();
129
+ expect(mockDelegate.discoverDataSources).toHaveBeenCalledOnce();
130
+ });
131
+
117
132
  it("should only call init once across multiple requests", async () => {
118
133
  const lazyRequests = createLazyRequests(
119
134
  mockDelegate,
@@ -130,6 +130,20 @@ describe("createNetworkRequests", () => {
130
130
  expect(mockClient.GET).toHaveBeenCalledWith("/api/export/availability");
131
131
  });
132
132
 
133
+ it("discoverDataSources should POST to the discovery endpoint", async () => {
134
+ const requests = createNetworkRequests();
135
+ const request = { requestId: "discovery-request" } as any;
136
+ await requests.discoverDataSources(request);
137
+
138
+ expect(mockClient.POST).toHaveBeenCalledWith(
139
+ "/api/datasources/discover",
140
+ expect.objectContaining({
141
+ body: request,
142
+ params: expect.anything(),
143
+ }),
144
+ );
145
+ });
146
+
133
147
  it("getPackageList should not require a kernel connection", async () => {
134
148
  const { waitForConnectionOpen, waitForConnectionOpenIfNotebook } =
135
149
  await import("../connection");
@@ -51,6 +51,7 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
51
51
  sendRunScratchpad: "startConnection",
52
52
  saveAppConfig: "startConnection",
53
53
  saveCellConfig: "startConnection",
54
+ discoverDataSources: "startConnection",
54
55
 
55
56
  // Export operations start a connection
56
57
  exportAsHTML: "startConnection",
@@ -267,6 +267,14 @@ export function createNetworkRequests(): EditRequests & RunRequests {
267
267
  })
268
268
  .then(handleResponseReturnNull);
269
269
  },
270
+ discoverDataSources: (request) => {
271
+ return getClient()
272
+ .POST("/api/datasources/discover", {
273
+ body: request,
274
+ params: getParams(),
275
+ })
276
+ .then(handleResponseReturnNull);
277
+ },
270
278
  validateSQL: (request) => {
271
279
  return getClient()
272
280
  .POST("/api/sql/validate", {
@@ -62,6 +62,7 @@ export function createStaticRequests(): EditRequests & RunRequests {
62
62
  previewSQLTableList: throwNotInEditMode,
63
63
  previewSQLSchemaList: throwNotInEditMode,
64
64
  previewDataSourceConnection: throwNotInEditMode,
65
+ discoverDataSources: throwNotInEditMode,
65
66
  validateSQL: throwNotInEditMode,
66
67
  openFile: throwNotInEditMode,
67
68
  getUsageStats: throwNotInEditMode,
@@ -43,6 +43,7 @@ export function createErrorToastingRequests(
43
43
  previewSQLTableList: "Failed to fetch SQL table list",
44
44
  previewSQLSchemaList: "Failed to fetch SQL schema list",
45
45
  previewDataSourceConnection: "Failed to preview data source connection",
46
+ discoverDataSources: "Failed to discover data sources",
46
47
  validateSQL: "Failed to validate SQL",
47
48
  openFile: "Failed to open file",
48
49
  getUsageStats: "", // No toast
@@ -75,6 +75,7 @@ export type ListSQLTablesRequest = schemas["ListSQLTablesRequest"];
75
75
  export type ListSQLSchemasRequest = schemas["ListSQLSchemasRequest"];
76
76
  export type ListDataSourceConnectionRequest =
77
77
  schemas["ListDataSourceConnectionRequest"];
78
+ export type DiscoverDataSourcesRequest = schemas["DiscoverDataSourcesRequest"];
78
79
  export type ValidateSQLRequest = schemas["ValidateSQLRequest"];
79
80
  export type DebugCellRequest = schemas["DebugCellRequest"];
80
81
  export type SetBreakpointsRequest = schemas["SetBreakpointsRequest"];
@@ -181,6 +182,7 @@ export interface EditRequests {
181
182
  previewDataSourceConnection: (
182
183
  request: ListDataSourceConnectionRequest,
183
184
  ) => Promise<null>;
185
+ discoverDataSources: (request: DiscoverDataSourcesRequest) => Promise<null>;
184
186
  validateSQL: (request: ValidateSQLRequest) => Promise<null>;
185
187
  openFile: (request: { path: string; lineNumber?: number }) => Promise<null>;
186
188
  getUsageStats: () => Promise<UsageResponse>;
@@ -630,6 +630,16 @@ export class PyodideBridge implements RunRequests, EditRequests {
630
630
  return null;
631
631
  };
632
632
 
633
+ discoverDataSources: EditRequests["discoverDataSources"] = async (
634
+ request,
635
+ ) => {
636
+ await this.putControlRequest({
637
+ type: "discover-data-sources",
638
+ ...request,
639
+ });
640
+ return null;
641
+ };
642
+
633
643
  getUsageStats = throwNotImplemented;
634
644
  getEnvironmentInfo: EditRequests["getEnvironmentInfo"] = async () => {
635
645
  const response = await this.rpc.proxy.request.bridge({
@@ -42,6 +42,7 @@ import { connectionTransportTypeAtom, useSetAppConfig } from "../config/config";
42
42
  import { useDataSourceActions } from "../datasets/data-source-connections";
43
43
  import type { ConnectionName } from "../datasets/engines";
44
44
  import {
45
+ DiscoverDataSources,
45
46
  PreviewSQLSchemaList,
46
47
  PreviewSQLTable,
47
48
  PreviewSQLTableList,
@@ -394,6 +395,9 @@ export function useMarimoKernelConnection(opts: {
394
395
  case "validate-sql-result":
395
396
  ValidateSQL.resolve(msg.data.request_id as RequestId, msg.data);
396
397
  return;
398
+ case "data-source-discovery-result":
399
+ DiscoverDataSources.resolve(msg.data.request_id as RequestId, msg.data);
400
+ return;
397
401
  case "secret-keys-result":
398
402
  SECRETS_REGISTRY.resolve(msg.data.request_id, msg.data);
399
403
  return;
@@ -0,0 +1,80 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import { DiscoverDataSources } from "@/core/datasets/request-registry";
5
+ import { loadDataSourceDiscovery } from "../useDataSourceDiscovery";
6
+
7
+ describe("loadDataSourceDiscovery", () => {
8
+ afterEach(() => {
9
+ vi.restoreAllMocks();
10
+ });
11
+
12
+ it("returns the kernel's secret-free discovery model", async () => {
13
+ const sources = [
14
+ {
15
+ id: "pyiceberg-prod",
16
+ integration: "pyiceberg",
17
+ category: "catalog" as const,
18
+ displayName: "PyIceberg (prod)",
19
+ confidence: "high" as const,
20
+ origins: [
21
+ {
22
+ type: "configuration" as const,
23
+ label: "Resolved PyIceberg configuration",
24
+ },
25
+ ],
26
+ configuration: [
27
+ {
28
+ field: "Catalog",
29
+ value: {
30
+ kind: "safe-literal" as const,
31
+ value: "prod",
32
+ },
33
+ },
34
+ ],
35
+ code: 'catalog = load_catalog("prod")',
36
+ },
37
+ ];
38
+ const request = vi.spyOn(DiscoverDataSources, "request").mockResolvedValue({
39
+ request_id: "request-id",
40
+ sources,
41
+ });
42
+
43
+ const detected = await loadDataSourceDiscovery();
44
+
45
+ expect({ detected, requests: request.mock.calls }).toMatchInlineSnapshot(`
46
+ {
47
+ "detected": [
48
+ {
49
+ "category": "catalog",
50
+ "code": "catalog = load_catalog("prod")",
51
+ "confidence": "high",
52
+ "configuration": [
53
+ {
54
+ "field": "Catalog",
55
+ "value": {
56
+ "kind": "safe-literal",
57
+ "value": "prod",
58
+ },
59
+ },
60
+ ],
61
+ "displayName": "PyIceberg (prod)",
62
+ "id": "pyiceberg-prod",
63
+ "integration": "pyiceberg",
64
+ "origins": [
65
+ {
66
+ "label": "Resolved PyIceberg configuration",
67
+ "type": "configuration",
68
+ },
69
+ ],
70
+ },
71
+ ],
72
+ "requests": [
73
+ [
74
+ {},
75
+ ],
76
+ ],
77
+ }
78
+ `);
79
+ });
80
+ });
@@ -0,0 +1,18 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { DetectedDataSource } from "@/core/datasets/data-source-discovery";
4
+ import { DiscoverDataSources } from "@/core/datasets/request-registry";
5
+ import { useAsyncData } from "./useAsyncData";
6
+
7
+ export async function loadDataSourceDiscovery(): Promise<DetectedDataSource[]> {
8
+ const result = await DiscoverDataSources.request({});
9
+ return result.sources;
10
+ }
11
+
12
+ /**
13
+ * Reusable UI-facing hook for kernel-managed datasource discovery.
14
+ * Consumers decide how to render, filter, or act on the detected model.
15
+ */
16
+ export function useDataSourceDiscovery() {
17
+ return useAsyncData(loadDataSourceDiscovery, []);
18
+ }