@exagone313/dsh-podman 0.2.0-rc.3 → 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 (191) hide show
  1. package/README.md +14 -2
  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 +154 -42
  11. package/dist/card-route.test.js +85 -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 +92 -50
  15. package/dist/client/card-protocol.d.ts +6 -3
  16. package/dist/client/container-card-caches.js +11 -6
  17. package/dist/client/container-card-controller.d.ts +16 -18
  18. package/dist/client/container-card-controller.js +92 -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 +13141 -2036
  45. package/dist/client/locales.d.ts +6 -1
  46. package/dist/client/locales.js +106 -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.d.ts +1 -0
  76. package/dist/dsh-version.js +46 -0
  77. package/dist/env-rows.d.ts +14 -0
  78. package/dist/env-rows.js +52 -0
  79. package/dist/env-rows.test.d.ts +1 -0
  80. package/dist/env-rows.test.js +43 -0
  81. package/dist/fs-provider.d.ts +1 -0
  82. package/dist/fs-provider.js +26 -12
  83. package/dist/fs-provider.test.js +51 -10
  84. package/dist/generated/version.d.ts +2 -2
  85. package/dist/generated/version.js +2 -2
  86. package/dist/grpc/proto/dshctl/v1/control.proto +11 -1
  87. package/dist/grpc/proto/dshguest/v1/guest.proto +5 -0
  88. package/dist/grpc/runtime-client.d.ts +3 -1
  89. package/dist/grpc/runtime-client.js +90 -17
  90. package/dist/guest-rpc.d.ts +15 -19
  91. package/dist/guest-rpc.js +289 -70
  92. package/dist/guest-rpc.test.d.ts +1 -0
  93. package/dist/guest-rpc.test.js +241 -0
  94. package/dist/guest-terminal.d.ts +30 -0
  95. package/dist/guest-terminal.js +196 -0
  96. package/dist/images.test.js +10 -3
  97. package/dist/index.d.ts +11 -16
  98. package/dist/index.js +42 -36
  99. package/dist/locales.test.d.ts +1 -0
  100. package/dist/locales.test.js +34 -0
  101. package/dist/misc.test.js +38 -10
  102. package/dist/mount-enums.js +2 -1
  103. package/dist/mount-input.d.ts +2 -0
  104. package/dist/mount-input.js +38 -22
  105. package/dist/mounts.test.js +81 -11
  106. package/dist/naming.test.d.ts +1 -0
  107. package/dist/naming.test.js +28 -0
  108. package/dist/output-reader.js +36 -4
  109. package/dist/package-deps.test.d.ts +1 -0
  110. package/dist/package-deps.test.js +49 -0
  111. package/dist/paths.test.js +4 -2
  112. package/dist/plugin-meta.test.d.ts +1 -0
  113. package/dist/plugin-meta.test.js +29 -0
  114. package/dist/project-path.js +33 -7
  115. package/dist/project-path.test.js +14 -0
  116. package/dist/prompts.d.ts +0 -4
  117. package/dist/prompts.js +15 -84
  118. package/dist/read-only-shell.js +3 -1
  119. package/dist/read-only-shell.test.js +80 -25
  120. package/dist/runtime-client.test.d.ts +1 -0
  121. package/dist/runtime-client.test.js +53 -0
  122. package/dist/secrets.test.js +21 -6
  123. package/dist/settings-commands.test.js +61 -14
  124. package/dist/settings-create.test.js +32 -10
  125. package/dist/settings-default-env.test.d.ts +1 -0
  126. package/dist/settings-default-env.test.js +175 -0
  127. package/dist/settings-mounts.test.js +46 -7
  128. package/dist/settings-schema.d.ts +6 -11
  129. package/dist/settings-schema.js +4 -8
  130. package/dist/settings-schema.test.d.ts +1 -0
  131. package/dist/settings-schema.test.js +35 -0
  132. package/dist/settings-workspaces.test.js +38 -63
  133. package/dist/spill-store.d.ts +1 -0
  134. package/dist/spill-store.js +17 -4
  135. package/dist/spill-store.test.js +31 -8
  136. package/dist/subprocess.d.ts +4 -0
  137. package/dist/subprocess.js +148 -162
  138. package/dist/subprocess.test.js +118 -8
  139. package/dist/terminal-preference.test.d.ts +1 -0
  140. package/dist/terminal-preference.test.js +62 -0
  141. package/dist/terminal-route.d.ts +10 -0
  142. package/dist/terminal-route.js +270 -0
  143. package/dist/terminal-route.test.d.ts +1 -0
  144. package/dist/terminal-route.test.js +265 -0
  145. package/dist/terminal-sessions.d.ts +69 -0
  146. package/dist/terminal-sessions.js +252 -0
  147. package/dist/terminal-shells.d.ts +22 -0
  148. package/dist/terminal-shells.js +99 -0
  149. package/dist/terminal-tab.test.d.ts +1 -0
  150. package/dist/terminal-tab.test.js +58 -0
  151. package/dist/terminal-targets.test.d.ts +1 -0
  152. package/dist/terminal-targets.test.js +43 -0
  153. package/dist/terminal-titles.test.d.ts +1 -0
  154. package/dist/terminal-titles.test.js +36 -0
  155. package/dist/test-support.d.ts +35 -5
  156. package/dist/test-support.js +91 -29
  157. package/dist/tool-defs.js +19 -4
  158. package/dist/tool-handlers.js +80 -28
  159. package/dist/tool-params.js +19 -5
  160. package/dist/version-compat.d.ts +4 -0
  161. package/dist/version-compat.js +29 -0
  162. package/dist/version-compat.test.d.ts +1 -0
  163. package/dist/version-compat.test.js +27 -0
  164. package/dist/volumes.test.js +1 -1
  165. package/dist/workspace-binding.d.ts +19 -10
  166. package/dist/workspace-binding.js +252 -78
  167. package/dist/workspace-binding.test.js +202 -27
  168. package/docs/architecture.md +71 -20
  169. package/docs/architecture.zh.md +62 -12
  170. package/docs/configuration.md +61 -20
  171. package/docs/configuration.zh.md +53 -19
  172. package/docs/development-prompt.md +72 -0
  173. package/docs/development-prompt.zh.md +69 -0
  174. package/docs/development.md +184 -29
  175. package/docs/development.zh.md +165 -26
  176. package/docs/install-dsh-and-dsh-podman.md +24 -21
  177. package/docs/install-dsh-and-dsh-podman.zh.md +20 -21
  178. package/docs/uninstall.md +83 -0
  179. package/docs/uninstall.zh.md +79 -0
  180. package/docs/update.md +120 -0
  181. package/docs/update.zh.md +114 -0
  182. package/docs/usage.md +108 -61
  183. package/docs/usage.zh.md +76 -38
  184. package/locale/en.json +6 -0
  185. package/locale/zh.json +6 -0
  186. package/package.json +58 -25
  187. package/quadlet/dsh-podman-orchestrator.container +46 -0
  188. package/quadlet/dsh.container +35 -0
  189. package/LICENSE.pkg +0 -17103
  190. package/dist/preferences.d.ts +0 -6
  191. package/dist/preferences.js +0 -27
@@ -0,0 +1,10 @@
1
+ import type { Translate } from "./locales.js";
2
+ import { type ReactNode } from "react";
3
+ export declare function DefaultEnvironmentSection(props: {
4
+ t: Translate;
5
+ env: Record<string, string>;
6
+ busy: boolean;
7
+ writable: boolean;
8
+ onSave: (env: Record<string, string>) => void;
9
+ onSync: (workspace: string) => void;
10
+ }): ReactNode;
@@ -0,0 +1,69 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
3
+ //
4
+ // SPDX-License-Identifier: MIT
5
+ // The default container environment: the map seeded into every new container,
6
+ // edited with the same EnvEditor a container row uses. The setting is
7
+ // persisted, so the draft is committed with Save, and the git shortcut fills
8
+ // the four identity variables in that same draft.
9
+ import { EnvEditor } from "./container-card-editors.js";
10
+ import { ConfirmButton, Field } from "./container-card-shared.js";
11
+ import { hint, sectionTitle, wsBody } from "./container-card-styles.js";
12
+ import { commitIdentityAction, readGitIdentity, setGitIdentity, } from "../container-env.js";
13
+ import { Button, Input, Modal } from "@deepseek-ai/dsh-client-ui-primitives";
14
+ import { useEffect, useId, useState } from "react";
15
+ export function DefaultEnvironmentSection(props) {
16
+ const { t, env, busy, writable, onSave, onSync } = props;
17
+ const [draft, setDraft] = useState(env);
18
+ const [gitOpen, setGitOpen] = useState(false);
19
+ const [gitName, setGitName] = useState("");
20
+ const [gitEmail, setGitEmail] = useState("");
21
+ const nameId = useId();
22
+ const emailId = useId();
23
+ // An external settings change resyncs the draft; an unrelated refresh does
24
+ // not, so an edit in progress is never clobbered. Same rule as a container
25
+ // row's environment editor.
26
+ const serverEnv = JSON.stringify(env);
27
+ useEffect(() => {
28
+ setDraft(env);
29
+ }, [serverEnv]);
30
+ const dirty = JSON.stringify(draft) !== serverEnv;
31
+ const disabled = busy || !writable;
32
+ // The identity popup applies into the draft; when the draft's only drift from
33
+ // the saved environment is inside the identity keys, it commits straight, so
34
+ // a name/email edit is not left unsaved behind an easy-to-miss Save click.
35
+ // When the popup restores the saved identity exactly, its pending change is
36
+ // discarded and the editor falls back to the persisted defaults, instead of
37
+ // keeping the stale identity (a no-op save would not resync the draft, since
38
+ // the server value did not change).
39
+ const commitIdentity = (next) => {
40
+ const action = commitIdentityAction(env, draft, next);
41
+ if (action === "save") {
42
+ onSave(next);
43
+ }
44
+ else if (action === "discard") {
45
+ setDraft(env);
46
+ }
47
+ else {
48
+ setDraft(next);
49
+ }
50
+ };
51
+ const openGit = () => {
52
+ const identity = readGitIdentity(draft);
53
+ setGitName(identity.name);
54
+ setGitEmail(identity.email);
55
+ setGitOpen(true);
56
+ };
57
+ return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "8px" }, children: [_jsx("div", { style: sectionTitle, children: t("defaultEnvironment") }), _jsx("div", { style: wsBody, children: _jsx(EnvEditor, { t: t, env: draft, busy: disabled, onChange: setDraft }) }), _jsxs("div", { style: {
58
+ display: "flex",
59
+ flexWrap: "wrap",
60
+ alignItems: "center",
61
+ gap: "8px",
62
+ }, children: [_jsx(Button, { variant: "outline", size: "sm", disabled: disabled, onClick: openGit, children: t("gitIdentity") }), _jsx(Button, { variant: "primary", size: "sm", disabled: disabled || !dirty, onClick: () => onSave(draft), children: t("save") }), _jsx(Button, { variant: "ghost", size: "sm", disabled: busy || !dirty, onClick: () => setDraft(env), children: t("discard") })] }), _jsx("div", { children: _jsx(ConfirmButton, { t: t, label: t("applyDefaults"), title: t("confirmTitle"), description: t("confirmApplyDefaults"), disabled: disabled || dirty || Object.keys(env).length === 0, onConfirm: () => onSync("") }) }), _jsx("p", { style: { ...hint, margin: 0 }, children: t("defaultEnvironmentHint") }), _jsx(Modal, { open: gitOpen, onClose: () => setGitOpen(false), title: t("gitIdentity"), closeLabel: t("cancel"), footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", size: "sm", disabled: busy || readGitIdentity(draft).name === "", onClick: () => {
63
+ commitIdentity(setGitIdentity(draft, "", ""));
64
+ setGitOpen(false);
65
+ }, children: t("clear") }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => setGitOpen(false), children: t("cancel") }), _jsx(Button, { variant: "primary", size: "sm", disabled: busy || gitName === "" || gitEmail === "", onClick: () => {
66
+ commitIdentity(setGitIdentity(draft, gitName, gitEmail));
67
+ setGitOpen(false);
68
+ }, children: t("apply") })] }), children: _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "12px" }, children: [_jsx(Field, { label: t("gitName"), htmlFor: nameId, children: _jsx(Input, { id: nameId, value: gitName, disabled: busy, onChange: (event) => setGitName(event.target.value) }) }), _jsx(Field, { label: t("gitEmail"), htmlFor: emailId, children: _jsx(Input, { id: emailId, value: gitEmail, disabled: busy, onChange: (event) => setGitEmail(event.target.value) }) }), _jsx("p", { style: { ...hint, margin: 0 }, children: t("gitIdentityHint") })] }) })] }));
69
+ }
@@ -1,8 +1,8 @@
1
1
  import { type DirectoryPickerFace } from "./directory-picker.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 DirectoryPickerModal(props: {
5
- t: (key: ContainerPluginKey) => string;
5
+ t: Translate;
6
6
  open: boolean;
7
7
  root: string;
8
8
  start: string;
@@ -1,7 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { DIRECTORY_CLASS } from "./container-card-directory-styles.js";
3
- import { confineToRoot, crumbLabel, rootCrumbs, } from "../project-path.js";
4
- import { Button, IconCheckOutline16, IconChevronRightOutline14, IconFolderClose16, IconFolderOpen16, Modal, } from "@deepseek-ai/dsh-client-ui-primitives";
3
+ import { confineToRoot, crumbLabel, rootCrumbs } from "../project-path.js";
4
+ import { Button, IconCheckOutlineRegular, IconChevronRightOutlineRegular, IconFolderCloseRegular, IconFolderOpenRegular, Modal, } from "@deepseek-ai/dsh-client-ui-primitives";
5
5
  import { useEffect, useRef, useState } from "react";
6
6
  // How long a listing may take before the browser admits it is still loading.
7
7
  const SLOW_SCAN_DELAY_MS = 300;
@@ -128,13 +128,21 @@ export function DirectoryPickerModal(props) {
128
128
  const crumbs = rootCrumbs(root, path);
129
129
  const visible = (entries) => entries.filter((entry) => showHidden || !entry.hidden);
130
130
  const truncated = level?.truncated === true || child?.truncated === true;
131
- return (_jsx(Modal, { open: open, onClose: onClose, title: t("browseTitle"), className: DIRECTORY_CLASS, headless: true, children: _jsxs("div", { className: "dsh-podman-directory-scope", children: [_jsxs("div", { className: "dsh-podman-directory-header", children: [_jsx("h2", { className: "dsh-podman-directory-title", children: t("browseTitle") }), _jsx("div", { className: "dsh-podman-directory-crumbbar", children: _jsxs("nav", { ref: trailRef, className: "dsh-podman-directory-crumbs", "aria-label": t("browseTitle"), children: [_jsx("span", { className: "dsh-podman-directory-crumbseat", children: _jsx("button", { type: "button", className: "dsh-podman-directory-crumb", disabled: busy, onClick: () => navigate(root), children: crumbLabel(root) }) }), crumbs.map((crumb) => (_jsxs("span", { className: "dsh-podman-directory-crumbseat", children: [_jsx(IconChevronRightOutline14, { size: 12, className: "dsh-podman-directory-crumbchevron" }), _jsx("button", { type: "button", className: "dsh-podman-directory-crumb", disabled: busy, onClick: () => navigate(crumb), children: crumbLabel(crumb) })] }, crumb)))] }) })] }), _jsxs("div", { className: "dsh-podman-directory-content", children: [_jsxs("div", { className: "dsh-podman-directory-miller", children: [_jsx("ul", { className: "dsh-podman-directory-column", children: (level?.entries ?? []).length === 0 ? null : (visible(level?.entries ?? []).map((entry) => (_jsx(DirectoryRow, { entry: entry, selected: selected?.path === entry.path, busy: busy, onPick: select }, entry.path)))) }), selected === null ? null : (_jsxs(_Fragment, { children: [_jsx("span", { className: "dsh-podman-directory-divider" }), _jsx("ul", { className: "dsh-podman-directory-column", children: visible(child?.entries ?? []).map((entry) => (_jsx(DirectoryRow, { entry: entry, selected: false, busy: busy, onPick: advance }, entry.path))) })] }))] }), error === "" ? null : (_jsx("p", { className: "dsh-podman-directory-error", role: "alert", children: error })), error === "" && truncated ? (_jsx("p", { className: "dsh-podman-directory-status", role: "status", children: t("browseTruncated") })) : null, slow ? (_jsx("span", { className: "dsh-podman-directory-loading", role: "status", children: t("browseLoading") })) : null] }), _jsxs("div", { className: "dsh-podman-directory-footer", children: [_jsxs("button", { type: "button", className: "dsh-podman-directory-toggle", "aria-pressed": showHidden, disabled: busy, onClick: () => setShowHidden((previous) => !previous), children: [t("browseHidden"), showHidden ? _jsx(IconCheckOutline16, { size: 14 }) : null] }), _jsx("span", { className: "dsh-podman-directory-gap" }), _jsx(Button, { variant: "outline", className: "dsh-podman-directory-action", disabled: busy, onClick: onClose, children: t("cancel") }), _jsx(Button, { variant: "primary", className: "dsh-podman-directory-action", disabled: busy || error !== "", onClick: () => onSelect(selected?.path ?? path), children: t("browseSelect") })] })] }) }));
131
+ return (_jsx(Modal, { open: open, onClose: onClose, title: t("browseTitle"), className: DIRECTORY_CLASS, headless: true, children: _jsxs("div", { className: "dsh-podman-directory-scope", children: [_jsxs("div", { className: "dsh-podman-directory-header", children: [_jsx("h2", { className: "dsh-podman-directory-title", children: t("browseTitle") }), _jsx("div", { className: "dsh-podman-directory-crumbbar", children: _jsxs("nav", { ref: trailRef, className: "dsh-podman-directory-crumbs", "aria-label": t("browseTitle"), children: [_jsx("span", { className: "dsh-podman-directory-crumbseat", children: _jsx("button", { type: "button", className: "dsh-podman-directory-crumb", disabled: busy, onClick: () => navigate(root), children: crumbLabel(root) }) }), crumbs.map((crumb) => (_jsxs("span", { className: "dsh-podman-directory-crumbseat", children: [_jsx(IconChevronRightOutlineRegular, { size: 12, className: "dsh-podman-directory-crumbchevron" }), _jsx("button", { type: "button", className: "dsh-podman-directory-crumb", disabled: busy, onClick: () => navigate(crumb), children: crumbLabel(crumb) })] }, crumb)))] }) })] }), _jsxs("div", { className: "dsh-podman-directory-content", children: [_jsxs("div", { className: "dsh-podman-directory-miller", children: [_jsx("ul", { className: "dsh-podman-directory-column", children: (level?.entries ?? []).length === 0 ? null : (visible(level?.entries ?? []).map((entry) => (_jsx(DirectoryRow, { entry: entry, selected: selected?.path === entry.path, busy: busy, onPick: select }, entry.path)))) }), selected === null ? null : (_jsxs(_Fragment, { children: [_jsx("span", { className: "dsh-podman-directory-divider" }), _jsx("ul", { className: "dsh-podman-directory-column", children: visible(child?.entries ?? []).map((entry) => (_jsx(DirectoryRow, { entry: entry, selected: false, busy: busy, onPick: advance }, entry.path))) })] }))] }), error === ""
132
+ ? null
133
+ : (_jsx("p", { className: "dsh-podman-directory-error", role: "alert", children: error })), error === "" && truncated
134
+ ? (_jsx("p", { className: "dsh-podman-directory-status", role: "status", children: t("browseTruncated") }))
135
+ : null, slow
136
+ ? (_jsx("span", { className: "dsh-podman-directory-loading", role: "status", children: t("browseLoading") }))
137
+ : null] }), _jsxs("div", { className: "dsh-podman-directory-footer", children: [_jsxs("button", { type: "button", className: "dsh-podman-directory-toggle", "aria-pressed": showHidden, disabled: busy, onClick: () => setShowHidden((previous) => !previous), children: [t("browseHidden"), showHidden ? _jsx(IconCheckOutlineRegular, { size: 14 }) : null] }), _jsx("span", { className: "dsh-podman-directory-gap" }), _jsx(Button, { variant: "outline", className: "dsh-podman-directory-action", disabled: busy, onClick: onClose, children: t("cancel") }), _jsx(Button, { variant: "primary", className: "dsh-podman-directory-action", disabled: busy || error !== "", onClick: () => onSelect(selected?.path ?? path), children: t("browseSelect") })] })] }) }));
132
138
  }
133
139
  function DirectoryRow(props) {
134
140
  const { entry, selected, busy, onPick } = props;
135
141
  return (_jsx("li", { className: "dsh-podman-directory-seat", children: _jsxs("button", { type: "button", className: selected
136
142
  ? "dsh-podman-directory-row dsh-podman-directory-row-selected"
137
- : "dsh-podman-directory-row", "aria-current": selected ? true : undefined, disabled: busy, onClick: () => onPick(entry), children: [selected ? (_jsx(IconFolderOpen16, { size: 16, className: "dsh-podman-directory-rowicon-selected" })) : (_jsx(IconFolderClose16, { size: 16, className: "dsh-podman-directory-rowicon" })), _jsx("span", { className: "dsh-podman-directory-rowname", children: entry.name }), _jsx(IconChevronRightOutline14, { size: 12, className: "dsh-podman-directory-rowchevron" })] }) }));
143
+ : "dsh-podman-directory-row", "aria-current": selected ? true : undefined, disabled: busy, onClick: () => onPick(entry), children: [selected
144
+ ? (_jsx(IconFolderOpenRegular, { size: 16, className: "dsh-podman-directory-rowicon-selected" }))
145
+ : (_jsx(IconFolderCloseRegular, { size: 16, className: "dsh-podman-directory-rowicon" })), _jsx("span", { className: "dsh-podman-directory-rowname", children: entry.name }), _jsx(IconChevronRightOutlineRegular, { size: 12, className: "dsh-podman-directory-rowchevron" })] }) }));
138
146
  }
139
147
  function toLevel(listing) {
140
148
  return {
@@ -1,15 +1,15 @@
1
1
  import { 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 { Translate } from "./locales.js";
4
4
  import { type ReactNode } from "react";
5
5
  export declare function EnvEditor(props: {
6
- t: (key: ContainerPluginKey) => string;
6
+ t: Translate;
7
7
  env: Record<string, string>;
8
8
  busy: boolean;
9
9
  onChange: (env: Record<string, string>) => void;
10
10
  }): ReactNode;
11
11
  export declare function MountsEditor(props: {
12
- t: (key: ContainerPluginKey) => string;
12
+ t: Translate;
13
13
  mounts: readonly MountInput[];
14
14
  volumes: readonly {
15
15
  name: string;
@@ -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;