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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +12 -0
  2. package/README.zh.md +14 -2
  3. package/cordis.patch.yml +58 -0
  4. package/dist/approval-reasons.d.ts +3 -4
  5. package/dist/approval-reasons.js +40 -49
  6. package/dist/approval-reasons.test.js +43 -15
  7. package/dist/approval.js +27 -10
  8. package/dist/approval.test.js +165 -34
  9. package/dist/card-route.d.ts +3 -2
  10. package/dist/card-route.js +103 -35
  11. package/dist/card-route.test.js +60 -43
  12. package/dist/card-test-support.js +39 -66
  13. package/dist/client/ContainerCard.d.ts +1 -1
  14. package/dist/client/ContainerCard.js +76 -70
  15. package/dist/client/card-protocol.d.ts +3 -3
  16. package/dist/client/container-card-caches.js +11 -6
  17. package/dist/client/container-card-controller.d.ts +13 -18
  18. package/dist/client/container-card-controller.js +86 -40
  19. package/dist/client/container-card-create-modal.d.ts +2 -2
  20. package/dist/client/container-card-create-modal.js +13 -5
  21. package/dist/client/container-card-default-env.d.ts +10 -0
  22. package/dist/client/container-card-default-env.js +69 -0
  23. package/dist/client/container-card-directory.d.ts +2 -2
  24. package/dist/client/container-card-directory.js +12 -4
  25. package/dist/client/container-card-editors.d.ts +3 -3
  26. package/dist/client/container-card-editors.js +104 -32
  27. package/dist/client/container-card-images.d.ts +4 -4
  28. package/dist/client/container-card-images.js +19 -6
  29. package/dist/client/container-card-paths.d.ts +3 -3
  30. package/dist/client/container-card-paths.js +18 -11
  31. package/dist/client/container-card-row.d.ts +10 -6
  32. package/dist/client/container-card-row.js +82 -13
  33. package/dist/client/container-card-secrets.d.ts +3 -3
  34. package/dist/client/container-card-secrets.js +8 -6
  35. package/dist/client/container-card-shared.d.ts +4 -17
  36. package/dist/client/container-card-shared.js +9 -25
  37. package/dist/client/container-card-styles.d.ts +2 -8
  38. package/dist/client/container-card-styles.js +14 -52
  39. package/dist/client/container-card-volumes.d.ts +2 -2
  40. package/dist/client/container-card-volumes.js +9 -5
  41. package/dist/client/container-card-workspace.d.ts +8 -6
  42. package/dist/client/container-card-workspace.js +9 -3
  43. package/dist/client/directory-picker.js +2 -1
  44. package/dist/client/index.js +13109 -2117
  45. package/dist/client/locales.d.ts +6 -1
  46. package/dist/client/locales.js +88 -38
  47. package/dist/client/podman-terminal-guide.d.ts +11 -0
  48. package/dist/client/podman-terminal-guide.js +208 -0
  49. package/dist/client/podman-terminal-title.d.ts +7 -0
  50. package/dist/client/podman-terminal-title.js +30 -0
  51. package/dist/client/podman-terminal.d.ts +26 -0
  52. package/dist/client/podman-terminal.js +412 -0
  53. package/dist/client/read-only-approval.d.ts +2 -2
  54. package/dist/client/read-only-approval.js +13 -3
  55. package/dist/client/slot-contract.d.ts +0 -11
  56. package/dist/client/terminal-preference.d.ts +26 -0
  57. package/dist/client/terminal-preference.js +67 -0
  58. package/dist/client/terminal-protocol.d.ts +85 -0
  59. package/dist/client/terminal-protocol.js +14 -0
  60. package/dist/client/terminal-tab.d.ts +60 -0
  61. package/dist/client/terminal-tab.js +35 -0
  62. package/dist/client/terminal-targets.d.ts +33 -0
  63. package/dist/client/terminal-targets.js +65 -0
  64. package/dist/client/terminal-titles.d.ts +23 -0
  65. package/dist/client/terminal-titles.js +52 -0
  66. package/dist/client/terminal-transport.d.ts +63 -0
  67. package/dist/client/terminal-transport.js +207 -0
  68. package/dist/client/tool-views.js +215 -49
  69. package/dist/container-env.d.ts +11 -0
  70. package/dist/container-env.js +96 -0
  71. package/dist/container-env.test.d.ts +1 -0
  72. package/dist/container-env.test.js +93 -0
  73. package/dist/containers.test.js +196 -18
  74. package/dist/daemons.test.js +18 -4
  75. package/dist/dsh-version.js +2 -1
  76. package/dist/env-rows.d.ts +14 -0
  77. package/dist/env-rows.js +52 -0
  78. package/dist/env-rows.test.d.ts +1 -0
  79. package/dist/env-rows.test.js +43 -0
  80. package/dist/fs-provider.d.ts +1 -0
  81. package/dist/fs-provider.js +26 -12
  82. package/dist/fs-provider.test.js +51 -10
  83. package/dist/generated/version.d.ts +2 -2
  84. package/dist/generated/version.js +2 -2
  85. package/dist/grpc/proto/dshctl/v1/control.proto +8 -1
  86. package/dist/grpc/proto/dshguest/v1/guest.proto +5 -0
  87. package/dist/grpc/runtime-client.d.ts +3 -1
  88. package/dist/grpc/runtime-client.js +90 -17
  89. package/dist/guest-rpc.d.ts +15 -19
  90. package/dist/guest-rpc.js +289 -70
  91. package/dist/guest-rpc.test.d.ts +1 -0
  92. package/dist/guest-rpc.test.js +241 -0
  93. package/dist/guest-terminal.d.ts +30 -0
  94. package/dist/guest-terminal.js +196 -0
  95. package/dist/images.test.js +10 -3
  96. package/dist/index.d.ts +11 -16
  97. package/dist/index.js +42 -36
  98. package/dist/locales.test.d.ts +1 -0
  99. package/dist/locales.test.js +34 -0
  100. package/dist/misc.test.js +38 -10
  101. package/dist/mount-enums.js +2 -1
  102. package/dist/mount-input.d.ts +2 -0
  103. package/dist/mount-input.js +38 -22
  104. package/dist/mounts.test.js +81 -11
  105. package/dist/naming.test.d.ts +1 -0
  106. package/dist/naming.test.js +28 -0
  107. package/dist/output-reader.js +36 -4
  108. package/dist/package-deps.test.d.ts +1 -0
  109. package/dist/package-deps.test.js +49 -0
  110. package/dist/paths.test.js +4 -2
  111. package/dist/plugin-meta.test.d.ts +1 -0
  112. package/dist/plugin-meta.test.js +29 -0
  113. package/dist/project-path.js +33 -7
  114. package/dist/project-path.test.js +14 -0
  115. package/dist/prompts.d.ts +0 -4
  116. package/dist/prompts.js +15 -84
  117. package/dist/read-only-shell.js +3 -1
  118. package/dist/read-only-shell.test.js +80 -25
  119. package/dist/runtime-client.test.d.ts +1 -0
  120. package/dist/runtime-client.test.js +53 -0
  121. package/dist/secrets.test.js +21 -6
  122. package/dist/settings-commands.test.js +61 -14
  123. package/dist/settings-create.test.js +32 -10
  124. package/dist/settings-default-env.test.d.ts +1 -0
  125. package/dist/settings-default-env.test.js +175 -0
  126. package/dist/settings-mounts.test.js +46 -7
  127. package/dist/settings-schema.d.ts +6 -11
  128. package/dist/settings-schema.js +4 -8
  129. package/dist/settings-schema.test.d.ts +1 -0
  130. package/dist/settings-schema.test.js +35 -0
  131. package/dist/settings-workspaces.test.js +36 -63
  132. package/dist/spill-store.d.ts +1 -0
  133. package/dist/spill-store.js +17 -4
  134. package/dist/spill-store.test.js +31 -8
  135. package/dist/subprocess.d.ts +4 -0
  136. package/dist/subprocess.js +148 -162
  137. package/dist/subprocess.test.js +118 -8
  138. package/dist/terminal-preference.test.d.ts +1 -0
  139. package/dist/terminal-preference.test.js +62 -0
  140. package/dist/terminal-route.d.ts +10 -0
  141. package/dist/terminal-route.js +270 -0
  142. package/dist/terminal-route.test.d.ts +1 -0
  143. package/dist/terminal-route.test.js +265 -0
  144. package/dist/terminal-sessions.d.ts +69 -0
  145. package/dist/terminal-sessions.js +252 -0
  146. package/dist/terminal-shells.d.ts +22 -0
  147. package/dist/terminal-shells.js +99 -0
  148. package/dist/terminal-tab.test.d.ts +1 -0
  149. package/dist/terminal-tab.test.js +58 -0
  150. package/dist/terminal-targets.test.d.ts +1 -0
  151. package/dist/terminal-targets.test.js +43 -0
  152. package/dist/terminal-titles.test.d.ts +1 -0
  153. package/dist/terminal-titles.test.js +36 -0
  154. package/dist/test-support.d.ts +35 -5
  155. package/dist/test-support.js +91 -29
  156. package/dist/tool-defs.js +19 -4
  157. package/dist/tool-handlers.js +80 -28
  158. package/dist/tool-params.js +19 -5
  159. package/dist/volumes.test.js +1 -1
  160. package/dist/workspace-binding.d.ts +18 -9
  161. package/dist/workspace-binding.js +244 -77
  162. package/dist/workspace-binding.test.js +189 -25
  163. package/docs/architecture.md +62 -21
  164. package/docs/architecture.zh.md +57 -13
  165. package/docs/configuration.md +61 -20
  166. package/docs/configuration.zh.md +53 -19
  167. package/docs/development-prompt.md +72 -0
  168. package/docs/development-prompt.zh.md +69 -0
  169. package/docs/development.md +141 -21
  170. package/docs/development.zh.md +125 -17
  171. package/docs/install-dsh-and-dsh-podman.md +21 -8
  172. package/docs/install-dsh-and-dsh-podman.zh.md +18 -8
  173. package/docs/uninstall.md +83 -0
  174. package/docs/uninstall.zh.md +79 -0
  175. package/docs/update.md +120 -0
  176. package/docs/update.zh.md +114 -0
  177. package/docs/usage.md +108 -61
  178. package/docs/usage.zh.md +76 -38
  179. package/locale/en.json +6 -0
  180. package/locale/zh.json +6 -0
  181. package/package.json +58 -25
  182. package/quadlet/dsh-podman-orchestrator.container +46 -0
  183. package/quadlet/dsh.container +35 -0
  184. package/LICENSE.pkg +0 -17103
  185. package/dist/preferences.d.ts +0 -6
  186. package/dist/preferences.js +0 -27
@@ -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;
@@ -36,9 +36,7 @@ export interface CardState {
36
36
  orchestratorVersion: string;
37
37
  versionState: "ok" | "minor-mismatch" | "major-mismatch";
38
38
  defaultImage: string;
39
- defaultImageDraft: string;
40
- socketsRoot: string;
41
- socketsRootDraft: string;
39
+ containerEnv: Record<string, string>;
42
40
  projectsRoot: string;
43
41
  directoryPicker?: DirectoryPickerFace;
44
42
  workspaces: readonly WorkspaceView[];
@@ -53,10 +51,10 @@ export interface ContainerCardFace {
53
51
  containerCard: SnapshotStore<CardState>;
54
52
  };
55
53
  reload: () => void;
56
- remove: (workspace: string) => void;
54
+ remove: (workspace: string, container: string) => void;
57
55
  cleanCaches: (mode: string) => void;
58
56
  removeWorkspace: (workspace: string) => void;
59
- recreate: (workspace: string, image: string, env?: Record<string, string>) => void;
57
+ recreate: (workspace: string, container: string, image: string, env?: Record<string, string>) => void;
60
58
  createContainer: (workspace: WorkspaceView, config?: ContainerCreateConfig) => void;
61
59
  startContainer: (workspace: WorkspaceView, container: string, config?: ContainerCreateConfig) => void;
62
60
  addContainerMount: (workspace: string, container: string, mount: MountInput) => void;
@@ -75,14 +73,10 @@ export interface ContainerCardFace {
75
73
  createSecret: (name: string, length?: number, charset?: string) => void;
76
74
  removeSecret: (name: string) => void;
77
75
  setSecret: (name: string, value: string) => void;
78
- addContainerSecret: (workspace: string, envVar: string, secret: string) => void;
79
- removeContainerSecret: (workspace: string, envVar: string) => void;
80
- editDefaultImage: (text: string) => void;
81
- saveDefaultImage: () => void;
82
- discardDefaultImage: () => void;
83
- editSocketsRoot: (text: string) => void;
84
- saveSocketsRoot: () => void;
85
- 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;
86
80
  }
87
81
  export declare class ContainerCardController {
88
82
  private readonly scope;
@@ -90,10 +84,11 @@ export declare class ContainerCardController {
90
84
  private readonly drafts;
91
85
  private readonly client;
92
86
  private snapshot;
93
- private busy;
87
+ private pending;
88
+ private generation;
94
89
  private notice;
95
90
  private directoryPicker;
96
- constructor(scope: SettingsScope<ContainerSettings>, client?: CardClient);
91
+ constructor(scope: ConfigForm<ContainerSettings>, client?: CardClient);
97
92
  private draft;
98
93
  private project;
99
94
  private publish;
@@ -107,4 +102,4 @@ export declare class ContainerCardController {
107
102
  inject(): ContainerCardFace;
108
103
  private defaultImage;
109
104
  }
110
- export type { SettingsScopeSnapshot };
105
+ export type { ConfigFormSnapshot };
@@ -1,7 +1,7 @@
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 = {
@@ -27,7 +27,10 @@ export class ContainerCardController {
27
27
  drafts = new Map();
28
28
  client;
29
29
  snapshot = EMPTY_SNAPSHOT;
30
- 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;
31
34
  notice = "";
32
35
  directoryPicker;
33
36
  constructor(scope, client = createCardClient()) {
@@ -48,7 +51,7 @@ export class ContainerCardController {
48
51
  return {
49
52
  available: snapshot.status === "ready" && value !== undefined,
50
53
  writable: snapshot.writable,
51
- busy: this.busy,
54
+ busy: this.pending > 0,
52
55
  notice: this.notice,
53
56
  version: this.snapshot.version,
54
57
  commit: this.snapshot.commit,
@@ -56,9 +59,7 @@ export class ContainerCardController {
56
59
  orchestratorVersion: this.snapshot.orchestratorVersion,
57
60
  versionState: this.snapshot.versionState,
58
61
  defaultImage: value?.defaultImage ?? "",
59
- defaultImageDraft: this.draft("defaultImage", value?.defaultImage ?? ""),
60
- socketsRoot: value?.socketsRoot ?? "",
61
- socketsRootDraft: this.draft("socketsRoot", value?.socketsRoot ?? ""),
62
+ containerEnv: value?.containerEnv ?? {},
62
63
  projectsRoot: this.snapshot.projectsRoot,
63
64
  ...(this.directoryPicker === undefined
64
65
  ? {}
@@ -92,43 +93,62 @@ export class ContainerCardController {
92
93
  env: extra.env ?? {},
93
94
  container: extra.container ?? "",
94
95
  secret: extra.secret ?? "",
95
- secretEnv: extra.secretEnv ?? "",
96
+ secretEnvName: extra.secretEnvName ?? "",
96
97
  length: extra.length ?? 0,
97
98
  charset: extra.charset ?? "",
98
99
  packages: extra.packages ?? [],
99
- secretEnvMap: extra.secretEnvMap ?? {},
100
+ secretEnv: extra.secretEnv ?? {},
100
101
  cacheMode: extra.cacheMode ?? "",
101
102
  mount: extra.mount ?? null,
102
103
  });
103
104
  }
104
105
  async dispatch(request) {
105
- this.busy = true;
106
+ const generation = ++this.generation;
107
+ this.pending++;
106
108
  this.notice = "";
107
109
  this.publish();
108
110
  try {
109
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;
110
117
  this.notice = result.notice ?? "";
111
- this.snapshot = await this.client.snapshot();
118
+ this.snapshot = snapshot;
112
119
  }
113
120
  catch (error) {
114
- 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
+ }
115
124
  }
116
125
  finally {
117
- this.busy = false;
126
+ this.pending--;
118
127
  this.publish();
119
128
  }
120
129
  }
121
130
  async reload() {
131
+ const generation = ++this.generation;
132
+ this.pending++;
133
+ this.publish();
122
134
  try {
123
- this.snapshot = await this.client.snapshot();
135
+ const snapshot = await this.client.snapshot();
136
+ if (generation !== this.generation)
137
+ return;
138
+ this.snapshot = snapshot;
124
139
  // A refresh clears the previous action's notice, so reopening the
125
140
  // settings does not replay feedback from an earlier session.
126
141
  this.notice = "";
127
142
  }
128
143
  catch (error) {
129
- 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();
130
151
  }
131
- this.publish();
132
152
  }
133
153
  edit(field, text) {
134
154
  this.drafts.set(field, text);
@@ -150,27 +170,55 @@ export class ContainerCardController {
150
170
  reload: () => {
151
171
  void this.reload();
152
172
  },
153
- remove: (workspace) => this.command("remove", workspace, ""),
173
+ remove: (workspace, container) => this.command("remove", workspace, "", { container }),
154
174
  cleanCaches: (mode) => this.command("cache_clean", "", "", { cacheMode: mode }),
155
175
  removeWorkspace: (workspace) => this.command("workspace_remove", workspace, ""),
156
- 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
+ }),
157
180
  createContainer: (workspace, config) => this.command("create", workspace.workspaceSlug, config?.image ?? (workspace.imageId || this.defaultImage() || ""), {
158
181
  projectName: workspace.projectName,
159
- ...(config?.mounts && config.mounts.length > 0 ? { mounts: config.mounts } : {}),
160
- ...(config?.env && Object.keys(config.env).length > 0 ? { env: config.env } : {}),
161
- ...(config?.paths && config.paths.length > 0 ? { paths: config.paths } : {}),
162
- ...(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
+ : {}),
163
194
  }),
164
195
  startContainer: (workspace, container, config) => this.command("create", workspace.workspaceSlug, config?.image ?? (workspace.imageId || this.defaultImage() || ""), {
165
196
  container,
166
- ...(config?.mounts && config.mounts.length > 0 ? { mounts: config.mounts } : {}),
167
- ...(config?.env && Object.keys(config.env).length > 0 ? { env: config.env } : {}),
168
- ...(config?.paths && config.paths.length > 0 ? { paths: config.paths } : {}),
169
- ...(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,
170
221
  }),
171
- addContainerMount: (workspace, container, mount) => this.command("container_mount_add", workspace, "", { container, mount }),
172
- removeContainerMount: (workspace, container, mount) => this.command("container_mount_remove", workspace, "", { container, mount }),
173
- updateContainerMount: (workspace, container, mount) => this.command("container_mount_update", workspace, "", { container, mount }),
174
222
  setContainerPaths: (workspace, container, paths) => this.command("container_path_set", workspace, "", { container, paths }),
175
223
  createVolume: (name) => this.command("volume_create", name, ""),
176
224
  removeVolume: (name) => this.command("volume_remove", name, ""),
@@ -190,21 +238,19 @@ export class ContainerCardController {
190
238
  }),
191
239
  removeSecret: (name) => this.command("secret_remove", name, ""),
192
240
  setSecret: (name, value) => this.command("secret_set", name, "", { value }),
193
- addContainerSecret: (workspace, envVar, secret) => this.command("container_secret_add", workspace, "", {
194
- container: "default",
195
- secretEnv: envVar,
241
+ addContainerSecret: (workspace, container, envVar, secret) => this.command("container_secret_add", workspace, "", {
242
+ container,
243
+ secretEnvName: envVar,
196
244
  secret,
197
245
  }),
198
- removeContainerSecret: (workspace, envVar) => this.command("container_secret_remove", workspace, "", {
199
- container: "default",
200
- secretEnv: envVar,
246
+ removeContainerSecret: (workspace, container, envVar) => this.command("container_secret_remove", workspace, "", {
247
+ container,
248
+ secretEnvName: envVar,
201
249
  }),
202
- editDefaultImage: (text) => this.edit("defaultImage", text),
203
- saveDefaultImage: () => this.save("defaultImage"),
204
- discardDefaultImage: () => this.discard("defaultImage"),
205
- editSocketsRoot: (text) => this.edit("socketsRoot", text),
206
- saveSocketsRoot: () => this.save("socketsRoot"),
207
- discardSocketsRoot: () => this.discard("socketsRoot"),
250
+ saveContainerEnv: (env) => {
251
+ void this.scope.set("containerEnv", env);
252
+ },
253
+ syncDefaultEnv: (workspace) => this.command("default_env_sync", workspace, ""),
208
254
  };
209
255
  }
210
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") })] })] }) }));
@@ -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;