@marimo-team/islands 0.23.16-dev51 → 0.23.16-dev52

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 (28) hide show
  1. package/dist/{chat-ui-1e7PcW56.js → chat-ui-CVU_VO50.js} +2 -2
  2. package/dist/{common-CD6phlRu.js → common-CvsviStD.js} +2 -2
  3. package/dist/{html-to-image-3w0AakWW.js → html-to-image-lsMuxJbp.js} +5 -1
  4. package/dist/main.js +7 -5
  5. package/dist/{process-output-BRwR4QGs.js → process-output-DiSWOzWN.js} +1 -1
  6. package/dist/{reveal-component-DmGGKS8z.js → reveal-component-D6N7pZkc.js} +2 -2
  7. package/dist/style.css +1 -1
  8. package/package.json +1 -1
  9. package/src/__mocks__/requests.ts +1 -0
  10. package/src/components/editor/connections/__tests__/quick-add-data-sources.test.tsx +113 -0
  11. package/src/components/editor/connections/add-connection-dialog.tsx +2 -0
  12. package/src/components/editor/connections/quick-add-data-sources.tsx +106 -0
  13. package/src/core/datasets/data-source-discovery.ts +5 -0
  14. package/src/core/datasets/request-registry.ts +11 -0
  15. package/src/core/islands/bootstrap.ts +1 -0
  16. package/src/core/islands/bridge.ts +1 -0
  17. package/src/core/kernel/messages.ts +2 -0
  18. package/src/core/network/__tests__/requests-lazy.test.ts +15 -0
  19. package/src/core/network/__tests__/requests-network.test.ts +14 -0
  20. package/src/core/network/requests-lazy.ts +1 -0
  21. package/src/core/network/requests-network.ts +8 -0
  22. package/src/core/network/requests-static.ts +1 -0
  23. package/src/core/network/requests-toasting.tsx +1 -0
  24. package/src/core/network/types.ts +2 -0
  25. package/src/core/wasm/bridge.ts +10 -0
  26. package/src/core/websocket/useMarimoKernelConnection.tsx +4 -0
  27. package/src/hooks/__tests__/useDataSourceDiscovery.test.ts +80 -0
  28. package/src/hooks/useDataSourceDiscovery.ts +18 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.23.16-dev51",
3
+ "version": "0.23.16-dev52",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -41,6 +41,7 @@ export const MockRequestClient = {
41
41
  previewSQLTableList: vi.fn().mockResolvedValue({ tables: [] }),
42
42
  previewSQLSchemaList: vi.fn().mockResolvedValue({ schemas: [] }),
43
43
  previewDataSourceConnection: vi.fn().mockResolvedValue({}),
44
+ discoverDataSources: vi.fn().mockResolvedValue({}),
44
45
  validateSQL: vi.fn().mockResolvedValue({}),
45
46
  openFile: vi.fn().mockResolvedValue({}),
46
47
  getUsageStats: vi.fn().mockResolvedValue({}),
@@ -0,0 +1,113 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import type { DetectedDataSource } from "@/core/datasets/data-source-discovery";
6
+ import { QuickAddDataSources } from "../quick-add-data-sources";
7
+
8
+ const sources: DetectedDataSource[] = [
9
+ {
10
+ id: "postgres-libpq-environment",
11
+ integration: "postgres",
12
+ category: "database",
13
+ displayName: "PostgreSQL",
14
+ confidence: "high",
15
+ origins: [{ type: "environment", label: "Kernel environment" }],
16
+ configuration: [
17
+ {
18
+ field: "Host",
19
+ value: { kind: "environment-variable", name: "PGHOST" },
20
+ },
21
+ {
22
+ field: "Username",
23
+ value: { kind: "environment-variable", name: "PGUSER" },
24
+ },
25
+ {
26
+ field: "Database",
27
+ value: { kind: "environment-variable", name: "PGDATABASE" },
28
+ },
29
+ ],
30
+ code: "engine = create_engine()",
31
+ },
32
+ {
33
+ id: "pyiceberg-prod",
34
+ integration: "pyiceberg",
35
+ category: "catalog",
36
+ displayName: "PyIceberg (prod)",
37
+ confidence: "high",
38
+ origins: [
39
+ {
40
+ type: "configuration",
41
+ label: "Resolved PyIceberg configuration",
42
+ },
43
+ ],
44
+ configuration: [
45
+ {
46
+ field: "Catalog",
47
+ value: { kind: "safe-literal", value: "prod" },
48
+ },
49
+ {
50
+ field: "Type",
51
+ value: { kind: "safe-literal", value: "REST" },
52
+ },
53
+ ],
54
+ code: 'catalog = load_catalog("prod")',
55
+ },
56
+ ];
57
+
58
+ describe("QuickAddDataSources", () => {
59
+ it("does not render an empty section", () => {
60
+ const { container } = render(
61
+ <QuickAddDataSources sources={[]} onAdd={vi.fn()} />,
62
+ );
63
+
64
+ expect(container).toBeEmptyDOMElement();
65
+ });
66
+
67
+ it("renders detected sources as clickable tags", () => {
68
+ const onAdd = vi.fn();
69
+
70
+ render(<QuickAddDataSources sources={sources} onAdd={onAdd} />);
71
+ fireEvent.click(
72
+ screen.getByRole("button", {
73
+ name: "Add PostgreSQL connection",
74
+ }),
75
+ );
76
+
77
+ expect(screen.getByText("Quick add")).toBeInTheDocument();
78
+ expect(onAdd).toHaveBeenCalledWith(sources[0]);
79
+ });
80
+
81
+ it("shows environment references on hover", async () => {
82
+ render(<QuickAddDataSources sources={sources} onAdd={vi.fn()} />);
83
+ const tag = screen.getByRole("button", {
84
+ name: "Add PostgreSQL connection",
85
+ });
86
+
87
+ fireEvent.pointerMove(tag);
88
+ fireEvent.mouseOver(tag);
89
+
90
+ await waitFor(() => {
91
+ expect(screen.getAllByText('os.environ["PGHOST"]')).not.toHaveLength(0);
92
+ });
93
+ expect(screen.getAllByText('os.environ["PGUSER"]')).not.toHaveLength(0);
94
+ expect(screen.getAllByText('os.environ["PGDATABASE"]')).not.toHaveLength(0);
95
+ });
96
+
97
+ it("shows safe configuration metadata on hover", async () => {
98
+ render(<QuickAddDataSources sources={sources} onAdd={vi.fn()} />);
99
+ const tag = screen.getByRole("button", {
100
+ name: "Add PyIceberg (prod) connection",
101
+ });
102
+
103
+ fireEvent.pointerMove(tag);
104
+ fireEvent.mouseOver(tag);
105
+
106
+ await waitFor(() => {
107
+ expect(
108
+ screen.getAllByText("Detected from Resolved PyIceberg configuration"),
109
+ ).not.toHaveLength(0);
110
+ });
111
+ expect(screen.getAllByText("REST")).not.toHaveLength(0);
112
+ });
113
+ });
@@ -12,6 +12,7 @@ import {
12
12
  import { ExternalLink } from "@/components/ui/links";
13
13
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
14
14
  import { AddDatabaseForm } from "./database/add-database-form";
15
+ import { AutoDiscoveredDataSources } from "./quick-add-data-sources";
15
16
  import { AddStorageForm } from "./storage/add-storage-form";
16
17
 
17
18
  type ConnectionTab = "databases" | "storage";
@@ -86,6 +87,7 @@ export const AddConnectionDialogContent: React.FC<{
86
87
  <span className="block">{codeSnippetHint}</span>
87
88
  </DialogDescription>
88
89
  </DialogHeader>
90
+ <AutoDiscoveredDataSources onSubmit={onClose} />
89
91
  <Tabs
90
92
  value={activeTab}
91
93
  onValueChange={(v) => setActiveTab(v as ConnectionTab)}
@@ -0,0 +1,106 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { PlusIcon, SparklesIcon } from "lucide-react";
4
+ import { Tooltip, TooltipProvider } from "@/components/ui/tooltip";
5
+ import type { DetectedDataSource } from "@/core/datasets/data-source-discovery";
6
+ import { useDataSourceDiscovery } from "@/hooks/useDataSourceDiscovery";
7
+ import { useInsertCode } from "./components";
8
+
9
+ export const QuickAddDataSources: React.FC<{
10
+ sources: DetectedDataSource[];
11
+ onAdd: (source: DetectedDataSource) => void;
12
+ }> = ({ sources, onAdd }) => {
13
+ if (sources.length === 0) {
14
+ return null;
15
+ }
16
+
17
+ return (
18
+ <section
19
+ aria-labelledby="quick-add-data-sources-title"
20
+ className="rounded-md border bg-muted/30 px-3 py-2"
21
+ >
22
+ <div className="flex flex-wrap items-center gap-2">
23
+ <div className="mr-1 flex items-center gap-1.5">
24
+ <SparklesIcon className="h-3.5 w-3.5 text-muted-foreground" />
25
+ <h3 id="quick-add-data-sources-title" className="text-sm font-medium">
26
+ Quick add
27
+ </h3>
28
+ </div>
29
+ <TooltipProvider delayDuration={200}>
30
+ {sources.map((source) => (
31
+ <Tooltip
32
+ key={source.id}
33
+ side="bottom"
34
+ content={<DetectedDataSourceDetails source={source} />}
35
+ >
36
+ <button
37
+ type="button"
38
+ aria-label={`Add ${source.displayName} connection`}
39
+ className="inline-flex items-center gap-1 rounded-full border border-(--blue-8) bg-(--blue-2) px-2.5 py-1 text-xs font-semibold text-(--blue-11) transition-colors hover:bg-(--blue-3) focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2"
40
+ onClick={() => onAdd(source)}
41
+ >
42
+ <PlusIcon className="h-3 w-3" />
43
+ {source.displayName}
44
+ </button>
45
+ </Tooltip>
46
+ ))}
47
+ </TooltipProvider>
48
+ </div>
49
+ </section>
50
+ );
51
+ };
52
+
53
+ const DetectedDataSourceDetails: React.FC<{
54
+ source: DetectedDataSource;
55
+ }> = ({ source }) => (
56
+ <div className="min-w-64 space-y-2 py-1">
57
+ <div>
58
+ <div className="font-medium">{source.displayName}</div>
59
+ <div className="text-xs text-muted-foreground">
60
+ Detected from{" "}
61
+ {source.origins.map((origin) => origin.label).join(" and ")}
62
+ </div>
63
+ </div>
64
+ <dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
65
+ {source.configuration.map((item) => (
66
+ <div
67
+ className="contents"
68
+ key={
69
+ item.value.kind === "environment-variable"
70
+ ? item.value.name
71
+ : `${item.field}:${item.value.value}`
72
+ }
73
+ >
74
+ <dt className="text-muted-foreground">{item.field}</dt>
75
+ <dd>
76
+ <code>
77
+ {item.value.kind === "environment-variable"
78
+ ? `os.environ["${item.value.name}"]`
79
+ : item.value.value}
80
+ </code>
81
+ </dd>
82
+ </div>
83
+ ))}
84
+ </dl>
85
+ <div className="text-xs text-muted-foreground">
86
+ Click to add a configured cell.
87
+ </div>
88
+ </div>
89
+ );
90
+
91
+ export const AutoDiscoveredDataSources: React.FC<{
92
+ onSubmit: () => void;
93
+ }> = ({ onSubmit }) => {
94
+ const insertCode = useInsertCode();
95
+ const { data } = useDataSourceDiscovery();
96
+
97
+ return (
98
+ <QuickAddDataSources
99
+ sources={data ?? []}
100
+ onAdd={(source) => {
101
+ insertCode(source.code);
102
+ onSubmit();
103
+ }}
104
+ />
105
+ );
106
+ };
@@ -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
+ }