@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
@@ -2,42 +2,83 @@ 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 { ConfirmButton, Field, emptyMount, mountLabel } from "./container-card-shared.js";
5
+ import { ConfirmButton, emptyMount, Field, mountLabel, } from "./container-card-shared.js";
6
6
  import { greyId, imageSelect } from "./container-card-styles.js";
7
7
  import { DirectoryPickerModal } from "./container-card-directory.js";
8
8
  import { forcedMountMode } from "../mount-enums.js";
9
- import { hostPathForProjectName, projectNameFromHostPath } from "../project-path.js";
9
+ import { commitEnvDraft, emptyEnvDraft, envRows, removeEnvRow, renameEnvKey, setEnvValue, } from "../env-rows.js";
10
+ import { hostPathForProjectName, projectNameFromHostPath, } from "../project-path.js";
10
11
  import { Button, Input, Modal } from "@deepseek-ai/dsh-client-ui-primitives";
11
- import { useId, useState } from "react";
12
+ import { useId, useRef, useState } from "react";
13
+ // One stable key per env row, so renaming a variable does not remount the row
14
+ // and drop focus mid-keystroke. The parent keeps env as a record; rows are
15
+ // positional, so the id list is aligned by index.
16
+ let nextEnvId = 0;
12
17
  export function EnvEditor(props) {
13
18
  const { t, env, busy, onChange } = props;
14
- const entries = Object.entries(env);
15
- const updateKey = (oldKey, key, value) => {
16
- const next = {};
17
- for (const [k, v] of entries) {
18
- if (k !== oldKey)
19
- next[k] = v;
19
+ // The row being typed: it becomes a record entry only once its key is a real,
20
+ // unused variable name, so an abandoned Add cannot apply a placeholder.
21
+ const [draft, setDraft] = useState(undefined);
22
+ const rows = envRows(env, draft);
23
+ const idsRef = useRef([]);
24
+ if (idsRef.current.length !== rows.length) {
25
+ const ids = idsRef.current.slice(0, rows.length);
26
+ while (ids.length < rows.length)
27
+ ids.push(nextEnvId++);
28
+ idsRef.current = ids;
29
+ }
30
+ const commitDraft = () => {
31
+ if (draft === undefined)
32
+ return;
33
+ if (draft.key === "") {
34
+ // An untouched row is dropped rather than kept as an empty entry.
35
+ setDraft(undefined);
36
+ return;
37
+ }
38
+ const committed = commitEnvDraft(env, draft);
39
+ // A duplicate key keeps the row open so the caller can fix it.
40
+ if (committed === undefined)
41
+ return;
42
+ setDraft(undefined);
43
+ onChange(committed);
44
+ };
45
+ const updateKey = (index, key) => {
46
+ const row = rows[index];
47
+ if (row.pending) {
48
+ setDraft({ key, value: row.value });
49
+ return;
20
50
  }
21
- next[key] = value;
22
- onChange(next);
51
+ onChange(renameEnvKey(env, index, key));
23
52
  };
24
- const updateValue = (key, value) => {
25
- onChange({ ...env, [key]: value });
53
+ const updateValue = (index, value) => {
54
+ const row = rows[index];
55
+ if (row.pending) {
56
+ setDraft({ key: row.key, value });
57
+ return;
58
+ }
59
+ onChange(setEnvValue(env, index, value));
26
60
  };
27
- const remove = (key) => {
28
- const next = { ...env };
29
- delete next[key];
30
- onChange(next);
61
+ const remove = (index) => {
62
+ if (rows[index]?.pending === true) {
63
+ setDraft(undefined);
64
+ return;
65
+ }
66
+ onChange(removeEnvRow(env, index));
31
67
  };
32
68
  const add = () => {
33
- onChange({ ...env, "": "" });
69
+ setDraft((current) => current ?? emptyEnvDraft());
34
70
  };
35
- return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [entries.map(([key, value]) => (_jsxs("div", { style: {
71
+ return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [rows.map((row, index) => (_jsxs("div", { style: {
36
72
  display: "flex",
37
73
  flexWrap: "wrap",
38
74
  alignItems: "center",
39
75
  gap: "8px",
40
- }, children: [_jsx(Input, { value: key, disabled: busy, placeholder: t("envKey"), "aria-label": t("envKey"), onChange: (event) => updateKey(key, event.target.value, value), style: { width: "160px" } }), _jsx(Input, { value: value, disabled: busy, placeholder: t("envValue"), "aria-label": t("envValue"), onChange: (event) => updateValue(key, event.target.value), style: { width: "200px" } }), _jsx(Button, { variant: "outline", size: "sm", disabled: busy, onClick: () => remove(key), children: t("removeEnv") })] }, key))), _jsx("div", { children: _jsx(Button, { variant: "outline", size: "sm", disabled: busy, onClick: add, children: t("addEnv") }) })] }));
76
+ }, children: [_jsx(Input, { value: row.key, disabled: busy, placeholder: t("envKey"), "aria-label": t("envKey"), autoFocus: row.pending, onChange: (event) => updateKey(index, event.target.value), onBlur: row.pending ? commitDraft : undefined, onKeyDown: row.pending
77
+ ? (event) => {
78
+ if (event.key === "Enter")
79
+ commitDraft();
80
+ }
81
+ : undefined, style: { width: "160px" } }), _jsx(Input, { value: row.value, disabled: busy, placeholder: t("envValue"), "aria-label": t("envValue"), onChange: (event) => updateValue(index, event.target.value), onBlur: row.pending ? commitDraft : undefined, style: { width: "200px" } }), _jsx(Button, { variant: "outline", size: "sm", disabled: busy, onClick: () => remove(index), children: t("removeEnv") })] }, idsRef.current[index]))), _jsx("div", { children: _jsx(Button, { variant: "outline", size: "sm", disabled: busy, onClick: add, children: t("addEnv") }) })] }));
41
82
  }
42
83
  export function MountsEditor(props) {
43
84
  const { t, mounts, volumes, secrets, busy, enabled, projectsRoot, directoryPicker, confirmRemove, modeControl = "remount", primaryProject = "", onAdd, onRemove, onUpdate, } = props;
@@ -72,12 +113,28 @@ export function MountsEditor(props) {
72
113
  onAdd(draft);
73
114
  setAdding(false);
74
115
  };
75
- const kindField = draft.kind === "tmpfs" ? (_jsx(Field, { label: t("mountDestination"), htmlFor: mountDestinationId, children: _jsx(Input, { id: mountDestinationId, value: draft.destination, disabled: busy, onChange: (event) => updateDraft({ destination: event.target.value }) }) })) : draft.kind === "volume" ? (_jsxs(_Fragment, { children: [_jsx(Field, { label: t("mountVolume"), htmlFor: mountVolumeId, children: _jsxs("select", { id: mountVolumeId, style: imageSelect, value: draft.volume, disabled: busy, onChange: (event) => updateDraft({ volume: event.target.value }), children: [volumes.length === 0 ? (_jsx("option", { value: "", children: t("none") })) : null, volumes.map((volume) => (_jsx("option", { value: volume.name, children: volume.name }, volume.name)))] }) }), _jsx(Field, { label: t("mountDestination"), htmlFor: mountDestinationId, children: _jsx(Input, { id: mountDestinationId, value: draft.destination, disabled: busy, onChange: (event) => updateDraft({ destination: event.target.value }) }) }), _jsx(Field, { label: t("mountMode"), htmlFor: mountModeId, children: _jsxs("select", { id: mountModeId, style: imageSelect, value: draft.mode, disabled: busy, onChange: (event) => updateDraft({ mode: event.target.value }), children: [_jsx("option", { value: "read_only", children: t("readOnly") }), _jsx("option", { value: "read_write", children: t("readWrite") })] }) })] })) : draft.kind === "secret" ? (_jsxs(_Fragment, { children: [_jsx(Field, { label: t("mountSecret"), htmlFor: mountSecretId, children: _jsxs("select", { id: mountSecretId, style: imageSelect, value: draft.secret, disabled: busy, onChange: (event) => updateDraft({ secret: event.target.value }), children: [secrets.length === 0 ? (_jsx("option", { value: "", children: t("none") })) : null, secrets.map((secret) => (_jsx("option", { value: secret.name, children: secret.name }, secret.name)))] }) }), _jsx(Field, { label: t("mountDestination"), htmlFor: mountDestinationId, children: _jsx(Input, { id: mountDestinationId, value: draft.destination, disabled: busy, onChange: (event) => updateDraft({ destination: event.target.value }) }) })] })) : (_jsxs(_Fragment, { children: [_jsx(Field, { label: t("mountProjectPath"), htmlFor: mountProjectId, children: _jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [_jsx(Input, { id: mountProjectId, value: draft.project, disabled: busy, onChange: (event) => updateDraft({ project: event.target.value }) }), directoryPicker !== undefined ? (_jsx(Button, { variant: "outline", size: "sm", disabled: busy, onClick: () => {
76
- setBrowseError("");
77
- setBrowsing(true);
78
- }, children: t("browse") })) : null] }) }), browseError === "" ? null : (_jsx("p", { style: { margin: 0, fontSize: "13px" }, role: "alert", children: browseError })), _jsx(Field, { label: t("mountMode"), htmlFor: mountModeId, children: _jsxs("select", { id: mountModeId, style: imageSelect, value: draft.mode, disabled: busy, onChange: (event) => updateDraft({ mode: event.target.value }), children: [_jsx("option", { value: "read_only", children: t("readOnly") }), _jsx("option", { value: "read_write", children: t("readWrite") })] }) })] }));
116
+ const kindField = draft.kind === "tmpfs"
117
+ ? (_jsx(Field, { label: t("mountDestination"), htmlFor: mountDestinationId, children: _jsx(Input, { id: mountDestinationId, value: draft.destination, disabled: busy, onChange: (event) => updateDraft({ destination: event.target.value }) }) }))
118
+ : draft.kind === "volume"
119
+ ? (_jsxs(_Fragment, { children: [_jsx(Field, { label: t("mountVolume"), htmlFor: mountVolumeId, children: _jsxs("select", { id: mountVolumeId, style: imageSelect, value: draft.volume, disabled: busy, onChange: (event) => updateDraft({ volume: event.target.value }), children: [volumes.length === 0
120
+ ? _jsx("option", { value: "", children: t("none") })
121
+ : null, volumes.map((volume) => (_jsx("option", { value: volume.name, children: volume.name }, volume.name)))] }) }), _jsx(Field, { label: t("mountDestination"), htmlFor: mountDestinationId, children: _jsx(Input, { id: mountDestinationId, value: draft.destination, disabled: busy, onChange: (event) => updateDraft({ destination: event.target.value }) }) }), _jsx(Field, { label: t("mountMode"), htmlFor: mountModeId, children: _jsxs("select", { id: mountModeId, style: imageSelect, value: draft.mode, disabled: busy, onChange: (event) => updateDraft({ mode: event.target.value }), children: [_jsx("option", { value: "read_only", children: t("readOnly") }), _jsx("option", { value: "read_write", children: t("readWrite") })] }) })] }))
122
+ : draft.kind === "secret"
123
+ ? (_jsxs(_Fragment, { children: [_jsx(Field, { label: t("mountSecret"), htmlFor: mountSecretId, children: _jsxs("select", { id: mountSecretId, style: imageSelect, value: draft.secret, disabled: busy, onChange: (event) => updateDraft({ secret: event.target.value }), children: [secrets.length === 0
124
+ ? _jsx("option", { value: "", children: t("none") })
125
+ : null, secrets.map((secret) => (_jsx("option", { value: secret.name, children: secret.name }, secret.name)))] }) }), _jsx(Field, { label: t("mountDestination"), htmlFor: mountDestinationId, children: _jsx(Input, { id: mountDestinationId, value: draft.destination, disabled: busy, onChange: (event) => updateDraft({ destination: event.target.value }) }) })] }))
126
+ : (_jsxs(_Fragment, { children: [_jsx(Field, { label: t("mountProjectPath"), htmlFor: mountProjectId, children: _jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px" }, children: [_jsx(Input, { id: mountProjectId, value: draft.project, disabled: busy, onChange: (event) => updateDraft({ project: event.target.value }) }), directoryPicker !== undefined
127
+ ? (_jsx(Button, { variant: "outline", size: "sm", disabled: busy, onClick: () => {
128
+ setBrowseError("");
129
+ setBrowsing(true);
130
+ }, children: t("browse") }))
131
+ : null] }) }), browseError === ""
132
+ ? null
133
+ : (_jsx("p", { style: { margin: 0, fontSize: "13px" }, role: "alert", children: browseError })), _jsx(Field, { label: t("mountMode"), htmlFor: mountModeId, children: _jsxs("select", { id: mountModeId, style: imageSelect, value: draft.mode, disabled: busy, onChange: (event) => updateDraft({ mode: event.target.value }), children: [_jsx("option", { value: "read_only", children: t("readOnly") }), _jsx("option", { value: "read_write", children: t("readWrite") })] }) })] }));
79
134
  return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [mounts.map((mount, index) => {
80
- const nextMode = mount.mode === "read_write" ? "read_only" : "read_write";
135
+ const nextMode = mount.mode === "read_write"
136
+ ? "read_only"
137
+ : "read_write";
81
138
  const forced = forcedMountMode(mount.kind);
82
139
  const editable = mount.kind === "project" || mount.kind === "volume";
83
140
  const removable = mount.kind !== "project" ||
@@ -93,17 +150,32 @@ export function MountsEditor(props) {
93
150
  flex: 1,
94
151
  fontSize: "13px",
95
152
  color: "var(--dsw-alias-label-primary)",
96
- }, children: mountLabel(t, mount, modeControl !== "select") }), modeControl === "select" && (editable || forced !== undefined) ? (_jsx("select", { style: imageSelect, value: forced ?? mount.mode, disabled: !enabled || forced !== undefined, "aria-label": t("mountMode"), onChange: (event) => onUpdate?.({ ...mount, mode: event.target.value }, index), children: forced !== undefined ? (_jsx("option", { value: forced, children: forced === "read_write" ? t("readWrite") : t("readOnly") })) : (_jsxs(_Fragment, { children: [_jsx("option", { value: "read_only", children: t("readOnly") }), _jsx("option", { value: "read_write", children: t("readWrite") })] })) })) : null, modeControl === "remount" && editable && onUpdate !== undefined && (_jsx(ConfirmButton, { t: t, label: nextMode === "read_only"
97
- ? t("remountReadOnly")
98
- : t("remountReadWrite"), title: t("confirmTitle"), description: nextMode === "read_only"
99
- ? t("confirmRemountReadOnly")
100
- : t("confirmRemountReadWrite"), disabled: !enabled, onConfirm: () => onUpdate({ ...mount, mode: nextMode }, index) })), removable &&
101
- (confirmRemove ? (_jsx(ConfirmButton, { t: t, label: t("remove"), title: t("confirmTitle"), description: t("confirmRemoveMount"), disabled: !enabled, onConfirm: () => onRemove(mount) })) : (_jsx(Button, { variant: "outline", size: "sm", disabled: !enabled, onClick: () => onRemove(mount), children: t("remove") })))] }, mountLabel(t, mount)));
153
+ }, children: mountLabel(t, mount, modeControl !== "select") }), modeControl === "select" && (editable || forced !== undefined)
154
+ ? (_jsx("select", { style: imageSelect, value: forced ?? mount.mode, disabled: !enabled || forced !== undefined, "aria-label": t("mountMode"), onChange: (event) => onUpdate?.({ ...mount, mode: event.target.value }, index), children: forced !== undefined
155
+ ? (_jsx("option", { value: forced, children: forced === "read_write"
156
+ ? t("readWrite")
157
+ : t("readOnly") }))
158
+ : (_jsxs(_Fragment, { children: [_jsx("option", { value: "read_only", children: t("readOnly") }), _jsx("option", { value: "read_write", children: t("readWrite") })] })) }))
159
+ : null, modeControl === "remount" && editable && onUpdate !== undefined &&
160
+ (_jsx(ConfirmButton, { t: t, label: nextMode === "read_only"
161
+ ? t("remountReadOnly")
162
+ : t("remountReadWrite"), title: t("confirmTitle"), description: nextMode === "read_only"
163
+ ? t("confirmRemountReadOnly", {
164
+ mount: mountLabel(t, mount, false),
165
+ })
166
+ : t("confirmRemountReadWrite", {
167
+ mount: mountLabel(t, mount, false),
168
+ }), disabled: !enabled, onConfirm: () => onUpdate({ ...mount, mode: nextMode }, index) })), removable &&
169
+ (confirmRemove
170
+ ? (_jsx(ConfirmButton, { t: t, label: t("remove"), title: t("confirmTitle"), description: t("confirmRemoveMount", {
171
+ mount: mountLabel(t, mount, false),
172
+ }), disabled: !enabled, onConfirm: () => onRemove(mount) }))
173
+ : (_jsx(Button, { variant: "outline", size: "sm", disabled: !enabled, onClick: () => onRemove(mount), children: t("remove") })))] }, index));
102
174
  }), _jsx("div", { children: _jsx(Button, { variant: "outline", size: "sm", disabled: !enabled, onClick: openAdd, children: t("addMount") }) }), _jsx(Modal, { open: adding, onClose: () => setAdding(false), title: t("addMount"), closeLabel: t("cancel"), footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => setAdding(false), children: t("cancel") }), _jsx(Button, { variant: "primary", size: "sm", disabled: busy || !canAdd, onClick: submit, children: t("addMount") })] }), children: _jsxs("div", { style: {
103
175
  display: "flex",
104
176
  flexDirection: "column",
105
177
  gap: "12px",
106
- }, children: [_jsx(Field, { label: t("mountKind"), htmlFor: mountKindId, children: _jsxs("select", { id: mountKindId, style: imageSelect, value: draft.kind, disabled: busy, onChange: (event) => setDraft(emptyMount(event.target.value)), children: [_jsx("option", { value: "project", children: t("mountProject") }), _jsx("option", { value: "tmpfs", children: "tmpfs" }), _jsx("option", { value: "volume", children: t("mountVolume") }), _jsx("option", { value: "secret", children: t("mountSecret") })] }) }), kindField] }) }), directoryPicker === undefined ? null : (_jsx(DirectoryPickerModal, { t: t, open: browsing, root: projectsRoot, start: hostPathForProjectName(projectsRoot, draft.project), picker: directoryPicker, onSelect: (path) => {
178
+ }, children: [_jsx(Field, { label: t("mountKind"), htmlFor: mountKindId, children: _jsxs("select", { id: mountKindId, style: imageSelect, value: draft.kind, disabled: busy, onChange: (event) => setDraft(emptyMount(event.target.value)), children: [_jsx("option", { value: "project", children: t("mountProject") }), _jsx("option", { value: "tmpfs", children: t("mountTmpfs") }), _jsx("option", { value: "volume", children: t("mountVolume") }), _jsx("option", { value: "secret", children: t("mountSecret") })] }) }), kindField] }) }), directoryPicker === undefined ? null : (_jsx(DirectoryPickerModal, { t: t, open: browsing, root: projectsRoot, start: hostPathForProjectName(projectsRoot, draft.project), picker: directoryPicker, onSelect: (path) => {
107
179
  const project = projectNameFromHostPath(projectsRoot, path);
108
180
  if (project === undefined) {
109
181
  setBrowseError(t("browseOutsideRoot"));
@@ -1,8 +1,8 @@
1
1
  import { type ImageView } from "./container-card-controller.js";
2
- import { type ContainerPluginKey } from "./locales.js";
2
+ import type { Translate } from "./locales.js";
3
3
  import { type ReactNode } from "react";
4
4
  export declare function ImageItem(props: {
5
- t: (key: ContainerPluginKey) => string;
5
+ t: Translate;
6
6
  image: ImageView;
7
7
  busy: boolean;
8
8
  defaultImage: string;
@@ -11,7 +11,7 @@ export declare function ImageItem(props: {
11
11
  onSetDefault: (imageId: string) => void;
12
12
  }): ReactNode;
13
13
  export declare function BaseImageRow(props: {
14
- t: (key: ContainerPluginKey) => string;
14
+ t: Translate;
15
15
  image: ImageView;
16
16
  busy: boolean;
17
17
  defaultImage: string;
@@ -20,7 +20,7 @@ export declare function BaseImageRow(props: {
20
20
  onSetDefault: (imageId: string) => void;
21
21
  }): ReactNode;
22
22
  export declare function ImageBuildModal(props: {
23
- t: (key: ContainerPluginKey) => string;
23
+ t: Translate;
24
24
  images: readonly {
25
25
  imageId: string;
26
26
  isBase: boolean;
@@ -2,22 +2,33 @@ 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 { Chip, ConfirmButton, Field, TagInput, imageIdPattern, sanitizeImageId, } from "./container-card-shared.js";
5
+ import { Chip, ConfirmButton, Field, imageIdPattern, sanitizeImageId, TagInput, } from "./container-card-shared.js";
6
6
  import { actions, hint, imageSelect, tableStyle, tdStyle, thStyle, wsBody, } from "./container-card-styles.js";
7
- import { Button, DisclosureRow, Input, Modal } from "@deepseek-ai/dsh-client-ui-primitives";
7
+ import { Button, DisclosureRow, Input, Modal, } from "@deepseek-ai/dsh-client-ui-primitives";
8
8
  import { useId, useState } from "react";
9
9
  export function ImageItem(props) {
10
10
  const { t, image, busy, defaultImage, onRemove, onRebuild, onSetDefault } = props;
11
11
  const [open, setOpen] = useState(false);
12
- return (_jsx(DisclosureRow, { icon: _jsx("span", {}), title: image.imageId, open: open, expandable: true, onToggle: () => setOpen(!open), children: _jsxs("div", { style: wsBody, children: [_jsx("table", { style: tableStyle, children: _jsxs("tbody", { children: [_jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("parent") }), _jsx("td", { style: tdStyle, children: image.parent })] }), _jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("imageTag") }), _jsx("td", { style: tdStyle, children: image.imageTag })] }), _jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("builtAt") }), _jsx("td", { style: tdStyle, children: image.builtAt })] }), _jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("packages") }), _jsx("td", { style: tdStyle, children: image.packages.length === 0 ? (t("none")) : (_jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px" }, children: image.packages.map((pkg) => (_jsx(Chip, { t: t, label: pkg }, pkg))) })) })] })] }) }), _jsxs("div", { style: actions, children: [_jsx(ConfirmButton, { t: t, label: t("rebuildImage"), title: t("confirmTitle"), description: t("confirmRebuildImage"), disabled: busy, onConfirm: () => onRebuild(image.imageId) }), _jsx(ConfirmButton, { t: t, label: t("removeImage"), title: t("confirmTitle"), description: t("confirmRemoveImage"), disabled: busy, onConfirm: () => onRemove(image.imageId) }), _jsx(Button, { variant: "outline", size: "sm", disabled: busy || image.imageId === defaultImage, onClick: () => onSetDefault(image.imageId), children: t("setDefaultImage") })] })] }) }));
12
+ return (_jsx(DisclosureRow, { icon: _jsx("span", {}), title: image.imageId, open: open, expandable: true, onToggle: () => setOpen(!open), children: _jsxs("div", { style: wsBody, children: [_jsx("table", { style: tableStyle, children: _jsxs("tbody", { children: [_jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("parent") }), _jsx("td", { style: tdStyle, children: image.parent })] }), _jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("imageTag") }), _jsx("td", { style: tdStyle, children: image.imageTag })] }), _jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("builtAt") }), _jsx("td", { style: tdStyle, children: image.builtAt })] }), _jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("packages") }), _jsx("td", { style: tdStyle, children: image.packages.length === 0
13
+ ? (t("none"))
14
+ : (_jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px" }, children: image.packages.map((pkg) => (_jsx(Chip, { t: t, label: pkg }, pkg))) })) })] })] }) }), _jsxs("div", { style: actions, children: [_jsx(ConfirmButton, { t: t, label: t("rebuildImage"), title: t("confirmTitle"), description: t("confirmRebuildImage", { image: image.imageId }), disabled: busy, onConfirm: () => onRebuild(image.imageId) }), _jsx(ConfirmButton, { t: t, label: t("removeImage"), title: t("confirmTitle"), description: t("confirmRemoveImage", { image: image.imageId }), disabled: busy, onConfirm: () => onRemove(image.imageId) }), _jsx(Button, { variant: "outline", size: "sm", disabled: busy || image.imageId === defaultImage, onClick: () => onSetDefault(image.imageId), children: t("setDefaultImage") })] })] }) }));
13
15
  }
14
16
  export function BaseImageRow(props) {
15
17
  const { t, image, busy, defaultImage, onRebuild, onPull, onSetDefault } = props;
16
18
  const [open, setOpen] = useState(false);
17
- const pull = image.basePublic && (image.status === "missing" || image.status === "pulled");
19
+ const pull = image.basePublic &&
20
+ (image.status === "missing" || image.status === "pulled");
18
21
  const build = !image.basePublic && image.status === "missing";
19
22
  const rebuild = !image.basePublic && image.status === "built";
20
- return (_jsx(DisclosureRow, { icon: _jsx("span", {}), title: image.imageId, open: open, expandable: true, onToggle: () => setOpen(!open), children: _jsxs("div", { style: wsBody, children: [_jsx("table", { style: tableStyle, children: _jsxs("tbody", { children: [_jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("primitive") }), _jsx("td", { style: tdStyle, children: image.primitive })] }), _jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("packageManager") }), _jsx("td", { style: tdStyle, children: image.packageManager })] }), _jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("status") }), _jsx("td", { style: tdStyle, children: image.status })] }), _jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("builtAt") }), _jsx("td", { style: tdStyle, children: image.builtAt === "" ? t("none") : image.builtAt })] }), _jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("packages") }), _jsx("td", { style: tdStyle, children: image.packages.length === 0 ? (t("none")) : (_jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px" }, children: image.packages.map((pkg) => (_jsx(Chip, { t: t, label: pkg }, pkg))) })) })] })] }) }), _jsxs("div", { style: actions, children: [pull ? (_jsx(Button, { variant: "outline", size: "sm", disabled: busy, onClick: () => onPull(image.imageId), children: t("pullImage") })) : null, build ? (_jsx(Button, { variant: "outline", size: "sm", disabled: busy, onClick: () => onRebuild(image.imageId), children: t("build") })) : null, rebuild ? (_jsx(Button, { variant: "outline", size: "sm", disabled: busy, onClick: () => onRebuild(image.imageId), children: t("rebuildImage") })) : null, _jsx(Button, { variant: "outline", size: "sm", disabled: busy || image.imageId === defaultImage, onClick: () => onSetDefault(image.imageId), children: t("setDefaultImage") })] })] }) }));
23
+ return (_jsx(DisclosureRow, { icon: _jsx("span", {}), title: image.imageId, open: open, expandable: true, onToggle: () => setOpen(!open), children: _jsxs("div", { style: wsBody, children: [_jsx("table", { style: tableStyle, children: _jsxs("tbody", { children: [_jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("primitive") }), _jsx("td", { style: tdStyle, children: image.primitive })] }), _jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("packageManager") }), _jsx("td", { style: tdStyle, children: image.packageManager })] }), _jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("status") }), _jsx("td", { style: tdStyle, children: image.status })] }), _jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("builtAt") }), _jsx("td", { style: tdStyle, children: image.builtAt === "" ? t("none") : image.builtAt })] }), _jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("packages") }), _jsx("td", { style: tdStyle, children: image.packages.length === 0
24
+ ? (t("none"))
25
+ : (_jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "6px" }, children: image.packages.map((pkg) => (_jsx(Chip, { t: t, label: pkg }, pkg))) })) })] })] }) }), _jsxs("div", { style: actions, children: [pull
26
+ ? (_jsx(Button, { variant: "outline", size: "sm", disabled: busy, onClick: () => onPull(image.imageId), children: t("pullImage") }))
27
+ : null, build
28
+ ? (_jsx(Button, { variant: "outline", size: "sm", disabled: busy, onClick: () => onRebuild(image.imageId), children: t("build") }))
29
+ : null, rebuild
30
+ ? (_jsx(Button, { variant: "outline", size: "sm", disabled: busy, onClick: () => onRebuild(image.imageId), children: t("rebuildImage") }))
31
+ : null, _jsx(Button, { variant: "outline", size: "sm", disabled: busy || image.imageId === defaultImage, onClick: () => onSetDefault(image.imageId), children: t("setDefaultImage") })] })] }) }));
21
32
  }
22
33
  export function ImageBuildModal(props) {
23
34
  const { t, images, open, imageId, parent, packages, onImageId, onParent, onPackages, onClose, onBuild, } = props;
@@ -31,5 +42,7 @@ export function ImageBuildModal(props) {
31
42
  display: "flex",
32
43
  flexDirection: "column",
33
44
  gap: "12px",
34
- }, children: [_jsxs(Field, { label: t("imageId"), htmlFor: imageIdLabel, children: [_jsx(Input, { id: imageIdLabel, value: imageId, onChange: (event) => onImageId(sanitizeImageId(event.target.value)) }), imageId !== "" && !imageIdPattern.test(imageId) ? (_jsx("p", { style: { ...hint, margin: 0 }, children: t("invalidImageId") })) : null] }), _jsx(Field, { label: t("parent"), htmlFor: parentLabel, children: _jsxs("select", { id: parentLabel, style: imageSelect, value: parent, onChange: (event) => onParent(event.target.value), children: [_jsx("option", { value: "", children: t("selectImage") }), baseImages.map((image) => (_jsx("option", { value: image.imageId, children: image.imageId }, image.imageId))), customImages.map((image) => (_jsx("option", { value: image.imageId, children: image.imageId }, image.imageId)))] }) }), _jsx(Field, { label: t("packages"), htmlFor: packagesLabel, children: _jsx(TagInput, { id: packagesLabel, t: t, value: packages, onChange: onPackages }) })] }) }));
45
+ }, children: [_jsxs(Field, { label: t("imageId"), htmlFor: imageIdLabel, children: [_jsx(Input, { id: imageIdLabel, value: imageId, onChange: (event) => onImageId(sanitizeImageId(event.target.value)) }), imageId !== "" && !imageIdPattern.test(imageId)
46
+ ? _jsx("p", { style: { ...hint, margin: 0 }, children: t("invalidImageId") })
47
+ : null] }), _jsx(Field, { label: t("parent"), htmlFor: parentLabel, children: _jsxs("select", { id: parentLabel, style: imageSelect, value: parent, onChange: (event) => onParent(event.target.value), children: [_jsx("option", { value: "", children: t("selectImage") }), baseImages.map((image) => (_jsx("option", { value: image.imageId, children: image.imageId }, image.imageId))), customImages.map((image) => (_jsx("option", { value: image.imageId, children: image.imageId }, image.imageId)))] }) }), _jsx(Field, { label: t("packages"), htmlFor: packagesLabel, children: _jsx(TagInput, { id: packagesLabel, t: t, value: packages, onChange: onPackages }) })] }) }));
35
48
  }
@@ -1,4 +1,4 @@
1
- import { type ContainerPluginKey } from "./locales.js";
1
+ import type { Translate } from "./locales.js";
2
2
  import { type ReactNode } from "react";
3
3
  export interface PathEntry {
4
4
  id: number;
@@ -6,13 +6,13 @@ export interface PathEntry {
6
6
  }
7
7
  export declare function pathEntries(paths: readonly string[]): PathEntry[];
8
8
  export declare function PathsList(props: {
9
- t: (key: ContainerPluginKey) => string;
9
+ t: Translate;
10
10
  entries: readonly PathEntry[];
11
11
  busy: boolean;
12
12
  onChange: (entries: PathEntry[]) => void;
13
13
  }): ReactNode;
14
14
  export declare function PathsEditor(props: {
15
- t: (key: ContainerPluginKey) => string;
15
+ t: Translate;
16
16
  paths: readonly string[];
17
17
  busy: boolean;
18
18
  enabled: boolean;
@@ -5,7 +5,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
5
5
  import { Field } from "./container-card-shared.js";
6
6
  import { greyId, hint } from "./container-card-styles.js";
7
7
  import { Button, Input, Modal } from "@deepseek-ai/dsh-client-ui-primitives";
8
- import { useId, useState } from "react";
8
+ import { useId, useMemo, useState } from "react";
9
9
  let nextPathId = 0;
10
10
  // pathEntries wraps a stored PATH list for editing.
11
11
  export function pathEntries(paths) {
@@ -60,11 +60,13 @@ export function PathsList(props) {
60
60
  const update = (index, path) => {
61
61
  onChange(entries.map((entry, at) => (at === index ? { ...entry, path } : entry)));
62
62
  };
63
- return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [_jsx(Field, { label: t("addPath"), htmlFor: addId, children: _jsxs("div", { style: { ...pathRow, gap: "8px" }, children: [_jsx(Input, { id: addId, value: draft, disabled: busy, onChange: (event) => setDraft(event.target.value), style: pathInput }), _jsx(Button, { variant: "outline", size: "sm", disabled: busy || draft.trim() === "", onClick: add, children: t("add") })] }) }), entries.length === 0 ? (_jsx("p", { style: { ...hint, margin: 0 }, children: t("noPaths") })) : (entries.map((entry, index) => (_jsxs("div", { style: pathRow, children: [_jsx("button", { type: "button", title: t("dragToReorder"), "aria-label": t("dragToReorder"), draggable: !busy, onDragStart: () => setDragIndex(index), onDragOver: (event) => event.preventDefault(), onDrop: () => {
64
- if (dragIndex !== null)
65
- move(dragIndex, index);
66
- setDragIndex(null);
67
- }, style: dragHandle, children: "\u2261" }), _jsx(Input, { value: entry.path, disabled: busy, onChange: (event) => update(index, event.target.value), style: pathInput }), _jsx(Button, { variant: "outline", size: "sm", title: t("moveUp"), "aria-label": t("moveUp"), disabled: busy || index === 0, onClick: () => move(index, index - 1), style: rowButton, children: "\u2191" }), _jsx(Button, { variant: "outline", size: "sm", title: t("moveDown"), "aria-label": t("moveDown"), disabled: busy || index === entries.length - 1, onClick: () => move(index, index + 1), style: rowButton, children: "\u2193" }), _jsx(Button, { variant: "outline", size: "sm", title: t("remove"), "aria-label": t("remove"), disabled: busy, onClick: () => onChange(entries.filter((_, at) => at !== index)), style: rowButton, children: "\u00D7" })] }, entry.id))))] }));
63
+ return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [_jsx(Field, { label: t("addPath"), htmlFor: addId, children: _jsxs("div", { style: { ...pathRow, gap: "8px" }, children: [_jsx(Input, { id: addId, value: draft, disabled: busy, onChange: (event) => setDraft(event.target.value), style: pathInput }), _jsx(Button, { variant: "outline", size: "sm", disabled: busy || draft.trim() === "", onClick: add, children: t("add") })] }) }), entries.length === 0
64
+ ? _jsx("p", { style: { ...hint, margin: 0 }, children: t("noPaths") })
65
+ : (entries.map((entry, index) => (_jsxs("div", { style: pathRow, children: [_jsx("button", { type: "button", title: t("dragToReorder"), "aria-label": t("dragToReorder"), draggable: !busy, onDragStart: () => setDragIndex(index), onDragOver: (event) => event.preventDefault(), onDrop: () => {
66
+ if (dragIndex !== null)
67
+ move(dragIndex, index);
68
+ setDragIndex(null);
69
+ }, style: dragHandle, children: "\u2261" }), _jsx(Input, { value: entry.path, disabled: busy, onChange: (event) => update(index, event.target.value), style: pathInput }), _jsx(Button, { variant: "outline", size: "sm", title: t("moveUp"), "aria-label": t("moveUp"), disabled: busy || index === 0, onClick: () => move(index, index - 1), style: rowButton, children: "\u2191" }), _jsx(Button, { variant: "outline", size: "sm", title: t("moveDown"), "aria-label": t("moveDown"), disabled: busy || index === entries.length - 1, onClick: () => move(index, index + 1), style: rowButton, children: "\u2193" }), _jsx(Button, { variant: "outline", size: "sm", title: t("remove"), "aria-label": t("remove"), disabled: busy, onClick: () => onChange(entries.filter((_, at) => at !== index)), style: rowButton, children: "\u00D7" })] }, entry.id))))] }));
68
70
  }
69
71
  // PathsEditor shows the container's ordered PATH additions and edits them in a
70
72
  // modal around PathsList. Apply sends the whole list at once.
@@ -72,6 +74,9 @@ export function PathsEditor(props) {
72
74
  const { t, paths, busy, enabled, onApply } = props;
73
75
  const [open, setOpen] = useState(false);
74
76
  const [draft, setDraft] = useState([]);
77
+ // The read-only rows reuse the same ids as the editor, so two identical PATH
78
+ // entries never collide on a React key.
79
+ const shown = useMemo(() => pathEntries(paths), [paths]);
75
80
  const openEditor = () => {
76
81
  setDraft(pathEntries(paths));
77
82
  setOpen(true);
@@ -80,9 +85,11 @@ export function PathsEditor(props) {
80
85
  onApply(draft.map((entry) => entry.path));
81
86
  setOpen(false);
82
87
  };
83
- return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [paths.length === 0 ? (_jsx("p", { style: { ...hint, margin: 0 }, children: t("noPaths") })) : (paths.map((path) => (_jsx("code", { style: {
84
- ...greyId,
85
- fontSize: "13px",
86
- color: "var(--dsw-alias-label-primary)",
87
- }, children: path }, path)))), _jsx("div", { children: _jsx(Button, { variant: "outline", size: "sm", disabled: !enabled, onClick: openEditor, children: t("editPaths") }) }), _jsx(Modal, { open: open, onClose: () => setOpen(false), title: t("pathsTitle"), closeLabel: t("cancel"), footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => setOpen(false), children: t("cancel") }), _jsx(Button, { variant: "primary", size: "sm", disabled: busy, onClick: apply, children: t("apply") })] }), children: _jsx(PathsList, { t: t, entries: draft, busy: busy, onChange: setDraft }) })] }));
88
+ return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [paths.length === 0
89
+ ? _jsx("p", { style: { ...hint, margin: 0 }, children: t("noPaths") })
90
+ : (shown.map((entry) => (_jsx("code", { style: {
91
+ ...greyId,
92
+ fontSize: "13px",
93
+ color: "var(--dsw-alias-label-primary)",
94
+ }, children: entry.path }, entry.id)))), _jsx("div", { children: _jsx(Button, { variant: "outline", size: "sm", disabled: !enabled, onClick: openEditor, children: t("editPaths") }) }), _jsx(Modal, { open: open, onClose: () => setOpen(false), title: t("pathsTitle"), closeLabel: t("cancel"), footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => setOpen(false), children: t("cancel") }), _jsx(Button, { variant: "primary", size: "sm", disabled: busy, onClick: apply, children: t("apply") })] }), children: _jsx(PathsList, { t: t, entries: draft, busy: busy, onChange: setDraft }) })] }));
88
95
  }
@@ -1,9 +1,13 @@
1
1
  import { type ContainerView, type MountInput } from "./container-card-controller.js";
2
2
  import { type DirectoryPickerFace } from "./directory-picker.js";
3
- import { type ContainerPluginKey } from "./locales.js";
3
+ import { type ContainerPluginKey, type Translate } from "./locales.js";
4
4
  import { type ReactNode } from "react";
5
+ export declare function mountSummaries(mounts: readonly ContainerView["mounts"][number][]): {
6
+ key: ContainerPluginKey;
7
+ values: string[];
8
+ }[];
5
9
  export declare function ContainerRow(props: {
6
- t: (key: ContainerPluginKey) => string;
10
+ t: Translate;
7
11
  container: ContainerView;
8
12
  images: readonly {
9
13
  imageId: string;
@@ -15,14 +19,14 @@ export declare function ContainerRow(props: {
15
19
  name: string;
16
20
  }[];
17
21
  busy: boolean;
18
- onRemove: (workspace: string) => void;
19
- onRecreate: (workspace: string, image: string, env?: Record<string, string>) => void;
22
+ onRemove: (workspace: string, container: string) => void;
23
+ onRecreate: (workspace: string, container: string, image: string, env?: Record<string, string>) => void;
20
24
  onAddContainerMount: (workspace: string, container: string, mount: MountInput) => void;
21
25
  onRemoveContainerMount: (workspace: string, container: string, mount: MountInput) => void;
22
26
  onUpdateContainerMount: (workspace: string, container: string, mount: MountInput) => void;
23
27
  onSetContainerPaths: (workspace: string, container: string, paths: readonly string[]) => void;
24
- onAddContainerSecret: (workspace: string, envVar: string, secret: string) => void;
25
- onRemoveContainerSecret: (workspace: string, envVar: string) => void;
28
+ onAddContainerSecret: (workspace: string, container: string, envVar: string, secret: string) => void;
29
+ onRemoveContainerSecret: (workspace: string, container: string, envVar: string) => void;
26
30
  projectName: string;
27
31
  projectsRoot: string;
28
32
  directoryPicker?: DirectoryPickerFace;
@@ -1,14 +1,62 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { EnvEditor, MountsEditor } from "./container-card-editors.js";
3
3
  import { PathsEditor } from "./container-card-paths.js";
4
- import { ConfirmButton, mountViewToInput } from "./container-card-shared.js";
4
+ import { ConfirmButton, mountKindShort, mountViewToInput, } from "./container-card-shared.js";
5
5
  import { actions, containerHeader, containerRow, greyId, imageSelect, tableStyle, tdStyle, thStyle, wsBody, } from "./container-card-styles.js";
6
- import { Button, DisclosureRow, Input, Pill } from "@deepseek-ai/dsh-client-ui-primitives";
7
- import { useState } from "react";
6
+ import { Button, DisclosureRow, Input, Pill, } from "@deepseek-ai/dsh-client-ui-primitives";
7
+ import { useEffect, useState } from "react";
8
+ // The order the summary table lists mount kinds in.
9
+ const MOUNT_ROWS = [
10
+ { key: "projects", kind: "project" },
11
+ { key: "volumesTitle", kind: "volume" },
12
+ { key: "mountTmpfs", kind: "tmpfs" },
13
+ { key: "secretsTitle", kind: "secret" },
14
+ ];
15
+ // One summary row per mount kind present on a container, so a volume, tmpfs or
16
+ // secret never contributes an empty project name to the Projects line. A mount
17
+ // whose identifying field is empty is skipped, and a kind with no mount left
18
+ // gets no row at all.
19
+ export function mountSummaries(mounts) {
20
+ const buckets = new Map(MOUNT_ROWS.map(({ kind }) => [kind, []]));
21
+ for (const mount of mounts) {
22
+ const kind = mountKindShort(mount.kind);
23
+ const values = buckets.get(kind);
24
+ if (values === undefined)
25
+ continue;
26
+ const identifier = kind === "project"
27
+ ? mount.projectName
28
+ : kind === "volume"
29
+ ? mount.volume
30
+ : kind === "secret"
31
+ ? mount.secret
32
+ : mount.destination;
33
+ if (identifier === "")
34
+ continue;
35
+ values.push(kind === "tmpfs" || mount.destination === ""
36
+ ? identifier
37
+ : `${identifier} → ${mount.destination}`);
38
+ }
39
+ return MOUNT_ROWS.flatMap(({ key, kind }) => {
40
+ const values = buckets.get(kind) ?? [];
41
+ return values.length === 0 ? [] : [{ key, values }];
42
+ });
43
+ }
8
44
  export function ContainerRow(props) {
9
45
  const { t, container, images, volumes, secrets, busy, onRemove, onRecreate, onAddContainerMount, onRemoveContainerMount, onUpdateContainerMount, onSetContainerPaths, onAddContainerSecret, onRemoveContainerSecret, projectName, projectsRoot, directoryPicker, } = props;
10
46
  const [selected, setSelected] = useState(container.imageId);
11
47
  const [env, setEnv] = useState(container.env);
48
+ // The row is keyed by container name, so it survives a snapshot refresh.
49
+ // Re-sync the image draft when the live image changes, or the select would
50
+ // keep offering and re-sending the previous one.
51
+ useEffect(() => {
52
+ setSelected(container.imageId);
53
+ }, [container.imageId]);
54
+ // Env is a fresh object on every snapshot; key the resync on its content so
55
+ // an unchanged refresh does not clobber an in-progress edit.
56
+ const serverEnv = JSON.stringify(container.env);
57
+ useEffect(() => {
58
+ setEnv(container.env);
59
+ }, [serverEnv]);
12
60
  const [envOpen, setEnvOpen] = useState(false);
13
61
  const [mountOpen, setMountOpen] = useState(false);
14
62
  const [pathOpen, setPathOpen] = useState(false);
@@ -16,21 +64,36 @@ export function ContainerRow(props) {
16
64
  const [attachSecret, setAttachSecret] = useState("");
17
65
  const [attachVar, setAttachVar] = useState("");
18
66
  const enabled = container.workspaceSlug !== "" && !busy;
19
- const projects = container.mounts
20
- .map((mount) => mount.projectName)
21
- .join(", ");
67
+ const mountRows = mountSummaries(container.mounts);
22
68
  const envEntries = Object.entries(container.env);
23
69
  const secretEntries = Object.entries(container.secretEnv);
24
70
  const attach = () => {
25
71
  const envVar = attachVar.trim();
26
72
  if (envVar === "" || attachSecret === "")
27
73
  return;
28
- onAddContainerSecret(container.workspaceSlug, envVar, attachSecret);
74
+ onAddContainerSecret(container.workspaceSlug, container.containerName, envVar, attachSecret);
29
75
  setAttachVar("");
30
76
  };
31
- return (_jsxs("div", { style: containerRow, children: [_jsxs("div", { style: containerHeader, children: [_jsx("strong", { style: { color: "var(--dsw-alias-label-primary)" }, children: container.containerName }), _jsx(Pill, { children: container.status })] }), _jsx("table", { style: tableStyle, children: _jsxs("tbody", { children: [_jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("image") }), _jsx("td", { style: tdStyle, children: container.imageId })] }), _jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("created") }), _jsx("td", { style: tdStyle, children: container.createdAt })] }), projects !== "" ? (_jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("projects") }), _jsx("td", { style: tdStyle, children: projects })] })) : null, envEntries.length > 0 ? (_jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("env") }), _jsx("td", { style: tdStyle, children: envEntries.map(([key]) => key).join(", ") })] })) : null, secretEntries.length > 0 ? (_jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("secretEnv") }), _jsx("td", { style: tdStyle, children: secretEntries
32
- .map(([envVar, secretName]) => `${envVar}=${secretName}`)
33
- .join(", ") })] })) : null] }) }), _jsxs("div", { style: actions, children: [_jsx(ConfirmButton, { t: t, label: t("remove"), title: t("confirmTitle"), description: t("confirmRemoveContainer"), disabled: !enabled, onConfirm: () => onRemove(container.workspaceSlug) }), _jsx(ConfirmButton, { t: t, label: t("recreate"), title: t("confirmTitle"), description: t("confirmRecreate"), disabled: !enabled, onConfirm: () => onRecreate(container.workspaceSlug, "", env) }), _jsxs("select", { style: imageSelect, value: selected, disabled: !enabled, onChange: (event) => setSelected(event.target.value), "aria-label": t("recreateWithImage"), children: [container.imageId === "" ? (_jsx("option", { value: "", children: t("none") })) : null, images.map((image) => (_jsx("option", { value: image.imageId, children: image.imageId }, image.imageId)))] }), _jsx(ConfirmButton, { t: t, label: t("recreateWithImage"), title: t("confirmTitle"), description: t("confirmRecreate"), disabled: !enabled || selected === "", onConfirm: () => onRecreate(container.workspaceSlug, selected, env) })] }), _jsx(DisclosureRow, { icon: _jsx("span", {}), title: t("envTitle"), open: envOpen, expandable: true, onToggle: () => setEnvOpen(!envOpen), children: _jsx("div", { style: wsBody, children: _jsx(EnvEditor, { t: t, env: env, busy: busy, onChange: setEnv }) }) }), _jsx(DisclosureRow, { icon: _jsx("span", {}), title: t("mountsTitle"), open: mountOpen, expandable: true, onToggle: () => setMountOpen(!mountOpen), children: _jsx("div", { style: wsBody, children: _jsx(MountsEditor, { t: t, mounts: container.mounts.map(mountViewToInput), volumes: volumes, secrets: secrets, busy: busy, enabled: enabled, projectsRoot: projectsRoot, directoryPicker: directoryPicker, confirmRemove: true, primaryProject: container.containerName === "default" ? projectName : "", onUpdate: (mount) => onUpdateContainerMount(container.workspaceSlug, container.containerName, mount), onRemove: (mount) => onRemoveContainerMount(container.workspaceSlug, container.containerName, mount), onAdd: (mount) => onAddContainerMount(container.workspaceSlug, container.containerName, mount) }) }) }), _jsx(DisclosureRow, { icon: _jsx("span", {}), title: t("pathsTitle"), open: pathOpen, expandable: true, onToggle: () => setPathOpen(!pathOpen), children: _jsx("div", { style: wsBody, children: _jsx(PathsEditor, { t: t, paths: container.paths, busy: busy, enabled: enabled, onApply: (paths) => onSetContainerPaths(container.workspaceSlug, container.containerName, paths) }) }) }), _jsx(DisclosureRow, { icon: _jsx("span", {}), title: t("containerSecretsTitle"), open: secretOpen, expandable: true, onToggle: () => setSecretOpen(!secretOpen), children: _jsxs("div", { style: wsBody, children: [secretEntries.map(([envVar, secretName]) => (_jsxs("div", { style: {
77
+ return (_jsxs("div", { style: containerRow, children: [_jsxs("div", { style: containerHeader, children: [_jsx("strong", { style: { color: "var(--dsw-alias-label-primary)" }, children: container.containerName }), _jsx(Pill, { children: container.status })] }), _jsx("table", { style: tableStyle, children: _jsxs("tbody", { children: [_jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("image") }), _jsx("td", { style: tdStyle, children: container.imageId })] }), _jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("created") }), _jsx("td", { style: tdStyle, children: container.createdAt })] }), mountRows.map(({ key, values }) => (_jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t(key) }), _jsx("td", { style: tdStyle, children: values.join(", ") })] }, key))), envEntries.length > 0
78
+ ? (_jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("env") }), _jsx("td", { style: tdStyle, children: envEntries.map(([key]) => key).join(", ") })] }))
79
+ : null, secretEntries.length > 0
80
+ ? (_jsxs("tr", { children: [_jsx("th", { style: thStyle, scope: "row", children: t("secretEnv") }), _jsx("td", { style: tdStyle, children: secretEntries
81
+ .map(([envVar, secretName]) => `${envVar}=${secretName}`)
82
+ .join(", ") })] }))
83
+ : null] }) }), _jsxs("div", { style: actions, children: [_jsx(ConfirmButton, { t: t, label: t("remove"), title: t("confirmTitle"), description: t("confirmRemoveContainer", {
84
+ workspace: projectName,
85
+ container: container.containerName,
86
+ }), disabled: !enabled, onConfirm: () => onRemove(container.workspaceSlug, container.containerName) }), _jsx(ConfirmButton, { t: t, label: t("recreate"), title: t("confirmTitle"), description: t("confirmRecreate", {
87
+ workspace: projectName,
88
+ container: container.containerName,
89
+ }), disabled: !enabled, onConfirm: () => onRecreate(container.workspaceSlug, container.containerName, "", env) }), _jsxs("select", { style: imageSelect, value: selected, disabled: !enabled, onChange: (event) => setSelected(event.target.value), "aria-label": t("recreateWithImage"), children: [container.imageId === ""
90
+ ? _jsx("option", { value: "", children: t("none") })
91
+ : null, images.map((image) => (_jsx("option", { value: image.imageId, children: image.imageId }, image.imageId)))] }), _jsx(ConfirmButton, { t: t, label: t("recreateWithImage"), title: t("confirmTitle"), description: t("confirmRecreate", {
92
+ workspace: projectName,
93
+ container: container.containerName,
94
+ }), disabled: !enabled || selected === "", onConfirm: () => onRecreate(container.workspaceSlug, container.containerName, selected, env) })] }), _jsx(DisclosureRow, { icon: _jsx("span", {}), title: t("envTitle"), open: envOpen, expandable: true, onToggle: () => setEnvOpen(!envOpen), children: _jsx("div", { style: wsBody, children: _jsx(EnvEditor, { t: t, env: env, busy: busy, onChange: setEnv }) }) }), _jsx(DisclosureRow, { icon: _jsx("span", {}), title: t("mountsTitle"), open: mountOpen, expandable: true, onToggle: () => setMountOpen(!mountOpen), children: _jsx("div", { style: wsBody, children: _jsx(MountsEditor, { t: t, mounts: container.mounts.map(mountViewToInput), volumes: volumes, secrets: secrets, busy: busy, enabled: enabled, projectsRoot: projectsRoot, directoryPicker: directoryPicker, confirmRemove: true, primaryProject: container.containerName === "default"
95
+ ? projectName
96
+ : "", onUpdate: (mount) => onUpdateContainerMount(container.workspaceSlug, container.containerName, mount), onRemove: (mount) => onRemoveContainerMount(container.workspaceSlug, container.containerName, mount), onAdd: (mount) => onAddContainerMount(container.workspaceSlug, container.containerName, mount) }) }) }), _jsx(DisclosureRow, { icon: _jsx("span", {}), title: t("pathsTitle"), open: pathOpen, expandable: true, onToggle: () => setPathOpen(!pathOpen), children: _jsx("div", { style: wsBody, children: _jsx(PathsEditor, { t: t, paths: container.paths, busy: busy, enabled: enabled, onApply: (paths) => onSetContainerPaths(container.workspaceSlug, container.containerName, paths) }) }) }), _jsx(DisclosureRow, { icon: _jsx("span", {}), title: t("containerSecretsTitle"), open: secretOpen, expandable: true, onToggle: () => setSecretOpen(!secretOpen), children: _jsxs("div", { style: wsBody, children: [secretEntries.map(([envVar, secretName]) => (_jsxs("div", { style: {
34
97
  display: "flex",
35
98
  flexWrap: "wrap",
36
99
  alignItems: "center",
@@ -40,15 +103,21 @@ export function ContainerRow(props) {
40
103
  flex: 1,
41
104
  fontSize: "13px",
42
105
  color: "var(--dsw-alias-label-primary)",
43
- }, children: [envVar, "=", secretName] }), _jsx(ConfirmButton, { t: t, label: t("detachSecret"), title: t("confirmTitle"), description: t("confirmDetachSecret"), disabled: busy, onConfirm: () => onRemoveContainerSecret(container.workspaceSlug, envVar) })] }, envVar))), _jsxs("div", { style: {
106
+ }, children: [envVar, "=", secretName] }), _jsx(ConfirmButton, { t: t, label: t("detachSecret"), title: t("confirmTitle"), description: t("confirmDetachSecret", {
107
+ env: envVar,
108
+ secret: secretName,
109
+ }), disabled: busy, onConfirm: () => onRemoveContainerSecret(container.workspaceSlug, container.containerName, envVar) })] }, envVar))), _jsxs("div", { style: {
44
110
  display: "flex",
45
111
  flexWrap: "wrap",
46
112
  alignItems: "center",
47
113
  gap: "8px",
48
114
  paddingTop: "8px",
49
- }, children: [_jsxs("select", { style: imageSelect, value: attachSecret, disabled: !enabled, onChange: (event) => setAttachSecret(event.target.value), "aria-label": t("attachSecret"), children: [secrets.length === 0 ? (_jsx("option", { value: "", children: t("none") })) : null, secrets.map((secret) => (_jsx("option", { value: secret.name, children: secret.name }, secret.name)))] }), _jsx(Input, { value: attachVar, disabled: !enabled, placeholder: t("secretEnvName"), onChange: (event) => setAttachVar(event.target.value), onKeyDown: (event) => {
115
+ }, children: [_jsxs("select", { style: imageSelect, value: attachSecret, disabled: !enabled, onChange: (event) => setAttachSecret(event.target.value), "aria-label": t("attachSecret"), children: [secrets.length === 0
116
+ ? _jsx("option", { value: "", children: t("none") })
117
+ : null, secrets.map((secret) => (_jsx("option", { value: secret.name, children: secret.name }, secret.name)))] }), _jsx(Input, { value: attachVar, disabled: !enabled, placeholder: t("secretEnvName"), onChange: (event) => setAttachVar(event.target.value), onKeyDown: (event) => {
50
118
  if (event.key === "Enter")
51
119
  attach();
52
- }, style: { width: "200px" } }), _jsx(Button, { variant: "outline", size: "sm", disabled: !enabled || attachVar.trim() === "" || attachSecret === "", onClick: attach, children: t("attachSecret") })] })] }) })] }));
120
+ }, style: { width: "200px" } }), _jsx(Button, { variant: "outline", size: "sm", disabled: !enabled || attachVar.trim() === "" ||
121
+ attachSecret === "", onClick: attach, children: t("attachSecret") })] })] }) })] }));
53
122
  }
54
123
  export { CreateContainerModal } from "./container-card-create-modal.js";
@@ -1,7 +1,7 @@
1
- import { type ContainerPluginKey } from "./locales.js";
1
+ import type { Translate } from "./locales.js";
2
2
  import { type ReactNode } from "react";
3
3
  export declare function SecretRow(props: {
4
- t: (key: ContainerPluginKey) => string;
4
+ t: Translate;
5
5
  name: string;
6
6
  busy: boolean;
7
7
  writable: boolean;
@@ -9,7 +9,7 @@ export declare function SecretRow(props: {
9
9
  onRemove: (name: string) => void;
10
10
  }): ReactNode;
11
11
  export declare function SecretsSection(props: {
12
- t: (key: ContainerPluginKey) => string;
12
+ t: Translate;
13
13
  secrets: readonly {
14
14
  name: string;
15
15
  }[];