@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
@@ -2,5 +2,5 @@ import { type ContainerCardFace } from "./container-card-controller.js";
2
2
  import { NS } from "./locales.js";
3
3
  import { type InjectFace, type PropsLocale, type PropsRuntime } from "@deepseek-ai/dsh-client-ui-slots";
4
4
  import { type ReactNode } from "react";
5
- export type ContainerCardProps = PropsRuntime<"settings.plugin.item"> & PropsLocale<typeof NS> & InjectFace<ContainerCardFace>;
5
+ export type ContainerCardProps = PropsRuntime<"plugins.bundle.config"> & PropsLocale<typeof NS> & InjectFace<ContainerCardFace>;
6
6
  export declare function ContainerCard(props: ContainerCardProps): ReactNode;
@@ -2,34 +2,63 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
3
3
  //
4
4
  // SPDX-License-Identifier: MIT
5
- import { BaseImageRow, ImageBuildModal, ImageItem } from "./container-card-images.js";
5
+ import { BaseImageRow, ImageBuildModal, ImageItem, } from "./container-card-images.js";
6
6
  import { CachesSection } from "./container-card-caches.js";
7
+ import { DefaultEnvironmentSection } from "./container-card-default-env.js";
7
8
  import { SecretsSection } from "./container-card-secrets.js";
8
- import { CardChevron, ConfigField, ConfirmButton } from "./container-card-shared.js";
9
- import { banner, cardBodyStyle, cardDescriptionStyle, cardHeadTextStyle, cardHeaderStyle, cardHoverStyle, cardNameStyle, cardOpenStyle, cardStyle, footerRow, hint, imageSelect, sectionTitle, } from "./container-card-styles.js";
9
+ import { ConfirmButton } from "./container-card-shared.js";
10
+ import { banner, footerRow, hint, imageSelect, sectionTitle, } from "./container-card-styles.js";
10
11
  import { VolumesSection } from "./container-card-volumes.js";
11
12
  import { WorkspaceSection } from "./container-card-workspace.js";
12
- import { Button, Input, Modal } from "@deepseek-ai/dsh-client-ui-primitives";
13
- import { useEffect, useId, useState } from "react";
13
+ import { Button, Modal, writeClipboard, } from "@deepseek-ai/dsh-client-ui-primitives";
14
+ import { useEffect, useRef, useState } from "react";
15
+ // The labels of the version lines, emphasised against their values.
16
+ const versionLabel = { fontWeight: 600 };
14
17
  export function ContainerCard(props) {
15
18
  const { t } = props;
16
19
  const state = props.useContainerCard((snapshot) => snapshot);
17
- const [open, setOpen] = useState(false);
18
- const [hovered, setHovered] = useState(false);
19
20
  const [buildOpen, setBuildOpen] = useState(false);
20
21
  const [imageId, setImageId] = useState("");
21
22
  const [parent, setParent] = useState("");
22
23
  const [packages, setPackages] = useState([]);
23
24
  const [defaultOpen, setDefaultOpen] = useState(false);
24
25
  const [defaultImage, setDefaultImage] = useState(state.defaultImage);
25
- const projectsRootId = useId();
26
- // The card mounts with the settings panel, so this re-reads the live state
27
- // every time the Plugins section is opened: a workspace created since the
28
- // last read shows up without pressing Reload this view.
29
- // biome-ignore lint/correctness/useExhaustiveDependencies: mount-only refresh
30
- useEffect(() => {
31
- props.reload();
26
+ const [copyState, setCopyState] = useState("idle");
27
+ const copyResetTimer = useRef();
28
+ // Copies the version lines as displayed, so a user can paste them into a bug
29
+ // report without transcribing anything.
30
+ const copyVersions = async () => {
31
+ const lines = [
32
+ `${t("dsh")} ${state.dshVersion || t("versionUnknown")}`,
33
+ `${t("cardTitle")} ${state.version}${state.commit ? ` · ${state.commit}` : ""}`,
34
+ `${t("orchestrator")} ${state.orchestratorVersion || t("versionUnknown")}`,
35
+ ];
36
+ setCopyState((await writeClipboard(lines.join("\n"))) ? "copied" : "failed");
37
+ if (copyResetTimer.current !== undefined) {
38
+ clearTimeout(copyResetTimer.current);
39
+ }
40
+ copyResetTimer.current = setTimeout(() => setCopyState("idle"), 1500);
41
+ };
42
+ // The reset timer is cancelled on unmount, so it cannot fire against an
43
+ // unmounted component.
44
+ useEffect(() => () => {
45
+ if (copyResetTimer.current !== undefined) {
46
+ clearTimeout(copyResetTimer.current);
47
+ }
32
48
  }, []);
49
+ // The Plugins page renders a one-liner in its list and this component again
50
+ // as the entry's page, so the orchestrator snapshot is only fetched for the
51
+ // page. Re-reading on every page view keeps a workspace created since the
52
+ // last visit visible without pressing Reload.
53
+ // biome-ignore lint/correctness/useExhaustiveDependencies: refresh per page view
54
+ useEffect(() => {
55
+ if (props.view === "page")
56
+ props.reload();
57
+ }, [props.view]);
58
+ // The list shows only the one-liner; the page heading already carries the
59
+ // plugin's title and description.
60
+ if (props.view === "summary")
61
+ return t("cardDescription");
33
62
  const baseImages = state.images.filter((image) => image.isBase);
34
63
  const customImages = state.images.filter((image) => !image.isBase);
35
64
  const defaultCandidates = [
@@ -37,43 +66,56 @@ export function ContainerCard(props) {
37
66
  ...customImages.map((image) => image.imageId),
38
67
  ];
39
68
  if (!state.available) {
40
- return (_jsx("p", { style: { padding: "8px 0", fontSize: "13px", opacity: 0.8 }, children: t("unavailable") }));
69
+ return (_jsxs("div", { style: { padding: "8px 0", fontSize: "13px", opacity: 0.8 }, children: [_jsx("p", { style: { margin: 0 }, children: t("unavailable") }), state.notice === ""
70
+ ? null
71
+ : (_jsxs("p", { style: { margin: "4px 0 0" }, role: "status", children: [t("notice"), ": ", state.notice] })), _jsx("div", { style: { marginTop: "8px" }, children: _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: props.reload, children: state.busy ? t("busy") : t("reload") }) })] }));
41
72
  }
42
- return (_jsxs("li", { style: {
43
- ...cardStyle,
44
- ...(open ? cardOpenStyle : {}),
45
- ...(hovered && !open ? cardHoverStyle : {}),
46
- }, onPointerEnter: () => setHovered(true), onPointerLeave: () => setHovered(false), children: [_jsxs("button", { type: "button", style: cardHeaderStyle, "aria-expanded": open, onClick: () => setOpen(!open), children: [_jsxs("span", { style: cardHeadTextStyle, children: [_jsxs("span", { style: cardNameStyle, children: [t("cardTitle"), state.version ? ` ${state.version}` : "", state.commit ? ` · ${state.commit}` : ""] }), _jsx("span", { style: cardDescriptionStyle, children: t("cardDescription") })] }), _jsx(CardChevron, { open: open })] }), open ? (_jsxs("div", { style: cardBodyStyle, children: [state.notice === "" ? null : (_jsxs("div", { style: banner, role: "status", children: [t("notice"), ": ", state.notice] })), _jsx("div", { style: sectionTitle, children: t("workspacesTitle") }), state.workspaces.length === 0 ? (_jsx("p", { style: hint, children: t("none") })) : (state.workspaces.map((workspace) => (_jsx(WorkspaceSection, { t: t, workspace: workspace, containers: state.containers.filter((container) => container.workspaceSlug === workspace.workspaceSlug), images: state.images, volumes: state.volumes, secrets: state.secrets, busy: state.busy, defaultImage: state.defaultImage, projectsRoot: state.projectsRoot, directoryPicker: state.directoryPicker, onRemove: props.remove, onRemoveWorkspace: props.removeWorkspace, onRecreate: props.recreate, onCreate: props.createContainer, onStartContainer: props.startContainer, onAddContainerMount: props.addContainerMount, onRemoveContainerMount: props.removeContainerMount, onUpdateContainerMount: props.updateContainerMount, onSetContainerPaths: props.setContainerPaths, onAddContainerSecret: props.addContainerSecret, onRemoveContainerSecret: props.removeContainerSecret }, workspace.workspaceSlug)))), _jsxs("div", { style: {
47
- display: "flex",
48
- flexWrap: "wrap",
49
- alignItems: "center",
50
- gap: "8px",
51
- }, children: [_jsx("div", { style: { ...sectionTitle, flex: 1 }, children: t("imagesTitle") }), _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: () => {
52
- setDefaultImage(state.defaultImage);
53
- setDefaultOpen(true);
54
- }, children: t("setDefaultImage") }), _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: () => {
55
- setImageId("");
56
- setParent(baseImages[0]?.imageId ?? customImages[0]?.imageId ?? "");
57
- setPackages([]);
58
- setBuildOpen(true);
59
- }, children: t("buildImage") }), _jsx(ConfirmButton, { t: t, label: t("rebuildAllImages"), title: t("confirmTitle"), description: t("confirmRebuildAllImages"), disabled: state.busy, onConfirm: props.rebuildAllImages })] }), _jsx("div", { style: sectionTitle, children: t("baseImagesTitle") }), baseImages.length === 0 ? (_jsx("p", { style: hint, children: t("none") })) : (baseImages.map((image) => (_jsx(BaseImageRow, { t: t, image: image, busy: state.busy, defaultImage: state.defaultImage, onRebuild: props.rebuildBaseImage, onPull: props.pullBaseImage, onSetDefault: props.setDefaultImage }, image.imageId)))), _jsx("div", { style: sectionTitle, children: t("customImagesTitle") }), customImages.length === 0 ? (_jsx("p", { style: hint, children: t("none") })) : (customImages.map((image) => (_jsx(ImageItem, { t: t, image: image, busy: state.busy, defaultImage: state.defaultImage, onRemove: props.removeImage, onRebuild: props.rebuildImage, onSetDefault: props.setDefaultImage }, image.imageId)))), _jsx(ImageBuildModal, { t: t, images: state.images, open: buildOpen, imageId: imageId, parent: parent, packages: packages, onImageId: setImageId, onParent: setParent, onPackages: setPackages, onClose: () => setBuildOpen(false), onBuild: () => {
60
- props.buildImage(imageId.trim(), parent.trim(), packages);
73
+ return (_jsxs("div", { children: [state.notice === "" ? null : (_jsxs("div", { style: banner, role: "status", children: [t("notice"), ": ", state.notice] })), state.versionState === "ok" ? null : (_jsx("div", { style: state.versionState === "major-mismatch"
74
+ ? { ...banner, borderColor: "var(--dsw-alias-state-error-primary)" }
75
+ : banner, role: state.versionState === "major-mismatch" ? "alert" : "status", children: t(state.versionState === "major-mismatch"
76
+ ? "versionMismatchMajor"
77
+ : "versionMismatchMinor") })), _jsx("div", { style: sectionTitle, children: t("workspacesTitle") }), state.workspaces.length === 0 ? _jsx("p", { style: hint, children: t("none") }) : (state.workspaces.map((workspace) => (_jsx(WorkspaceSection, { t: t, workspace: workspace, containers: state.containers.filter((container) => container.workspaceSlug === workspace.workspaceSlug), images: state.images, volumes: state.volumes, secrets: state.secrets, busy: state.busy, defaultImage: state.defaultImage, projectsRoot: state.projectsRoot, directoryPicker: state.directoryPicker, onRemove: props.remove, onRemoveWorkspace: props.removeWorkspace, onRecreate: props.recreate, onCreate: props.createContainer, onStartContainer: props.startContainer, onAddContainerMount: props.addContainerMount, onRemoveContainerMount: props.removeContainerMount, onUpdateContainerMount: props.updateContainerMount, onSetContainerPaths: props.setContainerPaths, onAddContainerSecret: props.addContainerSecret, onRemoveContainerSecret: props.removeContainerSecret, onSyncDefaults: props.syncDefaultEnv, defaultEnvCount: Object.keys(state.containerEnv).length }, workspace.workspaceSlug)))), _jsxs("div", { style: {
78
+ display: "flex",
79
+ flexWrap: "wrap",
80
+ alignItems: "center",
81
+ gap: "8px",
82
+ }, children: [_jsx("div", { style: { ...sectionTitle, flex: 1 }, children: t("imagesTitle") }), _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: () => {
83
+ setDefaultImage(state.defaultImage);
84
+ setDefaultOpen(true);
85
+ }, children: t("setDefaultImage") }), _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: () => {
61
86
  setImageId("");
62
- setParent("");
87
+ setParent(baseImages[0]?.imageId ?? customImages[0]?.imageId ?? "");
63
88
  setPackages([]);
64
- setBuildOpen(false);
65
- } }), _jsx(Modal, { open: defaultOpen, onClose: () => setDefaultOpen(false), title: t("setDefaultImageTitle"), closeLabel: t("cancel"), footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => setDefaultOpen(false), children: t("cancel") }), _jsx(Button, { variant: "primary", size: "sm", disabled: defaultImage === "", onClick: () => {
66
- props.setDefaultImage(defaultImage);
67
- setDefaultOpen(false);
68
- }, children: t("setDefaultImage") })] }), children: _jsxs("select", { style: imageSelect, value: defaultImage, onChange: (event) => setDefaultImage(event.target.value), children: [defaultCandidates.length === 0 ? (_jsx("option", { value: "", children: t("none") })) : null, defaultCandidates.map((name) => (_jsx("option", { value: name, children: name }, name)))] }) }), _jsx(CachesSection, { t: t, caches: state.caches, busy: state.busy, onClean: props.cleanCaches }), _jsx(VolumesSection, { t: t, volumes: state.volumes, busy: state.busy, writable: state.writable, onCreate: props.createVolume, onRemove: props.removeVolume }), _jsx(SecretsSection, { t: t, secrets: state.secrets, busy: state.busy, writable: state.writable, onCreate: props.createSecret, onRemove: props.removeSecret, onSet: props.setSecret }), _jsx("div", { style: sectionTitle, children: t("configTitle") }), _jsxs("div", { style: {
69
- display: "flex",
70
- flexWrap: "wrap",
71
- alignItems: "center",
72
- gap: "8px",
73
- padding: "8px 0",
74
- }, children: [_jsx("label", { htmlFor: projectsRootId, style: {
75
- fontSize: "13px",
76
- color: "var(--dsw-alias-label-secondary)",
77
- minWidth: "110px",
78
- }, children: t("projectsRoot") }), _jsx(Input, { id: projectsRootId, value: state.projectsRoot, disabled: true, style: { width: "200px" } })] }), _jsx(ConfigField, { t: t, label: t("socketsRoot"), value: state.socketsRootDraft, current: state.socketsRoot, writable: state.writable, onChange: props.editSocketsRoot, onSave: props.saveSocketsRoot, onDiscard: props.discardSocketsRoot }), _jsx("div", { style: footerRow, children: _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: props.reload, children: state.busy ? t("busy") : t("reload") }) })] })) : null] }));
89
+ setBuildOpen(true);
90
+ }, children: t("buildImage") }), _jsx(ConfirmButton, { t: t, label: t("rebuildAllImages"), title: t("confirmTitle"), description: t("confirmRebuildAllImages"), disabled: state.busy, onConfirm: props.rebuildAllImages })] }), _jsx("div", { style: sectionTitle, children: t("baseImagesTitle") }), baseImages.length === 0 ? _jsx("p", { style: hint, children: t("none") }) : (baseImages.map((image) => (_jsx(BaseImageRow, { t: t, image: image, busy: state.busy, defaultImage: state.defaultImage, onRebuild: props.rebuildBaseImage, onPull: props.pullBaseImage, onSetDefault: props.setDefaultImage }, image.imageId)))), _jsx("div", { style: sectionTitle, children: t("customImagesTitle") }), customImages.length === 0 ? _jsx("p", { style: hint, children: t("none") }) : (customImages.map((image) => (_jsx(ImageItem, { t: t, image: image, busy: state.busy, defaultImage: state.defaultImage, onRemove: props.removeImage, onRebuild: props.rebuildImage, onSetDefault: props.setDefaultImage }, image.imageId)))), _jsx(ImageBuildModal, { t: t, images: state.images, open: buildOpen, imageId: imageId, parent: parent, packages: packages, onImageId: setImageId, onParent: setParent, onPackages: setPackages, onClose: () => setBuildOpen(false), onBuild: () => {
91
+ props.buildImage(imageId.trim(), parent.trim(), packages);
92
+ setImageId("");
93
+ setParent("");
94
+ setPackages([]);
95
+ setBuildOpen(false);
96
+ } }), _jsxs(Modal, { open: defaultOpen, onClose: () => setDefaultOpen(false), title: t("setDefaultImageTitle"), closeLabel: t("cancel"), footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", size: "sm", onClick: () => setDefaultOpen(false), children: t("cancel") }), _jsx(Button, { variant: "primary", size: "sm", disabled: defaultImage === "", onClick: () => {
97
+ props.setDefaultImage(defaultImage);
98
+ setDefaultOpen(false);
99
+ }, children: t("setDefaultImage") })] }), children: [_jsxs("select", { style: imageSelect, value: defaultImage, onChange: (event) => setDefaultImage(event.target.value), children: [defaultCandidates.length === 0
100
+ ? _jsx("option", { value: "", children: t("none") })
101
+ : null, defaultCandidates.map((name) => (_jsx("option", { value: name, children: name }, name)))] }), _jsx("p", { style: hint, children: t("defaultImageHint") })] }), _jsx(CachesSection, { t: t, caches: state.caches, busy: state.busy, onClean: props.cleanCaches }), _jsx(VolumesSection, { t: t, volumes: state.volumes, busy: state.busy, writable: state.writable, onCreate: props.createVolume, onRemove: props.removeVolume }), _jsx(SecretsSection, { t: t, secrets: state.secrets, busy: state.busy, writable: state.writable, onCreate: props.createSecret, onRemove: props.removeSecret, onSet: props.setSecret }), _jsx(DefaultEnvironmentSection, { t: t, env: state.containerEnv, busy: state.busy, writable: state.writable, onSave: props.saveContainerEnv, onSync: props.syncDefaultEnv }), _jsxs("div", { style: {
102
+ display: "flex",
103
+ flexWrap: "wrap",
104
+ alignItems: "center",
105
+ gap: "8px",
106
+ }, children: [_jsx("div", { style: { ...sectionTitle, flex: 1 }, children: t("versionsTitle") }), _jsx(Button, { variant: "outline", size: "sm", onClick: () => void copyVersions(), children: copyState === "copied"
107
+ ? t("copied")
108
+ : copyState === "failed"
109
+ ? t("copyFailed")
110
+ : t("copyVersions") })] }), _jsxs("div", { style: {
111
+ ...hint,
112
+ display: "flex",
113
+ flexDirection: "column",
114
+ gap: "2px",
115
+ margin: 0,
116
+ }, children: [_jsxs("span", { children: [_jsx("span", { style: versionLabel, children: t("dsh") }), state.dshVersion
117
+ ? (` ${state.dshVersion}`)
118
+ : (_jsxs(_Fragment, { children: [" ", _jsx("span", { style: { color: "var(--dsw-alias-state-error-primary)" }, children: t("versionUnknown") })] }))] }), _jsxs("span", { children: [_jsx("span", { style: versionLabel, children: t("cardTitle") }), state.version ? ` ${state.version}` : "", state.commit ? ` · ${state.commit}` : ""] }), _jsxs("span", { children: [_jsx("span", { style: versionLabel, children: t("orchestrator") }), state.orchestratorVersion
119
+ ? (` ${state.orchestratorVersion}`)
120
+ : (_jsxs(_Fragment, { children: [" ", _jsx("span", { style: { color: "var(--dsw-alias-state-error-primary)" }, children: t("versionUnknown") })] }))] })] }), _jsx("div", { style: footerRow, children: _jsx(Button, { variant: "outline", size: "sm", disabled: state.busy, onClick: props.reload, children: state.busy ? t("busy") : t("reload") }) })] }));
79
121
  }
@@ -7,7 +7,7 @@ export interface MountInput {
7
7
  volume: string;
8
8
  secret: string;
9
9
  }
10
- export type CommandOp = "remove" | "workspace_remove" | "recreate" | "create" | "volume_create" | "volume_remove" | "image_remove" | "secret_create" | "secret_remove" | "secret_set" | "image_rebuild" | "image_rebuild_all" | "image_build" | "image_base_rebuild" | "image_base_pull" | "container_secret_add" | "container_secret_remove" | "container_mount_add" | "container_mount_remove" | "container_mount_update" | "container_path_set" | "cache_clean";
10
+ export type CommandOp = "remove" | "workspace_remove" | "recreate" | "create" | "volume_create" | "volume_remove" | "image_remove" | "secret_create" | "secret_remove" | "secret_set" | "image_rebuild" | "image_rebuild_all" | "image_build" | "image_base_rebuild" | "image_base_pull" | "container_secret_add" | "container_secret_remove" | "container_mount_add" | "container_mount_remove" | "container_mount_update" | "container_path_set" | "default_env_sync" | "cache_clean";
11
11
  export interface CommandRequest {
12
12
  op: CommandOp;
13
13
  workspace: string;
@@ -19,11 +19,11 @@ export interface CommandRequest {
19
19
  env: Record<string, string>;
20
20
  container: string;
21
21
  secret: string;
22
- secretEnv: string;
22
+ secretEnvName: string;
23
23
  length: number;
24
24
  charset: string;
25
25
  packages: string[];
26
- secretEnvMap: Record<string, string>;
26
+ secretEnv: Record<string, string>;
27
27
  cacheMode: string;
28
28
  mount: MountInput | null;
29
29
  }
@@ -85,6 +85,9 @@ export interface WorkspaceView {
85
85
  export interface CardSnapshot {
86
86
  version: string;
87
87
  commit: string;
88
+ dshVersion: string;
89
+ orchestratorVersion: string;
90
+ versionState: "ok" | "minor-mismatch" | "major-mismatch";
88
91
  projectsRoot: string;
89
92
  workspaces: readonly WorkspaceView[];
90
93
  containers: readonly ContainerView[];
@@ -22,10 +22,15 @@ function formatBytes(bytes) {
22
22
  export function CachesSection(props) {
23
23
  const { t, caches, busy, onClean } = props;
24
24
  const disabled = busy || caches.length === 0;
25
- return (_jsxs(_Fragment, { children: [_jsx("div", { style: sectionTitle, children: t("cachesTitle") }), _jsxs("div", { style: wsBody, children: [caches.length === 0 ? (_jsx("p", { style: { ...hint, margin: 0 }, children: t("noCaches") })) : (caches.map((cache) => (_jsxs("div", { style: {
26
- display: "flex",
27
- flexWrap: "wrap",
28
- alignItems: "center",
29
- gap: "8px",
30
- }, children: [_jsx("strong", { style: { color: "var(--dsw-alias-label-primary)" }, children: cache.manager }), _jsx("span", { style: hint, children: t("cacheUsage", { size: formatBytes(cache.bytes), n: cache.files }) })] }, cache.manager)))), _jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" }, children: [_jsx(ConfirmButton, { t: t, label: t("cacheKeepLatest"), title: t("confirmTitle"), description: t("confirmCacheKeepLatest"), disabled: disabled, onConfirm: () => onClean("keep-latest") }), _jsx(ConfirmButton, { t: t, label: t("cacheRemoveAll"), title: t("confirmTitle"), description: t("confirmCacheRemoveAll"), disabled: disabled, onConfirm: () => onClean("all") })] })] })] }));
25
+ return (_jsxs(_Fragment, { children: [_jsx("div", { style: sectionTitle, children: t("cachesTitle") }), _jsxs("div", { style: wsBody, children: [caches.length === 0
26
+ ? _jsx("p", { style: { ...hint, margin: 0 }, children: t("noCaches") })
27
+ : (caches.map((cache) => (_jsxs("div", { style: {
28
+ display: "flex",
29
+ flexWrap: "wrap",
30
+ alignItems: "center",
31
+ gap: "8px",
32
+ }, children: [_jsx("strong", { style: { color: "var(--dsw-alias-label-primary)" }, children: cache.manager }), _jsx("span", { style: hint, children: t("cacheUsage", {
33
+ size: formatBytes(cache.bytes),
34
+ n: cache.files,
35
+ }) })] }, cache.manager)))), _jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" }, children: [_jsx(ConfirmButton, { t: t, label: t("cacheKeepLatest"), title: t("confirmTitle"), description: t("confirmCacheKeepLatest"), disabled: disabled, onConfirm: () => onClean("keep-latest") }), _jsx(ConfirmButton, { t: t, label: t("cacheRemoveAll"), title: t("confirmTitle"), description: t("confirmCacheRemoveAll"), disabled: disabled, onConfirm: () => onClean("all") })] })] })] }));
31
36
  }
@@ -1,13 +1,13 @@
1
1
  import { type SnapshotStore } from "@deepseek-ai/dsh-client-store";
2
- import type { SettingsScope, SettingsScopeSnapshot } from "@deepseek-ai/dsh-client-ui-settings/client";
2
+ import type { ConfigForm, ConfigFormSnapshot } from "@deepseek-ai/dsh-client-ui-settings/client";
3
3
  import type { CacheView, ContainerView, ImageView, MountInput, SecretView, VolumeView, WorkspaceView } from "./card-protocol.js";
4
4
  import { type CardClient } from "./card-client.js";
5
5
  import { type DirectoryPickerFace } from "./directory-picker.js";
6
6
  export declare const CONTAINER_NS = "podman";
7
7
  export interface ContainerSettings {
8
8
  defaultImage: string;
9
- socketsRoot: string;
10
9
  uiLocale?: string;
10
+ containerEnv?: Record<string, string>;
11
11
  }
12
12
  export interface ProjectMountView {
13
13
  projectName: string;
@@ -32,10 +32,11 @@ export interface CardState {
32
32
  notice: string;
33
33
  version: string;
34
34
  commit: string;
35
+ dshVersion: string;
36
+ orchestratorVersion: string;
37
+ versionState: "ok" | "minor-mismatch" | "major-mismatch";
35
38
  defaultImage: string;
36
- defaultImageDraft: string;
37
- socketsRoot: string;
38
- socketsRootDraft: string;
39
+ containerEnv: Record<string, string>;
39
40
  projectsRoot: string;
40
41
  directoryPicker?: DirectoryPickerFace;
41
42
  workspaces: readonly WorkspaceView[];
@@ -50,10 +51,10 @@ export interface ContainerCardFace {
50
51
  containerCard: SnapshotStore<CardState>;
51
52
  };
52
53
  reload: () => void;
53
- remove: (workspace: string) => void;
54
+ remove: (workspace: string, container: string) => void;
54
55
  cleanCaches: (mode: string) => void;
55
56
  removeWorkspace: (workspace: string) => void;
56
- recreate: (workspace: string, image: string, env?: Record<string, string>) => void;
57
+ recreate: (workspace: string, container: string, image: string, env?: Record<string, string>) => void;
57
58
  createContainer: (workspace: WorkspaceView, config?: ContainerCreateConfig) => void;
58
59
  startContainer: (workspace: WorkspaceView, container: string, config?: ContainerCreateConfig) => void;
59
60
  addContainerMount: (workspace: string, container: string, mount: MountInput) => void;
@@ -72,14 +73,10 @@ export interface ContainerCardFace {
72
73
  createSecret: (name: string, length?: number, charset?: string) => void;
73
74
  removeSecret: (name: string) => void;
74
75
  setSecret: (name: string, value: string) => void;
75
- addContainerSecret: (workspace: string, envVar: string, secret: string) => void;
76
- removeContainerSecret: (workspace: string, envVar: string) => void;
77
- editDefaultImage: (text: string) => void;
78
- saveDefaultImage: () => void;
79
- discardDefaultImage: () => void;
80
- editSocketsRoot: (text: string) => void;
81
- saveSocketsRoot: () => void;
82
- discardSocketsRoot: () => void;
76
+ addContainerSecret: (workspace: string, container: string, envVar: string, secret: string) => void;
77
+ removeContainerSecret: (workspace: string, container: string, envVar: string) => void;
78
+ saveContainerEnv: (env: Record<string, string>) => void;
79
+ syncDefaultEnv: (workspace: string) => void;
83
80
  }
84
81
  export declare class ContainerCardController {
85
82
  private readonly scope;
@@ -87,10 +84,11 @@ export declare class ContainerCardController {
87
84
  private readonly drafts;
88
85
  private readonly client;
89
86
  private snapshot;
90
- private busy;
87
+ private pending;
88
+ private generation;
91
89
  private notice;
92
90
  private directoryPicker;
93
- constructor(scope: SettingsScope<ContainerSettings>, client?: CardClient);
91
+ constructor(scope: ConfigForm<ContainerSettings>, client?: CardClient);
94
92
  private draft;
95
93
  private project;
96
94
  private publish;
@@ -104,4 +102,4 @@ export declare class ContainerCardController {
104
102
  inject(): ContainerCardFace;
105
103
  private defaultImage;
106
104
  }
107
- export type { SettingsScopeSnapshot };
105
+ export type { ConfigFormSnapshot };
@@ -1,12 +1,15 @@
1
1
  // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
- import { createSnapshotStore } from "@deepseek-ai/dsh-client-store";
4
+ import { createSnapshotStore, } from "@deepseek-ai/dsh-client-store";
5
5
  import { createCardClient } from "./card-client.js";
6
6
  export const CONTAINER_NS = "podman";
7
7
  const EMPTY_SNAPSHOT = {
8
8
  version: "",
9
9
  commit: "",
10
+ dshVersion: "",
11
+ orchestratorVersion: "",
12
+ versionState: "ok",
10
13
  projectsRoot: "",
11
14
  workspaces: [],
12
15
  containers: [],
@@ -24,7 +27,10 @@ export class ContainerCardController {
24
27
  drafts = new Map();
25
28
  client;
26
29
  snapshot = EMPTY_SNAPSHOT;
27
- busy = false;
30
+ // Counts in-flight commands, so overlapping ones cannot clear the busy state
31
+ // early; generation makes a superseded response drop its stale snapshot.
32
+ pending = 0;
33
+ generation = 0;
28
34
  notice = "";
29
35
  directoryPicker;
30
36
  constructor(scope, client = createCardClient()) {
@@ -45,14 +51,15 @@ export class ContainerCardController {
45
51
  return {
46
52
  available: snapshot.status === "ready" && value !== undefined,
47
53
  writable: snapshot.writable,
48
- busy: this.busy,
54
+ busy: this.pending > 0,
49
55
  notice: this.notice,
50
56
  version: this.snapshot.version,
51
57
  commit: this.snapshot.commit,
58
+ dshVersion: this.snapshot.dshVersion,
59
+ orchestratorVersion: this.snapshot.orchestratorVersion,
60
+ versionState: this.snapshot.versionState,
52
61
  defaultImage: value?.defaultImage ?? "",
53
- defaultImageDraft: this.draft("defaultImage", value?.defaultImage ?? ""),
54
- socketsRoot: value?.socketsRoot ?? "",
55
- socketsRootDraft: this.draft("socketsRoot", value?.socketsRoot ?? ""),
62
+ containerEnv: value?.containerEnv ?? {},
56
63
  projectsRoot: this.snapshot.projectsRoot,
57
64
  ...(this.directoryPicker === undefined
58
65
  ? {}
@@ -86,43 +93,62 @@ export class ContainerCardController {
86
93
  env: extra.env ?? {},
87
94
  container: extra.container ?? "",
88
95
  secret: extra.secret ?? "",
89
- secretEnv: extra.secretEnv ?? "",
96
+ secretEnvName: extra.secretEnvName ?? "",
90
97
  length: extra.length ?? 0,
91
98
  charset: extra.charset ?? "",
92
99
  packages: extra.packages ?? [],
93
- secretEnvMap: extra.secretEnvMap ?? {},
100
+ secretEnv: extra.secretEnv ?? {},
94
101
  cacheMode: extra.cacheMode ?? "",
95
102
  mount: extra.mount ?? null,
96
103
  });
97
104
  }
98
105
  async dispatch(request) {
99
- this.busy = true;
106
+ const generation = ++this.generation;
107
+ this.pending++;
100
108
  this.notice = "";
101
109
  this.publish();
102
110
  try {
103
111
  const result = await this.client.command(request);
112
+ const snapshot = await this.client.snapshot();
113
+ // A reload (or another command) started after this one wins: applying
114
+ // this response would overwrite the fresher state with a stale snapshot.
115
+ if (generation !== this.generation)
116
+ return;
104
117
  this.notice = result.notice ?? "";
105
- this.snapshot = await this.client.snapshot();
118
+ this.snapshot = snapshot;
106
119
  }
107
120
  catch (error) {
108
- this.notice = error instanceof Error ? error.message : String(error);
121
+ if (generation === this.generation) {
122
+ this.notice = error instanceof Error ? error.message : String(error);
123
+ }
109
124
  }
110
125
  finally {
111
- this.busy = false;
126
+ this.pending--;
112
127
  this.publish();
113
128
  }
114
129
  }
115
130
  async reload() {
131
+ const generation = ++this.generation;
132
+ this.pending++;
133
+ this.publish();
116
134
  try {
117
- this.snapshot = await this.client.snapshot();
135
+ const snapshot = await this.client.snapshot();
136
+ if (generation !== this.generation)
137
+ return;
138
+ this.snapshot = snapshot;
118
139
  // A refresh clears the previous action's notice, so reopening the
119
140
  // settings does not replay feedback from an earlier session.
120
141
  this.notice = "";
121
142
  }
122
143
  catch (error) {
123
- this.notice = error instanceof Error ? error.message : String(error);
144
+ if (generation === this.generation) {
145
+ this.notice = error instanceof Error ? error.message : String(error);
146
+ }
147
+ }
148
+ finally {
149
+ this.pending--;
150
+ this.publish();
124
151
  }
125
- this.publish();
126
152
  }
127
153
  edit(field, text) {
128
154
  this.drafts.set(field, text);
@@ -144,27 +170,55 @@ export class ContainerCardController {
144
170
  reload: () => {
145
171
  void this.reload();
146
172
  },
147
- remove: (workspace) => this.command("remove", workspace, ""),
173
+ remove: (workspace, container) => this.command("remove", workspace, "", { container }),
148
174
  cleanCaches: (mode) => this.command("cache_clean", "", "", { cacheMode: mode }),
149
175
  removeWorkspace: (workspace) => this.command("workspace_remove", workspace, ""),
150
- recreate: (workspace, image, env) => this.command("recreate", workspace, image, { ...(env ? { env } : {}) }),
176
+ recreate: (workspace, container, image, env) => this.command("recreate", workspace, image, {
177
+ container,
178
+ ...(env ? { env } : {}),
179
+ }),
151
180
  createContainer: (workspace, config) => this.command("create", workspace.workspaceSlug, config?.image ?? (workspace.imageId || this.defaultImage() || ""), {
152
181
  projectName: workspace.projectName,
153
- ...(config?.mounts && config.mounts.length > 0 ? { mounts: config.mounts } : {}),
154
- ...(config?.env && Object.keys(config.env).length > 0 ? { env: config.env } : {}),
155
- ...(config?.paths && config.paths.length > 0 ? { paths: config.paths } : {}),
156
- ...(config?.secretEnv && Object.keys(config.secretEnv).length > 0 ? { secretEnvMap: config.secretEnv } : {}),
182
+ ...(config?.mounts && config.mounts.length > 0
183
+ ? { mounts: config.mounts }
184
+ : {}),
185
+ ...(config?.env && Object.keys(config.env).length > 0
186
+ ? { env: config.env }
187
+ : {}),
188
+ ...(config?.paths && config.paths.length > 0
189
+ ? { paths: config.paths }
190
+ : {}),
191
+ ...(config?.secretEnv && Object.keys(config.secretEnv).length > 0
192
+ ? { secretEnv: config.secretEnv }
193
+ : {}),
157
194
  }),
158
195
  startContainer: (workspace, container, config) => this.command("create", workspace.workspaceSlug, config?.image ?? (workspace.imageId || this.defaultImage() || ""), {
159
196
  container,
160
- ...(config?.mounts && config.mounts.length > 0 ? { mounts: config.mounts } : {}),
161
- ...(config?.env && Object.keys(config.env).length > 0 ? { env: config.env } : {}),
162
- ...(config?.paths && config.paths.length > 0 ? { paths: config.paths } : {}),
163
- ...(config?.secretEnv && Object.keys(config.secretEnv).length > 0 ? { secretEnvMap: config.secretEnv } : {}),
197
+ ...(config?.mounts && config.mounts.length > 0
198
+ ? { mounts: config.mounts }
199
+ : {}),
200
+ ...(config?.env && Object.keys(config.env).length > 0
201
+ ? { env: config.env }
202
+ : {}),
203
+ ...(config?.paths && config.paths.length > 0
204
+ ? { paths: config.paths }
205
+ : {}),
206
+ ...(config?.secretEnv && Object.keys(config.secretEnv).length > 0
207
+ ? { secretEnv: config.secretEnv }
208
+ : {}),
209
+ }),
210
+ addContainerMount: (workspace, container, mount) => this.command("container_mount_add", workspace, "", {
211
+ container,
212
+ mount,
213
+ }),
214
+ removeContainerMount: (workspace, container, mount) => this.command("container_mount_remove", workspace, "", {
215
+ container,
216
+ mount,
217
+ }),
218
+ updateContainerMount: (workspace, container, mount) => this.command("container_mount_update", workspace, "", {
219
+ container,
220
+ mount,
164
221
  }),
165
- addContainerMount: (workspace, container, mount) => this.command("container_mount_add", workspace, "", { container, mount }),
166
- removeContainerMount: (workspace, container, mount) => this.command("container_mount_remove", workspace, "", { container, mount }),
167
- updateContainerMount: (workspace, container, mount) => this.command("container_mount_update", workspace, "", { container, mount }),
168
222
  setContainerPaths: (workspace, container, paths) => this.command("container_path_set", workspace, "", { container, paths }),
169
223
  createVolume: (name) => this.command("volume_create", name, ""),
170
224
  removeVolume: (name) => this.command("volume_remove", name, ""),
@@ -184,21 +238,19 @@ export class ContainerCardController {
184
238
  }),
185
239
  removeSecret: (name) => this.command("secret_remove", name, ""),
186
240
  setSecret: (name, value) => this.command("secret_set", name, "", { value }),
187
- addContainerSecret: (workspace, envVar, secret) => this.command("container_secret_add", workspace, "", {
188
- container: "default",
189
- secretEnv: envVar,
241
+ addContainerSecret: (workspace, container, envVar, secret) => this.command("container_secret_add", workspace, "", {
242
+ container,
243
+ secretEnvName: envVar,
190
244
  secret,
191
245
  }),
192
- removeContainerSecret: (workspace, envVar) => this.command("container_secret_remove", workspace, "", {
193
- container: "default",
194
- secretEnv: envVar,
246
+ removeContainerSecret: (workspace, container, envVar) => this.command("container_secret_remove", workspace, "", {
247
+ container,
248
+ secretEnvName: envVar,
195
249
  }),
196
- editDefaultImage: (text) => this.edit("defaultImage", text),
197
- saveDefaultImage: () => this.save("defaultImage"),
198
- discardDefaultImage: () => this.discard("defaultImage"),
199
- editSocketsRoot: (text) => this.edit("socketsRoot", text),
200
- saveSocketsRoot: () => this.save("socketsRoot"),
201
- discardSocketsRoot: () => this.discard("socketsRoot"),
250
+ saveContainerEnv: (env) => {
251
+ void this.scope.set("containerEnv", env);
252
+ },
253
+ syncDefaultEnv: (workspace) => this.command("default_env_sync", workspace, ""),
202
254
  };
203
255
  }
204
256
  defaultImage() {
@@ -1,9 +1,9 @@
1
1
  import { type ContainerCreateConfig, type ImageView, type WorkspaceView } 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 CreateContainerModal(props: {
6
- t: (key: ContainerPluginKey) => string;
6
+ t: Translate;
7
7
  workspace: WorkspaceView;
8
8
  images: readonly ImageView[];
9
9
  volumes: readonly {
@@ -2,7 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { EnvEditor, MountsEditor } from "./container-card-editors.js";
3
3
  import { PathsList } from "./container-card-paths.js";
4
4
  import { Field, namePattern, sanitizeName } from "./container-card-shared.js";
5
- import { greyId, hint, imageSelect, sectionTitle } from "./container-card-styles.js";
5
+ import { greyId, hint, imageSelect, sectionTitle, } from "./container-card-styles.js";
6
6
  import { Button, Input, Modal } from "@deepseek-ai/dsh-client-ui-primitives";
7
7
  import { useEffect, useId, useState } from "react";
8
8
  export function CreateContainerModal(props) {
@@ -81,9 +81,15 @@ export function CreateContainerModal(props) {
81
81
  display: "flex",
82
82
  flexDirection: "column",
83
83
  gap: "12px",
84
- }, children: [_jsx(Field, { label: t("image"), htmlFor: imageLabel, children: _jsxs("select", { id: imageLabel, style: imageSelect, value: image, disabled: busy, onChange: (event) => setImage(event.target.value), children: [candidates.length === 0 ? (_jsx("option", { value: "", children: t("none") })) : null, candidates.map((imageId) => (_jsx("option", { value: imageId, children: imageId }, imageId)))] }) }), named ? (_jsxs(Field, { label: t("containerName"), htmlFor: nameLabel, children: [_jsx(Input, { id: nameLabel, value: name, disabled: busy, onChange: (event) => setName(sanitizeName(event.target.value)) }), name !== "" && !validName ? (_jsx("p", { style: { ...hint, margin: 0 }, children: nameTaken
85
- ? t("containerNameTaken")
86
- : t("invalidContainerName") })) : null] })) : null, _jsx("div", { style: sectionTitle, children: t("envTitle") }), _jsx(EnvEditor, { t: t, env: env, busy: busy, onChange: setEnv }), _jsx("div", { style: sectionTitle, children: t("mountsTitle") }), _jsx(MountsEditor, { t: t, mounts: mounts, volumes: volumes, secrets: secrets, busy: busy, enabled: !busy, projectsRoot: projectsRoot, directoryPicker: directoryPicker, modeControl: "select", onAdd: (mount) => setMounts([...mounts, mount]), onRemove: (mount) => setMounts(mounts.filter((item) => item !== mount)), onUpdate: (updated, index) => setMounts(mounts.map((item, at) => (at === index ? updated : item))) }), _jsx("div", { style: sectionTitle, children: t("pathsTitle") }), _jsx(PathsList, { t: t, entries: paths, busy: busy, onChange: setPaths }), _jsx("div", { style: sectionTitle, children: t("containerSecretsTitle") }), Object.entries(secretEnv).map(([envVar, secretName]) => (_jsxs("div", { style: {
84
+ }, children: [_jsx(Field, { label: t("image"), htmlFor: imageLabel, children: _jsxs("select", { id: imageLabel, style: imageSelect, value: image, disabled: busy, onChange: (event) => setImage(event.target.value), children: [candidates.length === 0
85
+ ? _jsx("option", { value: "", children: t("none") })
86
+ : null, candidates.map((imageId) => (_jsx("option", { value: imageId, children: imageId }, imageId)))] }) }), named
87
+ ? (_jsxs(Field, { label: t("containerName"), htmlFor: nameLabel, children: [_jsx(Input, { id: nameLabel, value: name, disabled: busy, onChange: (event) => setName(sanitizeName(event.target.value)) }), name !== "" && !validName
88
+ ? (_jsx("p", { style: { ...hint, margin: 0 }, children: nameTaken
89
+ ? t("containerNameTaken")
90
+ : t("invalidContainerName") }))
91
+ : null] }))
92
+ : null, _jsx("div", { style: sectionTitle, children: t("envTitle") }), _jsx(EnvEditor, { t: t, env: env, busy: busy, onChange: setEnv }), _jsx("div", { style: sectionTitle, children: t("mountsTitle") }), _jsx(MountsEditor, { t: t, mounts: mounts, volumes: volumes, secrets: secrets, busy: busy, enabled: !busy, projectsRoot: projectsRoot, directoryPicker: directoryPicker, modeControl: "select", onAdd: (mount) => setMounts([...mounts, mount]), onRemove: (mount) => setMounts(mounts.filter((item) => item !== mount)), onUpdate: (updated, index) => setMounts(mounts.map((item, at) => (at === index ? updated : item))) }), _jsx("div", { style: sectionTitle, children: t("pathsTitle") }), _jsx(PathsList, { t: t, entries: paths, busy: busy, onChange: setPaths }), _jsx("div", { style: sectionTitle, children: t("containerSecretsTitle") }), Object.entries(secretEnv).map(([envVar, secretName]) => (_jsxs("div", { style: {
87
93
  display: "flex",
88
94
  flexWrap: "wrap",
89
95
  alignItems: "center",
@@ -102,7 +108,9 @@ export function CreateContainerModal(props) {
102
108
  flexWrap: "wrap",
103
109
  alignItems: "center",
104
110
  gap: "8px",
105
- }, children: [_jsxs("select", { style: imageSelect, value: attachSecret, disabled: busy, 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: busy, placeholder: t("secretEnvName"), onChange: (event) => setAttachVar(event.target.value), onKeyDown: (event) => {
111
+ }, children: [_jsxs("select", { style: imageSelect, value: attachSecret, disabled: busy, onChange: (event) => setAttachSecret(event.target.value), "aria-label": t("attachSecret"), children: [secrets.length === 0
112
+ ? _jsx("option", { value: "", children: t("none") })
113
+ : null, secrets.map((secret) => (_jsx("option", { value: secret.name, children: secret.name }, secret.name)))] }), _jsx(Input, { value: attachVar, disabled: busy, placeholder: t("secretEnvName"), onChange: (event) => setAttachVar(event.target.value), onKeyDown: (event) => {
106
114
  if (event.key === "Enter")
107
115
  attach();
108
116
  }, style: { width: "200px" } }), _jsx(Button, { variant: "outline", size: "sm", disabled: busy || attachVar.trim() === "" || attachSecret === "", onClick: attach, children: t("attachSecret") })] })] }) }));