@marimo-team/islands 0.24.3-dev37 → 0.24.3-dev39
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.
- package/dist/{common-BsQjZnO0.js → common-eNTtBSo2.js} +2 -2
- package/dist/main.js +6 -2
- package/dist/{reveal-component-baBJ3nFu.js → reveal-component-CNqmC7b6.js} +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +7 -0
- package/src/components/editor/alerts/connecting-alert.tsx +1 -66
- package/src/components/editor/alerts/connection-notice.tsx +140 -0
- package/src/components/editor/app-container.tsx +9 -3
- package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +114 -7
- package/src/components/editor/chrome/panels/__tests__/sandbox-recovery.test.tsx +165 -0
- package/src/components/editor/chrome/panels/packages-panel.tsx +93 -166
- package/src/components/editor/chrome/panels/sandbox-controller.tsx +106 -0
- package/src/components/editor/chrome/panels/sandbox-panel.tsx +173 -0
- package/src/components/editor/chrome/wrapper/footer-items/backend-status.tsx +3 -1
- package/src/components/editor/header/__tests__/status.test.tsx +8 -20
- package/src/components/editor/header/status.tsx +5 -3
- package/src/components/editor/renderers/cell-array.tsx +1 -5
- package/src/core/MarimoApp.tsx +0 -2
- package/src/core/edit-app.tsx +8 -5
- package/src/core/errors/state.ts +1 -2
- package/src/core/islands/bootstrap.ts +1 -0
- package/src/core/islands/bridge.ts +3 -0
- package/src/core/network/connection-notice.ts +84 -0
- package/src/core/network/requests-lazy.ts +3 -0
- package/src/core/network/requests-network.ts +12 -0
- package/src/core/network/requests-static.ts +3 -0
- package/src/core/network/requests-toasting.tsx +3 -0
- package/src/core/network/resolve.ts +2 -1
- package/src/core/network/types.ts +7 -0
- package/src/core/packages/__tests__/usePackageAction.test.tsx +129 -0
- package/src/core/packages/__tests__/useSandboxController.test.tsx +165 -0
- package/src/core/packages/package-data.ts +35 -0
- package/src/core/packages/sandbox-state.ts +15 -0
- package/src/core/packages/toast-components.tsx +11 -0
- package/src/core/packages/useInstallPackage.ts +1 -1
- package/src/core/packages/usePackageAction.ts +51 -0
- package/src/core/packages/usePackageDependencies.ts +22 -0
- package/src/core/packages/useSandboxController.ts +188 -0
- package/src/core/run-app.tsx +2 -0
- package/src/core/wasm/bridge.ts +11 -0
- package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +212 -2
- package/src/core/websocket/types.ts +11 -1
- package/src/core/websocket/useMarimoKernelConnection.tsx +43 -8
- package/src/stories/sandbox.stories.tsx +308 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
+
import { invalidatePackageData } from "@/core/packages/package-data";
|
|
3
4
|
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
|
4
5
|
import { useRef } from "react";
|
|
5
6
|
import { useErrorBoundary } from "react-error-boundary";
|
|
@@ -240,7 +241,15 @@ export function useMarimoKernelConnection(opts: {
|
|
|
240
241
|
case "reload":
|
|
241
242
|
reloadSafe();
|
|
242
243
|
return;
|
|
244
|
+
case "startup-progress":
|
|
245
|
+
setConnection({
|
|
246
|
+
state: WebSocketState.CONNECTING,
|
|
247
|
+
phase: msg.data.phase,
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
243
250
|
case "kernel-ready": {
|
|
251
|
+
setKernelStartupError(null);
|
|
252
|
+
setConnection({ state: WebSocketState.OPEN });
|
|
244
253
|
setInitialRunCompleted(
|
|
245
254
|
Boolean(msg.data.resumed || msg.data.auto_instantiated),
|
|
246
255
|
);
|
|
@@ -363,6 +372,16 @@ export function useMarimoKernelConnection(opts: {
|
|
|
363
372
|
});
|
|
364
373
|
return;
|
|
365
374
|
case "installing-package-alert":
|
|
375
|
+
if (
|
|
376
|
+
msg.data.source !== "server" &&
|
|
377
|
+
msg.data.log_status !== "append" &&
|
|
378
|
+
msg.data.log_status !== "start" &&
|
|
379
|
+
Object.values(msg.data.packages).some(
|
|
380
|
+
(status) => status === "installed" || status === "restart-required",
|
|
381
|
+
)
|
|
382
|
+
) {
|
|
383
|
+
invalidatePackageData();
|
|
384
|
+
}
|
|
366
385
|
addPackageAlert({
|
|
367
386
|
...msg.data,
|
|
368
387
|
kind: "installing",
|
|
@@ -439,6 +458,8 @@ export function useMarimoKernelConnection(opts: {
|
|
|
439
458
|
return;
|
|
440
459
|
|
|
441
460
|
case "reconnected":
|
|
461
|
+
setKernelStartupError(null);
|
|
462
|
+
setConnection({ state: WebSocketState.OPEN });
|
|
442
463
|
return;
|
|
443
464
|
|
|
444
465
|
case "focus-cell":
|
|
@@ -482,6 +503,7 @@ export function useMarimoKernelConnection(opts: {
|
|
|
482
503
|
return;
|
|
483
504
|
}
|
|
484
505
|
shouldTryReconnecting.current = true;
|
|
506
|
+
setKernelStartupError(null);
|
|
485
507
|
setConnection({ state: WebSocketState.CONNECTING });
|
|
486
508
|
const healthy = await runtimeManager.reconcileFromHealth();
|
|
487
509
|
if (!healthy) {
|
|
@@ -513,17 +535,11 @@ export function useMarimoKernelConnection(opts: {
|
|
|
513
535
|
headers: () => runtimeManager.headers(),
|
|
514
536
|
|
|
515
537
|
/**
|
|
516
|
-
*
|
|
538
|
+
* The transport is open; kernel-ready establishes session readiness.
|
|
517
539
|
*/
|
|
518
540
|
onOpen: async () => {
|
|
519
541
|
// If we are open, we can reset our reconnecting flag.
|
|
520
542
|
shouldTryReconnecting.current = true;
|
|
521
|
-
|
|
522
|
-
// DO NOT COMMIT THIS UNCOMMENTED
|
|
523
|
-
// Uncomment to emulate a slow connection
|
|
524
|
-
// await new Promise((resolve) => setTimeout(resolve, 10_000));
|
|
525
|
-
|
|
526
|
-
setConnection({ state: WebSocketState.OPEN });
|
|
527
543
|
},
|
|
528
544
|
|
|
529
545
|
/**
|
|
@@ -565,7 +581,26 @@ export function useMarimoKernelConnection(opts: {
|
|
|
565
581
|
onClose: (e) => {
|
|
566
582
|
Logger.warn("WebSocket closed", e.code, e.reason);
|
|
567
583
|
const decision = classifyCloseEvent(e);
|
|
568
|
-
setConnection(
|
|
584
|
+
setConnection((previous) => {
|
|
585
|
+
const status = decision.status;
|
|
586
|
+
if (
|
|
587
|
+
status.state === WebSocketState.CLOSED &&
|
|
588
|
+
status.code === WebSocketClosedReason.KERNEL_STARTUP_ERROR &&
|
|
589
|
+
(previous.state === WebSocketState.CONNECTING ||
|
|
590
|
+
previous.state === WebSocketState.CLOSED)
|
|
591
|
+
) {
|
|
592
|
+
return { ...status, phase: previous.phase };
|
|
593
|
+
}
|
|
594
|
+
if (
|
|
595
|
+
status.state === WebSocketState.CONNECTING &&
|
|
596
|
+
(previous.state === WebSocketState.OPEN ||
|
|
597
|
+
(previous.state === WebSocketState.CONNECTING &&
|
|
598
|
+
previous.phase === "reconnecting"))
|
|
599
|
+
) {
|
|
600
|
+
return { ...status, phase: "reconnecting" };
|
|
601
|
+
}
|
|
602
|
+
return status;
|
|
603
|
+
});
|
|
569
604
|
if (decision.kind === "terminal" && decision.closeTransport) {
|
|
570
605
|
ws.close(); // close to prevent reconnecting
|
|
571
606
|
return;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { createStore, Provider, useAtomValue } from "jotai";
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { ConnectionNotice } from "@/components/editor/alerts/connection-notice";
|
|
6
|
+
import {
|
|
7
|
+
SandboxFooter,
|
|
8
|
+
SandboxStartupPanel,
|
|
9
|
+
} from "@/components/editor/chrome/panels/sandbox-panel";
|
|
10
|
+
import { SandboxController } from "@/components/editor/chrome/panels/sandbox-controller";
|
|
11
|
+
import { chromeAtom } from "@/components/editor/chrome/state";
|
|
12
|
+
import { Cell } from "@/components/editor/notebook-cell";
|
|
13
|
+
import { VerticalLayoutWrapper } from "@/components/editor/renderers/vertical-layout/vertical-layout-wrapper";
|
|
14
|
+
import {
|
|
15
|
+
createNotebookActions,
|
|
16
|
+
notebookAtom,
|
|
17
|
+
notebookReducer,
|
|
18
|
+
} from "@/core/cells/cells";
|
|
19
|
+
import { userConfigAtom } from "@/core/config/config";
|
|
20
|
+
import {
|
|
21
|
+
AppConfigSchema,
|
|
22
|
+
defaultUserConfig,
|
|
23
|
+
} from "@/core/config/config-schema";
|
|
24
|
+
import { kernelStartupErrorAtom } from "@/core/errors/state";
|
|
25
|
+
import { connectionAtom } from "@/core/network/connection";
|
|
26
|
+
import { requestClientAtom } from "@/core/network/requests";
|
|
27
|
+
import { createStaticRequests } from "@/core/network/requests-static";
|
|
28
|
+
import {
|
|
29
|
+
sandboxActionsAtom,
|
|
30
|
+
sandboxAtom,
|
|
31
|
+
sandboxSyncAtom,
|
|
32
|
+
} from "@/core/packages/sandbox-state";
|
|
33
|
+
import { filenameAtom } from "@/core/saving/file-state";
|
|
34
|
+
import { WebSocketClosedReason, WebSocketState } from "@/core/websocket/types";
|
|
35
|
+
import { HTTPError } from "@/utils/errors";
|
|
36
|
+
|
|
37
|
+
const manifest = `requires-python = ">=3.13"
|
|
38
|
+
dependencies = ["marimo", "polars", "rich==999.0.0"]
|
|
39
|
+
|
|
40
|
+
[tool.uv]
|
|
41
|
+
index-url = "https://pypi.org/simple"
|
|
42
|
+
|
|
43
|
+
[tool.notebook]
|
|
44
|
+
description = "Explore bike trips"
|
|
45
|
+
`;
|
|
46
|
+
const resolutionError = `No solution found when resolving dependencies:
|
|
47
|
+
Because there is no version of rich==999.0.0 and your script depends
|
|
48
|
+
on rich==999.0.0, your script's requirements are unsatisfiable.`;
|
|
49
|
+
|
|
50
|
+
interface Props {
|
|
51
|
+
surface: "notebook" | "packages" | "manifest";
|
|
52
|
+
phase:
|
|
53
|
+
| "preparing"
|
|
54
|
+
| "starting"
|
|
55
|
+
| "failed"
|
|
56
|
+
| "ready"
|
|
57
|
+
| "syncing"
|
|
58
|
+
| "sync-failed";
|
|
59
|
+
existingCells: boolean;
|
|
60
|
+
backend: "uv" | "pixi";
|
|
61
|
+
saveResult: "success" | "failure" | "stale";
|
|
62
|
+
theme: "light" | "dark";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function OpenManifest() {
|
|
66
|
+
const actions = useAtomValue(sandboxActionsAtom);
|
|
67
|
+
const opened = useRef(false);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (actions && !opened.current) {
|
|
70
|
+
opened.current = true;
|
|
71
|
+
void actions.editManifest();
|
|
72
|
+
}
|
|
73
|
+
}, [actions]);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function SandboxStory(props: Props) {
|
|
78
|
+
const [store] = useState(() => {
|
|
79
|
+
const state = createStore();
|
|
80
|
+
let currentManifest =
|
|
81
|
+
props.phase === "failed"
|
|
82
|
+
? manifest
|
|
83
|
+
: manifest.replace("rich==999.0.0", "rich>=13.0.0");
|
|
84
|
+
const config = defaultUserConfig();
|
|
85
|
+
state.set(userConfigAtom, {
|
|
86
|
+
...config,
|
|
87
|
+
display: { ...config.display, theme: props.theme },
|
|
88
|
+
});
|
|
89
|
+
state.set(filenameAtom, "bike_trips.py");
|
|
90
|
+
state.set(sandboxAtom, {
|
|
91
|
+
backend: props.backend,
|
|
92
|
+
manifest: currentManifest,
|
|
93
|
+
filename: "bike_trips.py",
|
|
94
|
+
});
|
|
95
|
+
state.set(chromeAtom, {
|
|
96
|
+
selectedPanel: "packages",
|
|
97
|
+
isSidebarOpen: props.surface !== "notebook",
|
|
98
|
+
isDeveloperPanelOpen: false,
|
|
99
|
+
selectedDeveloperPanelTab: "errors",
|
|
100
|
+
});
|
|
101
|
+
state.set(
|
|
102
|
+
connectionAtom,
|
|
103
|
+
props.phase === "preparing" || props.phase === "starting"
|
|
104
|
+
? {
|
|
105
|
+
state: WebSocketState.CONNECTING,
|
|
106
|
+
phase:
|
|
107
|
+
props.phase === "preparing"
|
|
108
|
+
? "preparing-environment"
|
|
109
|
+
: "starting-kernel",
|
|
110
|
+
}
|
|
111
|
+
: props.phase === "failed"
|
|
112
|
+
? {
|
|
113
|
+
state: WebSocketState.CLOSED,
|
|
114
|
+
code: WebSocketClosedReason.KERNEL_STARTUP_ERROR,
|
|
115
|
+
reason: "Kernel startup failed",
|
|
116
|
+
phase: "preparing-environment",
|
|
117
|
+
}
|
|
118
|
+
: { state: WebSocketState.OPEN },
|
|
119
|
+
);
|
|
120
|
+
state.set(
|
|
121
|
+
kernelStartupErrorAtom,
|
|
122
|
+
props.phase === "failed" ? resolutionError : null,
|
|
123
|
+
);
|
|
124
|
+
state.set(sandboxSyncAtom, {
|
|
125
|
+
pending: props.phase === "syncing",
|
|
126
|
+
error: props.phase === "sync-failed" ? resolutionError : null,
|
|
127
|
+
});
|
|
128
|
+
if (props.existingCells) {
|
|
129
|
+
const actions = createNotebookActions((action) =>
|
|
130
|
+
state.set(notebookAtom, (value) => notebookReducer(value, action)),
|
|
131
|
+
);
|
|
132
|
+
actions.createNewCell({
|
|
133
|
+
cellId: "__end__",
|
|
134
|
+
before: false,
|
|
135
|
+
code: 'import polars as pl\n\ntrips = pl.read_csv("bike_trips.csv")\ntrips.head()',
|
|
136
|
+
autoFocus: false,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
state.set(requestClientAtom, {
|
|
140
|
+
...createStaticRequests(),
|
|
141
|
+
getSandbox: async () => ({
|
|
142
|
+
backend: props.backend,
|
|
143
|
+
manifest: currentManifest,
|
|
144
|
+
filename: "bike_trips.py",
|
|
145
|
+
}),
|
|
146
|
+
updateManifest: async ({ contents }) => {
|
|
147
|
+
if (props.saveResult === "stale") {
|
|
148
|
+
throw new HTTPError(
|
|
149
|
+
409,
|
|
150
|
+
"The manifest changed on disk. Reopen it before saving.",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
currentManifest = contents;
|
|
154
|
+
return {
|
|
155
|
+
backend: props.backend,
|
|
156
|
+
manifest: currentManifest,
|
|
157
|
+
filename: "bike_trips.py",
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
syncSandbox: async () => {
|
|
161
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
162
|
+
if (props.saveResult === "failure") {
|
|
163
|
+
return {
|
|
164
|
+
success: false,
|
|
165
|
+
error:
|
|
166
|
+
"Failed to download rich from https://pypi.org/simple/rich/\n Request failed after 3 retries: connection timed out",
|
|
167
|
+
restartRequired: false,
|
|
168
|
+
reconnect: false,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
state.set(connectionAtom, { state: WebSocketState.OPEN });
|
|
172
|
+
state.set(kernelStartupErrorAtom, null);
|
|
173
|
+
return {
|
|
174
|
+
success: true,
|
|
175
|
+
error: null,
|
|
176
|
+
restartRequired: false,
|
|
177
|
+
reconnect: false,
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
return state;
|
|
182
|
+
});
|
|
183
|
+
const appConfig = AppConfigSchema.parse({ width: "full" });
|
|
184
|
+
const reconnect = async () => {
|
|
185
|
+
store.set(connectionAtom, { state: WebSocketState.OPEN });
|
|
186
|
+
};
|
|
187
|
+
return (
|
|
188
|
+
<Provider store={store}>
|
|
189
|
+
<SandboxController onReconnect={reconnect} />
|
|
190
|
+
{props.surface === "manifest" && <OpenManifest />}
|
|
191
|
+
<div
|
|
192
|
+
data-testid="sandbox-story"
|
|
193
|
+
className="bg-background text-foreground"
|
|
194
|
+
style={{
|
|
195
|
+
width: props.surface === "notebook" ? 820 : 400,
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
{props.surface === "notebook" && (
|
|
199
|
+
<>
|
|
200
|
+
<ConnectionNotice appConfig={appConfig} onRetry={reconnect} />
|
|
201
|
+
{store.get(notebookAtom).cellIds.inOrderIds.map((cellId) => (
|
|
202
|
+
<VerticalLayoutWrapper key={cellId} appConfig={appConfig}>
|
|
203
|
+
<Cell
|
|
204
|
+
cellId={cellId}
|
|
205
|
+
theme={props.theme}
|
|
206
|
+
mode="edit"
|
|
207
|
+
showPlaceholder={false}
|
|
208
|
+
canDelete={true}
|
|
209
|
+
isCollapsed={false}
|
|
210
|
+
collapseCount={0}
|
|
211
|
+
canMoveX={false}
|
|
212
|
+
userConfig={store.get(userConfigAtom)}
|
|
213
|
+
/>
|
|
214
|
+
</VerticalLayoutWrapper>
|
|
215
|
+
))}
|
|
216
|
+
</>
|
|
217
|
+
)}
|
|
218
|
+
{props.surface === "packages" &&
|
|
219
|
+
(props.phase === "failed" ||
|
|
220
|
+
props.phase === "sync-failed" ||
|
|
221
|
+
props.phase === "preparing" ? (
|
|
222
|
+
<SandboxStartupPanel />
|
|
223
|
+
) : (
|
|
224
|
+
<SandboxFooter />
|
|
225
|
+
))}
|
|
226
|
+
</div>
|
|
227
|
+
</Provider>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const meta = {
|
|
232
|
+
title: "Sandbox/States",
|
|
233
|
+
component: SandboxStory,
|
|
234
|
+
parameters: { layout: "centered" },
|
|
235
|
+
args: {
|
|
236
|
+
surface: "notebook",
|
|
237
|
+
phase: "preparing",
|
|
238
|
+
existingCells: true,
|
|
239
|
+
backend: "uv",
|
|
240
|
+
saveResult: "success",
|
|
241
|
+
theme: "light",
|
|
242
|
+
},
|
|
243
|
+
render: (args, context) => (
|
|
244
|
+
<SandboxStory
|
|
245
|
+
key={JSON.stringify([args, context.globals.theme])}
|
|
246
|
+
{...args}
|
|
247
|
+
theme={context.globals.theme === "dark" ? "dark" : "light"}
|
|
248
|
+
/>
|
|
249
|
+
),
|
|
250
|
+
} satisfies Meta<typeof SandboxStory>;
|
|
251
|
+
export default meta;
|
|
252
|
+
type Story = StoryObj<typeof meta>;
|
|
253
|
+
|
|
254
|
+
export const ExistingNotebookPreparing: Story = {
|
|
255
|
+
name: "Startup notice / preparing",
|
|
256
|
+
};
|
|
257
|
+
export const EmptyNotebookPreparing: Story = {
|
|
258
|
+
name: "Empty notebook / preparing",
|
|
259
|
+
args: { existingCells: false },
|
|
260
|
+
};
|
|
261
|
+
export const StartingKernel: Story = {
|
|
262
|
+
name: "Startup notice / starting kernel",
|
|
263
|
+
args: { phase: "starting" },
|
|
264
|
+
};
|
|
265
|
+
export const ExistingNotebookFailed: Story = {
|
|
266
|
+
name: "Startup notice / failed",
|
|
267
|
+
args: { phase: "failed" },
|
|
268
|
+
};
|
|
269
|
+
export const EmptyNotebookFailed: Story = {
|
|
270
|
+
name: "Empty notebook / failed",
|
|
271
|
+
args: { phase: "failed", existingCells: false },
|
|
272
|
+
};
|
|
273
|
+
export const PackagesPreparing: Story = {
|
|
274
|
+
name: "Setup details / preparing",
|
|
275
|
+
args: { surface: "packages" },
|
|
276
|
+
};
|
|
277
|
+
export const PackagesFailed: Story = {
|
|
278
|
+
name: "Setup details / failed",
|
|
279
|
+
args: { surface: "packages", phase: "failed" },
|
|
280
|
+
};
|
|
281
|
+
export const PackagesReady: Story = {
|
|
282
|
+
name: "Status row / ready",
|
|
283
|
+
args: { surface: "packages", phase: "ready" },
|
|
284
|
+
};
|
|
285
|
+
export const PackagesSyncing: Story = {
|
|
286
|
+
name: "Status row / syncing",
|
|
287
|
+
args: { surface: "packages", phase: "syncing" },
|
|
288
|
+
};
|
|
289
|
+
export const PackagesSyncFailed: Story = {
|
|
290
|
+
name: "Setup details / sync failed",
|
|
291
|
+
args: { surface: "packages", phase: "sync-failed" },
|
|
292
|
+
};
|
|
293
|
+
export const PixiPackagesReady: Story = {
|
|
294
|
+
name: "Status row / pixi",
|
|
295
|
+
args: { surface: "packages", phase: "ready", backend: "pixi" },
|
|
296
|
+
};
|
|
297
|
+
export const ManifestEditor: Story = {
|
|
298
|
+
name: "Manifest / edit",
|
|
299
|
+
args: { surface: "manifest", phase: "failed" },
|
|
300
|
+
};
|
|
301
|
+
export const ManifestSyncFails: Story = {
|
|
302
|
+
name: "Manifest / failed save",
|
|
303
|
+
args: { surface: "manifest", phase: "failed", saveResult: "failure" },
|
|
304
|
+
};
|
|
305
|
+
export const ManifestChangedOnDisk: Story = {
|
|
306
|
+
name: "Manifest / changed on disk",
|
|
307
|
+
args: { surface: "manifest", phase: "failed", saveResult: "stale" },
|
|
308
|
+
};
|