@exagone313/dsh-podman 0.2.0-rc.4 → 0.2.0

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 (186) hide show
  1. package/README.md +12 -0
  2. package/README.zh.md +14 -2
  3. package/cordis.patch.yml +58 -0
  4. package/dist/approval-reasons.d.ts +3 -4
  5. package/dist/approval-reasons.js +40 -49
  6. package/dist/approval-reasons.test.js +43 -15
  7. package/dist/approval.js +27 -10
  8. package/dist/approval.test.js +165 -34
  9. package/dist/card-route.d.ts +3 -2
  10. package/dist/card-route.js +103 -35
  11. package/dist/card-route.test.js +60 -43
  12. package/dist/card-test-support.js +39 -66
  13. package/dist/client/ContainerCard.d.ts +1 -1
  14. package/dist/client/ContainerCard.js +76 -70
  15. package/dist/client/card-protocol.d.ts +3 -3
  16. package/dist/client/container-card-caches.js +11 -6
  17. package/dist/client/container-card-controller.d.ts +13 -18
  18. package/dist/client/container-card-controller.js +86 -40
  19. package/dist/client/container-card-create-modal.d.ts +2 -2
  20. package/dist/client/container-card-create-modal.js +13 -5
  21. package/dist/client/container-card-default-env.d.ts +10 -0
  22. package/dist/client/container-card-default-env.js +69 -0
  23. package/dist/client/container-card-directory.d.ts +2 -2
  24. package/dist/client/container-card-directory.js +12 -4
  25. package/dist/client/container-card-editors.d.ts +3 -3
  26. package/dist/client/container-card-editors.js +104 -32
  27. package/dist/client/container-card-images.d.ts +4 -4
  28. package/dist/client/container-card-images.js +19 -6
  29. package/dist/client/container-card-paths.d.ts +3 -3
  30. package/dist/client/container-card-paths.js +18 -11
  31. package/dist/client/container-card-row.d.ts +10 -6
  32. package/dist/client/container-card-row.js +82 -13
  33. package/dist/client/container-card-secrets.d.ts +3 -3
  34. package/dist/client/container-card-secrets.js +8 -6
  35. package/dist/client/container-card-shared.d.ts +4 -17
  36. package/dist/client/container-card-shared.js +9 -25
  37. package/dist/client/container-card-styles.d.ts +2 -8
  38. package/dist/client/container-card-styles.js +14 -52
  39. package/dist/client/container-card-volumes.d.ts +2 -2
  40. package/dist/client/container-card-volumes.js +9 -5
  41. package/dist/client/container-card-workspace.d.ts +8 -6
  42. package/dist/client/container-card-workspace.js +9 -3
  43. package/dist/client/directory-picker.js +2 -1
  44. package/dist/client/index.js +13109 -2117
  45. package/dist/client/locales.d.ts +6 -1
  46. package/dist/client/locales.js +88 -38
  47. package/dist/client/podman-terminal-guide.d.ts +11 -0
  48. package/dist/client/podman-terminal-guide.js +208 -0
  49. package/dist/client/podman-terminal-title.d.ts +7 -0
  50. package/dist/client/podman-terminal-title.js +30 -0
  51. package/dist/client/podman-terminal.d.ts +26 -0
  52. package/dist/client/podman-terminal.js +412 -0
  53. package/dist/client/read-only-approval.d.ts +2 -2
  54. package/dist/client/read-only-approval.js +13 -3
  55. package/dist/client/slot-contract.d.ts +0 -11
  56. package/dist/client/terminal-preference.d.ts +26 -0
  57. package/dist/client/terminal-preference.js +67 -0
  58. package/dist/client/terminal-protocol.d.ts +85 -0
  59. package/dist/client/terminal-protocol.js +14 -0
  60. package/dist/client/terminal-tab.d.ts +60 -0
  61. package/dist/client/terminal-tab.js +35 -0
  62. package/dist/client/terminal-targets.d.ts +33 -0
  63. package/dist/client/terminal-targets.js +65 -0
  64. package/dist/client/terminal-titles.d.ts +23 -0
  65. package/dist/client/terminal-titles.js +52 -0
  66. package/dist/client/terminal-transport.d.ts +63 -0
  67. package/dist/client/terminal-transport.js +207 -0
  68. package/dist/client/tool-views.js +215 -49
  69. package/dist/container-env.d.ts +11 -0
  70. package/dist/container-env.js +96 -0
  71. package/dist/container-env.test.d.ts +1 -0
  72. package/dist/container-env.test.js +93 -0
  73. package/dist/containers.test.js +196 -18
  74. package/dist/daemons.test.js +18 -4
  75. package/dist/dsh-version.js +2 -1
  76. package/dist/env-rows.d.ts +14 -0
  77. package/dist/env-rows.js +52 -0
  78. package/dist/env-rows.test.d.ts +1 -0
  79. package/dist/env-rows.test.js +43 -0
  80. package/dist/fs-provider.d.ts +1 -0
  81. package/dist/fs-provider.js +26 -12
  82. package/dist/fs-provider.test.js +51 -10
  83. package/dist/generated/version.d.ts +2 -2
  84. package/dist/generated/version.js +2 -2
  85. package/dist/grpc/proto/dshctl/v1/control.proto +8 -1
  86. package/dist/grpc/proto/dshguest/v1/guest.proto +5 -0
  87. package/dist/grpc/runtime-client.d.ts +3 -1
  88. package/dist/grpc/runtime-client.js +90 -17
  89. package/dist/guest-rpc.d.ts +15 -19
  90. package/dist/guest-rpc.js +289 -70
  91. package/dist/guest-rpc.test.d.ts +1 -0
  92. package/dist/guest-rpc.test.js +241 -0
  93. package/dist/guest-terminal.d.ts +30 -0
  94. package/dist/guest-terminal.js +196 -0
  95. package/dist/images.test.js +10 -3
  96. package/dist/index.d.ts +11 -16
  97. package/dist/index.js +42 -36
  98. package/dist/locales.test.d.ts +1 -0
  99. package/dist/locales.test.js +34 -0
  100. package/dist/misc.test.js +38 -10
  101. package/dist/mount-enums.js +2 -1
  102. package/dist/mount-input.d.ts +2 -0
  103. package/dist/mount-input.js +38 -22
  104. package/dist/mounts.test.js +81 -11
  105. package/dist/naming.test.d.ts +1 -0
  106. package/dist/naming.test.js +28 -0
  107. package/dist/output-reader.js +36 -4
  108. package/dist/package-deps.test.d.ts +1 -0
  109. package/dist/package-deps.test.js +49 -0
  110. package/dist/paths.test.js +4 -2
  111. package/dist/plugin-meta.test.d.ts +1 -0
  112. package/dist/plugin-meta.test.js +29 -0
  113. package/dist/project-path.js +33 -7
  114. package/dist/project-path.test.js +14 -0
  115. package/dist/prompts.d.ts +0 -4
  116. package/dist/prompts.js +15 -84
  117. package/dist/read-only-shell.js +3 -1
  118. package/dist/read-only-shell.test.js +80 -25
  119. package/dist/runtime-client.test.d.ts +1 -0
  120. package/dist/runtime-client.test.js +53 -0
  121. package/dist/secrets.test.js +21 -6
  122. package/dist/settings-commands.test.js +61 -14
  123. package/dist/settings-create.test.js +32 -10
  124. package/dist/settings-default-env.test.d.ts +1 -0
  125. package/dist/settings-default-env.test.js +175 -0
  126. package/dist/settings-mounts.test.js +46 -7
  127. package/dist/settings-schema.d.ts +6 -11
  128. package/dist/settings-schema.js +4 -8
  129. package/dist/settings-schema.test.d.ts +1 -0
  130. package/dist/settings-schema.test.js +35 -0
  131. package/dist/settings-workspaces.test.js +36 -63
  132. package/dist/spill-store.d.ts +1 -0
  133. package/dist/spill-store.js +17 -4
  134. package/dist/spill-store.test.js +31 -8
  135. package/dist/subprocess.d.ts +4 -0
  136. package/dist/subprocess.js +148 -162
  137. package/dist/subprocess.test.js +118 -8
  138. package/dist/terminal-preference.test.d.ts +1 -0
  139. package/dist/terminal-preference.test.js +62 -0
  140. package/dist/terminal-route.d.ts +10 -0
  141. package/dist/terminal-route.js +270 -0
  142. package/dist/terminal-route.test.d.ts +1 -0
  143. package/dist/terminal-route.test.js +265 -0
  144. package/dist/terminal-sessions.d.ts +69 -0
  145. package/dist/terminal-sessions.js +252 -0
  146. package/dist/terminal-shells.d.ts +22 -0
  147. package/dist/terminal-shells.js +99 -0
  148. package/dist/terminal-tab.test.d.ts +1 -0
  149. package/dist/terminal-tab.test.js +58 -0
  150. package/dist/terminal-targets.test.d.ts +1 -0
  151. package/dist/terminal-targets.test.js +43 -0
  152. package/dist/terminal-titles.test.d.ts +1 -0
  153. package/dist/terminal-titles.test.js +36 -0
  154. package/dist/test-support.d.ts +35 -5
  155. package/dist/test-support.js +91 -29
  156. package/dist/tool-defs.js +19 -4
  157. package/dist/tool-handlers.js +80 -28
  158. package/dist/tool-params.js +19 -5
  159. package/dist/volumes.test.js +1 -1
  160. package/dist/workspace-binding.d.ts +18 -9
  161. package/dist/workspace-binding.js +244 -77
  162. package/dist/workspace-binding.test.js +189 -25
  163. package/docs/architecture.md +62 -21
  164. package/docs/architecture.zh.md +57 -13
  165. package/docs/configuration.md +61 -20
  166. package/docs/configuration.zh.md +53 -19
  167. package/docs/development-prompt.md +72 -0
  168. package/docs/development-prompt.zh.md +69 -0
  169. package/docs/development.md +141 -21
  170. package/docs/development.zh.md +125 -17
  171. package/docs/install-dsh-and-dsh-podman.md +21 -8
  172. package/docs/install-dsh-and-dsh-podman.zh.md +18 -8
  173. package/docs/uninstall.md +83 -0
  174. package/docs/uninstall.zh.md +79 -0
  175. package/docs/update.md +120 -0
  176. package/docs/update.zh.md +114 -0
  177. package/docs/usage.md +108 -61
  178. package/docs/usage.zh.md +76 -38
  179. package/locale/en.json +6 -0
  180. package/locale/zh.json +6 -0
  181. package/package.json +58 -25
  182. package/quadlet/dsh-podman-orchestrator.container +46 -0
  183. package/quadlet/dsh.container +35 -0
  184. package/LICENSE.pkg +0 -17103
  185. package/dist/preferences.d.ts +0 -6
  186. package/dist/preferences.js +0 -27
@@ -5,7 +5,6 @@ import { test } from "node:test";
5
5
  import { strict as assert } from "node:assert";
6
6
  import { CARD_PATH } from "./client/card-protocol.js";
7
7
  import { registerCardRoute } from "./card-route.js";
8
- import { installContainerPreferences } from "./preferences.js";
9
8
  function fakeResolver() {
10
9
  const calls = [];
11
10
  const resolver = {
@@ -19,8 +18,9 @@ function fakeResolver() {
19
18
  calls.push([method, request]);
20
19
  if (method === "getVersion")
21
20
  return { version: "9.9.9", commit: "abc" };
22
- if (method === "listContainers")
21
+ if (method === "listContainers") {
23
22
  return { containers: [{ containerName: "c1" }] };
23
+ }
24
24
  if (method === "listImages")
25
25
  return { images: [{ imageId: "img1" }] };
26
26
  if (method === "listWorkspaces")
@@ -29,8 +29,9 @@ function fakeResolver() {
29
29
  return { volumes: [{ name: "data" }] };
30
30
  if (method === "listSecrets")
31
31
  return { secrets: [{ name: "s1" }] };
32
- if (method === "listCaches")
32
+ if (method === "listCaches") {
33
33
  return { caches: [{ manager: "pacman", files: 1, bytes: "1024" }] };
34
+ }
34
35
  if (method === "cleanCaches")
35
36
  return { removedFiles: 2 };
36
37
  if (method === "removeContainer")
@@ -44,7 +45,7 @@ function fakeRouteContext() {
44
45
  const routes = [];
45
46
  const ctx = {
46
47
  inject(deps, callback) {
47
- assert.deepEqual(deps, ["connection", "settings"]);
48
+ assert.deepEqual(deps, ["connection"]);
48
49
  callback({
49
50
  connection: {
50
51
  fetch: {
@@ -59,10 +60,6 @@ function fakeRouteContext() {
59
60
  },
60
61
  },
61
62
  },
62
- settings: {
63
- register: () => ({ watch: () => () => { } }),
64
- get: () => undefined,
65
- },
66
63
  });
67
64
  },
68
65
  };
@@ -94,7 +91,15 @@ test("the card route serves the live snapshot on GET", async () => {
94
91
  assert.deepEqual(snapshot.volumes, [{ name: "data" }]);
95
92
  assert.deepEqual(snapshot.secrets, [{ name: "s1" }]);
96
93
  assert.equal(snapshot.caches[0].bytes, 1024);
97
- assert.deepEqual(calls.map(([method]) => method), ["getVersion", "listContainers", "listImages", "listWorkspaces", "listVolumes", "listSecrets", "listCaches"]);
94
+ assert.deepEqual(calls.map(([method]) => method), [
95
+ "getVersion",
96
+ "listContainers",
97
+ "listImages",
98
+ "listWorkspaces",
99
+ "listVolumes",
100
+ "listSecrets",
101
+ "listCaches",
102
+ ]);
98
103
  assert.equal(typeof snapshot.dshVersion, "string");
99
104
  assert.equal(snapshot.orchestratorVersion, "9.9.9");
100
105
  assert.equal(snapshot.versionState, "major-mismatch", "a differing major is surfaced on the card");
@@ -129,9 +134,51 @@ test("the card route runs one command per POST and returns its notice", async ()
129
134
  }));
130
135
  assert.equal(remove.status, 200);
131
136
  assert.deepEqual(await remove.json(), {});
132
- assert.deepEqual(calls.find(([method]) => method === "removeContainer")?.[1], {
137
+ // A command that names no container keeps the orchestrator's default; one that
138
+ // names a container must reach it, or a row's Remove would hit another one.
139
+ const removeNamed = await routes[0].fetch(cardRequest({
140
+ method: "POST",
141
+ body: JSON.stringify({
142
+ command: { op: "remove", workspace: "w1", container: "foo" },
143
+ }),
144
+ }));
145
+ assert.equal(removeNamed.status, 200);
146
+ const removeCalls = calls.filter(([method]) => method === "removeContainer");
147
+ assert.deepEqual(removeCalls[0]?.[1], { workspaceSlug: "w1" });
148
+ assert.deepEqual(removeCalls[1]?.[1], {
149
+ workspaceSlug: "w1",
150
+ container: "foo",
151
+ });
152
+ const recreate = await routes[0].fetch(cardRequest({
153
+ method: "POST",
154
+ body: JSON.stringify({
155
+ command: {
156
+ op: "recreate",
157
+ workspace: "w1",
158
+ container: "foo",
159
+ image: "img1",
160
+ },
161
+ }),
162
+ }));
163
+ assert.equal(recreate.status, 200);
164
+ assert.deepEqual(calls.find(([method]) => method === "recreateContainer")?.[1], {
133
165
  workspaceSlug: "w1",
166
+ container: "foo",
167
+ imageId: "img1",
134
168
  });
169
+ const detach = await routes[0].fetch(cardRequest({
170
+ method: "POST",
171
+ body: JSON.stringify({
172
+ command: {
173
+ op: "container_secret_remove",
174
+ workspace: "w1",
175
+ container: "foo",
176
+ secretEnvName: "TOKEN",
177
+ },
178
+ }),
179
+ }));
180
+ assert.equal(detach.status, 200);
181
+ assert.deepEqual(calls.find(([method]) => method === "removeContainerSecret")?.[1], { workspaceSlug: "w1", container: "foo", env: "TOKEN" });
135
182
  const clean = await routes[0].fetch(cardRequest({
136
183
  method: "POST",
137
184
  body: JSON.stringify({ command: { op: "cache_clean", cacheMode: "all" } }),
@@ -170,37 +217,7 @@ test("the card route reports a failed command as an error", async () => {
170
217
  body: JSON.stringify({ command: { op: "remove", workspace: "w1" } }),
171
218
  }));
172
219
  assert.equal(response.status, 500);
173
- assert.deepEqual(await response.json(), { error: "control plane unavailable" });
174
- });
175
- test("installContainerPreferences seeds the base and syncs the resolver config", () => {
176
- let base;
177
- let watcher;
178
- const ctx = {
179
- inject(deps, callback) {
180
- assert.deepEqual(deps, ["settings"]);
181
- callback({
182
- settings: {
183
- register(_ns, _schema, options) {
184
- base = options.base;
185
- return {
186
- watch(callback) {
187
- watcher = callback;
188
- return () => { };
189
- },
190
- };
191
- },
192
- },
193
- });
194
- },
195
- };
196
- const configs = [];
197
- const resolver = {
198
- getConfig: () => ({ defaultImage: "archlinux", socketsRoot: "/run/dsh-podman" }),
199
- setConfig: (patch) => configs.push(patch),
200
- };
201
- installContainerPreferences(ctx, resolver);
202
- assert.deepEqual(base, { defaultImage: "archlinux", socketsRoot: "/run/dsh-podman" });
203
- assert.ok(watcher !== undefined);
204
- watcher({ defaultImage: "ubuntu", socketsRoot: "/run/other" });
205
- assert.deepEqual(configs, [{ defaultImage: "ubuntu", socketsRoot: "/run/other" }]);
220
+ assert.deepEqual(await response.json(), {
221
+ error: "control plane unavailable",
222
+ });
206
223
  });
@@ -6,8 +6,6 @@
6
6
  // `runCommand` and folds the real `cardSnapshot` back into the fake scope, so
7
7
  // the command tests stay focused on the command-to-control mapping. The HTTP
8
8
  // layer itself is covered by card-route.test.ts.
9
- import { strict as assert } from "node:assert";
10
- import { CONTAINER_NS, settingsSchema } from "./settings-schema.js";
11
9
  import { cardSnapshot, runCommand } from "./card-route.js";
12
10
  export function fakeScope(initial) {
13
11
  const scope = {
@@ -30,26 +28,14 @@ export function fakeScope(initial) {
30
28
  return scope;
31
29
  }
32
30
  export function fakeContext(scope) {
33
- return {
34
- inject(deps, callback) {
35
- assert.deepEqual(deps, ["settings"]);
36
- callback({
37
- settings: {
38
- register() {
39
- return scope;
40
- },
41
- get() {
42
- return undefined;
43
- },
44
- },
45
- });
46
- },
47
- };
31
+ // The host half no longer registers a settings namespace: the card's
32
+ // preferences are volatile plugin-config fields, so the fake document is
33
+ // handed to the driver directly.
34
+ return { fakeScope: scope };
48
35
  }
49
36
  export function baseValue() {
50
37
  return {
51
38
  defaultImage: "archlinux",
52
- socketsRoot: "/run/dsh-podman",
53
39
  uiLocale: "",
54
40
  notice: "",
55
41
  command: null,
@@ -61,62 +47,49 @@ export function baseValue() {
61
47
  caches: [],
62
48
  };
63
49
  }
64
- // installCardCommandDriver wires a fake settings scope to the real command
50
+ // installCardCommandDriver wires a fake settings document to the real command
65
51
  // runner and snapshot builder, standing in for the browser transport: it
66
52
  // mirrors what the card does per request — run the command, then refresh the
67
53
  // live snapshot and record the notice.
68
54
  export function installCardCommandDriver(ctx, resolver, registry) {
69
- let installed = Promise.resolve();
70
- ctx.inject(["settings"], (sctx) => {
71
- const scope = sctx.settings.register(CONTAINER_NS, settingsSchema, {
72
- base: {
73
- defaultImage: resolver.getConfig().defaultImage,
74
- socketsRoot: resolver.getConfig().socketsRoot,
75
- },
76
- });
77
- const refresh = async (notice = "") => {
78
- let view = {};
55
+ const scope = ctx.fakeScope;
56
+ const refresh = async (notice = "") => {
57
+ let view = {};
58
+ try {
59
+ const snapshot = await cardSnapshot(resolver, registry);
60
+ view = {
61
+ workspaces: snapshot.workspaces,
62
+ containers: snapshot.containers,
63
+ images: snapshot.images,
64
+ volumes: snapshot.volumes,
65
+ secrets: snapshot.secrets,
66
+ caches: snapshot.caches,
67
+ };
68
+ }
69
+ catch {
70
+ // Command tests do not always stub every list call.
71
+ }
72
+ await scope.update({ ...view, notice });
73
+ };
74
+ scope.watch((next) => {
75
+ const command = next.command;
76
+ if (command === null || command === undefined)
77
+ return;
78
+ void (async () => {
79
+ // Clear the command before running it, so the refresh below cannot
80
+ // re-deliver it.
81
+ await scope.update({ command: null });
82
+ let notice = "";
79
83
  try {
80
- const snapshot = await cardSnapshot(resolver, registry);
81
- view = {
82
- workspaces: snapshot.workspaces,
83
- containers: snapshot.containers,
84
- images: snapshot.images,
85
- volumes: snapshot.volumes,
86
- secrets: snapshot.secrets,
87
- caches: snapshot.caches,
88
- };
84
+ notice = await runCommand(ctx, resolver, registry, command);
89
85
  }
90
- catch {
91
- // Command tests do not always stub every list call.
86
+ catch (error) {
87
+ notice = error instanceof Error ? error.message : String(error);
92
88
  }
93
- await scope.update({ ...view, notice });
94
- };
95
- scope.watch((next) => {
96
- resolver.setConfig({
97
- defaultImage: next.defaultImage,
98
- socketsRoot: next.socketsRoot,
99
- });
100
- const command = next.command;
101
- if (command === null || command === undefined)
102
- return;
103
- void (async () => {
104
- // Clear the command before running it, so the refresh below cannot
105
- // re-deliver it.
106
- await scope.update({ command: null });
107
- let notice = "";
108
- try {
109
- notice = await runCommand(sctx, resolver, registry, command);
110
- }
111
- catch (error) {
112
- notice = error instanceof Error ? error.message : String(error);
113
- }
114
- await refresh(notice);
115
- })();
116
- });
117
- installed = refresh();
89
+ await refresh(notice);
90
+ })();
118
91
  });
119
- return installed;
92
+ return refresh();
120
93
  }
121
94
  export async function installedMountScope() {
122
95
  const scope = fakeScope(baseValue());
@@ -2,5 +2,5 @@ import { type ContainerCardFace } from "./container-card-controller.js";
2
2
  import { NS } from "./locales.js";
3
3
  import { type InjectFace, type PropsLocale, type PropsRuntime } from "@deepseek-ai/dsh-client-ui-slots";
4
4
  import { type ReactNode } from "react";
5
- export type ContainerCardProps = PropsRuntime<"settings.plugin.item"> & PropsLocale<typeof NS> & InjectFace<ContainerCardFace>;
5
+ export type ContainerCardProps = PropsRuntime<"plugins.bundle.config"> & PropsLocale<typeof NS> & InjectFace<ContainerCardFace>;
6
6
  export declare function ContainerCard(props: ContainerCardProps): ReactNode;
@@ -2,22 +2,21 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
3
3
  //
4
4
  // SPDX-License-Identifier: MIT
5
- import { BaseImageRow, ImageBuildModal, ImageItem } from "./container-card-images.js";
5
+ import { BaseImageRow, ImageBuildModal, ImageItem, } from "./container-card-images.js";
6
6
  import { CachesSection } from "./container-card-caches.js";
7
+ import { DefaultEnvironmentSection } from "./container-card-default-env.js";
7
8
  import { SecretsSection } from "./container-card-secrets.js";
8
- import { CardChevron, ConfigField, ConfirmButton } from "./container-card-shared.js";
9
- import { banner, cardBodyStyle, cardDescriptionStyle, cardHeadTextStyle, cardHeaderStyle, cardHoverStyle, cardNameStyle, cardOpenStyle, cardStyle, footerRow, hint, imageSelect, sectionTitle, } from "./container-card-styles.js";
9
+ import { ConfirmButton } from "./container-card-shared.js";
10
+ import { banner, footerRow, hint, imageSelect, sectionTitle, } from "./container-card-styles.js";
10
11
  import { VolumesSection } from "./container-card-volumes.js";
11
12
  import { WorkspaceSection } from "./container-card-workspace.js";
12
- import { Button, Input, Modal, writeClipboard, } from "@deepseek-ai/dsh-client-ui-primitives";
13
- import { useEffect, useId, useRef, useState, } from "react";
13
+ import { Button, Modal, writeClipboard, } from "@deepseek-ai/dsh-client-ui-primitives";
14
+ import { useEffect, useRef, useState } from "react";
14
15
  // The labels of the version lines, emphasised against their values.
15
16
  const versionLabel = { fontWeight: 600 };
16
17
  export function ContainerCard(props) {
17
18
  const { t } = props;
18
19
  const state = props.useContainerCard((snapshot) => snapshot);
19
- const [open, setOpen] = useState(false);
20
- const [hovered, setHovered] = useState(false);
21
20
  const [buildOpen, setBuildOpen] = useState(false);
22
21
  const [imageId, setImageId] = useState("");
23
22
  const [parent, setParent] = useState("");
@@ -26,7 +25,6 @@ export function ContainerCard(props) {
26
25
  const [defaultImage, setDefaultImage] = useState(state.defaultImage);
27
26
  const [copyState, setCopyState] = useState("idle");
28
27
  const copyResetTimer = useRef();
29
- const projectsRootId = useId();
30
28
  // Copies the version lines as displayed, so a user can paste them into a bug
31
29
  // report without transcribing anything.
32
30
  const copyVersions = async () => {
@@ -36,17 +34,31 @@ export function ContainerCard(props) {
36
34
  `${t("orchestrator")} ${state.orchestratorVersion || t("versionUnknown")}`,
37
35
  ];
38
36
  setCopyState((await writeClipboard(lines.join("\n"))) ? "copied" : "failed");
39
- if (copyResetTimer.current !== undefined)
37
+ if (copyResetTimer.current !== undefined) {
40
38
  clearTimeout(copyResetTimer.current);
39
+ }
41
40
  copyResetTimer.current = setTimeout(() => setCopyState("idle"), 1500);
42
41
  };
43
- // The card mounts with the settings panel, so this re-reads the live state
44
- // every time the Plugins section is opened: a workspace created since the
45
- // last read shows up without pressing Reload this view.
46
- // biome-ignore lint/correctness/useExhaustiveDependencies: mount-only refresh
47
- useEffect(() => {
48
- props.reload();
42
+ // The reset timer is cancelled on unmount, so it cannot fire against an
43
+ // unmounted component.
44
+ useEffect(() => () => {
45
+ if (copyResetTimer.current !== undefined) {
46
+ clearTimeout(copyResetTimer.current);
47
+ }
49
48
  }, []);
49
+ // The Plugins page renders a one-liner in its list and this component again
50
+ // as the entry's page, so the orchestrator snapshot is only fetched for the
51
+ // page. Re-reading on every page view keeps a workspace created since the
52
+ // last visit visible without pressing Reload.
53
+ // biome-ignore lint/correctness/useExhaustiveDependencies: refresh per page view
54
+ useEffect(() => {
55
+ if (props.view === "page")
56
+ props.reload();
57
+ }, [props.view]);
58
+ // The list shows only the one-liner; the page heading already carries the
59
+ // plugin's title and description.
60
+ if (props.view === "summary")
61
+ return t("cardDescription");
50
62
  const baseImages = state.images.filter((image) => image.isBase);
51
63
  const customImages = state.images.filter((image) => !image.isBase);
52
64
  const defaultCandidates = [
@@ -54,62 +66,56 @@ export function ContainerCard(props) {
54
66
  ...customImages.map((image) => image.imageId),
55
67
  ];
56
68
  if (!state.available) {
57
- return (_jsx("p", { style: { padding: "8px 0", fontSize: "13px", opacity: 0.8 }, children: t("unavailable") }));
69
+ return (_jsxs("div", { style: { padding: "8px 0", fontSize: "13px", opacity: 0.8 }, children: [_jsx("p", { style: { margin: 0 }, children: t("unavailable") }), state.notice === ""
70
+ ? null
71
+ : (_jsxs("p", { style: { margin: "4px 0 0" }, role: "status", children: [t("notice"), ": ", state.notice] })), _jsx("div", { style: { marginTop: "8px" }, children: _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: props.reload, children: state.busy ? t("busy") : t("reload") }) })] }));
58
72
  }
59
- return (_jsxs("li", { style: {
60
- ...cardStyle,
61
- ...(open ? cardOpenStyle : {}),
62
- ...(hovered && !open ? cardHoverStyle : {}),
63
- }, onPointerEnter: () => setHovered(true), onPointerLeave: () => setHovered(false), children: [_jsxs("button", { type: "button", style: cardHeaderStyle, "aria-expanded": open, onClick: () => setOpen(!open), children: [_jsxs("span", { style: cardHeadTextStyle, children: [_jsx("span", { style: cardNameStyle, children: t("cardTitle") }), _jsx("span", { style: cardDescriptionStyle, children: t("cardDescription") })] }), _jsx(CardChevron, { open: open })] }), open ? (_jsxs("div", { style: cardBodyStyle, children: [state.notice === "" ? null : (_jsxs("div", { style: banner, role: "status", children: [t("notice"), ": ", state.notice] })), state.versionState === "ok" ? null : (_jsx("div", { style: state.versionState === "major-mismatch"
64
- ? { ...banner, borderColor: "var(--dsw-alias-state-error-primary)" }
65
- : banner, role: state.versionState === "major-mismatch" ? "alert" : "status", children: t(state.versionState === "major-mismatch"
66
- ? "versionMismatchMajor"
67
- : "versionMismatchMinor") })), _jsx("div", { style: sectionTitle, children: t("workspacesTitle") }), state.workspaces.length === 0 ? (_jsx("p", { style: hint, children: t("none") })) : (state.workspaces.map((workspace) => (_jsx(WorkspaceSection, { t: t, workspace: workspace, containers: state.containers.filter((container) => container.workspaceSlug === workspace.workspaceSlug), images: state.images, volumes: state.volumes, secrets: state.secrets, busy: state.busy, defaultImage: state.defaultImage, projectsRoot: state.projectsRoot, directoryPicker: state.directoryPicker, onRemove: props.remove, onRemoveWorkspace: props.removeWorkspace, onRecreate: props.recreate, onCreate: props.createContainer, onStartContainer: props.startContainer, onAddContainerMount: props.addContainerMount, onRemoveContainerMount: props.removeContainerMount, onUpdateContainerMount: props.updateContainerMount, onSetContainerPaths: props.setContainerPaths, onAddContainerSecret: props.addContainerSecret, onRemoveContainerSecret: props.removeContainerSecret }, workspace.workspaceSlug)))), _jsxs("div", { style: {
68
- display: "flex",
69
- flexWrap: "wrap",
70
- alignItems: "center",
71
- gap: "8px",
72
- }, children: [_jsx("div", { style: { ...sectionTitle, flex: 1 }, children: t("imagesTitle") }), _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: () => {
73
- setDefaultImage(state.defaultImage);
74
- setDefaultOpen(true);
75
- }, children: t("setDefaultImage") }), _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: () => {
76
- setImageId("");
77
- setParent(baseImages[0]?.imageId ?? customImages[0]?.imageId ?? "");
78
- setPackages([]);
79
- setBuildOpen(true);
80
- }, children: t("buildImage") }), _jsx(ConfirmButton, { t: t, label: t("rebuildAllImages"), title: t("confirmTitle"), description: t("confirmRebuildAllImages"), disabled: state.busy, onConfirm: props.rebuildAllImages })] }), _jsx("div", { style: sectionTitle, children: t("baseImagesTitle") }), baseImages.length === 0 ? (_jsx("p", { style: hint, children: t("none") })) : (baseImages.map((image) => (_jsx(BaseImageRow, { t: t, image: image, busy: state.busy, defaultImage: state.defaultImage, onRebuild: props.rebuildBaseImage, onPull: props.pullBaseImage, onSetDefault: props.setDefaultImage }, image.imageId)))), _jsx("div", { style: sectionTitle, children: t("customImagesTitle") }), customImages.length === 0 ? (_jsx("p", { style: hint, children: t("none") })) : (customImages.map((image) => (_jsx(ImageItem, { t: t, image: image, busy: state.busy, defaultImage: state.defaultImage, onRemove: props.removeImage, onRebuild: props.rebuildImage, onSetDefault: props.setDefaultImage }, image.imageId)))), _jsx(ImageBuildModal, { t: t, images: state.images, open: buildOpen, imageId: imageId, parent: parent, packages: packages, onImageId: setImageId, onParent: setParent, onPackages: setPackages, onClose: () => setBuildOpen(false), onBuild: () => {
81
- props.buildImage(imageId.trim(), parent.trim(), packages);
73
+ return (_jsxs("div", { children: [state.notice === "" ? null : (_jsxs("div", { style: banner, role: "status", children: [t("notice"), ": ", state.notice] })), state.versionState === "ok" ? null : (_jsx("div", { style: state.versionState === "major-mismatch"
74
+ ? { ...banner, borderColor: "var(--dsw-alias-state-error-primary)" }
75
+ : banner, role: state.versionState === "major-mismatch" ? "alert" : "status", children: t(state.versionState === "major-mismatch"
76
+ ? "versionMismatchMajor"
77
+ : "versionMismatchMinor") })), _jsx("div", { style: sectionTitle, children: t("workspacesTitle") }), state.workspaces.length === 0 ? _jsx("p", { style: hint, children: t("none") }) : (state.workspaces.map((workspace) => (_jsx(WorkspaceSection, { t: t, workspace: workspace, containers: state.containers.filter((container) => container.workspaceSlug === workspace.workspaceSlug), images: state.images, volumes: state.volumes, secrets: state.secrets, busy: state.busy, defaultImage: state.defaultImage, projectsRoot: state.projectsRoot, directoryPicker: state.directoryPicker, onRemove: props.remove, onRemoveWorkspace: props.removeWorkspace, onRecreate: props.recreate, onCreate: props.createContainer, onStartContainer: props.startContainer, onAddContainerMount: props.addContainerMount, onRemoveContainerMount: props.removeContainerMount, onUpdateContainerMount: props.updateContainerMount, onSetContainerPaths: props.setContainerPaths, onAddContainerSecret: props.addContainerSecret, onRemoveContainerSecret: props.removeContainerSecret, onSyncDefaults: props.syncDefaultEnv, defaultEnvCount: Object.keys(state.containerEnv).length }, workspace.workspaceSlug)))), _jsxs("div", { style: {
78
+ display: "flex",
79
+ flexWrap: "wrap",
80
+ alignItems: "center",
81
+ gap: "8px",
82
+ }, children: [_jsx("div", { style: { ...sectionTitle, flex: 1 }, children: t("imagesTitle") }), _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: () => {
83
+ setDefaultImage(state.defaultImage);
84
+ setDefaultOpen(true);
85
+ }, children: t("setDefaultImage") }), _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: () => {
82
86
  setImageId("");
83
- setParent("");
87
+ setParent(baseImages[0]?.imageId ?? customImages[0]?.imageId ?? "");
84
88
  setPackages([]);
85
- setBuildOpen(false);
86
- } }), _jsx(Modal, { open: defaultOpen, onClose: () => setDefaultOpen(false), title: t("setDefaultImageTitle"), closeLabel: t("cancel"), footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => setDefaultOpen(false), children: t("cancel") }), _jsx(Button, { variant: "primary", size: "sm", disabled: defaultImage === "", onClick: () => {
87
- props.setDefaultImage(defaultImage);
88
- setDefaultOpen(false);
89
- }, children: t("setDefaultImage") })] }), children: _jsxs("select", { style: imageSelect, value: defaultImage, onChange: (event) => setDefaultImage(event.target.value), children: [defaultCandidates.length === 0 ? (_jsx("option", { value: "", children: t("none") })) : null, defaultCandidates.map((name) => (_jsx("option", { value: name, children: name }, name)))] }) }), _jsx(CachesSection, { t: t, caches: state.caches, busy: state.busy, onClean: props.cleanCaches }), _jsx(VolumesSection, { t: t, volumes: state.volumes, busy: state.busy, writable: state.writable, onCreate: props.createVolume, onRemove: props.removeVolume }), _jsx(SecretsSection, { t: t, secrets: state.secrets, busy: state.busy, writable: state.writable, onCreate: props.createSecret, onRemove: props.removeSecret, onSet: props.setSecret }), _jsx("div", { style: sectionTitle, children: t("configTitle") }), _jsxs("div", { style: {
90
- display: "flex",
91
- flexWrap: "wrap",
92
- alignItems: "center",
93
- gap: "8px",
94
- padding: "8px 0",
95
- }, children: [_jsx("label", { htmlFor: projectsRootId, style: {
96
- fontSize: "13px",
97
- color: "var(--dsw-alias-label-secondary)",
98
- minWidth: "110px",
99
- }, children: t("projectsRoot") }), _jsx(Input, { id: projectsRootId, value: state.projectsRoot, disabled: true, style: { width: "200px" } })] }), _jsx(ConfigField, { t: t, label: t("socketsRoot"), value: state.socketsRootDraft, current: state.socketsRoot, writable: state.writable, onChange: props.editSocketsRoot, onSave: props.saveSocketsRoot, onDiscard: props.discardSocketsRoot }), _jsxs("div", { style: {
100
- display: "flex",
101
- flexWrap: "wrap",
102
- alignItems: "center",
103
- gap: "8px",
104
- }, children: [_jsx("div", { style: { ...sectionTitle, flex: 1 }, children: t("versionsTitle") }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => void copyVersions(), children: copyState === "copied"
105
- ? t("copied")
106
- : copyState === "failed"
107
- ? t("copyFailed")
108
- : t("copyVersions") })] }), _jsxs("div", { style: {
109
- ...hint,
110
- display: "flex",
111
- flexDirection: "column",
112
- gap: "2px",
113
- margin: 0,
114
- }, children: [_jsxs("span", { children: [_jsx("span", { style: versionLabel, children: t("dsh") }), state.dshVersion ? (` ${state.dshVersion}`) : (_jsxs(_Fragment, { children: [" ", _jsx("span", { style: { color: "var(--dsw-alias-state-error-primary)" }, children: t("versionUnknown") })] }))] }), _jsxs("span", { children: [_jsx("span", { style: versionLabel, children: t("cardTitle") }), state.version ? ` ${state.version}` : "", state.commit ? ` · ${state.commit}` : ""] }), _jsxs("span", { children: [_jsx("span", { style: versionLabel, children: t("orchestrator") }), state.orchestratorVersion ? (` ${state.orchestratorVersion}`) : (_jsxs(_Fragment, { children: [" ", _jsx("span", { style: { color: "var(--dsw-alias-state-error-primary)" }, children: t("versionUnknown") })] }))] })] }), _jsx("div", { style: footerRow, children: _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: props.reload, children: state.busy ? t("busy") : t("reload") }) })] })) : null] }));
89
+ setBuildOpen(true);
90
+ }, children: t("buildImage") }), _jsx(ConfirmButton, { t: t, label: t("rebuildAllImages"), title: t("confirmTitle"), description: t("confirmRebuildAllImages"), disabled: state.busy, onConfirm: props.rebuildAllImages })] }), _jsx("div", { style: sectionTitle, children: t("baseImagesTitle") }), baseImages.length === 0 ? _jsx("p", { style: hint, children: t("none") }) : (baseImages.map((image) => (_jsx(BaseImageRow, { t: t, image: image, busy: state.busy, defaultImage: state.defaultImage, onRebuild: props.rebuildBaseImage, onPull: props.pullBaseImage, onSetDefault: props.setDefaultImage }, image.imageId)))), _jsx("div", { style: sectionTitle, children: t("customImagesTitle") }), customImages.length === 0 ? _jsx("p", { style: hint, children: t("none") }) : (customImages.map((image) => (_jsx(ImageItem, { t: t, image: image, busy: state.busy, defaultImage: state.defaultImage, onRemove: props.removeImage, onRebuild: props.rebuildImage, onSetDefault: props.setDefaultImage }, image.imageId)))), _jsx(ImageBuildModal, { t: t, images: state.images, open: buildOpen, imageId: imageId, parent: parent, packages: packages, onImageId: setImageId, onParent: setParent, onPackages: setPackages, onClose: () => setBuildOpen(false), onBuild: () => {
91
+ props.buildImage(imageId.trim(), parent.trim(), packages);
92
+ setImageId("");
93
+ setParent("");
94
+ setPackages([]);
95
+ setBuildOpen(false);
96
+ } }), _jsxs(Modal, { open: defaultOpen, onClose: () => setDefaultOpen(false), title: t("setDefaultImageTitle"), closeLabel: t("cancel"), footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => setDefaultOpen(false), children: t("cancel") }), _jsx(Button, { variant: "primary", size: "sm", disabled: defaultImage === "", onClick: () => {
97
+ props.setDefaultImage(defaultImage);
98
+ setDefaultOpen(false);
99
+ }, children: t("setDefaultImage") })] }), children: [_jsxs("select", { style: imageSelect, value: defaultImage, onChange: (event) => setDefaultImage(event.target.value), children: [defaultCandidates.length === 0
100
+ ? _jsx("option", { value: "", children: t("none") })
101
+ : null, defaultCandidates.map((name) => (_jsx("option", { value: name, children: name }, name)))] }), _jsx("p", { style: hint, children: t("defaultImageHint") })] }), _jsx(CachesSection, { t: t, caches: state.caches, busy: state.busy, onClean: props.cleanCaches }), _jsx(VolumesSection, { t: t, volumes: state.volumes, busy: state.busy, writable: state.writable, onCreate: props.createVolume, onRemove: props.removeVolume }), _jsx(SecretsSection, { t: t, secrets: state.secrets, busy: state.busy, writable: state.writable, onCreate: props.createSecret, onRemove: props.removeSecret, onSet: props.setSecret }), _jsx(DefaultEnvironmentSection, { t: t, env: state.containerEnv, busy: state.busy, writable: state.writable, onSave: props.saveContainerEnv, onSync: props.syncDefaultEnv }), _jsxs("div", { style: {
102
+ display: "flex",
103
+ flexWrap: "wrap",
104
+ alignItems: "center",
105
+ gap: "8px",
106
+ }, children: [_jsx("div", { style: { ...sectionTitle, flex: 1 }, children: t("versionsTitle") }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => void copyVersions(), children: copyState === "copied"
107
+ ? t("copied")
108
+ : copyState === "failed"
109
+ ? t("copyFailed")
110
+ : t("copyVersions") })] }), _jsxs("div", { style: {
111
+ ...hint,
112
+ display: "flex",
113
+ flexDirection: "column",
114
+ gap: "2px",
115
+ margin: 0,
116
+ }, children: [_jsxs("span", { children: [_jsx("span", { style: versionLabel, children: t("dsh") }), state.dshVersion
117
+ ? (` ${state.dshVersion}`)
118
+ : (_jsxs(_Fragment, { children: [" ", _jsx("span", { style: { color: "var(--dsw-alias-state-error-primary)" }, children: t("versionUnknown") })] }))] }), _jsxs("span", { children: [_jsx("span", { style: versionLabel, children: t("cardTitle") }), state.version ? ` ${state.version}` : "", state.commit ? ` · ${state.commit}` : ""] }), _jsxs("span", { children: [_jsx("span", { style: versionLabel, children: t("orchestrator") }), state.orchestratorVersion
119
+ ? (` ${state.orchestratorVersion}`)
120
+ : (_jsxs(_Fragment, { children: [" ", _jsx("span", { style: { color: "var(--dsw-alias-state-error-primary)" }, children: t("versionUnknown") })] }))] })] }), _jsx("div", { style: footerRow, children: _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: props.reload, children: state.busy ? t("busy") : t("reload") }) })] }));
115
121
  }
@@ -7,7 +7,7 @@ export interface MountInput {
7
7
  volume: string;
8
8
  secret: string;
9
9
  }
10
- export type CommandOp = "remove" | "workspace_remove" | "recreate" | "create" | "volume_create" | "volume_remove" | "image_remove" | "secret_create" | "secret_remove" | "secret_set" | "image_rebuild" | "image_rebuild_all" | "image_build" | "image_base_rebuild" | "image_base_pull" | "container_secret_add" | "container_secret_remove" | "container_mount_add" | "container_mount_remove" | "container_mount_update" | "container_path_set" | "cache_clean";
10
+ export type CommandOp = "remove" | "workspace_remove" | "recreate" | "create" | "volume_create" | "volume_remove" | "image_remove" | "secret_create" | "secret_remove" | "secret_set" | "image_rebuild" | "image_rebuild_all" | "image_build" | "image_base_rebuild" | "image_base_pull" | "container_secret_add" | "container_secret_remove" | "container_mount_add" | "container_mount_remove" | "container_mount_update" | "container_path_set" | "default_env_sync" | "cache_clean";
11
11
  export interface CommandRequest {
12
12
  op: CommandOp;
13
13
  workspace: string;
@@ -19,11 +19,11 @@ export interface CommandRequest {
19
19
  env: Record<string, string>;
20
20
  container: string;
21
21
  secret: string;
22
- secretEnv: string;
22
+ secretEnvName: string;
23
23
  length: number;
24
24
  charset: string;
25
25
  packages: string[];
26
- secretEnvMap: Record<string, string>;
26
+ secretEnv: Record<string, string>;
27
27
  cacheMode: string;
28
28
  mount: MountInput | null;
29
29
  }
@@ -22,10 +22,15 @@ function formatBytes(bytes) {
22
22
  export function CachesSection(props) {
23
23
  const { t, caches, busy, onClean } = props;
24
24
  const disabled = busy || caches.length === 0;
25
- return (_jsxs(_Fragment, { children: [_jsx("div", { style: sectionTitle, children: t("cachesTitle") }), _jsxs("div", { style: wsBody, children: [caches.length === 0 ? (_jsx("p", { style: { ...hint, margin: 0 }, children: t("noCaches") })) : (caches.map((cache) => (_jsxs("div", { style: {
26
- display: "flex",
27
- flexWrap: "wrap",
28
- alignItems: "center",
29
- gap: "8px",
30
- }, children: [_jsx("strong", { style: { color: "var(--dsw-alias-label-primary)" }, children: cache.manager }), _jsx("span", { style: hint, children: t("cacheUsage", { size: formatBytes(cache.bytes), n: cache.files }) })] }, cache.manager)))), _jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" }, children: [_jsx(ConfirmButton, { t: t, label: t("cacheKeepLatest"), title: t("confirmTitle"), description: t("confirmCacheKeepLatest"), disabled: disabled, onConfirm: () => onClean("keep-latest") }), _jsx(ConfirmButton, { t: t, label: t("cacheRemoveAll"), title: t("confirmTitle"), description: t("confirmCacheRemoveAll"), disabled: disabled, onConfirm: () => onClean("all") })] })] })] }));
25
+ return (_jsxs(_Fragment, { children: [_jsx("div", { style: sectionTitle, children: t("cachesTitle") }), _jsxs("div", { style: wsBody, children: [caches.length === 0
26
+ ? _jsx("p", { style: { ...hint, margin: 0 }, children: t("noCaches") })
27
+ : (caches.map((cache) => (_jsxs("div", { style: {
28
+ display: "flex",
29
+ flexWrap: "wrap",
30
+ alignItems: "center",
31
+ gap: "8px",
32
+ }, children: [_jsx("strong", { style: { color: "var(--dsw-alias-label-primary)" }, children: cache.manager }), _jsx("span", { style: hint, children: t("cacheUsage", {
33
+ size: formatBytes(cache.bytes),
34
+ n: cache.files,
35
+ }) })] }, cache.manager)))), _jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" }, children: [_jsx(ConfirmButton, { t: t, label: t("cacheKeepLatest"), title: t("confirmTitle"), description: t("confirmCacheKeepLatest"), disabled: disabled, onConfirm: () => onClean("keep-latest") }), _jsx(ConfirmButton, { t: t, label: t("cacheRemoveAll"), title: t("confirmTitle"), description: t("confirmCacheRemoveAll"), disabled: disabled, onConfirm: () => onClean("all") })] })] })] }));
31
36
  }