@marimo-team/frontend 0.24.3-dev82 → 0.24.3-dev83
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/assets/{ConnectedDataExplorerComponent-DkTZ7AvX.js → ConnectedDataExplorerComponent-BpquZl-D.js} +1 -1
- package/dist/assets/{ErrorBoundary-DTpbqvzY.js → ErrorBoundary-BQqmHnXk.js} +2 -2
- package/dist/assets/{LazyAnyLanguageCodeMirror-DYsZfkSk.js → LazyAnyLanguageCodeMirror-CvxfNkWH.js} +2 -2
- package/dist/assets/{RunButton-IMtEkRXV.js → RunButton-DNkIh9-L.js} +8 -8
- package/dist/assets/{abnfDiagram-VCTEODGH-pTFf_XLR.js → abnfDiagram-VCTEODGH-CnUMzJx3.js} +1 -1
- package/dist/assets/add-cell-with-ai-BMP2uVIa.js +76 -0
- package/dist/assets/{agent-panel-DxU4qgxk.js → agent-panel-45n6y1yx.js} +5 -5
- package/dist/assets/ai-model-dropdown-DHCy-cih.js +3 -0
- package/dist/assets/{any-language-editor-DBp5CC3w.js → any-language-editor-C4RQC2Cx.js} +1 -1
- package/dist/assets/{app-config-button-BjxXAmKd.js → app-config-button-_8eL7EEp.js} +1 -1
- package/dist/assets/{architectureDiagram-5GKGNRK7-C7t0AR-N.js → architectureDiagram-5GKGNRK7-bfcUxn_j.js} +1 -1
- package/dist/assets/arrow-down-BFFrjzad.js +1 -0
- package/dist/assets/{blockDiagram-I7D4REHJ-DZgXohrV.js → blockDiagram-I7D4REHJ-BpF8yI-R.js} +1 -1
- package/dist/assets/{cache-panel-DDlfv8BP.js → cache-panel-BR4yhYeu.js} +1 -1
- package/dist/assets/cells-BcQ8Tm3j.js +220 -0
- package/dist/assets/{chat-display-CHxh_MGC.js → chat-display-C_qIajXO.js} +1 -1
- package/dist/assets/{chat-panel-Ca6yjxkg.js → chat-panel-DZ_Wgf9u.js} +3 -3
- package/dist/assets/{chat-ui-D5zW1hYM.js → chat-ui-BrP572Cm.js} +1 -1
- package/dist/assets/{chunk-IMKFNOWR-CAmhQu-F.js → chunk-IMKFNOWR-C-rPN2GF.js} +1 -1
- package/dist/assets/{chunk-SHT3W25Y-BqEFXoRq.js → chunk-SHT3W25Y-P5kn0hWZ.js} +1 -1
- package/dist/assets/{chunk-SVP7TREG-Dxu0eRzD.js → chunk-SVP7TREG-BDSmMBZT.js} +1 -1
- package/dist/assets/{chunk-TICWLB2K-gHNMAWa9.js → chunk-TICWLB2K-D3dtJ1vz.js} +1 -1
- package/dist/assets/{classDiagram-ZZMXUADV-DE3i3SzX.js → classDiagram-ZZMXUADV-DHIDyAFc.js} +1 -1
- package/dist/assets/{classDiagram-v2-VYDZK3BY-DE3i3SzX.js → classDiagram-v2-VYDZK3BY-DHIDyAFc.js} +1 -1
- package/dist/assets/{code-visibility-kUtIY_sH.js → code-visibility-DB0T__on.js} +1 -1
- package/dist/assets/{command-palette-BZJRR34P.js → command-palette-DvYu24TH.js} +1 -1
- package/dist/assets/{common-DtiDLSXB.js → common-C53kupuv.js} +1 -1
- package/dist/assets/{components-DScgyZ8j.js → components-BrsyCK82.js} +1 -1
- package/dist/assets/{components-CHh1MGbL.js → components-BuvKvfSS.js} +1 -1
- package/dist/assets/config-CmBpjGXk.js +1 -0
- package/dist/assets/{cynefinDiagram-5FMLGOSQ-C8RK-PLH.js → cynefinDiagram-5FMLGOSQ-CYdbFzEV.js} +1 -1
- package/dist/assets/{dagre-GXQ25YYZ-DZ6JGcBM.js → dagre-GXQ25YYZ-CSEPMX9I.js} +1 -1
- package/dist/assets/dagre-SOh4Qguh.js +1 -0
- package/dist/assets/{data-grid-overlay-editor-BkZfO-uG.js → data-grid-overlay-editor-ierQMJA6.js} +1 -1
- package/dist/assets/dependency-graph-panel-DBaLCinF.js +5 -0
- package/dist/assets/{diagram-S7CK7UJ4-BN77DDKM.js → diagram-S7CK7UJ4-D5YIr9T4.js} +1 -1
- package/dist/assets/{diagram-UQ7AKVKN-BZthP0T4.js → diagram-UQ7AKVKN-uqarxJJM.js} +1 -1
- package/dist/assets/{diagram-VX7I27RA-D3GF3lRq.js → diagram-VX7I27RA-HcvMY8pK.js} +1 -1
- package/dist/assets/{diagram-Z3DM3KII-DnsKITpy.js → diagram-Z3DM3KII-fJYqqcs8.js} +1 -1
- package/dist/assets/{documentation-panel-D8fz9UDV.js → documentation-panel-LhtR1Gdz.js} +1 -1
- package/dist/assets/{ebnfDiagram-PWID7BFC-CHqqdhbQ.js → ebnfDiagram-PWID7BFC-z1dFbGgp.js} +1 -1
- package/dist/assets/edit-page-B28h9Fb6.js +176 -0
- package/dist/assets/{erDiagram-RLTQ6QDP-ChDyfc3w.js → erDiagram-RLTQ6QDP-D0QTGZaN.js} +1 -1
- package/dist/assets/{error-entries-Dy7E9tUS.js → error-entries-C-LFkdXb.js} +1 -1
- package/dist/assets/{error-panel-FYd4Tvsc.js → error-panel-JrTKWez-.js} +1 -1
- package/dist/assets/{feedback-button-Zjt-Mi38.js → feedback-button-CXZUuGO0.js} +2 -2
- package/dist/assets/file-explorer-panel-0uckfsy9.js +1 -0
- package/dist/assets/{file-name-input-DifonhZ7.js → file-name-input-B1oP4qI1.js} +1 -1
- package/dist/assets/{flowDiagram-HODETNUW-CelwDl0Y.js → flowDiagram-HODETNUW-CvnIcAyp.js} +1 -1
- package/dist/assets/{gallery-page-C0T1oyzr.js → gallery-page-Dr6gmPct.js} +1 -1
- package/dist/assets/{glide-data-editor-Dqd-if6R.js → glide-data-editor-BdNlPATo.js} +4 -4
- package/dist/assets/{graphlib-6_f7N0YM.js → graphlib-coUtpRhg.js} +1 -1
- package/dist/assets/has-HvSiby4C.js +1 -0
- package/dist/assets/home-page-DU_vk7YP.js +4 -0
- package/dist/assets/index-C0mx13kv.css +2 -0
- package/dist/assets/index-anJECncb.js +62 -0
- package/dist/assets/{infoDiagram-27XIBGKW-Dh0ztpYZ.js → infoDiagram-27XIBGKW-B-ESiKKL.js} +1 -1
- package/dist/assets/{ishikawaDiagram-5VMMS53U-Yy11Lj-8.js → ishikawaDiagram-5VMMS53U-958453Qs.js} +1 -1
- package/dist/assets/{kanban-definition-UXKFOSKX-_hCm9ITM.js → kanban-definition-UXKFOSKX-Cl3MvgdV.js} +1 -1
- package/dist/assets/{logs-panel-D_Ki0PRi.js → logs-panel-B6dW8QcS.js} +1 -1
- package/dist/assets/{mermaid-DUZOkwYg.js → mermaid-Dk7Zo_kD.js} +4 -4
- package/dist/assets/{mindmap-definition-YA3MSWOX-CqKVab9Z.js → mindmap-definition-YA3MSWOX-ObRzpp8l.js} +1 -1
- package/dist/assets/{number-overlay-editor-BFwgibS5.js → number-overlay-editor-BaA6YHpZ.js} +1 -1
- package/dist/assets/{outline-panel-BVLrHsJa.js → outline-panel-AF6eqkBP.js} +1 -1
- package/dist/assets/packages-panel-DnQBjrbw.js +1 -0
- package/dist/assets/panel-context-BPaQeOuV.js +1 -0
- package/dist/assets/panels-Gl9xHiUG.js +33 -0
- package/dist/assets/{pegDiagram-XKGWAZYB-Cq-QrQFc.js → pegDiagram-XKGWAZYB-Dr1N-uz9.js} +1 -1
- package/dist/assets/{pieDiagram-E7YTZNPT-DXU0ndOt.js → pieDiagram-E7YTZNPT-CuOB3ooB.js} +1 -1
- package/dist/assets/{process-output-B76bPsnA.js → process-output-dY19eMY7.js} +1 -1
- package/dist/assets/{railroadDiagram-O6MQD6OU-CFI_pgSK.js → railroadDiagram-O6MQD6OU-B-2q9lGc.js} +1 -1
- package/dist/assets/{requirementDiagram-BXWQKSXE-DgdygEWz.js → requirementDiagram-BXWQKSXE-DDO3d-q-.js} +1 -1
- package/dist/assets/{reveal-component-BqiZOsW3.js → reveal-component-CVZuomdL.js} +3 -3
- package/dist/assets/run-page-q9RV38Se.js +1 -0
- package/dist/assets/scratchpad-panel-CB7Fy9xQ.js +1 -0
- package/dist/assets/scroll-to-bottom-button-BSCkDXet.js +1 -0
- package/dist/assets/{secrets-panel-CeovmJAu.js → secrets-panel-fyK3DrMo.js} +1 -1
- package/dist/assets/{sequenceDiagram-WJ2MYXX4-Cq7P3SPg.js → sequenceDiagram-WJ2MYXX4-09ej9W_c.js} +1 -1
- package/dist/assets/session-panel-CQR7EQYe.js +1 -0
- package/dist/assets/snippets-panel-C8pCQY6-.js +1 -0
- package/dist/assets/{stateDiagram-D77RDMKH-CO8GuJdh.js → stateDiagram-D77RDMKH-Dwreh_3X.js} +1 -1
- package/dist/assets/{stateDiagram-v2-MP3YSRHH-DA7CflqT.js → stateDiagram-v2-MP3YSRHH-CYctHUHC.js} +1 -1
- package/dist/assets/{swimlanes-42K2YHIH-Bkf4BWja.js → swimlanes-42K2YHIH-CqtUaFZN.js} +1 -1
- package/dist/assets/{swimlanesDiagram-VR7AAH4N-Bf0FHwi-.js → swimlanesDiagram-VR7AAH4N-BTvyGqcs.js} +1 -1
- package/dist/assets/{swiper-component-Dcb47cI1.js → swiper-component-BPjhKuFC.js} +1 -1
- package/dist/assets/{terminal-bvd8BLSi.js → terminal-CWmhjUVd.js} +2 -2
- package/dist/assets/{timeline-definition-24CTP7MA-CWdT681e.js → timeline-definition-24CTP7MA-DESfjZe6.js} +1 -1
- package/dist/assets/tracing-DHQdLJrw.js +1 -0
- package/dist/assets/{tracing-panel-AQUl8b58.js → tracing-panel-DkDpfzxp.js} +2 -2
- package/dist/assets/{useCellActionButton-DIiKKxoN.js → useCellActionButton-JBMxiVrN.js} +1 -1
- package/dist/assets/{vega-component-C5LY21OO.js → vega-component-C3dIfNSQ.js} +1 -1
- package/dist/assets/{vega-embed-container-C6Hr9CZj.js → vega-embed-container-DRnOQFW3.js} +1 -1
- package/dist/assets/{vennDiagram-4TSXK5OY-B3uJF3gV.js → vennDiagram-4TSXK5OY-DVP0MtbH.js} +1 -1
- package/dist/assets/{wardleyDiagram-VM6X3IG4-DGmcLh5J.js → wardleyDiagram-VM6X3IG4-Boyg2AlL.js} +1 -1
- package/dist/assets/{xychartDiagram-S5SC5T6Z-kFxqIAk2.js → xychartDiagram-S5SC5T6Z-6MNoagFi.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +1 -1
- package/src/components/editor/alerts/__tests__/connection-notice.test.tsx +186 -0
- package/src/components/editor/alerts/__tests__/startup-progress.test.tsx +185 -0
- package/src/components/editor/alerts/connection-notice.tsx +100 -54
- package/src/components/editor/alerts/startup-output.tsx +173 -0
- package/src/components/editor/alerts/startup-progress.tsx +204 -0
- package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +53 -4
- package/src/components/editor/chrome/panels/__tests__/sandbox-details.test.tsx +358 -0
- package/src/components/editor/chrome/panels/__tests__/sandbox-recovery.test.tsx +5 -2
- package/src/components/editor/chrome/panels/packages-panel.tsx +32 -21
- package/src/components/editor/chrome/panels/sandbox-details-state.ts +38 -0
- package/src/components/editor/chrome/panels/sandbox-panel.tsx +188 -131
- package/src/components/editor/chrome/panels/sandbox-toggle.tsx +52 -0
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +7 -0
- package/src/components/editor/package-alert.tsx +112 -66
- package/src/core/alerts/__tests__/environment.test.ts +183 -0
- package/src/core/alerts/__tests__/state.test.tsx +240 -0
- package/src/core/alerts/environment.ts +57 -0
- package/src/core/alerts/state.ts +155 -99
- package/src/core/islands/bootstrap.ts +2 -1
- package/src/core/kernel/messages.ts +2 -2
- package/src/core/network/connection-notice.ts +45 -12
- package/src/core/network/connection.ts +19 -0
- package/src/core/network/useConnectionNotice.ts +53 -0
- package/src/core/packages/__tests__/useSandboxController.test.tsx +86 -5
- package/src/core/packages/sandbox-state.ts +56 -4
- package/src/core/packages/useSandboxController.ts +27 -7
- package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +199 -8
- package/src/core/websocket/types.ts +1 -1
- package/src/core/websocket/useMarimoKernelConnection.tsx +31 -19
- package/src/stories/sandbox.stories.tsx +230 -50
- package/dist/assets/add-cell-with-ai-CSNC0MIy.js +0 -76
- package/dist/assets/ai-model-dropdown-Ct4CTWKe.js +0 -3
- package/dist/assets/cells-C0itXtA7.js +0 -220
- package/dist/assets/config-CBLBAU5c.js +0 -1
- package/dist/assets/dagre-Be6hV3Qt.js +0 -1
- package/dist/assets/dependency-graph-panel-DhGTFsO9.js +0 -5
- package/dist/assets/edit-page-Cw1A5Rhq.js +0 -176
- package/dist/assets/file-explorer-panel-BpklXFSu.js +0 -1
- package/dist/assets/has-D5vyh6J_.js +0 -1
- package/dist/assets/home-page-CN05bwXu.js +0 -4
- package/dist/assets/index-B8OSEJVD.css +0 -2
- package/dist/assets/index-CcaMC2ZX.js +0 -62
- package/dist/assets/packages-panel-cq-qnvM5.js +0 -1
- package/dist/assets/panels-DwxpnIFf.js +0 -33
- package/dist/assets/run-page-DVqnr-8x.js +0 -1
- package/dist/assets/scratchpad-panel-BT2lF-Gs.js +0 -1
- package/dist/assets/scroll-to-bottom-button-D4qWtyN8.js +0 -1
- package/dist/assets/session-panel-CGddxX8c.js +0 -1
- package/dist/assets/snippets-panel-UawnfExC.js +0 -1
- package/dist/assets/tracing-BbV840Cl.js +0 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
// @vitest-environment jsdom
|
|
3
|
+
|
|
4
|
+
import { act, renderHook } from "@testing-library/react";
|
|
5
|
+
import { createStore, Provider } from "jotai";
|
|
6
|
+
import { expect, it } from "vitest";
|
|
7
|
+
import type { EnvironmentOperation } from "../environment";
|
|
8
|
+
import { alertAtom, getPackageAlert, useAlertActions } from "../state";
|
|
9
|
+
|
|
10
|
+
it.each(["install", "remove"] as const)(
|
|
11
|
+
"restores active %s progress without announcing old successes",
|
|
12
|
+
(action) => {
|
|
13
|
+
const store = createStore();
|
|
14
|
+
const { result } = renderHook(() => useAlertActions(), {
|
|
15
|
+
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
|
|
16
|
+
});
|
|
17
|
+
const operation: EnvironmentOperation = {
|
|
18
|
+
operation_id: "package",
|
|
19
|
+
action,
|
|
20
|
+
source: "kernel",
|
|
21
|
+
status: { kind: "succeeded" },
|
|
22
|
+
packages: { numpy: "succeeded" },
|
|
23
|
+
logs: { numpy: "Done\n" },
|
|
24
|
+
};
|
|
25
|
+
const snapshot = (value: EnvironmentOperation) =>
|
|
26
|
+
result.current.setEnvironment({
|
|
27
|
+
source: value.source,
|
|
28
|
+
state: { restart_required: false, operations: [value] },
|
|
29
|
+
});
|
|
30
|
+
act(() => snapshot(operation));
|
|
31
|
+
expect(getPackageAlert(store.get(alertAtom))).toBeNull();
|
|
32
|
+
expect(store.get(alertAtom).environments.kernel.operations).toEqual([
|
|
33
|
+
operation,
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const active: EnvironmentOperation = {
|
|
37
|
+
...operation,
|
|
38
|
+
status: { kind: "running" },
|
|
39
|
+
packages: { numpy: "running" },
|
|
40
|
+
logs: { numpy: "Working\n" },
|
|
41
|
+
};
|
|
42
|
+
act(() => snapshot(active));
|
|
43
|
+
expect(getPackageAlert(store.get(alertAtom))).toEqual({
|
|
44
|
+
...active,
|
|
45
|
+
id: active.operation_id,
|
|
46
|
+
kind: "environment",
|
|
47
|
+
restartRequired: false,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Reconnecting can complete work that was already visible on this page.
|
|
51
|
+
act(() => snapshot(operation));
|
|
52
|
+
expect(getPackageAlert(store.get(alertAtom))).toEqual({
|
|
53
|
+
...operation,
|
|
54
|
+
id: operation.operation_id,
|
|
55
|
+
kind: "environment",
|
|
56
|
+
restartRequired: false,
|
|
57
|
+
});
|
|
58
|
+
act(() => result.current.clearPackageAlert(operation.operation_id));
|
|
59
|
+
act(() => snapshot(operation));
|
|
60
|
+
expect(getPackageAlert(store.get(alertAtom))).toBeNull();
|
|
61
|
+
|
|
62
|
+
// A live result still gives feedback for a new package action.
|
|
63
|
+
act(() =>
|
|
64
|
+
result.current.updateEnvironment({
|
|
65
|
+
...operation,
|
|
66
|
+
operation_id: "new",
|
|
67
|
+
log_mode: "replace",
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
expect(getPackageAlert(store.get(alertAtom))?.id).toBe("new");
|
|
71
|
+
|
|
72
|
+
// Earlier changes can still require a restart after a later action succeeds.
|
|
73
|
+
act(() => result.current.clearPackageAlert("new"));
|
|
74
|
+
act(() =>
|
|
75
|
+
result.current.setEnvironment({
|
|
76
|
+
source: operation.source,
|
|
77
|
+
state: { restart_required: true, operations: [operation] },
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
expect(getPackageAlert(store.get(alertAtom))).toEqual({
|
|
81
|
+
...operation,
|
|
82
|
+
id: operation.operation_id,
|
|
83
|
+
kind: "environment",
|
|
84
|
+
restartRequired: true,
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
it.each(["prepare", "sync"] as const)(
|
|
90
|
+
"retains %s progress without turning it into a package alert",
|
|
91
|
+
(action) => {
|
|
92
|
+
const store = createStore();
|
|
93
|
+
const { result } = renderHook(() => useAlertActions(), {
|
|
94
|
+
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
|
|
95
|
+
});
|
|
96
|
+
const operation: EnvironmentOperation = {
|
|
97
|
+
operation_id: "sandbox",
|
|
98
|
+
action,
|
|
99
|
+
source: "kernel",
|
|
100
|
+
status: { kind: "succeeded" },
|
|
101
|
+
packages: {},
|
|
102
|
+
logs: { environment: "Environment ready\n" },
|
|
103
|
+
};
|
|
104
|
+
const environment = { restart_required: false, operations: [operation] };
|
|
105
|
+
|
|
106
|
+
// A refreshed browser restores the result without announcing it again.
|
|
107
|
+
act(() =>
|
|
108
|
+
result.current.setEnvironment({ source: "kernel", state: environment }),
|
|
109
|
+
);
|
|
110
|
+
expect(getPackageAlert(store.get(alertAtom))).toBeNull();
|
|
111
|
+
expect(store.get(alertAtom).environments.kernel).toEqual(environment);
|
|
112
|
+
|
|
113
|
+
const install: EnvironmentOperation = {
|
|
114
|
+
...operation,
|
|
115
|
+
operation_id: "install",
|
|
116
|
+
action: "install",
|
|
117
|
+
status: { kind: "running" },
|
|
118
|
+
packages: { numpy: "running" },
|
|
119
|
+
logs: { numpy: "Installing\n" },
|
|
120
|
+
};
|
|
121
|
+
act(() =>
|
|
122
|
+
result.current.updateEnvironment({ ...install, log_mode: "replace" }),
|
|
123
|
+
);
|
|
124
|
+
const packageAlert = getPackageAlert(store.get(alertAtom));
|
|
125
|
+
for (const status of [
|
|
126
|
+
{ kind: "running" },
|
|
127
|
+
{ kind: "failed", error: "Sync failed" },
|
|
128
|
+
] as const) {
|
|
129
|
+
act(() =>
|
|
130
|
+
result.current.updateEnvironment({
|
|
131
|
+
...operation,
|
|
132
|
+
status,
|
|
133
|
+
log_mode: "replace",
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
expect(getPackageAlert(store.get(alertAtom))).toEqual(packageAlert);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
act(() => result.current.clearPackageAlert(install.operation_id));
|
|
140
|
+
act(() =>
|
|
141
|
+
result.current.updateEnvironment({ ...operation, log_mode: "replace" }),
|
|
142
|
+
);
|
|
143
|
+
expect(getPackageAlert(store.get(alertAtom))).toBeNull();
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
it.each(["kernel", "server"] as const)(
|
|
148
|
+
"preserves the selected %s result across snapshots and follows new work",
|
|
149
|
+
(source) => {
|
|
150
|
+
const store = createStore();
|
|
151
|
+
const { result } = renderHook(() => useAlertActions(), {
|
|
152
|
+
wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
|
|
153
|
+
});
|
|
154
|
+
const selected: EnvironmentOperation = {
|
|
155
|
+
operation_id: "selected",
|
|
156
|
+
action: "install",
|
|
157
|
+
source,
|
|
158
|
+
status: { kind: "failed", error: "Network unavailable" },
|
|
159
|
+
packages: { numpy: "failed" },
|
|
160
|
+
logs: { numpy: "Selected logs\n" },
|
|
161
|
+
};
|
|
162
|
+
const other: EnvironmentOperation = {
|
|
163
|
+
...selected,
|
|
164
|
+
operation_id: "other",
|
|
165
|
+
source: source === "kernel" ? "server" : "kernel",
|
|
166
|
+
status: { kind: "succeeded" },
|
|
167
|
+
packages: { numpy: "succeeded" },
|
|
168
|
+
logs: { numpy: "Other logs\n" },
|
|
169
|
+
};
|
|
170
|
+
const snapshot = (operation: EnvironmentOperation) =>
|
|
171
|
+
result.current.setEnvironment({
|
|
172
|
+
source: operation.source,
|
|
173
|
+
state: { restart_required: false, operations: [operation] },
|
|
174
|
+
});
|
|
175
|
+
act(() => snapshot(selected));
|
|
176
|
+
for (const operations of [
|
|
177
|
+
[selected, other],
|
|
178
|
+
[other, selected],
|
|
179
|
+
]) {
|
|
180
|
+
act(() => operations.forEach(snapshot));
|
|
181
|
+
expect(getPackageAlert(store.get(alertAtom))).toEqual({
|
|
182
|
+
...selected,
|
|
183
|
+
id: selected.operation_id,
|
|
184
|
+
kind: "environment",
|
|
185
|
+
restartRequired: false,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Live work in the other environment should still take over the banner.
|
|
190
|
+
const active: EnvironmentOperation = {
|
|
191
|
+
...other,
|
|
192
|
+
operation_id: "new-work",
|
|
193
|
+
status: { kind: "running" },
|
|
194
|
+
packages: { numpy: "running" },
|
|
195
|
+
logs: { numpy: "Installing\n" },
|
|
196
|
+
};
|
|
197
|
+
act(() =>
|
|
198
|
+
result.current.updateEnvironment({ ...active, log_mode: "replace" }),
|
|
199
|
+
);
|
|
200
|
+
act(() => snapshot(selected));
|
|
201
|
+
expect(getPackageAlert(store.get(alertAtom))?.id).toBe(active.operation_id);
|
|
202
|
+
act(() =>
|
|
203
|
+
result.current.updateEnvironment({
|
|
204
|
+
...active,
|
|
205
|
+
status: { kind: "succeeded" },
|
|
206
|
+
packages: { numpy: "succeeded" },
|
|
207
|
+
logs: {},
|
|
208
|
+
log_mode: "append",
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
act(() => snapshot(selected));
|
|
212
|
+
expect(getPackageAlert(store.get(alertAtom))).toEqual({
|
|
213
|
+
...active,
|
|
214
|
+
id: active.operation_id,
|
|
215
|
+
kind: "environment",
|
|
216
|
+
restartRequired: false,
|
|
217
|
+
status: { kind: "succeeded" },
|
|
218
|
+
packages: { numpy: "succeeded" },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Replacing that environment with an empty snapshot removes its selection.
|
|
222
|
+
act(() =>
|
|
223
|
+
result.current.setEnvironment({
|
|
224
|
+
source: active.source,
|
|
225
|
+
state: { restart_required: false, operations: [] },
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
expect(getPackageAlert(store.get(alertAtom))?.id).toBe(
|
|
229
|
+
selected.operation_id,
|
|
230
|
+
);
|
|
231
|
+
act(() =>
|
|
232
|
+
result.current.updateEnvironment({
|
|
233
|
+
...other,
|
|
234
|
+
operation_id: "latest-result",
|
|
235
|
+
log_mode: "replace",
|
|
236
|
+
}),
|
|
237
|
+
);
|
|
238
|
+
expect(getPackageAlert(store.get(alertAtom))?.id).toBe("latest-result");
|
|
239
|
+
},
|
|
240
|
+
);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { NotificationMessageData, schemas } from "@/core/kernel/messages";
|
|
4
|
+
|
|
5
|
+
export type EnvironmentState = schemas["EnvironmentState"];
|
|
6
|
+
export type EnvironmentOperation = schemas["EnvironmentOperation"];
|
|
7
|
+
export type EnvironmentSource = EnvironmentOperation["source"];
|
|
8
|
+
|
|
9
|
+
export function emptyEnvironmentState(): EnvironmentState {
|
|
10
|
+
return { restart_required: false, operations: [] };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Apply a live delta after the connection's authoritative snapshot. */
|
|
14
|
+
export function reduceEnvironmentState(
|
|
15
|
+
state: EnvironmentState,
|
|
16
|
+
update: NotificationMessageData<"environment-operation">,
|
|
17
|
+
): EnvironmentState {
|
|
18
|
+
const previous = state.operations.find(
|
|
19
|
+
(operation) => operation.operation_id === update.operation_id,
|
|
20
|
+
);
|
|
21
|
+
const logs = { ...previous?.logs };
|
|
22
|
+
for (const [name, content] of Object.entries(update.logs)) {
|
|
23
|
+
logs[name] =
|
|
24
|
+
update.log_mode === "replace" ? content : (logs[name] ?? "") + content;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const operation: EnvironmentOperation = {
|
|
28
|
+
operation_id: update.operation_id,
|
|
29
|
+
action: update.action,
|
|
30
|
+
status: update.status,
|
|
31
|
+
packages: update.packages,
|
|
32
|
+
logs,
|
|
33
|
+
source: update.source,
|
|
34
|
+
};
|
|
35
|
+
const operations = state.operations.filter(
|
|
36
|
+
(item) =>
|
|
37
|
+
(previous && update.status.kind === "running") ||
|
|
38
|
+
item.status.kind === "running" ||
|
|
39
|
+
(item.action === "prepare" && update.action !== "prepare"),
|
|
40
|
+
);
|
|
41
|
+
const index = operations.findIndex(
|
|
42
|
+
(item) => item.operation_id === update.operation_id,
|
|
43
|
+
);
|
|
44
|
+
if (index === -1) {
|
|
45
|
+
operations.push(operation);
|
|
46
|
+
} else {
|
|
47
|
+
operations[index] = operation;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
restart_required:
|
|
52
|
+
state.restart_required ||
|
|
53
|
+
update.status.kind === "restart-required" ||
|
|
54
|
+
Object.values(update.packages).includes("restart-required"),
|
|
55
|
+
operations,
|
|
56
|
+
};
|
|
57
|
+
}
|
package/src/core/alerts/state.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { useAtomValue } from "jotai";
|
|
4
|
-
import type {
|
|
4
|
+
import type { NotificationMessageData } from "@/core/kernel/messages";
|
|
5
5
|
import { createReducerAndAtoms } from "@/utils/createReducer";
|
|
6
6
|
import { generateUUID } from "@/utils/uuid";
|
|
7
|
+
import {
|
|
8
|
+
emptyEnvironmentState,
|
|
9
|
+
type EnvironmentOperation,
|
|
10
|
+
type EnvironmentSource,
|
|
11
|
+
type EnvironmentState,
|
|
12
|
+
reduceEnvironmentState,
|
|
13
|
+
} from "./environment";
|
|
7
14
|
|
|
8
15
|
type Identified<T> = { id: string } & T;
|
|
9
16
|
|
|
@@ -14,12 +21,18 @@ export interface MissingPackageAlert {
|
|
|
14
21
|
source?: "kernel" | "server";
|
|
15
22
|
}
|
|
16
23
|
|
|
17
|
-
export interface
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
export interface EnvironmentOperationAlert extends EnvironmentOperation {
|
|
25
|
+
action: "install" | "remove";
|
|
26
|
+
kind: "environment";
|
|
27
|
+
restartRequired: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isPackageOperation(
|
|
31
|
+
operation: EnvironmentOperation,
|
|
32
|
+
): operation is EnvironmentOperation & {
|
|
33
|
+
action: EnvironmentOperationAlert["action"];
|
|
34
|
+
} {
|
|
35
|
+
return operation.action === "install" || operation.action === "remove";
|
|
23
36
|
}
|
|
24
37
|
|
|
25
38
|
export interface StartupLogsAlert {
|
|
@@ -28,118 +41,161 @@ export interface StartupLogsAlert {
|
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
export function isMissingPackageAlert(
|
|
31
|
-
alert: MissingPackageAlert |
|
|
44
|
+
alert: MissingPackageAlert | EnvironmentOperationAlert,
|
|
32
45
|
): alert is MissingPackageAlert {
|
|
33
46
|
return alert.kind === "missing";
|
|
34
47
|
}
|
|
35
48
|
|
|
36
|
-
export function
|
|
37
|
-
alert: MissingPackageAlert |
|
|
38
|
-
): alert is
|
|
39
|
-
return alert.kind === "
|
|
49
|
+
export function isEnvironmentOperationAlert(
|
|
50
|
+
alert: MissingPackageAlert | EnvironmentOperationAlert,
|
|
51
|
+
): alert is EnvironmentOperationAlert {
|
|
52
|
+
return alert.kind === "environment";
|
|
40
53
|
}
|
|
41
54
|
|
|
42
|
-
/** Prominent alerts.
|
|
43
|
-
*
|
|
44
|
-
* Right now we only have one type of alert.
|
|
45
|
-
*/
|
|
46
55
|
interface AlertState {
|
|
47
56
|
packageAlert:
|
|
48
57
|
| Identified<MissingPackageAlert>
|
|
49
|
-
|
|
|
58
|
+
| { kind: "environment"; id: string; source: EnvironmentSource }
|
|
50
59
|
| null;
|
|
60
|
+
environments: Record<EnvironmentSource, EnvironmentState>;
|
|
51
61
|
startupLogsAlert: StartupLogsAlert | null;
|
|
52
|
-
packageLogs: { [packageName: string]: string };
|
|
53
62
|
}
|
|
54
63
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// No default
|
|
94
|
-
}
|
|
64
|
+
function setEnvironment(
|
|
65
|
+
state: AlertState,
|
|
66
|
+
source: EnvironmentSource,
|
|
67
|
+
environment: EnvironmentState,
|
|
68
|
+
): AlertState {
|
|
69
|
+
// Sandbox preparation and sync use the existing sandbox UI.
|
|
70
|
+
const environments = { ...state.environments, [source]: environment };
|
|
71
|
+
const operations = Object.values(environments)
|
|
72
|
+
.flatMap((item) => item.operations)
|
|
73
|
+
.filter(isPackageOperation);
|
|
74
|
+
const incoming = environment.operations.filter(isPackageOperation);
|
|
75
|
+
const alert = state.packageAlert;
|
|
76
|
+
const selected =
|
|
77
|
+
alert?.kind === "environment"
|
|
78
|
+
? operations.find(
|
|
79
|
+
(item) =>
|
|
80
|
+
item.operation_id === alert.id && item.source === alert.source,
|
|
81
|
+
)
|
|
82
|
+
: undefined;
|
|
83
|
+
// A snapshot restores work and unresolved issues, not old success banners.
|
|
84
|
+
const needsAttention = (operation: EnvironmentOperation) =>
|
|
85
|
+
operation.status.kind !== "succeeded" ||
|
|
86
|
+
environments[operation.source].restart_required;
|
|
87
|
+
const operation =
|
|
88
|
+
(selected?.status.kind === "running" ? selected : undefined) ??
|
|
89
|
+
incoming.findLast((item) => item.status.kind === "running") ??
|
|
90
|
+
operations.findLast((item) => item.status.kind === "running") ??
|
|
91
|
+
selected ??
|
|
92
|
+
incoming.findLast(needsAttention) ??
|
|
93
|
+
operations.findLast(needsAttention);
|
|
94
|
+
return {
|
|
95
|
+
...state,
|
|
96
|
+
environments,
|
|
97
|
+
packageAlert: operation
|
|
98
|
+
? {
|
|
99
|
+
kind: "environment",
|
|
100
|
+
id: operation.operation_id,
|
|
101
|
+
source: operation.source,
|
|
95
102
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
: state.packageAlert?.kind === "missing"
|
|
104
|
+
? state.packageAlert
|
|
105
|
+
: null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
100
108
|
|
|
101
|
-
|
|
109
|
+
export const { valueAtom: alertAtom, useActions: useAlertActions } =
|
|
110
|
+
createReducerAndAtoms(
|
|
111
|
+
(): AlertState => ({
|
|
112
|
+
packageAlert: null,
|
|
113
|
+
startupLogsAlert: null,
|
|
114
|
+
environments: {
|
|
115
|
+
kernel: emptyEnvironmentState(),
|
|
116
|
+
server: emptyEnvironmentState(),
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
{
|
|
120
|
+
addMissingPackageAlert: (state, alert: MissingPackageAlert) => ({
|
|
102
121
|
...state,
|
|
103
|
-
packageAlert: { id:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
packageAlert: { id: generateUUID(), ...alert },
|
|
123
|
+
}),
|
|
124
|
+
|
|
125
|
+
updateEnvironment: (
|
|
126
|
+
state,
|
|
127
|
+
update: NotificationMessageData<"environment-operation">,
|
|
128
|
+
) => {
|
|
129
|
+
const source = update.source;
|
|
130
|
+
const environment = reduceEnvironmentState(
|
|
131
|
+
state.environments[source],
|
|
132
|
+
update,
|
|
133
|
+
);
|
|
134
|
+
if (!isPackageOperation(update)) {
|
|
135
|
+
return {
|
|
136
|
+
...state,
|
|
137
|
+
environments: { ...state.environments, [source]: environment },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return setEnvironment(
|
|
141
|
+
{
|
|
142
|
+
...state,
|
|
143
|
+
packageAlert: {
|
|
144
|
+
kind: "environment",
|
|
145
|
+
id: update.operation_id,
|
|
146
|
+
source,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
source,
|
|
150
|
+
environment,
|
|
151
|
+
);
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
setEnvironment: (
|
|
155
|
+
state,
|
|
156
|
+
snapshot: NotificationMessageData<"environment-state">,
|
|
157
|
+
) => setEnvironment(state, snapshot.source, snapshot.state),
|
|
158
|
+
|
|
159
|
+
clearPackageAlert: (state, id: string) =>
|
|
160
|
+
state.packageAlert?.id === id
|
|
161
|
+
? { ...state, packageAlert: null }
|
|
162
|
+
: state,
|
|
163
|
+
|
|
164
|
+
addStartupLog: (
|
|
165
|
+
state,
|
|
166
|
+
logData: { content: string; status: "append" | "start" | "done" },
|
|
167
|
+
) => ({
|
|
120
168
|
...state,
|
|
121
169
|
startupLogsAlert: {
|
|
122
|
-
|
|
123
|
-
content: prevContent + logData.content,
|
|
170
|
+
content: (state.startupLogsAlert?.content ?? "") + logData.content,
|
|
124
171
|
status: logData.status,
|
|
125
172
|
},
|
|
126
|
-
}
|
|
127
|
-
},
|
|
173
|
+
}),
|
|
128
174
|
|
|
129
|
-
|
|
130
|
-
return { ...state, startupLogsAlert: null };
|
|
175
|
+
clearStartupLogsAlert: (state) => ({ ...state, startupLogsAlert: null }),
|
|
131
176
|
},
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
export function getPackageAlert(
|
|
180
|
+
state: AlertState,
|
|
181
|
+
): Identified<MissingPackageAlert | EnvironmentOperationAlert> | null {
|
|
182
|
+
const alert = state.packageAlert;
|
|
183
|
+
if (alert?.kind !== "environment") {
|
|
184
|
+
return alert;
|
|
185
|
+
}
|
|
186
|
+
const environment = state.environments[alert.source];
|
|
187
|
+
const operation = environment.operations.find(
|
|
188
|
+
(item) => item.operation_id === alert.id,
|
|
189
|
+
);
|
|
190
|
+
return operation && isPackageOperation(operation)
|
|
191
|
+
? { ...operation, ...alert, restartRequired: environment.restart_required }
|
|
192
|
+
: null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function useAlerts() {
|
|
196
|
+
const state = useAtomValue(alertAtom);
|
|
197
|
+
return {
|
|
198
|
+
packageAlert: getPackageAlert(state),
|
|
199
|
+
startupLogsAlert: state.startupLogsAlert,
|
|
200
|
+
};
|
|
145
201
|
}
|
|
@@ -181,7 +181,8 @@ function handleMessage(
|
|
|
181
181
|
// Unsupported operations in islands mode
|
|
182
182
|
case "banner":
|
|
183
183
|
case "missing-package-alert":
|
|
184
|
-
case "
|
|
184
|
+
case "environment-operation":
|
|
185
|
+
case "environment-state":
|
|
185
186
|
case "completion-result":
|
|
186
187
|
case "reload":
|
|
187
188
|
case "focus-cell":
|
|
@@ -36,8 +36,8 @@ export type CompletionResultMessage =
|
|
|
36
36
|
export type HumanReadableStatus = schemas["HumanReadableStatus"];
|
|
37
37
|
export type FunctionCallResultMessage =
|
|
38
38
|
NotificationMessageData<"function-call-result">;
|
|
39
|
-
export type
|
|
40
|
-
schemas["
|
|
39
|
+
export type PackageOperationStatus =
|
|
40
|
+
schemas["EnvironmentOperationNotification"]["packages"];
|
|
41
41
|
export type DataColumnPreview = NotificationMessageData<"data-column-preview">;
|
|
42
42
|
export type SQLTablePreview = NotificationMessageData<"sql-table-preview">;
|
|
43
43
|
export type SQLTableListPreview =
|