@marimo-team/islands 0.20.3-dev94 → 0.20.3-dev97

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 (29) hide show
  1. package/dist/main.js +2 -2
  2. package/dist/style.css +1 -1
  3. package/package.json +1 -1
  4. package/src/components/chat/acp/agent-panel.tsx +29 -8
  5. package/src/components/databases/icons/google-drive.svg +8 -0
  6. package/src/components/datasources/datasources.tsx +5 -5
  7. package/src/components/editor/actions/useNotebookActions.tsx +15 -2
  8. package/src/components/editor/connections/add-connection-dialog.tsx +83 -0
  9. package/src/components/editor/connections/components.tsx +177 -0
  10. package/src/components/editor/{database → connections/database}/__tests__/as-code.test.ts +1 -1
  11. package/src/components/editor/connections/database/add-database-form.tsx +303 -0
  12. package/src/components/editor/{database → connections/database}/as-code.ts +1 -1
  13. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +100 -0
  14. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +166 -0
  15. package/src/components/editor/connections/storage/add-storage-form.tsx +135 -0
  16. package/src/components/editor/connections/storage/as-code.ts +188 -0
  17. package/src/components/editor/connections/storage/schemas.ts +141 -0
  18. package/src/components/storage/components.tsx +9 -3
  19. package/src/components/storage/storage-inspector.tsx +20 -1
  20. package/src/core/codemirror/__tests__/format.test.ts +9 -1
  21. package/src/core/saving/file-state.ts +7 -0
  22. package/src/core/storage/types.ts +1 -0
  23. package/src/mount.tsx +6 -1
  24. package/src/components/editor/database/add-database-form.tsx +0 -420
  25. /package/src/components/editor/{database → connections}/__tests__/secrets.test.ts +0 -0
  26. /package/src/components/editor/{database → connections/database}/__tests__/__snapshots__/as-code.test.ts.snap +0 -0
  27. /package/src/components/editor/{database → connections/database}/schemas.ts +0 -0
  28. /package/src/components/editor/{database → connections}/form-renderers.tsx +0 -0
  29. /package/src/components/editor/{database → connections}/secrets.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.20.3-dev94",
3
+ "version": "0.20.3-dev97",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -60,11 +60,11 @@ import {
60
60
  import { toast } from "@/components/ui/use-toast";
61
61
  import { DelayMount } from "@/components/utils/delay-mount";
62
62
  import { useRequestClient } from "@/core/network/requests";
63
- import { filenameAtom } from "@/core/saving/file-state";
63
+ import { cwdAtom, filenameAtom } from "@/core/saving/file-state";
64
64
  import { store } from "@/core/state/jotai";
65
65
  import { ErrorBanner } from "@/plugins/impl/common/error-banner";
66
66
  import { Functions } from "@/utils/functions";
67
- import { Paths } from "@/utils/paths";
67
+ import { PathBuilder, Paths } from "@/utils/paths";
68
68
  import {
69
69
  AddContextButton,
70
70
  AttachFileButton,
@@ -614,7 +614,11 @@ ChatContent.displayName = "ChatContent";
614
614
 
615
615
  const NO_WS_SET = "_skip_auto_connect_";
616
616
 
617
- function getCwd() {
617
+ function getCwd(): string {
618
+ const cwd = store.get(cwdAtom);
619
+ if (cwd) {
620
+ return cwd;
621
+ }
618
622
  const filename = store.get(filenameAtom);
619
623
  if (!filename) {
620
624
  throw new Error(
@@ -624,6 +628,21 @@ function getCwd() {
624
628
  return Paths.dirname(filename);
625
629
  }
626
630
 
631
+ function getAbsoluteFilename(): string {
632
+ const filename = store.get(filenameAtom);
633
+ if (!filename) {
634
+ throw new Error(
635
+ "Please save the notebook and refresh the browser to use the agent",
636
+ );
637
+ }
638
+ const cwd = store.get(cwdAtom);
639
+ if (cwd) {
640
+ const builder = PathBuilder.guessDeliminator(cwd);
641
+ return builder.join(cwd, String(Paths.basename(filename)));
642
+ }
643
+ return filename;
644
+ }
645
+
627
646
  const AgentPanel: React.FC = () => {
628
647
  const [isLoading, setIsLoading] = useState(false);
629
648
  const [error, setError] = useState<Error | string | null>(null);
@@ -862,8 +881,10 @@ const AgentPanel: React.FC = () => {
862
881
  setSessionState((prev) => updateSessionTitle(prev, prompt));
863
882
  }
864
883
 
865
- const filename = store.get(filenameAtom);
866
- if (!filename) {
884
+ let absoluteFilename: string;
885
+ try {
886
+ absoluteFilename = getAbsoluteFilename();
887
+ } catch {
867
888
  toast({
868
889
  title: "Notebook must be named",
869
890
  description: "Please name the notebook to use the agent",
@@ -894,16 +915,16 @@ const AgentPanel: React.FC = () => {
894
915
  promptBlocks.push(
895
916
  {
896
917
  type: "resource_link",
897
- uri: filename,
918
+ uri: absoluteFilename,
898
919
  mimeType: "text/x-python",
899
- name: filename,
920
+ name: absoluteFilename,
900
921
  },
901
922
  {
902
923
  type: "resource",
903
924
  resource: {
904
925
  uri: "marimo_rules.md",
905
926
  mimeType: "text/plain",
906
- text: getAgentPrompt(filename),
927
+ text: getAgentPrompt(absoluteFilename),
907
928
  },
908
929
  },
909
930
  );
@@ -0,0 +1,8 @@
1
+ <svg viewBox="0 0 87.3 78" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#0066da"/>
3
+ <path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#00ac47"/>
4
+ <path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z" fill="#ea4335"/>
5
+ <path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#00832d"/>
6
+ <path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#2684fc"/>
7
+ <path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z" fill="#ffba00"/>
8
+ </svg>
@@ -60,7 +60,7 @@ import { ErrorBoundary } from "../editor/boundary/ErrorBoundary";
60
60
  import { PythonIcon } from "../editor/cell/code/icons";
61
61
  import { useAddCodeToNewCell } from "../editor/cell/useAddCell";
62
62
  import { PanelEmptyState } from "../editor/chrome/panels/empty-state";
63
- import { AddDatabaseDialog } from "../editor/database/add-database-form";
63
+ import { AddConnectionDialog } from "../editor/connections/add-connection-dialog";
64
64
  import { DatasetColumnPreview } from "./column-preview";
65
65
  import {
66
66
  ColumnName,
@@ -160,12 +160,12 @@ export const DataSources: React.FC = () => {
160
160
  title="No tables found"
161
161
  description="Any datasets/dataframes in the global scope will be shown here."
162
162
  action={
163
- <AddDatabaseDialog>
163
+ <AddConnectionDialog>
164
164
  <Button variant="outline" size="sm">
165
165
  Add database or catalog
166
166
  <PlusIcon className="h-4 w-4 ml-2" />
167
167
  </Button>
168
- </AddDatabaseDialog>
168
+ </AddConnectionDialog>
169
169
  }
170
170
  icon={<DatabaseIcon />}
171
171
  />
@@ -201,7 +201,7 @@ export const DataSources: React.FC = () => {
201
201
  </button>
202
202
  )}
203
203
 
204
- <AddDatabaseDialog>
204
+ <AddConnectionDialog>
205
205
  <Button
206
206
  variant="ghost"
207
207
  size="sm"
@@ -209,7 +209,7 @@ export const DataSources: React.FC = () => {
209
209
  >
210
210
  <PlusIcon className="h-4 w-4" />
211
211
  </Button>
212
- </AddDatabaseDialog>
212
+ </AddConnectionDialog>
213
213
  </div>
214
214
 
215
215
  <CommandList className="flex flex-col">
@@ -24,6 +24,7 @@ import {
24
24
  FolderDownIcon,
25
25
  GithubIcon,
26
26
  GlobeIcon,
27
+ HardDrive,
27
28
  Home,
28
29
  ImageIcon,
29
30
  KeyboardIcon,
@@ -78,9 +79,9 @@ import { newNotebookURL } from "@/utils/urls";
78
79
  import { useRunAllCells } from "../cell/useRunCells";
79
80
  import { useChromeActions, useChromeState } from "../chrome/state";
80
81
  import { PANELS } from "../chrome/types";
82
+ import { AddConnectionDialogContent } from "../connections/add-connection-dialog";
81
83
  import { keyboardShortcutsAtom } from "../controls/keyboard-shortcuts";
82
84
  import { commandPaletteAtom } from "../controls/state";
83
- import { AddDatabaseDialogContent } from "../database/add-database-form";
84
85
  import { displayLayoutName, getLayoutIcon } from "../renderers/layout-select";
85
86
  import { LAYOUT_TYPES } from "../renderers/types";
86
87
  import { runServerSidePDFDownload } from "./pdf-export";
@@ -450,7 +451,19 @@ export function useNotebookActions() {
450
451
  icon: <DatabaseIcon size={14} strokeWidth={1.5} />,
451
452
  label: "Add database connection",
452
453
  handle: () => {
453
- openModal(<AddDatabaseDialogContent onClose={closeModal} />);
454
+ openModal(<AddConnectionDialogContent onClose={closeModal} />);
455
+ },
456
+ },
457
+ {
458
+ icon: <HardDrive size={14} strokeWidth={1.5} />,
459
+ label: "Add remote storage",
460
+ handle: () => {
461
+ openModal(
462
+ <AddConnectionDialogContent
463
+ defaultTab="storage"
464
+ onClose={closeModal}
465
+ />,
466
+ );
454
467
  },
455
468
  },
456
469
  {
@@ -0,0 +1,83 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { useState } from "react";
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ DialogTrigger,
11
+ } from "@/components/ui/dialog";
12
+ import { ExternalLink } from "@/components/ui/links";
13
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
14
+ import { AddDatabaseForm } from "./database/add-database-form";
15
+ import { AddStorageForm } from "./storage/add-storage-form";
16
+
17
+ type ConnectionTab = "databases" | "storage";
18
+
19
+ export const AddConnectionDialog: React.FC<{
20
+ children: React.ReactNode;
21
+ defaultTab?: ConnectionTab;
22
+ }> = ({ children, defaultTab = "databases" }) => {
23
+ const [open, setOpen] = useState(false);
24
+
25
+ return (
26
+ <Dialog open={open} onOpenChange={setOpen}>
27
+ <DialogTrigger asChild={true}>{children}</DialogTrigger>
28
+ <AddConnectionDialogContent
29
+ defaultTab={defaultTab}
30
+ onClose={() => setOpen(false)}
31
+ />
32
+ </Dialog>
33
+ );
34
+ };
35
+
36
+ export const AddConnectionDialogContent: React.FC<{
37
+ defaultTab?: ConnectionTab;
38
+ onClose: () => void;
39
+ }> = ({ defaultTab = "databases", onClose }) => {
40
+ const tabHeader = (
41
+ <TabsList className="w-full mb-4">
42
+ <TabsTrigger value="databases" className="flex-1">
43
+ Databases & Catalogs
44
+ </TabsTrigger>
45
+ <TabsTrigger value="storage" className="flex-1">
46
+ Remote Storages
47
+ </TabsTrigger>
48
+ </TabsList>
49
+ );
50
+
51
+ return (
52
+ <DialogContent className="max-h-[75vh] overflow-y-auto">
53
+ <DialogHeader>
54
+ <DialogTitle>Add Connection</DialogTitle>
55
+ <DialogDescription>
56
+ Connect to a{" "}
57
+ <ExternalLink href="https://docs.marimo.io/guides/working_with_data/sql/#connecting-to-a-custom-database">
58
+ database, data catalog
59
+ </ExternalLink>{" "}
60
+ or{" "}
61
+ <ExternalLink href="https://docs.marimo.io/guides/working_with_data/remote_storage/">
62
+ remote storage
63
+ </ExternalLink>{" "}
64
+ to work with data directly from your notebook.
65
+ </DialogDescription>
66
+ </DialogHeader>
67
+ <Tabs defaultValue={defaultTab}>
68
+ <TabsContent
69
+ value="databases"
70
+ className="mt-0 focus-visible:ring-0 focus-visible:ring-offset-0"
71
+ >
72
+ <AddDatabaseForm onSubmit={onClose} header={tabHeader} />
73
+ </TabsContent>
74
+ <TabsContent
75
+ value="storage"
76
+ className="mt-0 focus-visible:ring-0 focus-visible:ring-offset-0"
77
+ >
78
+ <AddStorageForm onSubmit={onClose} header={tabHeader} />
79
+ </TabsContent>
80
+ </Tabs>
81
+ </DialogContent>
82
+ );
83
+ };
@@ -0,0 +1,177 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { zodResolver } from "@hookform/resolvers/zod";
4
+ import React from "react";
5
+ import { type DefaultValues, type FieldValues, useForm } from "react-hook-form";
6
+ import type { z } from "zod";
7
+ import { type FormRenderer, ZodForm } from "@/components/forms/form";
8
+ import { getDefaults } from "@/components/forms/form-utils";
9
+ import { Button } from "@/components/ui/button";
10
+ import { FormErrorsBanner } from "@/components/ui/form";
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectGroup,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from "@/components/ui/select";
19
+ import { useCellActions } from "@/core/cells/cells";
20
+ import { useLastFocusedCellId } from "@/core/cells/focus";
21
+ import { ENV_RENDERER, SecretsProvider } from "./form-renderers";
22
+
23
+ const RENDERERS: FormRenderer[] = [ENV_RENDERER];
24
+
25
+ /**
26
+ * Grid layout for provider/database selector buttons.
27
+ */
28
+ export const SelectorGrid: React.FC<{ children: React.ReactNode }> = ({
29
+ children,
30
+ }) => <div className="grid grid-cols-2 md:grid-cols-3 gap-4">{children}</div>;
31
+
32
+ /**
33
+ * A colored button tile for selecting a provider/database.
34
+ */
35
+ export const SelectorButton: React.FC<{
36
+ name: string;
37
+ color: string;
38
+ icon: React.ReactNode;
39
+ onSelect: () => void;
40
+ }> = ({ name, color, icon, onSelect }) => (
41
+ <button
42
+ type="button"
43
+ className="py-3 flex flex-col items-center justify-center gap-1 transition-all hover:scale-105 hover:brightness-110 rounded shadow-sm-solid hover:shadow-md-solid"
44
+ style={{ backgroundColor: color }}
45
+ onClick={onSelect}
46
+ >
47
+ {icon}
48
+ <span className="text-white font-medium text-lg">{name}</span>
49
+ </button>
50
+ );
51
+
52
+ /**
53
+ * Footer with Back/Add buttons and a library picker.
54
+ */
55
+ export const ConnectionFormFooter = <L extends string>({
56
+ onBack,
57
+ isValid,
58
+ libraries,
59
+ preferredLibrary,
60
+ onLibraryChange,
61
+ displayNames,
62
+ libraryLabel = "Preferred library",
63
+ }: {
64
+ onBack: () => void;
65
+ isValid: boolean;
66
+ libraries: L[];
67
+ preferredLibrary: L;
68
+ onLibraryChange: (library: L) => void;
69
+ displayNames: Record<L, string>;
70
+ libraryLabel?: string;
71
+ }) => (
72
+ <div className="flex gap-2 justify-between">
73
+ <div className="flex gap-2">
74
+ <Button type="button" variant="outline" onClick={onBack}>
75
+ Back
76
+ </Button>
77
+ <Button type="submit" disabled={!isValid}>
78
+ Add
79
+ </Button>
80
+ </div>
81
+ <div>
82
+ <Select value={preferredLibrary} onValueChange={onLibraryChange}>
83
+ <div className="flex flex-col gap-1 items-end">
84
+ <SelectTrigger>
85
+ <SelectValue placeholder="Select a library" />
86
+ </SelectTrigger>
87
+ <span className="text-xs text-muted-foreground">{libraryLabel}</span>
88
+ </div>
89
+ <SelectContent>
90
+ <SelectGroup>
91
+ {libraries.map((library) => (
92
+ <SelectItem key={library} value={library}>
93
+ {displayNames[library] ?? library}
94
+ </SelectItem>
95
+ ))}
96
+ </SelectGroup>
97
+ </SelectContent>
98
+ </Select>
99
+ </div>
100
+ </div>
101
+ );
102
+
103
+ /**
104
+ * Returns a callback that inserts code into a new cell after the last focused cell.
105
+ */
106
+ export function useInsertCode() {
107
+ const { createNewCell } = useCellActions();
108
+ const lastFocusedCellId = useLastFocusedCellId();
109
+
110
+ return (code: string) => {
111
+ createNewCell({
112
+ code,
113
+ before: false,
114
+ cellId: lastFocusedCellId ?? "__end__",
115
+ skipIfCodeExists: true,
116
+ });
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Generic connection form: Zod-driven form with secrets support, a library
122
+ * picker, and Back/Add buttons. Used by both database and storage forms.
123
+ */
124
+ export const ConnectionForm = <T extends FieldValues, L extends string>({
125
+ schema,
126
+ libraries,
127
+ preferredLibrary: initialPreferred,
128
+ displayNames,
129
+ libraryLabel,
130
+ generateCode,
131
+ onSubmit,
132
+ onBack,
133
+ }: {
134
+ schema: z.ZodType<T, FieldValues>;
135
+ libraries: L[];
136
+ preferredLibrary: L;
137
+ displayNames: Record<L, string>;
138
+ libraryLabel?: string;
139
+ generateCode: (values: T, library: L) => string;
140
+ onSubmit: () => void;
141
+ onBack: () => void;
142
+ }) => {
143
+ const defaults = getDefaults(schema);
144
+ const form = useForm<T>({
145
+ defaultValues: defaults as DefaultValues<T>,
146
+ resolver: zodResolver(schema),
147
+ reValidateMode: "onChange",
148
+ });
149
+
150
+ const [preferredLibrary, setPreferredLibrary] =
151
+ React.useState<L>(initialPreferred);
152
+ const insertCode = useInsertCode();
153
+
154
+ const handleSubmit = (values: T) => {
155
+ insertCode(generateCode(values, preferredLibrary));
156
+ onSubmit();
157
+ };
158
+
159
+ return (
160
+ <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
161
+ <SecretsProvider>
162
+ <ZodForm schema={schema} form={form} renderers={RENDERERS}>
163
+ <FormErrorsBanner />
164
+ </ZodForm>
165
+ </SecretsProvider>
166
+ <ConnectionFormFooter
167
+ onBack={onBack}
168
+ isValid={form.formState.isValid}
169
+ libraries={libraries}
170
+ preferredLibrary={preferredLibrary}
171
+ onLibraryChange={setPreferredLibrary}
172
+ displayNames={displayNames}
173
+ libraryLabel={libraryLabel}
174
+ />
175
+ </form>
176
+ );
177
+ };
@@ -1,8 +1,8 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { describe, expect, it } from "vitest";
3
+ import { prefixSecret } from "../../secrets";
3
4
  import { type ConnectionLibrary, generateDatabaseCode } from "../as-code";
4
5
  import type { DatabaseConnection } from "../schemas";
5
- import { prefixSecret } from "../secrets";
6
6
 
7
7
  describe("generateDatabaseCode", () => {
8
8
  // Test fixtures