@marimo-team/islands 0.23.11-dev2 → 0.23.11-dev21

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 (42) hide show
  1. package/dist/{ConnectedDataExplorerComponent-Z3RP7Vmm.js → ConnectedDataExplorerComponent-BnPKA-xR.js} +4 -4
  2. package/dist/assets/__vite-browser-external-DAm_YW43.js +1 -0
  3. package/dist/assets/{worker-CC0Oul9k.js → worker-DEDLIQQV.js} +2 -2
  4. package/dist/{chat-ui-c1FdlK-C.js → chat-ui-Bf2NICxi.js} +6 -6
  5. package/dist/{code-visibility-CdW8VWzm.js → code-visibility-DvrnpkbY.js} +8 -8
  6. package/dist/{formats-C4wO47tk.js → formats-BOYEeoL_.js} +1 -1
  7. package/dist/{glide-data-editor-BPkCPs7L.js → glide-data-editor-A_l7_PQZ.js} +2 -2
  8. package/dist/{html-to-image-D5-EpALB.js → html-to-image-BJbLjZyG.js} +6 -6
  9. package/dist/{input-OdWHkobi.js → input-Cn-SZdXY.js} +1 -1
  10. package/dist/main.js +18 -18
  11. package/dist/{mermaid-BotvIKg9.js → mermaid-Br973nIL.js} +2 -2
  12. package/dist/{process-output-WDZE0cyS.js → process-output-Dw_ngzH2.js} +1 -1
  13. package/dist/{reveal-component-cfStduPb.js → reveal-component-BsmfWYqg.js} +5 -5
  14. package/dist/{spec-X7FwLJni.js → spec-f8-xgaui.js} +1 -1
  15. package/dist/style.css +1 -1
  16. package/dist/{toDate-d8RCRrRd.js → toDate-B4-pUFYh.js} +1 -1
  17. package/dist/{useAsyncData-PonK__yh.js → useAsyncData-B3PfqrwD.js} +1 -1
  18. package/dist/{useDeepCompareMemoize-D3NGWke6.js → useDeepCompareMemoize-YInAvJP3.js} +1 -1
  19. package/dist/{useLifecycle-00mO3OSS.js → useLifecycle-B3NOEiY5.js} +1 -1
  20. package/dist/{useTheme-DEhDzATN.js → useTheme-BPeFKs1Q.js} +2 -1
  21. package/dist/{vega-component-DGPUhbDs.js → vega-component-CMicMpWX.js} +5 -5
  22. package/package.json +1 -1
  23. package/src/components/ai/ai-model-dropdown.tsx +6 -0
  24. package/src/components/app-config/ai-config.tsx +73 -55
  25. package/src/components/app-config/data-form.tsx +33 -43
  26. package/src/components/app-config/is-overridden.tsx +125 -58
  27. package/src/components/app-config/user-config-form.tsx +178 -227
  28. package/src/components/chat/chat-panel.tsx +17 -8
  29. package/src/components/datasources/__tests__/column-preview.test.tsx +97 -0
  30. package/src/components/datasources/__tests__/utils.test.ts +127 -2
  31. package/src/components/datasources/column-preview.tsx +2 -4
  32. package/src/components/datasources/datasources.tsx +44 -17
  33. package/src/components/datasources/utils.ts +45 -0
  34. package/src/components/editor/actions/useNotebookActions.tsx +35 -17
  35. package/src/components/editor/connections/components.tsx +13 -0
  36. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +4 -4
  37. package/src/components/editor/connections/storage/as-code.ts +11 -4
  38. package/src/core/cells/__tests__/cells.test.ts +33 -0
  39. package/src/core/cells/cells.ts +1 -1
  40. package/src/core/config/config-schema.ts +6 -1
  41. package/src/css/md.css +12 -0
  42. package/dist/assets/__vite-browser-external-eshhtsgZ.js +0 -1
@@ -23,6 +23,7 @@ import {
23
23
  PlusIcon,
24
24
  SparklesIcon,
25
25
  SettingsIcon,
26
+ CodeIcon,
26
27
  } from "lucide-react";
27
28
  import { memo, useEffect, useRef, useState } from "react";
28
29
  import useEvent from "react-use-event-hook";
@@ -268,24 +269,32 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
268
269
  {
269
270
  value: "ask",
270
271
  label: "Ask",
271
- subtitle:
272
- "Use AI with access to read-only tools like documentation search",
272
+ subtitle: "AI with access to read-only tools like documentation search",
273
273
  Icon: NotebookText,
274
274
  },
275
275
  {
276
276
  value: "agent",
277
- label: "Agent (beta)",
278
- subtitle: "Use AI with access to read and write tools",
277
+ label: "Agent",
278
+ subtitle: "AI with access to read and write tools",
279
279
  Icon: HatGlasses,
280
280
  },
281
281
  ];
282
282
 
283
+ if (import.meta.env.DEV) {
284
+ modeOptions.push({
285
+ value: "code_mode",
286
+ label: "Code Mode (experimental)",
287
+ subtitle: "AI with access to the notebook's kernel. Use with caution.",
288
+ Icon: CodeIcon,
289
+ });
290
+ }
291
+
283
292
  const isAttachmentSupported =
284
293
  PROVIDERS_THAT_SUPPORT_ATTACHMENTS.has(currentProvider);
285
294
 
286
- const CurrentModeIcon = modeOptions.find(
287
- (o) => o.value === currentMode,
288
- )?.Icon;
295
+ const currentModeOption = modeOptions.find((o) => o.value === currentMode);
296
+ const CurrentModeIcon = currentModeOption?.Icon;
297
+ const CurrentModeLabel = currentModeOption?.label;
289
298
 
290
299
  return (
291
300
  <TooltipProvider>
@@ -294,7 +303,7 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
294
303
  <Select value={currentMode} onValueChange={saveModeChange}>
295
304
  <SelectTrigger className="h-6 text-xs border-border shadow-none! ring-0! bg-muted hover:bg-muted/30 py-0 px-2 gap-1.5">
296
305
  {CurrentModeIcon && <CurrentModeIcon className="h-3 w-3" />}
297
- <span className="capitalize">{currentMode}</span>
306
+ <span>{CurrentModeLabel}</span>
298
307
  </SelectTrigger>
299
308
  <SelectContent>
300
309
  <SelectGroup>
@@ -0,0 +1,97 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { render } from "@testing-library/react";
4
+ import { Provider } from "jotai";
5
+ import type React from "react";
6
+ import { beforeEach, describe, expect, it, vi } from "vitest";
7
+ import { MockRequestClient } from "@/__mocks__/requests";
8
+ import type { SQLTableContext } from "@/core/datasets/data-source-connections";
9
+ import type { DataTable, DataTableColumn } from "@/core/kernel/messages";
10
+ import { requestClientAtom } from "@/core/network/requests";
11
+ import { store } from "@/core/state/jotai";
12
+ import { DatasetColumnPreview } from "../column-preview";
13
+
14
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
15
+ <Provider store={store}>{children}</Provider>
16
+ );
17
+
18
+ const table: DataTable = {
19
+ name: "users",
20
+ columns: [],
21
+ source: "my_engine",
22
+ // Not "connection"/"catalog", so a preview is requested on mount.
23
+ source_type: "duckdb",
24
+ type: "table",
25
+ engine: "my_engine" as DataTable["engine"],
26
+ indexes: null,
27
+ num_columns: null,
28
+ num_rows: null,
29
+ variable_name: null,
30
+ primary_keys: null,
31
+ };
32
+
33
+ const column = { name: "email" } as DataTableColumn;
34
+
35
+ function renderPreview(sqlTableContext?: SQLTableContext) {
36
+ const client = MockRequestClient.create();
37
+ store.set(requestClientAtom, client);
38
+ render(
39
+ <DatasetColumnPreview
40
+ table={table}
41
+ column={column}
42
+ preview={undefined}
43
+ onAddColumnChart={vi.fn()}
44
+ sqlTableContext={sqlTableContext}
45
+ />,
46
+ { wrapper },
47
+ );
48
+ return client;
49
+ }
50
+
51
+ const ctx = (
52
+ overrides: Partial<SQLTableContext> & { database: string; schema: string },
53
+ ): SQLTableContext => ({
54
+ engine: "my_engine",
55
+ dialect: "duckdb",
56
+ ...overrides,
57
+ });
58
+
59
+ describe("DatasetColumnPreview fullyQualifiedTableName", () => {
60
+ beforeEach(() => {
61
+ vi.clearAllMocks();
62
+ });
63
+
64
+ it("uses just the table name without a SQL context", () => {
65
+ const client = renderPreview(undefined);
66
+ expect(client.previewDatasetColumn).toHaveBeenCalledWith(
67
+ expect.objectContaining({ fullyQualifiedTableName: "users" }),
68
+ );
69
+ });
70
+
71
+ it("qualifies with database and schema for flat engines", () => {
72
+ const client = renderPreview(ctx({ database: "db", schema: "public" }));
73
+ expect(client.previewDatasetColumn).toHaveBeenCalledWith(
74
+ expect.objectContaining({ fullyQualifiedTableName: "db.public.users" }),
75
+ );
76
+ });
77
+
78
+ it("does not emit a double dot for schemaless tables", () => {
79
+ // Regression: previously produced "db..users".
80
+ const client = renderPreview(ctx({ database: "db", schema: "" }));
81
+ expect(client.previewDatasetColumn).toHaveBeenCalledWith(
82
+ expect.objectContaining({ fullyQualifiedTableName: "db.users" }),
83
+ );
84
+ });
85
+
86
+ it("includes the full schema path for nested namespaces", () => {
87
+ // Regression: previously dropped schemaPath and emitted "top.deep.users".
88
+ const client = renderPreview(
89
+ ctx({ database: "top", schema: "deep", schemaPath: ["nested", "deep"] }),
90
+ );
91
+ expect(client.previewDatasetColumn).toHaveBeenCalledWith(
92
+ expect.objectContaining({
93
+ fullyQualifiedTableName: "top.nested.deep.users",
94
+ }),
95
+ );
96
+ });
97
+ });
@@ -3,8 +3,18 @@
3
3
  import { describe, expect, it } from "vitest";
4
4
  import type { SQLTableContext } from "@/core/datasets/data-source-connections";
5
5
  import { DUCKDB_ENGINE } from "@/core/datasets/engines";
6
- import type { DataTable, DataTableColumn } from "@/core/kernel/messages";
7
- import { sqlCode, tableUniqueId } from "../utils";
6
+ import type {
7
+ Database,
8
+ DatabaseSchema,
9
+ DataTable,
10
+ DataTableColumn,
11
+ } from "@/core/kernel/messages";
12
+ import {
13
+ schemaSubtreeMatchesSearch,
14
+ shouldExpandDatabaseForSearch,
15
+ sqlCode,
16
+ tableUniqueId,
17
+ } from "../utils";
8
18
 
9
19
  describe("sqlCode", () => {
10
20
  const mockTable: DataTable = {
@@ -519,3 +529,118 @@ describe("tableUniqueId", () => {
519
529
  );
520
530
  });
521
531
  });
532
+
533
+ function makeTable(name: string): DataTable {
534
+ return {
535
+ name,
536
+ columns: [],
537
+ source: "memory",
538
+ source_type: "local",
539
+ type: "table",
540
+ engine: null,
541
+ indexes: null,
542
+ num_columns: null,
543
+ num_rows: null,
544
+ variable_name: null,
545
+ primary_keys: null,
546
+ };
547
+ }
548
+
549
+ function makeSchema(opts: {
550
+ name: string;
551
+ tables: DataTable[];
552
+ tables_resolved?: boolean;
553
+ child_schemas?: DatabaseSchema[];
554
+ child_schemas_resolved?: boolean;
555
+ }): DatabaseSchema {
556
+ return {
557
+ name: opts.name,
558
+ tables: opts.tables,
559
+ tables_resolved: opts.tables_resolved ?? true,
560
+ child_schemas: opts.child_schemas ?? [],
561
+ child_schemas_resolved: opts.child_schemas_resolved ?? true,
562
+ };
563
+ }
564
+
565
+ function makeDatabase(
566
+ name: string,
567
+ schemas: DatabaseSchema[],
568
+ schemas_resolved = true,
569
+ ): Database {
570
+ return { name, dialect: "duckdb", schemas, schemas_resolved, engine: null };
571
+ }
572
+
573
+ describe("schemaSubtreeMatchesSearch", () => {
574
+ it("returns false for an empty query", () => {
575
+ const schema = makeSchema({ name: "main", tables: [makeTable("users")] });
576
+ expect(schemaSubtreeMatchesSearch(schema, "")).toBe(false);
577
+ expect(schemaSubtreeMatchesSearch(schema, " ")).toBe(false);
578
+ expect(schemaSubtreeMatchesSearch(schema, undefined)).toBe(false);
579
+ });
580
+
581
+ it("matches a table name case-insensitively", () => {
582
+ const schema = makeSchema({ name: "main", tables: [makeTable("Users")] });
583
+ expect(schemaSubtreeMatchesSearch(schema, "user")).toBe(true);
584
+ expect(schemaSubtreeMatchesSearch(schema, "orders")).toBe(false);
585
+ });
586
+
587
+ it("matches a table in a resolved child schema", () => {
588
+ const schema = makeSchema({
589
+ name: "parent",
590
+ tables: [],
591
+ child_schemas: [
592
+ makeSchema({ name: "child", tables: [makeTable("orders")] }),
593
+ ],
594
+ });
595
+ expect(schemaSubtreeMatchesSearch(schema, "orders")).toBe(true);
596
+ });
597
+
598
+ it("ignores deferred tables so search never triggers a fetch", () => {
599
+ const schema = makeSchema({
600
+ name: "main",
601
+ tables: [makeTable("users")],
602
+ tables_resolved: false,
603
+ });
604
+ expect(schemaSubtreeMatchesSearch(schema, "users")).toBe(false);
605
+ });
606
+
607
+ it("ignores deferred child schemas", () => {
608
+ const schema = makeSchema({
609
+ name: "parent",
610
+ tables: [],
611
+ child_schemas: [
612
+ makeSchema({ name: "child", tables: [makeTable("orders")] }),
613
+ ],
614
+ child_schemas_resolved: false,
615
+ });
616
+ expect(schemaSubtreeMatchesSearch(schema, "orders")).toBe(false);
617
+ });
618
+ });
619
+
620
+ describe("shouldExpandDatabaseForSearch", () => {
621
+ it("returns false for an empty query", () => {
622
+ const db = makeDatabase("memory", [
623
+ makeSchema({ name: "main", tables: [makeTable("users")] }),
624
+ ]);
625
+ expect(shouldExpandDatabaseForSearch(db, "")).toBe(false);
626
+ expect(shouldExpandDatabaseForSearch(db, undefined)).toBe(false);
627
+ });
628
+
629
+ it("expands when a loaded schema contains a matching table", () => {
630
+ const db = makeDatabase("memory", [
631
+ makeSchema({ name: "main", tables: [makeTable("users")] }),
632
+ makeSchema({ name: "other", tables: [makeTable("orders")] }),
633
+ ]);
634
+ expect(shouldExpandDatabaseForSearch(db, "user")).toBe(true);
635
+ expect(shouldExpandDatabaseForSearch(db, "nomatch")).toBe(false);
636
+ });
637
+
638
+ it("does not expand when the schema list itself is deferred", () => {
639
+ const db = makeDatabase(
640
+ "memory",
641
+ [makeSchema({ name: "main", tables: [makeTable("users")] })],
642
+ /* schemas_resolved */ false,
643
+ );
644
+ expect(shouldExpandDatabaseForSearch(db, "users")).toBe(false);
645
+ });
646
+ });
@@ -29,7 +29,7 @@ import { Button } from "../ui/button";
29
29
  import { Tooltip } from "../ui/tooltip";
30
30
  import { ColumnPreviewContainer } from "./components";
31
31
  import { InstallPackageButton } from "./install-package-button";
32
- import { convertStatsName, sqlCode } from "./utils";
32
+ import { convertStatsName, sqlCode, tableUniqueId } from "./utils";
33
33
 
34
34
  export const DatasetColumnPreview: React.FC<{
35
35
  table: DataTable;
@@ -48,9 +48,7 @@ export const DatasetColumnPreview: React.FC<{
48
48
  tableName: table.name,
49
49
  columnName: column.name,
50
50
  sourceType: table.source_type,
51
- fullyQualifiedTableName: sqlTableContext
52
- ? `${sqlTableContext.database}.${sqlTableContext.schema}.${table.name}`
53
- : table.name,
51
+ fullyQualifiedTableName: tableUniqueId(sqlTableContext, table.name),
54
52
  });
55
53
  };
56
54
 
@@ -83,6 +83,8 @@ import {
83
83
  areSchemasResolved,
84
84
  areTablesResolved,
85
85
  isSchemaless,
86
+ schemaSubtreeMatchesSearch,
87
+ shouldExpandDatabaseForSearch,
86
88
  sqlCode,
87
89
  tableUniqueId,
88
90
  } from "./utils";
@@ -360,7 +362,6 @@ export const DataSources: React.FC = () => {
360
362
  key={database.name}
361
363
  connection={connection}
362
364
  database={database}
363
- hasSearch={hasSearch}
364
365
  searchValue={searchValue}
365
366
  />
366
367
  ))}
@@ -435,7 +436,6 @@ interface DataSourceTree {
435
436
  dialect: string;
436
437
  engineName: string;
437
438
  databaseName: string;
438
- hasSearch: boolean;
439
439
  searchValue?: string;
440
440
  }
441
441
 
@@ -470,9 +470,8 @@ function buildSqlTableContext(
470
470
  const DatabaseTree: React.FC<{
471
471
  connection: DataSourceConnection;
472
472
  database: Database;
473
- hasSearch: boolean;
474
473
  searchValue?: string;
475
- }> = ({ connection, database, hasSearch, searchValue }) => {
474
+ }> = ({ connection, database, searchValue }) => {
476
475
  const tree = React.useMemo<DataSourceTree>(
477
476
  () => ({
478
477
  engineName: connection.name,
@@ -480,7 +479,6 @@ const DatabaseTree: React.FC<{
480
479
  dialect: connection.dialect,
481
480
  defaultSchema: connection.default_schema,
482
481
  defaultDatabase: connection.default_database,
483
- hasSearch,
484
482
  searchValue,
485
483
  }),
486
484
  [
@@ -489,7 +487,6 @@ const DatabaseTree: React.FC<{
489
487
  connection.default_schema,
490
488
  connection.default_database,
491
489
  database.name,
492
- hasSearch,
493
490
  searchValue,
494
491
  ],
495
492
  );
@@ -498,7 +495,7 @@ const DatabaseTree: React.FC<{
498
495
  <DatabaseItem
499
496
  engineName={connection.name}
500
497
  database={database}
501
- hasSearch={hasSearch}
498
+ searchValue={searchValue}
502
499
  >
503
500
  <DataSourceTreeContext.Provider value={tree}>
504
501
  <SchemaList
@@ -513,18 +510,30 @@ const DatabaseTree: React.FC<{
513
510
  };
514
511
 
515
512
  const DatabaseItem: React.FC<{
516
- hasSearch: boolean;
513
+ searchValue?: string;
517
514
  engineName: string;
518
515
  database: Database;
519
516
  children: React.ReactNode;
520
- }> = ({ hasSearch, engineName, database, children }) => {
521
- const [isExpanded, setIsExpanded] = React.useState(false);
517
+ }> = ({ searchValue, engineName, database, children }) => {
518
+ // Auto-expand during search only when a loaded schema under this database
519
+ // contains a matching table.
520
+ const expandForSearch = shouldExpandDatabaseForSearch(database, searchValue);
521
+ const [isExpanded, setIsExpanded] = React.useState(expandForSearch);
522
522
  const [isSelected, setIsSelected] = React.useState(false);
523
- const [prevHasSearch, setPrevHasSearch] = React.useState(hasSearch);
524
-
525
- if (prevHasSearch !== hasSearch) {
526
- setPrevHasSearch(hasSearch);
527
- setIsExpanded(hasSearch);
523
+ const [prevSearchValue, setPrevSearchValue] = React.useState(searchValue);
524
+ const [prevExpandForSearch, setPrevExpandForSearch] =
525
+ React.useState(expandForSearch);
526
+
527
+ // Re-sync when the query changes or when newly resolved data turns this
528
+ // branch into a match under the same query (deferred subtrees resolve
529
+ // asynchronously, so the decision can flip without the query changing).
530
+ if (
531
+ prevSearchValue !== searchValue ||
532
+ prevExpandForSearch !== expandForSearch
533
+ ) {
534
+ setPrevSearchValue(searchValue);
535
+ setPrevExpandForSearch(expandForSearch);
536
+ setIsExpanded(expandForSearch);
528
537
  }
529
538
 
530
539
  return (
@@ -657,12 +666,30 @@ interface SchemaNodeProps {
657
666
  const SchemaNode: React.FC<SchemaNodeProps> = (props) => {
658
667
  const { schema, schemaPath, depth } = props;
659
668
  const tree = useDataSourceTree();
660
- const { databaseName, hasSearch, searchValue } = tree;
661
- const [isExpanded, setIsExpanded] = React.useState(hasSearch);
669
+ const { databaseName, searchValue } = tree;
670
+ // Auto-expand during search only when this schema's loaded subtree contains a
671
+ // matching table.
672
+ const expandForSearch = schemaSubtreeMatchesSearch(schema, searchValue);
673
+ const [isExpanded, setIsExpanded] = React.useState(expandForSearch);
662
674
  const [isSelected, setIsSelected] = React.useState(false);
675
+ const [prevSearchValue, setPrevSearchValue] = React.useState(searchValue);
676
+ const [prevExpandForSearch, setPrevExpandForSearch] =
677
+ React.useState(expandForSearch);
663
678
  const uniqueValue = `${databaseName}:${schemaPath.join(".")}`;
664
679
  const childSchemas = schema.child_schemas ?? [];
665
680
 
681
+ // Re-sync when the query changes or when newly resolved data turns this
682
+ // subtree into a match under the same query (deferred tables/child schemas
683
+ // resolve asynchronously, so the decision can flip without the query changing).
684
+ if (
685
+ prevSearchValue !== searchValue ||
686
+ prevExpandForSearch !== expandForSearch
687
+ ) {
688
+ setPrevSearchValue(searchValue);
689
+ setPrevExpandForSearch(expandForSearch);
690
+ setIsExpanded(expandForSearch);
691
+ }
692
+
666
693
  return (
667
694
  <>
668
695
  <CommandItem
@@ -52,6 +52,51 @@ export function areChildSchemasResolved(schema: DatabaseSchema): boolean {
52
52
  return schema.child_schemas_resolved !== false;
53
53
  }
54
54
 
55
+ /**
56
+ * Whether a loaded schema subtree contains a table whose name matches
57
+ * `searchValue`. Deferred (not-yet-fetched) tables and child schemas are
58
+ * treated as non-matching, so searching never triggers a lazy catalog fetch on
59
+ * large/remote connections.
60
+ */
61
+ export function schemaSubtreeMatchesSearch(
62
+ schema: DatabaseSchema,
63
+ searchValue: string | undefined,
64
+ ): boolean {
65
+ const query = searchValue?.trim().toLowerCase();
66
+ if (!query) {
67
+ return false;
68
+ }
69
+ if (
70
+ areTablesResolved(schema) &&
71
+ schema.tables.some((table) => table.name.toLowerCase().includes(query))
72
+ ) {
73
+ return true;
74
+ }
75
+ if (areChildSchemasResolved(schema)) {
76
+ return (schema.child_schemas ?? []).some((child) =>
77
+ schemaSubtreeMatchesSearch(child, query),
78
+ );
79
+ }
80
+ return false;
81
+ }
82
+
83
+ /**
84
+ * Auto-expand a database row during search only when one of its loaded schemas
85
+ * contains a matching table. Returns false when the schema list itself is
86
+ * deferred.
87
+ */
88
+ export function shouldExpandDatabaseForSearch(
89
+ database: Database,
90
+ searchValue: string | undefined,
91
+ ): boolean {
92
+ if (!searchValue?.trim() || !areSchemasResolved(database)) {
93
+ return false;
94
+ }
95
+ return database.schemas.some((schema) =>
96
+ schemaSubtreeMatchesSearch(schema, searchValue),
97
+ );
98
+ }
99
+
55
100
  interface SqlCodeFormatter {
56
101
  /**
57
102
  * Format the table path based on dialect-specific rules
@@ -41,7 +41,10 @@ import {
41
41
  YoutubeIcon,
42
42
  ZapIcon,
43
43
  } from "lucide-react";
44
- import { settingDialogAtom } from "@/components/app-config/state";
44
+ import {
45
+ settingDialogAtom,
46
+ useOpenSettingsToTab,
47
+ } from "@/components/app-config/state";
45
48
  import { MarkdownIcon } from "@/components/editor/cell/code/icons";
46
49
  import { MarimoPlusIcon } from "@/components/icons/marimo-icons";
47
50
  import { useImperativeModal } from "@/components/modal/ImperativeModal";
@@ -57,7 +60,8 @@ import {
57
60
  useCellActions,
58
61
  } from "@/core/cells/cells";
59
62
  import { disabledCellIds } from "@/core/cells/utils";
60
- import { useResolvedMarimoConfig } from "@/core/config/config";
63
+ import { capabilitiesAtom } from "@/core/config/capabilities";
64
+ import { aiEnabledAtom, useResolvedMarimoConfig } from "@/core/config/config";
61
65
  import { Constants } from "@/core/constants";
62
66
  import {
63
67
  updateCellOutputsWithScreenshots,
@@ -86,7 +90,7 @@ import { Strings } from "@/utils/strings";
86
90
  import { newNotebookURL } from "@/utils/urls";
87
91
  import { useRunAllCells } from "../cell/useRunCells";
88
92
  import { useChromeActions, useChromeState } from "../chrome/state";
89
- import { PANELS } from "../chrome/types";
93
+ import { isPanelHidden, PANELS } from "../chrome/types";
90
94
  import { AddConnectionDialogContent } from "../connections/add-connection-dialog";
91
95
  import { keyboardShortcutsAtom } from "../controls/keyboard-shortcuts";
92
96
  import { commandPaletteAtom } from "../controls/state";
@@ -112,6 +116,8 @@ export function useNotebookActions() {
112
116
  const kioskMode = useAtomValue(kioskModeAtom);
113
117
  const hideAllMarkdownCode = useHideAllMarkdownCode();
114
118
  const [resolvedConfig] = useResolvedMarimoConfig();
119
+ const capabilities = useAtomValue(capabilitiesAtom);
120
+ const aiEnabled = useAtomValue(aiEnabledAtom);
115
121
 
116
122
  const {
117
123
  updateCellConfig,
@@ -126,6 +132,7 @@ export function useNotebookActions() {
126
132
  const copyNotebook = useCopyNotebook(filename);
127
133
  const setCommandPaletteOpen = useSetAtom(commandPaletteAtom);
128
134
  const setSettingsDialogOpen = useSetAtom(settingDialogAtom);
135
+ const { handleClick: openSettings } = useOpenSettingsToTab();
129
136
  const setKeyboardShortcutsOpen = useSetAtom(keyboardShortcutsAtom);
130
137
  const {
131
138
  exportAsIPYNB,
@@ -407,20 +414,31 @@ export function useNotebookActions() {
407
414
  label: "Helper panel",
408
415
  redundant: true,
409
416
  handle: NOOP_HANDLER,
410
- dropdown: PANELS.flatMap(
411
- ({ type: id, Icon, hidden, additionalKeywords }) => {
412
- if (hidden) {
413
- return [];
414
- }
415
- return {
416
- label: Strings.startCase(id),
417
- rightElement: renderCheckboxElement(selectedPanel === id),
418
- icon: <Icon size={14} strokeWidth={1.5} />,
419
- handle: () => toggleApplication(id),
420
- additionalKeywords,
421
- };
422
- },
423
- ),
417
+ dropdown: PANELS.flatMap((panel) => {
418
+ // Still show the AI panel in the command palette so users can try AI
419
+ // features. When AI is disabled, open settings instead of the panel.
420
+ const openAiSettingsWhenDisabled = panel.type === "ai" && !aiEnabled;
421
+ if (
422
+ isPanelHidden({ panel, capabilities, aiEnabled }) &&
423
+ !openAiSettingsWhenDisabled
424
+ ) {
425
+ return [];
426
+ }
427
+ const { type: id, Icon, additionalKeywords } = panel;
428
+ return {
429
+ label: Strings.startCase(id),
430
+ rightElement: renderCheckboxElement(selectedPanel === id),
431
+ icon: <Icon size={14} strokeWidth={1.5} />,
432
+ handle: () => {
433
+ if (openAiSettingsWhenDisabled) {
434
+ openSettings("ai", "ai-features");
435
+ return;
436
+ }
437
+ toggleApplication(id);
438
+ },
439
+ additionalKeywords,
440
+ };
441
+ }),
424
442
  },
425
443
 
426
444
  {
@@ -1,6 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import { zodResolver } from "@hookform/resolvers/zod";
4
+ import { useAtomValue } from "jotai";
4
5
  import React from "react";
5
6
  import { type DefaultValues, type FieldValues, useForm } from "react-hook-form";
6
7
  import type { z } from "zod";
@@ -16,8 +17,10 @@ import {
16
17
  SelectTrigger,
17
18
  SelectValue,
18
19
  } from "@/components/ui/select";
20
+ import { maybeAddMarimoImport } from "@/core/cells/add-missing-import";
19
21
  import { useCellActions } from "@/core/cells/cells";
20
22
  import { useLastFocusedCellId } from "@/core/cells/focus";
23
+ import { autoInstantiateAtom } from "@/core/config/config";
21
24
  import { ENV_RENDERER, SecretsProvider } from "./form-renderers";
22
25
 
23
26
  const RENDERERS: FormRenderer[] = [ENV_RENDERER];
@@ -106,8 +109,18 @@ export const ConnectionFormFooter = <L extends string>({
106
109
  export function useInsertCode() {
107
110
  const { createNewCell } = useCellActions();
108
111
  const lastFocusedCellId = useLastFocusedCellId();
112
+ const autoInstantiate = useAtomValue(autoInstantiateAtom);
109
113
 
110
114
  return (code: string) => {
115
+ // Ensure `mo` is importable when the generated code references it
116
+ if (/\bmo\./.test(code)) {
117
+ maybeAddMarimoImport({
118
+ autoInstantiate,
119
+ createNewCell,
120
+ fromCellId: lastFocusedCellId,
121
+ });
122
+ }
123
+
111
124
  createNewCell({
112
125
  code,
113
126
  before: false,
@@ -85,14 +85,14 @@ store = GCSStore("my-bucket")"
85
85
  exports[`generateStorageCode > Google Drive > with default auth (no credentials) 1`] = `
86
86
  "from gdrive_fsspec import GoogleDriveFileSystem
87
87
 
88
- fs = GoogleDriveFileSystem(use_listings_cache=False)"
88
+ fs = GoogleDriveFileSystem(use_listings_cache=False, skip_instance_cache=True)"
89
89
  `;
90
90
 
91
91
  exports[`generateStorageCode > Google Drive > with embedded auth (no credentials) 1`] = `
92
92
  "from gdrive_fsspec import GoogleDriveFileSystem
93
93
 
94
- fs = GoogleDriveFileSystem(use_listings_cache=False, auth_kwargs={"use_local_webserver": False})
95
- print("Google Drive connected! Important: Run this cell again to clear the console")"
94
+ fs = GoogleDriveFileSystem(use_listings_cache=False, skip_instance_cache=True, auth_kwargs={"use_local_webserver": False})
95
+ mo.output.clear_console()"
96
96
  `;
97
97
 
98
98
  exports[`generateStorageCode > Google Drive > with service account credentials 1`] = `
@@ -100,7 +100,7 @@ exports[`generateStorageCode > Google Drive > with service account credentials 1
100
100
  import json
101
101
 
102
102
  _creds = json.loads("""{"type": "service_account", "client_email": "test@test.iam.gserviceaccount.com"}""")
103
- fs = GoogleDriveFileSystem(creds=_creds, token="service_account", use_listings_cache=False)"
103
+ fs = GoogleDriveFileSystem(creds=_creds, token="service_account", use_listings_cache=False, skip_instance_cache=True)"
104
104
  `;
105
105
 
106
106
  exports[`generateStorageCode > S3 > basic connection with all fields 1`] = `