@marimo-team/islands 0.23.11-dev3 → 0.23.11-dev32

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 (86) hide show
  1. package/dist/{ConnectedDataExplorerComponent-Z3RP7Vmm.js → ConnectedDataExplorerComponent-WqG-xX4l.js} +13 -13
  2. package/dist/{ErrorBoundary-DpbaKVv7.js → ErrorBoundary-BNx_OSVo.js} +2 -2
  3. package/dist/{any-language-editor-CAgFD4Kd.js → any-language-editor-rPSlOll9.js} +5 -5
  4. package/dist/assets/__vite-browser-external-DAm_YW43.js +1 -0
  5. package/dist/assets/{worker-CC0Oul9k.js → worker-DEDLIQQV.js} +2 -2
  6. package/dist/{button-C5K9fIPF.js → button-vQhauTmO.js} +20 -0
  7. package/dist/{capabilities-BceAxrAW.js → capabilities-BEHzIS99.js} +1 -1
  8. package/dist/{chat-ui-c1FdlK-C.js → chat-ui-k2kqhCv5.js} +135 -106
  9. package/dist/{check-BCaJeT-J.js → check-nrzHDi45.js} +1 -1
  10. package/dist/{code-visibility-Csek3T1q.js → code-visibility-vP4YMX-2.js} +81 -68
  11. package/dist/{copy-UqRYxiOg.js → copy-UhDed7D4.js} +2 -2
  12. package/dist/{dist-Dk6PV_d3.js → dist-DYGLrbYQ.js} +2 -2
  13. package/dist/{error-banner-CJXYJ6Sb.js → error-banner-BHAkVFc2.js} +2 -2
  14. package/dist/{esm-BaH2eg5-.js → esm-Bqu9AE2K.js} +1 -1
  15. package/dist/{extends-D_hDsj6R.js → extends-9Yl5BEcg.js} +4 -4
  16. package/dist/{formats-C4wO47tk.js → formats-BV4bOfMI.js} +3 -3
  17. package/dist/{glide-data-editor-BPkCPs7L.js → glide-data-editor-BDTq6YUb.js} +9 -9
  18. package/dist/{html-to-image-D5-EpALB.js → html-to-image-C86pQALH.js} +16 -16
  19. package/dist/{input-OdWHkobi.js → input-AKkGXdyV.js} +6 -6
  20. package/dist/{label-CC4ytI1X.js → label-E3ZJXHu8.js} +2 -2
  21. package/dist/{loader-BWLPpjKK.js → loader-YPuQvn1Y.js} +1 -1
  22. package/dist/main.js +981 -978
  23. package/dist/{mermaid-BotvIKg9.js → mermaid-QFAR9YgY.js} +5 -5
  24. package/dist/{process-output-WDZE0cyS.js → process-output-nNw4OpSj.js} +3 -3
  25. package/dist/{reveal-component-D7GJ7KT7.js → reveal-component-DL7bkggE.js} +10 -10
  26. package/dist/{spec-X7FwLJni.js → spec-B45_YCNI.js} +4 -4
  27. package/dist/{strings-J57tzLr3.js → strings-Cq2s9_EQ.js} +4 -4
  28. package/dist/style.css +1 -1
  29. package/dist/{swiper-component-B2t5sN1q.js → swiper-component-BNa_4kh2.js} +2 -2
  30. package/dist/{toDate-d8RCRrRd.js → toDate-Do1xRzAo.js} +3 -3
  31. package/dist/{tooltip-DpcyNkQ2.js → tooltip-Bz3OAwrU.js} +3 -3
  32. package/dist/{types-ChtMFmZ2.js → types-D8gEGs4R.js} +1 -1
  33. package/dist/{useAsyncData-PonK__yh.js → useAsyncData-CL3o2p4i.js} +1 -1
  34. package/dist/{useDateFormatter-QB-3MpYr.js → useDateFormatter-BC6iSz9g.js} +2 -2
  35. package/dist/{useDeepCompareMemoize-D3NGWke6.js → useDeepCompareMemoize-BPx2MuOK.js} +1 -1
  36. package/dist/{useIframeCapabilities-C4JTXTIh.js → useIframeCapabilities-C6Ta3EyP.js} +1 -1
  37. package/dist/{useLifecycle-00mO3OSS.js → useLifecycle-C3Ec71q0.js} +3 -3
  38. package/dist/{useTheme-DEhDzATN.js → useTheme-ZhT6uIu3.js} +4 -3
  39. package/dist/{vega-component-DGPUhbDs.js → vega-component-C3AWYGAL.js} +10 -10
  40. package/dist/{zod-aLSua2NL.js → zod-DXqkaI_w.js} +1 -1
  41. package/package.json +1 -1
  42. package/src/components/ai/ai-model-dropdown.tsx +6 -0
  43. package/src/components/app-config/ai-config.tsx +73 -55
  44. package/src/components/app-config/data-form.tsx +33 -43
  45. package/src/components/app-config/is-overridden.tsx +125 -58
  46. package/src/components/app-config/user-config-form.tsx +178 -227
  47. package/src/components/chat/chat-display.tsx +5 -0
  48. package/src/components/chat/chat-panel.tsx +21 -8
  49. package/src/components/chat/tool-call/tool-call-view.tsx +4 -0
  50. package/src/components/chat/tool-call/tool-history-row.tsx +26 -5
  51. package/src/components/data-table/TableTopBar.tsx +4 -4
  52. package/src/components/data-table/__tests__/columns.test.tsx +20 -0
  53. package/src/components/data-table/__tests__/data-table.test.tsx +28 -0
  54. package/src/components/data-table/columns.tsx +16 -1
  55. package/src/components/data-table/data-table.tsx +3 -3
  56. package/src/components/data-table/filters/types.ts +1 -0
  57. package/src/components/data-table/renderers.tsx +5 -1
  58. package/src/components/datasources/__tests__/column-preview.test.tsx +97 -0
  59. package/src/components/datasources/__tests__/install-package-button.test.tsx +11 -0
  60. package/src/components/datasources/__tests__/utils.test.ts +127 -2
  61. package/src/components/datasources/column-preview.tsx +2 -4
  62. package/src/components/datasources/datasources.tsx +44 -17
  63. package/src/components/datasources/install-package-button.tsx +3 -1
  64. package/src/components/datasources/utils.ts +45 -0
  65. package/src/components/editor/actions/useNotebookActions.tsx +62 -21
  66. package/src/components/editor/actions/useSetCodeVisibility.ts +57 -0
  67. package/src/components/editor/connections/components.tsx +13 -0
  68. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +4 -4
  69. package/src/components/editor/connections/storage/as-code.ts +11 -4
  70. package/src/components/editor/header/__tests__/status.test.tsx +0 -4
  71. package/src/components/editor/header/status.tsx +2 -3
  72. package/src/core/cells/__tests__/cells.test.ts +33 -0
  73. package/src/core/cells/cells.ts +1 -1
  74. package/src/core/config/config-schema.ts +6 -1
  75. package/src/core/edit-app.tsx +14 -0
  76. package/src/core/hotkeys/hotkeys.ts +20 -0
  77. package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +0 -13
  78. package/src/core/websocket/types.ts +0 -1
  79. package/src/core/websocket/useMarimoKernelConnection.tsx +0 -14
  80. package/src/css/md.css +12 -0
  81. package/src/plugins/impl/DataTablePlugin.tsx +12 -5
  82. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +1 -1
  83. package/src/plugins/impl/chat/chat-ui.tsx +1 -0
  84. package/src/plugins/impl/data-frames/DataFramePlugin.tsx +1 -1
  85. package/dist/assets/__vite-browser-external-eshhtsgZ.js +0 -1
  86. package/src/components/editor/actions/useHideAllMarkdownCode.ts +0 -53
@@ -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
@@ -2,6 +2,7 @@
2
2
 
3
3
  import React from "react";
4
4
  import { Button } from "@/components/ui/button";
5
+ import { useInstallAllowed } from "@/core/mode";
5
6
  import { useInstallPackages } from "@/core/packages/useInstallPackage";
6
7
  import { cn } from "@/utils/cn";
7
8
 
@@ -23,8 +24,9 @@ export const InstallPackageButton: React.FC<InstallPackageButtonProps> = ({
23
24
  onInstall,
24
25
  }) => {
25
26
  const { handleInstallPackages } = useInstallPackages();
27
+ const installAllowed = useInstallAllowed();
26
28
 
27
- if (!packages || packages.length === 0) {
29
+ if (!packages || packages.length === 0 || !installAllowed) {
28
30
  return null;
29
31
  }
30
32
 
@@ -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
@@ -14,6 +14,7 @@ import {
14
14
  DownloadIcon,
15
15
  EditIcon,
16
16
  ExternalLinkIcon,
17
+ EyeIcon,
17
18
  EyeOffIcon,
18
19
  FastForwardIcon,
19
20
  FileIcon,
@@ -41,7 +42,10 @@ import {
41
42
  YoutubeIcon,
42
43
  ZapIcon,
43
44
  } from "lucide-react";
44
- import { settingDialogAtom } from "@/components/app-config/state";
45
+ import {
46
+ settingDialogAtom,
47
+ useOpenSettingsToTab,
48
+ } from "@/components/app-config/state";
45
49
  import { MarkdownIcon } from "@/components/editor/cell/code/icons";
46
50
  import { MarimoPlusIcon } from "@/components/icons/marimo-icons";
47
51
  import { useImperativeModal } from "@/components/modal/ImperativeModal";
@@ -57,7 +61,8 @@ import {
57
61
  useCellActions,
58
62
  } from "@/core/cells/cells";
59
63
  import { disabledCellIds } from "@/core/cells/utils";
60
- import { useResolvedMarimoConfig } from "@/core/config/config";
64
+ import { capabilitiesAtom } from "@/core/config/capabilities";
65
+ import { aiEnabledAtom, useResolvedMarimoConfig } from "@/core/config/config";
61
66
  import { Constants } from "@/core/constants";
62
67
  import {
63
68
  updateCellOutputsWithScreenshots,
@@ -86,7 +91,7 @@ import { Strings } from "@/utils/strings";
86
91
  import { newNotebookURL } from "@/utils/urls";
87
92
  import { useRunAllCells } from "../cell/useRunCells";
88
93
  import { useChromeActions, useChromeState } from "../chrome/state";
89
- import { PANELS } from "../chrome/types";
94
+ import { isPanelHidden, PANELS } from "../chrome/types";
90
95
  import { AddConnectionDialogContent } from "../connections/add-connection-dialog";
91
96
  import { keyboardShortcutsAtom } from "../controls/keyboard-shortcuts";
92
97
  import { commandPaletteAtom } from "../controls/state";
@@ -95,8 +100,8 @@ import { LAYOUT_TYPES } from "../renderers/types";
95
100
  import { runServerSidePDFDownload } from "./pdf-export";
96
101
  import type { ActionButton } from "./types";
97
102
  import { useCopyNotebook } from "./useCopyNotebook";
98
- import { useHideAllMarkdownCode } from "./useHideAllMarkdownCode";
99
103
  import { useRestartKernel } from "./useRestartKernel";
104
+ import { useSetCodeVisibility } from "./useSetCodeVisibility";
100
105
 
101
106
  const NOOP_HANDLER = (event?: Event) => {
102
107
  event?.preventDefault();
@@ -110,8 +115,10 @@ export function useNotebookActions() {
110
115
  const { selectedPanel } = useChromeState();
111
116
  const [viewState] = useAtom(viewStateAtom);
112
117
  const kioskMode = useAtomValue(kioskModeAtom);
113
- const hideAllMarkdownCode = useHideAllMarkdownCode();
118
+ const setCodeVisibility = useSetCodeVisibility();
114
119
  const [resolvedConfig] = useResolvedMarimoConfig();
120
+ const capabilities = useAtomValue(capabilitiesAtom);
121
+ const aiEnabled = useAtomValue(aiEnabledAtom);
115
122
 
116
123
  const {
117
124
  updateCellConfig,
@@ -126,6 +133,7 @@ export function useNotebookActions() {
126
133
  const copyNotebook = useCopyNotebook(filename);
127
134
  const setCommandPaletteOpen = useSetAtom(commandPaletteAtom);
128
135
  const setSettingsDialogOpen = useSetAtom(settingDialogAtom);
136
+ const { handleClick: openSettings } = useOpenSettingsToTab();
129
137
  const setKeyboardShortcutsOpen = useSetAtom(keyboardShortcutsAtom);
130
138
  const {
131
139
  exportAsIPYNB,
@@ -407,20 +415,31 @@ export function useNotebookActions() {
407
415
  label: "Helper panel",
408
416
  redundant: true,
409
417
  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
- ),
418
+ dropdown: PANELS.flatMap((panel) => {
419
+ // Still show the AI panel in the command palette so users can try AI
420
+ // features. When AI is disabled, open settings instead of the panel.
421
+ const openAiSettingsWhenDisabled = panel.type === "ai" && !aiEnabled;
422
+ if (
423
+ isPanelHidden({ panel, capabilities, aiEnabled }) &&
424
+ !openAiSettingsWhenDisabled
425
+ ) {
426
+ return [];
427
+ }
428
+ const { type: id, Icon, additionalKeywords } = panel;
429
+ return {
430
+ label: Strings.startCase(id),
431
+ rightElement: renderCheckboxElement(selectedPanel === id),
432
+ icon: <Icon size={14} strokeWidth={1.5} />,
433
+ handle: () => {
434
+ if (openAiSettingsWhenDisabled) {
435
+ openSettings("ai", "ai-features");
436
+ return;
437
+ }
438
+ toggleApplication(id);
439
+ },
440
+ additionalKeywords,
441
+ };
442
+ }),
424
443
  },
425
444
 
426
445
  {
@@ -560,11 +579,33 @@ export function useNotebookActions() {
560
579
  clearAllCellOutputs();
561
580
  },
562
581
  },
582
+ {
583
+ icon: <EyeIcon size={14} strokeWidth={1.5} />,
584
+ label: "Show all code",
585
+ hotkey: "global.showAllCode",
586
+ handle: () => setCodeVisibility(false, "code"),
587
+ redundant: true,
588
+ },
589
+ {
590
+ icon: <EyeOffIcon size={14} strokeWidth={1.5} />,
591
+ label: "Hide all code",
592
+ hotkey: "global.hideAllCode",
593
+ handle: () => setCodeVisibility(true, "code"),
594
+ redundant: true,
595
+ },
596
+ {
597
+ icon: <EyeIcon size={14} strokeWidth={1.5} />,
598
+ label: "Show all markdown code",
599
+ hotkey: "global.showAllMarkdownCode",
600
+ handle: () => setCodeVisibility(false, "markdown"),
601
+ redundant: true,
602
+ },
563
603
  {
564
604
  icon: <EyeOffIcon size={14} strokeWidth={1.5} />,
565
605
  label: "Hide all markdown code",
566
- handle: hideAllMarkdownCode,
567
- redundant: true, // hidden by default
606
+ hotkey: "global.hideAllMarkdownCode",
607
+ handle: () => setCodeVisibility(true, "markdown"),
608
+ redundant: true,
568
609
  },
569
610
  {
570
611
  icon: <ChevronRightCircleIcon size={14} strokeWidth={1.5} />,
@@ -0,0 +1,57 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { useCallback } from "react";
4
+ import { getNotebook, useCellActions } from "@/core/cells/cells";
5
+ import type { CellId } from "@/core/cells/ids";
6
+ import { MarkdownLanguageAdapter } from "@/core/codemirror/language/languages/markdown";
7
+ import { useRequestClient } from "@/core/network/requests";
8
+ import type { CellConfig } from "@/core/network/types";
9
+ import { Objects } from "@/utils/objects";
10
+
11
+ const markdownAdapter = new MarkdownLanguageAdapter();
12
+
13
+ /** The kind of cell a visibility change applies to. */
14
+ export type CellCodeKind = "code" | "markdown";
15
+
16
+ const cellCodeKind = (code: string): CellCodeKind =>
17
+ markdownAdapter.isSupported(code) ? "markdown" : "code";
18
+
19
+ /**
20
+ * Hook returning a callback that hides or shows the code editor for every
21
+ * cell of the given kind.
22
+ */
23
+ export const useSetCodeVisibility = () => {
24
+ const { updateCellConfig } = useCellActions();
25
+ const { saveCellConfig } = useRequestClient();
26
+
27
+ return useCallback(
28
+ async (hidden: boolean, kind: CellCodeKind) => {
29
+ const notebook = getNotebook();
30
+
31
+ const newConfigs: Record<CellId, Partial<CellConfig>> = {};
32
+
33
+ for (const cellId of notebook.cellIds.inOrderIds) {
34
+ const cell = notebook.cellData[cellId];
35
+ if (cell === undefined || cell.config.hide_code === hidden) {
36
+ continue;
37
+ }
38
+ if (cellCodeKind(cell.code) !== kind) {
39
+ continue;
40
+ }
41
+ newConfigs[cellId] = { hide_code: hidden };
42
+ }
43
+
44
+ const entries = Objects.entries(newConfigs);
45
+ if (entries.length === 0) {
46
+ return;
47
+ }
48
+
49
+ await saveCellConfig({ configs: newConfigs });
50
+
51
+ for (const [cellId, config] of entries) {
52
+ updateCellConfig({ cellId, config });
53
+ }
54
+ },
55
+ [updateCellConfig, saveCellConfig],
56
+ );
57
+ };
@@ -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`] = `
@@ -168,6 +168,10 @@ function generateGDriveCode(
168
168
  connection: Extract<StorageConnection, { type: "gdrive" }>,
169
169
  options: { secrets: SecretContainer; isEmbedded?: boolean },
170
170
  ): { imports: Set<string>; code: string } {
171
+ /**
172
+ * Skip instance cache True so you can create multiple connections which don't reference the same creds.
173
+ * Use listings cache False so we don't get stale reads.
174
+ */
171
175
  const { secrets, isEmbedded = false } = options;
172
176
  const imports = new Set(["from gdrive_fsspec import GoogleDriveFileSystem"]);
173
177
 
@@ -179,18 +183,21 @@ function generateGDriveCode(
179
183
  );
180
184
  const code = dedent(`
181
185
  _creds = json.loads("""${connection.credentials_json?.startsWith("ENV:") ? `{${creds}}` : connection.credentials_json}""")
182
- fs = GoogleDriveFileSystem(creds=_creds, token="service_account", use_listings_cache=False)
186
+ fs = GoogleDriveFileSystem(creds=_creds, token="service_account", use_listings_cache=False, skip_instance_cache=True)
183
187
  `);
184
188
  return { imports, code };
185
189
  }
186
190
 
191
+ // In the iframe (embedded) flow we authenticate via the console-based OOB
192
+ // flow, which prints an auth URL and reads the code from stdin. Clear the
193
+ // console afterwards so the (single-use) auth code doesn't linger.
187
194
  const code = isEmbedded
188
195
  ? dedent(`
189
- fs = GoogleDriveFileSystem(use_listings_cache=False, auth_kwargs={"use_local_webserver": False})
190
- print("Google Drive connected! Important: Run this cell again to clear the console")
196
+ fs = GoogleDriveFileSystem(use_listings_cache=False, skip_instance_cache=True, auth_kwargs={"use_local_webserver": False})
197
+ mo.output.clear_console()
191
198
  `)
192
199
  : dedent(`
193
- fs = GoogleDriveFileSystem(use_listings_cache=False)
200
+ fs = GoogleDriveFileSystem(use_listings_cache=False, skip_instance_cache=True)
194
201
  `);
195
202
  return { imports, code };
196
203
  }
@@ -67,10 +67,6 @@ describe("StatusOverlay disconnect indicator", () => {
67
67
  });
68
68
 
69
69
  it.each([
70
- [
71
- WebSocketClosedReason.MALFORMED_QUERY,
72
- "the kernel did not recognize a request; please file a bug with marimo",
73
- ],
74
70
  [
75
71
  WebSocketClosedReason.KERNEL_STARTUP_ERROR,
76
72
  "Failed to start kernel sandbox",
@@ -22,9 +22,8 @@ export const StatusOverlay: React.FC<{
22
22
  const { mode } = useAtomValue(viewStateAtom);
23
23
  const isClosed = connection.state === WebSocketState.CLOSED;
24
24
  const isOpen = connection.state === WebSocketState.OPEN;
25
- // Only KERNEL_DISCONNECTED is recoverable by a retry. Other terminal
26
- // reasons (MALFORMED_QUERY, KERNEL_STARTUP_ERROR) would deterministically
27
- // fail the same way.
25
+ // Only KERNEL_DISCONNECTED is recoverable by a retry. KERNEL_STARTUP_ERROR
26
+ // would deterministically fail the same way.
28
27
  const canReconnect =
29
28
  isClosed && connection.code === WebSocketClosedReason.KERNEL_DISCONNECTED;
30
29
 
@@ -2920,6 +2920,39 @@ describe("cell reducer", () => {
2920
2920
  expect(state.untouchedNewCells.has(newCellId)).toBe(true);
2921
2921
  });
2922
2922
 
2923
+ it("adds interactively-created hidden cell with boilerplate code to untouchedNewCells", () => {
2924
+ // Markdown cells are created with hideCode and non-empty default code
2925
+ // (e.g. `mo.md(r"""\n""")`). They are user-initiated, so their editor
2926
+ // should be shown until first blur.
2927
+ actions.createNewCell({
2928
+ cellId: "__end__",
2929
+ before: false,
2930
+ code: 'mo.md(r"""\n""")',
2931
+ hideCode: true,
2932
+ autoFocus: true,
2933
+ });
2934
+
2935
+ const newCellId =
2936
+ state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
2937
+ expect(state.untouchedNewCells.has(newCellId)).toBe(true);
2938
+ });
2939
+
2940
+ it("does not add programmatically-created hidden cell with code to untouchedNewCells", () => {
2941
+ // Cells created by the kernel (e.g. via code_mode) carry code and
2942
+ // autoFocus=false; their hide_code must take effect immediately.
2943
+ actions.createNewCell({
2944
+ cellId: "__end__",
2945
+ before: false,
2946
+ code: "x = 1",
2947
+ hideCode: true,
2948
+ autoFocus: false,
2949
+ });
2950
+
2951
+ const newCellId =
2952
+ state.cellIds.inOrderIds[state.cellIds.inOrderIds.length - 1];
2953
+ expect(state.untouchedNewCells.has(newCellId)).toBe(false);
2954
+ });
2955
+
2923
2956
  it("does not add cell to untouchedNewCells when hideCode is false", () => {
2924
2957
  actions.createNewCell({
2925
2958
  cellId: "__end__",
@@ -276,7 +276,7 @@ const {
276
276
  },
277
277
  scrollKey: autoFocus ? newCellId : null,
278
278
  untouchedNewCells:
279
- hideCode && !code
279
+ hideCode && autoFocus
280
280
  ? new Set([...state.untouchedNewCells, newCellId])
281
281
  : state.untouchedNewCells,
282
282
  };
@@ -45,7 +45,12 @@ export const DEFAULT_AI_MODEL = "openai/gpt-4o";
45
45
  const AUTO_DOWNLOAD_FORMATS = ["html", "markdown", "ipynb"] as const;
46
46
 
47
47
  export type CopilotMode = NonNullable<schemas["AiConfig"]["mode"]>;
48
- export const COPILOT_MODES: CopilotMode[] = ["manual", "ask", "agent"];
48
+ export const COPILOT_MODES: CopilotMode[] = [
49
+ "manual",
50
+ "ask",
51
+ "agent",
52
+ "code_mode",
53
+ ];
49
54
 
50
55
  const AiConfigSchema = z
51
56
  .object({
@@ -20,6 +20,7 @@ import {
20
20
  useRunAllCells,
21
21
  useRunStaleCells,
22
22
  } from "../components/editor/cell/useRunCells";
23
+ import { useSetCodeVisibility } from "../components/editor/actions/useSetCodeVisibility";
23
24
  import { CellArray } from "../components/editor/renderers/cell-array";
24
25
  import { CellsRenderer } from "../components/editor/renderers/cells-renderer";
25
26
  import { useHotkey } from "../hooks/useHotkey";
@@ -112,6 +113,7 @@ export const EditApp: React.FC<AppProps> = ({
112
113
  const runStaleCells = useRunStaleCells();
113
114
  const runAllCells = useRunAllCells();
114
115
  const togglePresenting = useTogglePresenting();
116
+ const setCodeVisibility = useSetCodeVisibility();
115
117
 
116
118
  // HOTKEYS
117
119
  useHotkey("global.runStale", () => {
@@ -126,6 +128,18 @@ export const EditApp: React.FC<AppProps> = ({
126
128
  useHotkey("global.runAll", () => {
127
129
  runAllCells();
128
130
  });
131
+ useHotkey("global.showAllCode", () => {
132
+ setCodeVisibility(false, "code");
133
+ });
134
+ useHotkey("global.hideAllCode", () => {
135
+ setCodeVisibility(true, "code");
136
+ });
137
+ useHotkey("global.showAllMarkdownCode", () => {
138
+ setCodeVisibility(false, "markdown");
139
+ });
140
+ useHotkey("global.hideAllMarkdownCode", () => {
141
+ setCodeVisibility(true, "markdown");
142
+ });
129
143
  useHotkey("global.collapseAllSections", () => {
130
144
  collapseAllCells();
131
145
  });