@marimo-team/islands 0.24.3-dev82 → 0.24.3-dev87

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 (52) hide show
  1. package/dist/{ErrorBoundary-CegtSjfd.js → ErrorBoundary-Bc52FkKN.js} +1 -1
  2. package/dist/{cells-BIfOnGBK.js → cells-DYIkSxs6.js} +4 -0
  3. package/dist/{chat-ui-LWkjhObe.js → chat-ui-BXj3uYjy.js} +4 -4
  4. package/dist/{code-visibility-Blq7oGrC.js → code-visibility-CI3ySb6F.js} +2 -2
  5. package/dist/{common-BQZDWy8O.js → common-BOJoCp6P.js} +3 -3
  6. package/dist/{data-grid-overlay-editor-DsU0Zpgt.js → data-grid-overlay-editor-BsT4_auq.js} +1 -1
  7. package/dist/{error-entries-CR0oClSL.js → error-entries-C0NsUqXl.js} +1 -1
  8. package/dist/{feedback-button-pGaDcCJS.js → feedback-button-B7KS_3RP.js} +4 -4
  9. package/dist/{glide-data-editor-DtJQCRsf.js → glide-data-editor-DtckYC_c.js} +3 -3
  10. package/dist/{globals-DBAC4Ihd.js → globals-CruHzAUQ.js} +1 -1
  11. package/dist/{html-to-image-_i03yR4q.js → html-to-image-CeYYvQNb.js} +1 -1
  12. package/dist/main.js +25 -14
  13. package/dist/{number-overlay-editor-9M7FnzxL.js → number-overlay-editor-CV1RN5Cg.js} +1 -1
  14. package/dist/{process-output-D3zQyTAS.js → process-output-BCTjlyuH.js} +2 -2
  15. package/dist/{reveal-component-ovaRyk0R.js → reveal-component-C47L1uNq.js} +3 -3
  16. package/dist/style.css +1 -1
  17. package/package.json +1 -1
  18. package/src/__tests__/fixtures/pair-preview.ts +1 -1
  19. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +6 -6
  20. package/src/components/editor/actions/__tests__/pair-with-agent-modal.test.tsx +1 -1
  21. package/src/components/editor/alerts/__tests__/connection-notice.test.tsx +186 -0
  22. package/src/components/editor/alerts/__tests__/startup-progress.test.tsx +185 -0
  23. package/src/components/editor/alerts/connection-notice.tsx +100 -54
  24. package/src/components/editor/alerts/startup-output.tsx +173 -0
  25. package/src/components/editor/alerts/startup-progress.tsx +204 -0
  26. package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +53 -4
  27. package/src/components/editor/chrome/panels/__tests__/sandbox-details.test.tsx +358 -0
  28. package/src/components/editor/chrome/panels/__tests__/sandbox-recovery.test.tsx +5 -2
  29. package/src/components/editor/chrome/panels/packages-panel.tsx +32 -21
  30. package/src/components/editor/chrome/panels/sandbox-details-state.ts +38 -0
  31. package/src/components/editor/chrome/panels/sandbox-panel.tsx +188 -131
  32. package/src/components/editor/chrome/panels/sandbox-toggle.tsx +52 -0
  33. package/src/components/editor/chrome/wrapper/app-chrome.tsx +7 -0
  34. package/src/components/editor/package-alert.tsx +112 -66
  35. package/src/core/alerts/__tests__/environment.test.ts +183 -0
  36. package/src/core/alerts/__tests__/state.test.tsx +240 -0
  37. package/src/core/alerts/environment.ts +57 -0
  38. package/src/core/alerts/state.ts +155 -99
  39. package/src/core/islands/bootstrap.ts +2 -1
  40. package/src/core/kernel/messages.ts +2 -2
  41. package/src/core/network/connection-notice.ts +45 -12
  42. package/src/core/network/connection.ts +19 -0
  43. package/src/core/network/useConnectionNotice.ts +53 -0
  44. package/src/core/packages/__tests__/useSandboxController.test.tsx +86 -5
  45. package/src/core/packages/sandbox-state.ts +56 -4
  46. package/src/core/packages/useSandboxController.ts +27 -7
  47. package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +199 -8
  48. package/src/core/websocket/types.ts +1 -1
  49. package/src/core/websocket/useMarimoKernelConnection.tsx +31 -19
  50. package/src/plugins/impl/FileBrowserPlugin.tsx +10 -4
  51. package/src/plugins/impl/__tests__/FileBrowserPlugin.test.tsx +50 -1
  52. package/src/stories/sandbox.stories.tsx +230 -50
@@ -1,28 +1,29 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import { useAtomValue } from "jotai";
3
- import {
4
- AlertCircleIcon,
5
- ChevronUpIcon,
6
- FileCodeIcon,
7
- RefreshCwIcon,
8
- } from "lucide-react";
2
+ import { useAtom, useAtomValue } from "jotai";
3
+ import { RefreshCwIcon } from "lucide-react";
9
4
  import { connectionNoticeAtom } from "@/core/network/connection-notice";
10
- import { Spinner } from "@/components/icons/spinner";
11
5
  import { Button } from "@/components/ui/button";
6
+ import { useRestartKernel } from "@/components/editor/actions/useRestartKernel";
12
7
  import {
13
- DropdownMenu,
14
- DropdownMenuContent,
15
- DropdownMenuItem,
16
- DropdownMenuTrigger,
17
- } from "@/components/ui/dropdown-menu";
18
- import { Tooltip } from "@/components/ui/tooltip";
19
- import { isConnectedAtom } from "@/core/network/connection";
8
+ isConnectedAtom,
9
+ startupProgressAtom,
10
+ } from "@/core/network/connection";
11
+ import type { DisplayConnectionNotice } from "@/core/network/useConnectionNotice";
20
12
  import {
13
+ preparationAtom,
21
14
  sandboxActionsAtom,
22
15
  sandboxAtom,
23
16
  sandboxSyncAtom,
17
+ sandboxSyncOperationAtom,
24
18
  } from "@/core/packages/sandbox-state";
25
19
  import { cn } from "@/utils/cn";
20
+ import { usePanelSection } from "./panel-context";
21
+ import { sandboxDetailsExpandedAtom } from "./sandbox-details-state";
22
+ import { StartupOutput } from "../../alerts/startup-output";
23
+ import {
24
+ ConnectionStatusIcon,
25
+ StartupProgress,
26
+ } from "../../alerts/startup-progress";
26
27
 
27
28
  export function SandboxErrorOutput({ error }: { error: string }) {
28
29
  return (
@@ -36,138 +37,194 @@ export function SandboxErrorOutput({ error }: { error: string }) {
36
37
  );
37
38
  }
38
39
 
39
- export function SandboxStartupPanel() {
40
- const notice = useAtomValue(connectionNoticeAtom);
40
+ function SandboxRecovery({ notice }: { notice: DisplayConnectionNotice }) {
41
41
  const sandbox = useAtomValue(sandboxAtom);
42
42
  const actions = useAtomValue(sandboxActionsAtom);
43
- if (!notice) {
43
+ const sync = useAtomValue(sandboxSyncAtom);
44
+ if (notice.pending || notice.ready) {
44
45
  return null;
45
46
  }
46
47
  return (
47
- <div className="flex-1 min-h-0 overflow-auto p-4 text-sm">
48
- <div
49
- className={cn(
50
- "flex items-center gap-2",
51
- notice.pending
52
- ? "text-muted-foreground"
53
- : "font-medium text-(--red-11)",
54
- )}
55
- >
56
- {notice.pending ? (
57
- <Spinner className="size-4 shrink-0" aria-hidden={true} />
48
+ <>
49
+ <p className="mt-2 text-foreground">{notice.description}</p>
50
+ {notice.error && <SandboxErrorOutput error={notice.error} />}
51
+ <div className="flex items-center gap-2 mt-3">
52
+ <Button
53
+ variant="outline"
54
+ size="xs"
55
+ disabled={!actions || sandbox?.manifest == null}
56
+ onClick={() => actions?.editManifest()}
57
+ >
58
+ Edit manifest…
59
+ </Button>
60
+ {notice.kind === "sync" && sync.kind === "restart-required" ? (
61
+ <SandboxRestart />
58
62
  ) : (
59
- <AlertCircleIcon className="size-4 shrink-0" aria-hidden={true} />
63
+ <Button
64
+ variant="text"
65
+ size="xs"
66
+ disabled={!actions}
67
+ onClick={() => actions?.sync()}
68
+ >
69
+ Retry sync
70
+ </Button>
60
71
  )}
61
- <h2>{notice.title}</h2>
62
72
  </div>
63
- {!notice.pending && (
64
- <>
65
- <p className="mt-2 text-foreground">{notice.description}</p>
66
- {notice.error && <SandboxErrorOutput error={notice.error} />}
67
- <div className="flex items-center gap-2 mt-3">
68
- <Button
69
- variant="outline"
70
- size="xs"
71
- disabled={!actions || sandbox?.manifest == null}
72
- onClick={() => actions?.editManifest()}
73
- >
74
- Edit manifest…
75
- </Button>
76
- <Button
77
- variant="text"
78
- size="xs"
79
- disabled={!actions}
80
- onClick={() => actions?.sync()}
81
- >
82
- Retry sync
83
- </Button>
84
- </div>
85
- {sandbox?.manifest == null && (
86
- <p className="mt-3 text-xs text-muted-foreground">
87
- A manifest will be available once this new notebook starts.
88
- </p>
89
- )}
90
- </>
73
+ {sandbox?.manifest == null && (
74
+ <p className="mt-3 text-xs text-muted-foreground">
75
+ A manifest will be available once this new notebook starts.
76
+ </p>
77
+ )}
78
+ </>
79
+ );
80
+ }
81
+
82
+ function SandboxRestart() {
83
+ const restartKernel = useRestartKernel();
84
+ return (
85
+ <Button variant="text" size="xs" onClick={restartKernel}>
86
+ Restart Kernel
87
+ </Button>
88
+ );
89
+ }
90
+
91
+ function SandboxStartupPanel({ notice }: { notice: DisplayConnectionNotice }) {
92
+ const showSteps =
93
+ notice.kind === "startup" &&
94
+ (notice.ready ||
95
+ notice.phase === "preparing-environment" ||
96
+ notice.phase === "starting-kernel");
97
+ return (
98
+ <div className="min-w-0 text-sm">
99
+ {showSteps ? (
100
+ <StartupProgress notice={notice} surface="sidebar" />
101
+ ) : (
102
+ <div className="flex items-center gap-2" role="status">
103
+ <ConnectionStatusIcon notice={notice} />
104
+ <h2>{notice.title}</h2>
105
+ </div>
91
106
  )}
107
+ <SandboxRecovery notice={notice} />
92
108
  </div>
93
109
  );
94
110
  }
95
111
 
96
- export function SandboxFooter() {
97
- const sandbox = useAtomValue(sandboxAtom);
98
- const connected = useAtomValue(isConnectedAtom);
99
- const actions = useAtomValue(sandboxActionsAtom);
100
- const operation = useAtomValue(sandboxSyncAtom);
101
- const notice = useAtomValue(connectionNoticeAtom);
102
- if (!sandbox?.backend) {
112
+ function SandboxSyncStatus({ notice }: { notice: DisplayConnectionNotice }) {
113
+ return (
114
+ <div className="mb-5 text-sm">
115
+ <output className="flex items-center gap-2 text-xs">
116
+ <ConnectionStatusIcon notice={notice} />
117
+ <span>{notice.title}</span>
118
+ </output>
119
+ <SandboxRecovery notice={notice} />
120
+ </div>
121
+ );
122
+ }
123
+
124
+ function SandboxSyncDetails({
125
+ notice,
126
+ }: {
127
+ notice: DisplayConnectionNotice | null;
128
+ }) {
129
+ const operation = useAtomValue(sandboxSyncOperationAtom);
130
+ if (!operation && !notice) {
103
131
  return null;
104
132
  }
105
- const pending = operation.pending || notice?.pending;
106
- const status = operation.pending
107
- ? "Syncing sandbox…"
108
- : (notice?.title ?? (connected ? "Sandbox ready" : "Sandbox not started"));
109
133
  return (
110
- <div className="border-t p-1.5 shrink-0">
111
- <DropdownMenu>
112
- <Tooltip content={status}>
113
- <DropdownMenuTrigger asChild={true}>
114
- <button
115
- type="button"
116
- className="flex items-center gap-2 w-full px-2 py-1.5 rounded text-xs text-muted-foreground hover:bg-accent data-[state=open]:bg-accent"
117
- aria-label={`${sandbox.backend} sandbox actions`}
118
- >
119
- <span
120
- role="status"
121
- aria-label={status}
122
- className={cn(
123
- "size-1.5 shrink-0 rounded-full",
124
- pending
125
- ? "bg-amber-500 motion-safe:animate-pulse"
126
- : notice
127
- ? "bg-(--red-9)"
128
- : connected
129
- ? "bg-emerald-500"
130
- : "bg-muted-foreground",
131
- )}
132
- />
133
- <span>{sandbox.backend} sandbox</span>
134
- <span className="ml-auto">
135
- {pending
136
- ? operation.pending
137
- ? "Syncing…"
138
- : notice?.title
139
- : ""}
140
- </span>
141
- <ChevronUpIcon className="size-3" />
142
- </button>
143
- </DropdownMenuTrigger>
144
- </Tooltip>
145
- <DropdownMenuContent
146
- side="top"
147
- align="start"
148
- className="w-52"
149
- sideOffset={8}
150
- >
151
- <DropdownMenuItem
152
- disabled={pending || !actions}
153
- onSelect={() => {
154
- void actions?.sync();
155
- }}
156
- >
157
- <RefreshCwIcon className="size-3.5 mr-2" />
158
- Sync
159
- </DropdownMenuItem>
160
- <DropdownMenuItem
161
- disabled={!actions || sandbox.manifest === null}
162
- onSelect={() => {
163
- void actions?.editManifest();
164
- }}
165
- >
166
- <FileCodeIcon className="size-3.5 mr-2" />
167
- Edit manifest…
168
- </DropdownMenuItem>
169
- </DropdownMenuContent>
170
- </DropdownMenu>
134
+ <div className="mt-5 min-w-0">
135
+ {notice ? (
136
+ <SandboxSyncStatus notice={notice} />
137
+ ) : (
138
+ <p className="text-sm">Environment synced</p>
139
+ )}
140
+ <StartupOutput
141
+ key={operation?.operation_id}
142
+ logs={operation?.logs.environment ?? ""}
143
+ state={
144
+ operation?.status.kind === "running"
145
+ ? "running"
146
+ : operation?.status.kind === "succeeded"
147
+ ? "succeeded"
148
+ : "failed"
149
+ }
150
+ label="sandbox sync output"
151
+ />
152
+ </div>
153
+ );
154
+ }
155
+
156
+ export function SandboxDetails() {
157
+ const section = usePanelSection();
158
+ const connected = useAtomValue(isConnectedAtom);
159
+ const notice = useAtomValue(connectionNoticeAtom);
160
+ const preparation = useAtomValue(preparationAtom);
161
+ const progress = useAtomValue(startupProgressAtom);
162
+ const [expanded, setExpanded] = useAtom(sandboxDetailsExpandedAtom);
163
+ const startup: DisplayConnectionNotice | null =
164
+ notice?.kind === "startup"
165
+ ? { ...notice, ready: false }
166
+ : preparation || progress
167
+ ? {
168
+ kind: "startup",
169
+ sandbox: true,
170
+ title: "Notebook started",
171
+ description: "The notebook is ready to run.",
172
+ pending: false,
173
+ ready: true,
174
+ error: null,
175
+ }
176
+ : null;
177
+ return (
178
+ <section
179
+ id={`sandbox-details-${section}`}
180
+ aria-label="Sandbox details"
181
+ hidden={!expanded}
182
+ className={cn(
183
+ "min-w-0 overflow-auto p-4",
184
+ connected ? "shrink-0 max-h-[65%] border-b" : "flex-1",
185
+ )}
186
+ onPointerDownCapture={() => setExpanded(true)}
187
+ onFocusCapture={() => setExpanded(true)}
188
+ >
189
+ {notice?.kind === "connection" && (
190
+ <SandboxSyncStatus notice={{ ...notice, ready: false }} />
191
+ )}
192
+ {startup && <SandboxStartupPanel notice={startup} />}
193
+ {connected && (
194
+ <SandboxSyncDetails
195
+ notice={notice?.kind === "sync" ? { ...notice, ready: false } : null}
196
+ />
197
+ )}
198
+ {connected && (!notice || notice.pending) && <SandboxActions />}
199
+ </section>
200
+ );
201
+ }
202
+
203
+ function SandboxActions() {
204
+ const sandbox = useAtomValue(sandboxAtom);
205
+ const actions = useAtomValue(sandboxActionsAtom);
206
+ const pending = useAtomValue(sandboxSyncAtom).kind === "running";
207
+ return (
208
+ <div className="flex items-center gap-4 mt-5 border-t pt-3">
209
+ <Button
210
+ variant="text"
211
+ size="xs"
212
+ className="text-muted-foreground gap-1.5 p-0"
213
+ disabled={pending || !actions}
214
+ onClick={() => actions?.sync()}
215
+ >
216
+ <RefreshCwIcon className="size-3" aria-hidden={true} />
217
+ Sync
218
+ </Button>
219
+ <Button
220
+ variant="text"
221
+ size="xs"
222
+ className="text-muted-foreground p-0"
223
+ disabled={!actions || sandbox?.manifest == null}
224
+ onClick={() => actions?.editManifest()}
225
+ >
226
+ Edit manifest…
227
+ </Button>
171
228
  </div>
172
229
  );
173
230
  }
@@ -0,0 +1,52 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { useAtom, useAtomValue } from "jotai";
3
+ import { ChevronDownIcon } from "lucide-react";
4
+ import { connectionNoticeAtom } from "@/core/network/connection-notice";
5
+ import { sandboxAtom } from "@/core/packages/sandbox-state";
6
+ import { cn } from "@/utils/cn";
7
+ import type { PanelSection } from "./panel-context";
8
+ import { sandboxDetailsExpandedAtom } from "./sandbox-details-state";
9
+
10
+ export function SandboxToggle({ section }: { section: PanelSection }) {
11
+ const sandbox = useAtomValue(sandboxAtom);
12
+ const notice = useAtomValue(connectionNoticeAtom);
13
+ const [expanded, setExpanded] = useAtom(sandboxDetailsExpandedAtom);
14
+ if (!sandbox?.backend) {
15
+ return null;
16
+ }
17
+ const status = notice ? (notice.pending ? "pending" : "error") : "healthy";
18
+ return (
19
+ <button
20
+ type="button"
21
+ aria-label={`${sandbox.backend} sandbox`}
22
+ aria-expanded={expanded}
23
+ aria-controls={`sandbox-details-${section}`}
24
+ title={notice?.title ?? "Sandbox details"}
25
+ className={cn(
26
+ "flex items-center gap-1.5 rounded px-1.5 py-1 text-xs whitespace-nowrap hover:bg-accent focus-visible:outline-2 focus-visible:outline-ring",
27
+ status === "error" ? "text-(--red-11)" : "text-foreground",
28
+ )}
29
+ onClick={() => setExpanded(!expanded)}
30
+ >
31
+ <output
32
+ aria-label={notice?.title ?? "Sandbox healthy"}
33
+ className={cn(
34
+ "size-1.5 shrink-0 rounded-full",
35
+ status === "error"
36
+ ? "bg-(--red-9)"
37
+ : status === "pending"
38
+ ? "bg-muted-foreground motion-safe:animate-pulse"
39
+ : "bg-(--grass-9)",
40
+ )}
41
+ />
42
+ {sandbox.backend} sandbox
43
+ <ChevronDownIcon
44
+ aria-hidden={true}
45
+ className={cn(
46
+ "size-3 shrink-0 text-muted-foreground",
47
+ expanded && "rotate-180",
48
+ )}
49
+ />
50
+ </button>
51
+ );
52
+ }
@@ -34,6 +34,7 @@ import { ErrorBoundary } from "../../boundary/ErrorBoundary";
34
34
  import { raf2 } from "../../navigation/focus-utils";
35
35
  import { ContextAwarePanel } from "../panels/context-aware-panel/context-aware-panel";
36
36
  import { PanelSectionProvider } from "../panels/panel-context";
37
+ import { SandboxToggle } from "../panels/sandbox-toggle";
37
38
  import { useTheme } from "@/theme/useTheme";
38
39
  import {
39
40
  LazyAgentPanel,
@@ -389,6 +390,9 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
389
390
  {selectedPanel}
390
391
  </span>
391
392
  )}
393
+ {selectedPanel === "packages" && (
394
+ <SandboxToggle section="sidebar" />
395
+ )}
392
396
  <Button
393
397
  data-testid="close-helper-pane"
394
398
  className="m-0"
@@ -544,6 +548,9 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
544
548
  <BackendConnectionStatus />
545
549
  <LspStatus />
546
550
  <div className="flex-1" />
551
+ {selectedDeveloperPanelTab === "packages" && (
552
+ <SandboxToggle section="developer-panel" />
553
+ )}
547
554
  <Button
548
555
  size="xs"
549
556
  variant="text"