@exagone313/dsh-podman 0.2.0-rc.2

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 (165) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -0
  3. package/README.zh.md +41 -0
  4. package/cordis.patch.yml +17 -0
  5. package/dist/approval-reasons.d.ts +129 -0
  6. package/dist/approval-reasons.js +253 -0
  7. package/dist/approval-reasons.test.d.ts +1 -0
  8. package/dist/approval-reasons.test.js +115 -0
  9. package/dist/approval.d.ts +29 -0
  10. package/dist/approval.js +412 -0
  11. package/dist/approval.test.d.ts +1 -0
  12. package/dist/approval.test.js +462 -0
  13. package/dist/card-route.d.ts +15 -0
  14. package/dist/card-route.js +478 -0
  15. package/dist/card-route.test.d.ts +1 -0
  16. package/dist/card-route.test.js +181 -0
  17. package/dist/card-test-support.d.ts +15 -0
  18. package/dist/card-test-support.js +160 -0
  19. package/dist/client/ContainerCard.d.ts +6 -0
  20. package/dist/client/ContainerCard.js +79 -0
  21. package/dist/client/card-client.d.ts +6 -0
  22. package/dist/client/card-client.js +47 -0
  23. package/dist/client/card-protocol.d.ts +101 -0
  24. package/dist/client/card-protocol.js +10 -0
  25. package/dist/client/container-card-caches.d.ts +10 -0
  26. package/dist/client/container-card-caches.js +31 -0
  27. package/dist/client/container-card-controller.d.ts +107 -0
  28. package/dist/client/container-card-controller.js +207 -0
  29. package/dist/client/container-card-create-modal.d.ts +24 -0
  30. package/dist/client/container-card-create-modal.js +109 -0
  31. package/dist/client/container-card-directory-styles.d.ts +5 -0
  32. package/dist/client/container-card-directory-styles.js +246 -0
  33. package/dist/client/container-card-directory.d.ts +12 -0
  34. package/dist/client/container-card-directory.js +144 -0
  35. package/dist/client/container-card-editors.d.ts +30 -0
  36. package/dist/client/container-card-editors.js +116 -0
  37. package/dist/client/container-card-images.d.ts +37 -0
  38. package/dist/client/container-card-images.js +35 -0
  39. package/dist/client/container-card-paths.d.ts +20 -0
  40. package/dist/client/container-card-paths.js +88 -0
  41. package/dist/client/container-card-row.d.ts +30 -0
  42. package/dist/client/container-card-row.js +54 -0
  43. package/dist/client/container-card-secrets.d.ts +21 -0
  44. package/dist/client/container-card-secrets.js +71 -0
  45. package/dist/client/container-card-shared.d.ts +55 -0
  46. package/dist/client/container-card-shared.js +152 -0
  47. package/dist/client/container-card-styles.d.ts +21 -0
  48. package/dist/client/container-card-styles.js +143 -0
  49. package/dist/client/container-card-volumes.d.ts +12 -0
  50. package/dist/client/container-card-volumes.js +37 -0
  51. package/dist/client/container-card-workspace.d.ts +31 -0
  52. package/dist/client/container-card-workspace.js +24 -0
  53. package/dist/client/directory-picker.d.ts +12 -0
  54. package/dist/client/directory-picker.js +30 -0
  55. package/dist/client/index.d.ts +5 -0
  56. package/dist/client/index.js +4419 -0
  57. package/dist/client/locales.d.ts +9 -0
  58. package/dist/client/locales.js +370 -0
  59. package/dist/client/read-only-approval.d.ts +8 -0
  60. package/dist/client/read-only-approval.js +63 -0
  61. package/dist/client/slot-contract.d.ts +12 -0
  62. package/dist/client/slot-contract.js +4 -0
  63. package/dist/client/terminal-styles.d.ts +5 -0
  64. package/dist/client/terminal-styles.js +31 -0
  65. package/dist/client/tool-views.d.ts +8 -0
  66. package/dist/client/tool-views.js +241 -0
  67. package/dist/containers.test.d.ts +1 -0
  68. package/dist/containers.test.js +443 -0
  69. package/dist/daemons.test.d.ts +1 -0
  70. package/dist/daemons.test.js +153 -0
  71. package/dist/fs-provider.d.ts +23 -0
  72. package/dist/fs-provider.js +184 -0
  73. package/dist/fs-provider.test.d.ts +1 -0
  74. package/dist/fs-provider.test.js +260 -0
  75. package/dist/generated/version.d.ts +2 -0
  76. package/dist/generated/version.js +6 -0
  77. package/dist/grpc/proto/dshctl/v1/control.proto +133 -0
  78. package/dist/grpc/proto/dshguest/v1/guest.proto +79 -0
  79. package/dist/grpc/runtime-client.d.ts +5 -0
  80. package/dist/grpc/runtime-client.js +43 -0
  81. package/dist/guest-rpc.d.ts +62 -0
  82. package/dist/guest-rpc.js +391 -0
  83. package/dist/images.test.d.ts +1 -0
  84. package/dist/images.test.js +79 -0
  85. package/dist/index.d.ts +22 -0
  86. package/dist/index.js +140 -0
  87. package/dist/misc.test.d.ts +1 -0
  88. package/dist/misc.test.js +141 -0
  89. package/dist/mount-enums.d.ts +22 -0
  90. package/dist/mount-enums.js +81 -0
  91. package/dist/mount-input.d.ts +14 -0
  92. package/dist/mount-input.js +64 -0
  93. package/dist/mounts.test.d.ts +1 -0
  94. package/dist/mounts.test.js +408 -0
  95. package/dist/output-reader.d.ts +34 -0
  96. package/dist/output-reader.js +87 -0
  97. package/dist/paths.test.d.ts +1 -0
  98. package/dist/paths.test.js +74 -0
  99. package/dist/preferences.d.ts +6 -0
  100. package/dist/preferences.js +27 -0
  101. package/dist/project-path.d.ts +6 -0
  102. package/dist/project-path.js +73 -0
  103. package/dist/project-path.test.d.ts +1 -0
  104. package/dist/project-path.test.js +52 -0
  105. package/dist/prompts.d.ts +13 -0
  106. package/dist/prompts.js +149 -0
  107. package/dist/public.d.ts +4 -0
  108. package/dist/public.js +63 -0
  109. package/dist/read-only-shell.d.ts +14 -0
  110. package/dist/read-only-shell.js +150 -0
  111. package/dist/read-only-shell.test.d.ts +1 -0
  112. package/dist/read-only-shell.test.js +127 -0
  113. package/dist/secrets.test.d.ts +1 -0
  114. package/dist/secrets.test.js +181 -0
  115. package/dist/settings-commands.test.d.ts +1 -0
  116. package/dist/settings-commands.test.js +549 -0
  117. package/dist/settings-create.test.d.ts +1 -0
  118. package/dist/settings-create.test.js +394 -0
  119. package/dist/settings-mounts.test.d.ts +1 -0
  120. package/dist/settings-mounts.test.js +467 -0
  121. package/dist/settings-paths.test.d.ts +1 -0
  122. package/dist/settings-paths.test.js +65 -0
  123. package/dist/settings-schema.d.ts +14 -0
  124. package/dist/settings-schema.js +14 -0
  125. package/dist/settings-workspaces.test.d.ts +1 -0
  126. package/dist/settings-workspaces.test.js +265 -0
  127. package/dist/spill-store.d.ts +17 -0
  128. package/dist/spill-store.js +54 -0
  129. package/dist/spill-store.test.d.ts +1 -0
  130. package/dist/spill-store.test.js +59 -0
  131. package/dist/subprocess.d.ts +9 -0
  132. package/dist/subprocess.js +369 -0
  133. package/dist/subprocess.test.d.ts +1 -0
  134. package/dist/subprocess.test.js +234 -0
  135. package/dist/test-support.d.ts +328 -0
  136. package/dist/test-support.js +437 -0
  137. package/dist/tool-defs.d.ts +26 -0
  138. package/dist/tool-defs.js +188 -0
  139. package/dist/tool-handlers.d.ts +2 -0
  140. package/dist/tool-handlers.js +546 -0
  141. package/dist/tool-params.d.ts +803 -0
  142. package/dist/tool-params.js +494 -0
  143. package/dist/tool-schemas.d.ts +2 -0
  144. package/dist/tool-schemas.js +5 -0
  145. package/dist/tool-views.d.ts +2 -0
  146. package/dist/tool-views.js +56 -0
  147. package/dist/views.test.d.ts +1 -0
  148. package/dist/views.test.js +23 -0
  149. package/dist/volumes.test.d.ts +1 -0
  150. package/dist/volumes.test.js +40 -0
  151. package/dist/workspace-binding.d.ts +40 -0
  152. package/dist/workspace-binding.js +275 -0
  153. package/dist/workspace-binding.test.d.ts +1 -0
  154. package/dist/workspace-binding.test.js +337 -0
  155. package/docs/architecture.md +173 -0
  156. package/docs/architecture.zh.md +129 -0
  157. package/docs/configuration.md +160 -0
  158. package/docs/configuration.zh.md +147 -0
  159. package/docs/development.md +142 -0
  160. package/docs/development.zh.md +135 -0
  161. package/docs/install-dsh-and-dsh-podman.md +165 -0
  162. package/docs/install-dsh-and-dsh-podman.zh.md +145 -0
  163. package/docs/usage.md +393 -0
  164. package/docs/usage.zh.md +315 -0
  165. package/package.json +119 -0
@@ -0,0 +1,207 @@
1
+ // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+ import { createSnapshotStore } from "@deepseek-ai/dsh-client-store";
5
+ import { createCardClient } from "./card-client.js";
6
+ export const CONTAINER_NS = "podman";
7
+ const EMPTY_SNAPSHOT = {
8
+ version: "",
9
+ commit: "",
10
+ projectsRoot: "",
11
+ workspaces: [],
12
+ containers: [],
13
+ images: [],
14
+ volumes: [],
15
+ secrets: [],
16
+ caches: [],
17
+ };
18
+ // The card reads its live state from the host route and its preferences from
19
+ // the settings namespace. A command is one request, so nothing is re-delivered
20
+ // and no document round-trip is involved.
21
+ export class ContainerCardController {
22
+ scope;
23
+ store;
24
+ drafts = new Map();
25
+ client;
26
+ snapshot = EMPTY_SNAPSHOT;
27
+ busy = false;
28
+ notice = "";
29
+ directoryPicker;
30
+ constructor(scope, client = createCardClient()) {
31
+ this.scope = scope;
32
+ this.client = client;
33
+ this.store = createSnapshotStore(this.project());
34
+ scope.subscribe(() => {
35
+ this.publish();
36
+ });
37
+ void this.reload();
38
+ }
39
+ draft(field, current) {
40
+ return this.drafts.get(field) ?? current;
41
+ }
42
+ project() {
43
+ const snapshot = this.scope.getSnapshot();
44
+ const value = snapshot.value;
45
+ return {
46
+ available: snapshot.status === "ready" && value !== undefined,
47
+ writable: snapshot.writable,
48
+ busy: this.busy,
49
+ notice: this.notice,
50
+ version: this.snapshot.version,
51
+ commit: this.snapshot.commit,
52
+ defaultImage: value?.defaultImage ?? "",
53
+ defaultImageDraft: this.draft("defaultImage", value?.defaultImage ?? ""),
54
+ socketsRoot: value?.socketsRoot ?? "",
55
+ socketsRootDraft: this.draft("socketsRoot", value?.socketsRoot ?? ""),
56
+ projectsRoot: this.snapshot.projectsRoot,
57
+ ...(this.directoryPicker === undefined
58
+ ? {}
59
+ : { directoryPicker: this.directoryPicker }),
60
+ workspaces: this.snapshot.workspaces,
61
+ containers: this.snapshot.containers,
62
+ images: this.snapshot.images,
63
+ volumes: this.snapshot.volumes,
64
+ secrets: this.snapshot.secrets,
65
+ caches: this.snapshot.caches,
66
+ };
67
+ }
68
+ publish() {
69
+ this.store.set(this.project());
70
+ }
71
+ // setDirectoryPicker records the harness's directory picker (or clears it)
72
+ // and republishes, so the card shows or hides the browse affordance.
73
+ setDirectoryPicker(picker) {
74
+ this.directoryPicker = picker;
75
+ this.publish();
76
+ }
77
+ command(op, workspace, image, extra = {}) {
78
+ void this.dispatch({
79
+ op,
80
+ workspace,
81
+ image,
82
+ projectName: extra.projectName ?? "",
83
+ mounts: extra.mounts ?? [],
84
+ paths: extra.paths ?? [],
85
+ value: extra.value ?? "",
86
+ env: extra.env ?? {},
87
+ container: extra.container ?? "",
88
+ secret: extra.secret ?? "",
89
+ secretEnv: extra.secretEnv ?? "",
90
+ length: extra.length ?? 0,
91
+ charset: extra.charset ?? "",
92
+ packages: extra.packages ?? [],
93
+ secretEnvMap: extra.secretEnvMap ?? {},
94
+ cacheMode: extra.cacheMode ?? "",
95
+ mount: extra.mount ?? null,
96
+ });
97
+ }
98
+ async dispatch(request) {
99
+ this.busy = true;
100
+ this.notice = "";
101
+ this.publish();
102
+ try {
103
+ const result = await this.client.command(request);
104
+ this.notice = result.notice ?? "";
105
+ this.snapshot = await this.client.snapshot();
106
+ }
107
+ catch (error) {
108
+ this.notice = error instanceof Error ? error.message : String(error);
109
+ }
110
+ finally {
111
+ this.busy = false;
112
+ this.publish();
113
+ }
114
+ }
115
+ async reload() {
116
+ try {
117
+ this.snapshot = await this.client.snapshot();
118
+ // A refresh clears the previous action's notice, so reopening the
119
+ // settings does not replay feedback from an earlier session.
120
+ this.notice = "";
121
+ }
122
+ catch (error) {
123
+ this.notice = error instanceof Error ? error.message : String(error);
124
+ }
125
+ this.publish();
126
+ }
127
+ edit(field, text) {
128
+ this.drafts.set(field, text);
129
+ this.publish();
130
+ }
131
+ save(field) {
132
+ const draft = this.drafts.get(field);
133
+ if (draft === undefined)
134
+ return;
135
+ void this.scope.set(field, draft);
136
+ }
137
+ discard(field) {
138
+ this.drafts.delete(field);
139
+ this.publish();
140
+ }
141
+ inject() {
142
+ return {
143
+ hooks: { containerCard: this.store },
144
+ reload: () => {
145
+ void this.reload();
146
+ },
147
+ remove: (workspace) => this.command("remove", workspace, ""),
148
+ cleanCaches: (mode) => this.command("cache_clean", "", "", { cacheMode: mode }),
149
+ removeWorkspace: (workspace) => this.command("workspace_remove", workspace, ""),
150
+ recreate: (workspace, image, env) => this.command("recreate", workspace, image, { ...(env ? { env } : {}) }),
151
+ createContainer: (workspace, config) => this.command("create", workspace.workspaceSlug, config?.image ?? (workspace.imageId || this.defaultImage() || ""), {
152
+ 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 } : {}),
157
+ }),
158
+ startContainer: (workspace, container, config) => this.command("create", workspace.workspaceSlug, config?.image ?? (workspace.imageId || this.defaultImage() || ""), {
159
+ 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 } : {}),
164
+ }),
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
+ setContainerPaths: (workspace, container, paths) => this.command("container_path_set", workspace, "", { container, paths }),
169
+ createVolume: (name) => this.command("volume_create", name, ""),
170
+ removeVolume: (name) => this.command("volume_remove", name, ""),
171
+ removeImage: (imageId) => this.command("image_remove", imageId, ""),
172
+ rebuildImage: (imageId) => this.command("image_rebuild", imageId, ""),
173
+ rebuildAllImages: () => this.command("image_rebuild_all", "", ""),
174
+ buildImage: (imageId, parent, packages) => this.command("image_build", imageId, parent, { packages }),
175
+ rebuildBaseImage: (name) => this.command("image_base_rebuild", name, ""),
176
+ pullBaseImage: (name) => this.command("image_base_pull", name, ""),
177
+ setDefaultImage: (name) => {
178
+ this.edit("defaultImage", name);
179
+ this.save("defaultImage");
180
+ },
181
+ createSecret: (name, length, charset) => this.command("secret_create", name, "", {
182
+ ...(length ? { length } : {}),
183
+ ...(charset ? { charset } : {}),
184
+ }),
185
+ removeSecret: (name) => this.command("secret_remove", name, ""),
186
+ 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,
190
+ secret,
191
+ }),
192
+ removeContainerSecret: (workspace, envVar) => this.command("container_secret_remove", workspace, "", {
193
+ container: "default",
194
+ secretEnv: envVar,
195
+ }),
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"),
202
+ };
203
+ }
204
+ defaultImage() {
205
+ return this.scope.getSnapshot().value?.defaultImage ?? "";
206
+ }
207
+ }
@@ -0,0 +1,24 @@
1
+ import { type ContainerCreateConfig, type ImageView, type WorkspaceView } from "./container-card-controller.js";
2
+ import { type DirectoryPickerFace } from "./directory-picker.js";
3
+ import { type ContainerPluginKey } from "./locales.js";
4
+ import { type ReactNode } from "react";
5
+ export declare function CreateContainerModal(props: {
6
+ t: (key: ContainerPluginKey) => string;
7
+ workspace: WorkspaceView;
8
+ images: readonly ImageView[];
9
+ volumes: readonly {
10
+ name: string;
11
+ }[];
12
+ secrets: readonly {
13
+ name: string;
14
+ }[];
15
+ busy: boolean;
16
+ defaultImage: string;
17
+ projectsRoot: string;
18
+ directoryPicker?: DirectoryPickerFace;
19
+ named: boolean;
20
+ existing: readonly string[];
21
+ open: boolean;
22
+ onClose: () => void;
23
+ onCreate: (container: string, config: ContainerCreateConfig) => void;
24
+ }): ReactNode;
@@ -0,0 +1,109 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { EnvEditor, MountsEditor } from "./container-card-editors.js";
3
+ import { PathsList } from "./container-card-paths.js";
4
+ import { Field, namePattern, sanitizeName } from "./container-card-shared.js";
5
+ import { greyId, hint, imageSelect, sectionTitle } from "./container-card-styles.js";
6
+ import { Button, Input, Modal } from "@deepseek-ai/dsh-client-ui-primitives";
7
+ import { useEffect, useId, useState } from "react";
8
+ export function CreateContainerModal(props) {
9
+ const { t, workspace, images, volumes, secrets, busy, defaultImage, projectsRoot, directoryPicker, named, existing, open, onClose, onCreate, } = props;
10
+ const [image, setImage] = useState(workspace.imageId || defaultImage);
11
+ const [name, setName] = useState("");
12
+ const [env, setEnv] = useState({});
13
+ const [mounts, setMounts] = useState(() => [
14
+ {
15
+ kind: "project",
16
+ project: workspace.projectName,
17
+ destination: "",
18
+ mode: "read_write",
19
+ volume: "",
20
+ secret: "",
21
+ },
22
+ ]);
23
+ const [secretEnv, setSecretEnv] = useState({});
24
+ const [paths, setPaths] = useState([]);
25
+ const [attachSecret, setAttachSecret] = useState("");
26
+ const [attachVar, setAttachVar] = useState("");
27
+ const [submitting, setSubmitting] = useState(false);
28
+ const imageLabel = useId();
29
+ const nameLabel = useId();
30
+ useEffect(() => {
31
+ if (!open)
32
+ return;
33
+ setImage(workspace.imageId || defaultImage);
34
+ setName("");
35
+ setEnv({});
36
+ setMounts([
37
+ {
38
+ kind: "project",
39
+ project: workspace.projectName,
40
+ destination: "",
41
+ mode: "read_write",
42
+ volume: "",
43
+ secret: "",
44
+ },
45
+ ]);
46
+ setSecretEnv({});
47
+ setPaths([]);
48
+ setAttachSecret("");
49
+ setAttachVar("");
50
+ setSubmitting(false);
51
+ }, [open, workspace, defaultImage]);
52
+ const baseImages = images.filter((image) => image.isBase);
53
+ const customImages = images.filter((image) => !image.isBase);
54
+ const candidates = [
55
+ ...baseImages.map((image) => image.imageId),
56
+ ...customImages.map((image) => image.imageId),
57
+ ];
58
+ const nameTaken = existing.includes(name);
59
+ const validName = namePattern.test(name) && name !== "default" && !nameTaken;
60
+ const canCreate = !named || validName;
61
+ const submit = () => {
62
+ if (!canCreate || submitting)
63
+ return;
64
+ setSubmitting(true);
65
+ onCreate(named ? name : "", {
66
+ image,
67
+ env,
68
+ mounts,
69
+ paths: paths.map((entry) => entry.path).filter((path) => path !== ""),
70
+ secretEnv,
71
+ });
72
+ };
73
+ const attach = () => {
74
+ const envVar = attachVar.trim();
75
+ if (envVar === "" || attachSecret === "")
76
+ return;
77
+ setSecretEnv({ ...secretEnv, [envVar]: attachSecret });
78
+ setAttachVar("");
79
+ };
80
+ return (_jsx(Modal, { open: open, onClose: onClose, title: named ? t("addContainer") : t("createContainer"), closeLabel: t("cancel"), footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "outline", size: "sm", onClick: onClose, children: t("cancel") }), _jsx(Button, { variant: "primary", size: "sm", disabled: !canCreate || submitting, onClick: submit, children: named ? t("addContainer") : t("createContainer") })] }), children: _jsxs("div", { style: {
81
+ display: "flex",
82
+ flexDirection: "column",
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: {
87
+ display: "flex",
88
+ flexWrap: "wrap",
89
+ alignItems: "center",
90
+ gap: "8px",
91
+ }, children: [_jsxs("code", { style: {
92
+ ...greyId,
93
+ flex: 1,
94
+ fontSize: "13px",
95
+ color: "var(--dsw-alias-label-primary)",
96
+ }, children: [envVar, "=", secretName] }), _jsx(Button, { variant: "outline", size: "sm", disabled: busy, onClick: () => {
97
+ const next = { ...secretEnv };
98
+ delete next[envVar];
99
+ setSecretEnv(next);
100
+ }, children: t("remove") })] }, envVar))), _jsxs("div", { style: {
101
+ display: "flex",
102
+ flexWrap: "wrap",
103
+ alignItems: "center",
104
+ 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) => {
106
+ if (event.key === "Enter")
107
+ attach();
108
+ }, style: { width: "200px" } }), _jsx(Button, { variant: "outline", size: "sm", disabled: busy || attachVar.trim() === "" || attachSecret === "", onClick: attach, children: t("attachSecret") })] })] }) }));
109
+ }
@@ -0,0 +1,5 @@
1
+ import type { Context as ClientContext } from "@deepseek-ai/cordis";
2
+ export declare const DIRECTORY_CLASS = "dsh-podman-directory";
3
+ export declare const DIRECTORY_STYLES = "\n.dsh-podman-directory.dsh-podman-directory {\n width: min(680px, 100%);\n height: min(500px, calc(100dvh - 32px));\n padding: 0;\n gap: 0;\n --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);\n --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);\n}\n.dsh-podman-directory-scope {\n display: contents;\n}\n.dsh-podman-directory-header {\n display: flex;\n flex-direction: column;\n flex: none;\n gap: 8px;\n padding: 16px 14px 8px 24px;\n border-bottom: 0.5px solid var(--dsw-alias-border-l3);\n}\n.dsh-podman-directory-title {\n margin: 0;\n min-height: 28px;\n font-size: 16px;\n line-height: 24px;\n font-weight: 510;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-podman-directory-crumbbar {\n display: flex;\n align-items: center;\n gap: 4px;\n box-sizing: border-box;\n min-height: 24px;\n margin-left: -9px;\n padding: 0 8px;\n border: 1px solid transparent;\n border-radius: 8px;\n}\n.dsh-podman-directory-crumbs {\n display: flex;\n align-items: center;\n gap: 4px;\n flex: 0 1 auto;\n min-width: 0;\n overflow-x: auto;\n scrollbar-width: none;\n}\n.dsh-podman-directory-crumbseat {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n flex: none;\n min-width: 0;\n}\n.dsh-podman-directory-crumb {\n border: none;\n background: transparent;\n padding: 0;\n max-width: 160px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13px;\n line-height: 20px;\n font-weight: 500;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.dsh-podman-directory-crumb:hover {\n color: var(--dsw-alias-label-primary);\n}\n.dsh-podman-directory-crumb:disabled {\n cursor: default;\n color: var(--dsw-alias-label-caption);\n}\n.dsh-podman-directory-crumbchevron {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-podman-directory-content {\n display: flex;\n flex-direction: column;\n flex: 1 1 0;\n min-height: 0;\n position: relative;\n padding: 16px 16px 16px 24px;\n}\n.dsh-podman-directory-miller {\n display: flex;\n align-items: stretch;\n flex: 1 1 0;\n min-height: 0;\n gap: 12px;\n overflow-x: auto;\n scrollbar-width: none;\n}\n.dsh-podman-directory-column {\n display: flex;\n flex-direction: column;\n gap: 2px;\n flex: 1 1 0;\n min-width: 256px;\n margin: 0;\n list-style: none;\n overflow-y: auto;\n padding-right: 8px;\n}\n.dsh-podman-directory-divider {\n flex: none;\n width: 0.5px;\n background: var(--dsw-alias-border-l3);\n}\n.dsh-podman-directory-row {\n width: 100%;\n display: flex;\n align-items: center;\n gap: 4px;\n height: 28px;\n flex: none;\n padding: 4px;\n border: none;\n border-radius: 6px;\n background: transparent;\n text-align: left;\n cursor: pointer;\n}\n.dsh-podman-directory-seat {\n display: flex;\n flex: none;\n}\n.dsh-podman-directory-row:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.dsh-podman-directory-row-selected,\n.dsh-podman-directory-row-selected:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-active, var(--dsw-alias-interactive-bg-hover));\n}\n.dsh-podman-directory-row:disabled {\n cursor: default;\n}\n.dsh-podman-directory-rowicon {\n flex: none;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-podman-directory-rowicon-selected {\n flex: none;\n color: var(--dsw-alias-button-info-fill);\n}\n.dsh-podman-directory-rowname {\n flex: 1 1 0;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 13px;\n line-height: 20px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-podman-directory-rowchevron {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-podman-directory-status,\n.dsh-podman-directory-error {\n margin: 0;\n padding: 4px;\n font-size: 12px;\n line-height: 18px;\n}\n.dsh-podman-directory-status {\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-podman-directory-error {\n color: var(--dsw-alias-state-error-primary);\n}\n.dsh-podman-directory-loading {\n position: absolute;\n right: 16px;\n bottom: 8px;\n padding: 2px 8px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n}\n.dsh-podman-directory-footer {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px;\n flex: none;\n padding: 16px 24px;\n border-top: 0.5px solid var(--dsw-alias-border-l3);\n}\n.dsh-podman-directory-toggle {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 0;\n border: none;\n background: transparent;\n font-size: 13px;\n line-height: 20px;\n font-weight: 500;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n}\n.dsh-podman-directory-toggle[aria-pressed=\"true\"] {\n color: var(--dsw-alias-label-primary);\n}\n.dsh-podman-directory-toggle:disabled {\n cursor: default;\n color: var(--dsw-alias-label-caption);\n}\n.dsh-podman-directory-gap {\n flex: 1 1 0;\n}\n.dsh-podman-directory-action {\n min-width: 72px;\n}\n";
4
+ /** Inject the directory browser stylesheet for the owning plugin lifetime. */
5
+ export declare function installDirectoryStyles(ctx: ClientContext): void;
@@ -0,0 +1,246 @@
1
+ // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+ // The class passed to Modal for the project directory browser. The layout,
5
+ // typography and tokens mirror dsh's own directory browser (680x500 dialog,
6
+ // two Miller columns, chevron breadcrumbs, 28px rows with folder icons), so the
7
+ // plugin's picker reads like the harness's workspace directory selection. The
8
+ // dialog is clamped to the projects root, so its breadcrumb trail starts there.
9
+ export const DIRECTORY_CLASS = "dsh-podman-directory";
10
+ export const DIRECTORY_STYLES = `
11
+ .dsh-podman-directory.dsh-podman-directory {
12
+ width: min(680px, 100%);
13
+ height: min(500px, calc(100dvh - 32px));
14
+ padding: 0;
15
+ gap: 0;
16
+ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
17
+ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
18
+ }
19
+ .dsh-podman-directory-scope {
20
+ display: contents;
21
+ }
22
+ .dsh-podman-directory-header {
23
+ display: flex;
24
+ flex-direction: column;
25
+ flex: none;
26
+ gap: 8px;
27
+ padding: 16px 14px 8px 24px;
28
+ border-bottom: 0.5px solid var(--dsw-alias-border-l3);
29
+ }
30
+ .dsh-podman-directory-title {
31
+ margin: 0;
32
+ min-height: 28px;
33
+ font-size: 16px;
34
+ line-height: 24px;
35
+ font-weight: 510;
36
+ color: var(--dsw-alias-label-primary);
37
+ }
38
+ .dsh-podman-directory-crumbbar {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: 4px;
42
+ box-sizing: border-box;
43
+ min-height: 24px;
44
+ margin-left: -9px;
45
+ padding: 0 8px;
46
+ border: 1px solid transparent;
47
+ border-radius: 8px;
48
+ }
49
+ .dsh-podman-directory-crumbs {
50
+ display: flex;
51
+ align-items: center;
52
+ gap: 4px;
53
+ flex: 0 1 auto;
54
+ min-width: 0;
55
+ overflow-x: auto;
56
+ scrollbar-width: none;
57
+ }
58
+ .dsh-podman-directory-crumbseat {
59
+ display: inline-flex;
60
+ align-items: center;
61
+ gap: 4px;
62
+ flex: none;
63
+ min-width: 0;
64
+ }
65
+ .dsh-podman-directory-crumb {
66
+ border: none;
67
+ background: transparent;
68
+ padding: 0;
69
+ max-width: 160px;
70
+ overflow: hidden;
71
+ text-overflow: ellipsis;
72
+ white-space: nowrap;
73
+ font-size: 13px;
74
+ line-height: 20px;
75
+ font-weight: 500;
76
+ color: var(--dsw-alias-label-tertiary);
77
+ cursor: pointer;
78
+ }
79
+ .dsh-podman-directory-crumb:hover {
80
+ color: var(--dsw-alias-label-primary);
81
+ }
82
+ .dsh-podman-directory-crumb:disabled {
83
+ cursor: default;
84
+ color: var(--dsw-alias-label-caption);
85
+ }
86
+ .dsh-podman-directory-crumbchevron {
87
+ flex: none;
88
+ color: var(--dsw-alias-label-tertiary);
89
+ }
90
+ .dsh-podman-directory-content {
91
+ display: flex;
92
+ flex-direction: column;
93
+ flex: 1 1 0;
94
+ min-height: 0;
95
+ position: relative;
96
+ padding: 16px 16px 16px 24px;
97
+ }
98
+ .dsh-podman-directory-miller {
99
+ display: flex;
100
+ align-items: stretch;
101
+ flex: 1 1 0;
102
+ min-height: 0;
103
+ gap: 12px;
104
+ overflow-x: auto;
105
+ scrollbar-width: none;
106
+ }
107
+ .dsh-podman-directory-column {
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: 2px;
111
+ flex: 1 1 0;
112
+ min-width: 256px;
113
+ margin: 0;
114
+ list-style: none;
115
+ overflow-y: auto;
116
+ padding-right: 8px;
117
+ }
118
+ .dsh-podman-directory-divider {
119
+ flex: none;
120
+ width: 0.5px;
121
+ background: var(--dsw-alias-border-l3);
122
+ }
123
+ .dsh-podman-directory-row {
124
+ width: 100%;
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 4px;
128
+ height: 28px;
129
+ flex: none;
130
+ padding: 4px;
131
+ border: none;
132
+ border-radius: 6px;
133
+ background: transparent;
134
+ text-align: left;
135
+ cursor: pointer;
136
+ }
137
+ .dsh-podman-directory-seat {
138
+ display: flex;
139
+ flex: none;
140
+ }
141
+ .dsh-podman-directory-row:hover:not(:disabled) {
142
+ background: var(--dsw-alias-interactive-bg-hover);
143
+ }
144
+ .dsh-podman-directory-row-selected,
145
+ .dsh-podman-directory-row-selected:hover:not(:disabled) {
146
+ background: var(--dsw-alias-interactive-bg-active, var(--dsw-alias-interactive-bg-hover));
147
+ }
148
+ .dsh-podman-directory-row:disabled {
149
+ cursor: default;
150
+ }
151
+ .dsh-podman-directory-rowicon {
152
+ flex: none;
153
+ color: var(--dsw-alias-label-secondary);
154
+ }
155
+ .dsh-podman-directory-rowicon-selected {
156
+ flex: none;
157
+ color: var(--dsw-alias-button-info-fill);
158
+ }
159
+ .dsh-podman-directory-rowname {
160
+ flex: 1 1 0;
161
+ min-width: 0;
162
+ overflow: hidden;
163
+ text-overflow: ellipsis;
164
+ white-space: nowrap;
165
+ font-size: 13px;
166
+ line-height: 20px;
167
+ font-weight: 500;
168
+ color: var(--dsw-alias-label-primary);
169
+ }
170
+ .dsh-podman-directory-rowchevron {
171
+ flex: none;
172
+ color: var(--dsw-alias-label-tertiary);
173
+ }
174
+ .dsh-podman-directory-status,
175
+ .dsh-podman-directory-error {
176
+ margin: 0;
177
+ padding: 4px;
178
+ font-size: 12px;
179
+ line-height: 18px;
180
+ }
181
+ .dsh-podman-directory-status {
182
+ color: var(--dsw-alias-label-secondary);
183
+ }
184
+ .dsh-podman-directory-error {
185
+ color: var(--dsw-alias-state-error-primary);
186
+ }
187
+ .dsh-podman-directory-loading {
188
+ position: absolute;
189
+ right: 16px;
190
+ bottom: 8px;
191
+ padding: 2px 8px;
192
+ font-size: 12px;
193
+ line-height: 18px;
194
+ color: var(--dsw-alias-label-secondary);
195
+ background: var(--dsw-alias-bg-layer-2);
196
+ }
197
+ .dsh-podman-directory-footer {
198
+ display: flex;
199
+ align-items: center;
200
+ flex-wrap: wrap;
201
+ gap: 8px;
202
+ flex: none;
203
+ padding: 16px 24px;
204
+ border-top: 0.5px solid var(--dsw-alias-border-l3);
205
+ }
206
+ .dsh-podman-directory-toggle {
207
+ display: inline-flex;
208
+ align-items: center;
209
+ gap: 4px;
210
+ padding: 0;
211
+ border: none;
212
+ background: transparent;
213
+ font-size: 13px;
214
+ line-height: 20px;
215
+ font-weight: 500;
216
+ color: var(--dsw-alias-label-secondary);
217
+ cursor: pointer;
218
+ }
219
+ .dsh-podman-directory-toggle[aria-pressed="true"] {
220
+ color: var(--dsw-alias-label-primary);
221
+ }
222
+ .dsh-podman-directory-toggle:disabled {
223
+ cursor: default;
224
+ color: var(--dsw-alias-label-caption);
225
+ }
226
+ .dsh-podman-directory-gap {
227
+ flex: 1 1 0;
228
+ }
229
+ .dsh-podman-directory-action {
230
+ min-width: 72px;
231
+ }
232
+ `;
233
+ /** Inject the directory browser stylesheet for the owning plugin lifetime. */
234
+ export function installDirectoryStyles(ctx) {
235
+ if (typeof document === "undefined")
236
+ return;
237
+ ctx.effect(() => {
238
+ const tag = document.createElement("style");
239
+ tag.dataset.plugin = "@exagone313/dsh-podman";
240
+ tag.textContent = DIRECTORY_STYLES;
241
+ document.head.appendChild(tag);
242
+ return () => {
243
+ tag.remove();
244
+ };
245
+ }, "podman: directory styles");
246
+ }
@@ -0,0 +1,12 @@
1
+ import { type DirectoryPickerFace } from "./directory-picker.js";
2
+ import { type ContainerPluginKey } from "./locales.js";
3
+ import { type ReactNode } from "react";
4
+ export declare function DirectoryPickerModal(props: {
5
+ t: (key: ContainerPluginKey) => string;
6
+ open: boolean;
7
+ root: string;
8
+ start: string;
9
+ picker: DirectoryPickerFace;
10
+ onSelect: (path: string) => void;
11
+ onClose: () => void;
12
+ }): ReactNode;