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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/README.md +14 -2
  2. package/README.zh.md +14 -2
  3. package/cordis.patch.yml +58 -0
  4. package/dist/approval-reasons.d.ts +3 -4
  5. package/dist/approval-reasons.js +40 -49
  6. package/dist/approval-reasons.test.js +43 -15
  7. package/dist/approval.js +27 -10
  8. package/dist/approval.test.js +165 -34
  9. package/dist/card-route.d.ts +3 -2
  10. package/dist/card-route.js +154 -42
  11. package/dist/card-route.test.js +85 -43
  12. package/dist/card-test-support.js +39 -66
  13. package/dist/client/ContainerCard.d.ts +1 -1
  14. package/dist/client/ContainerCard.js +92 -50
  15. package/dist/client/card-protocol.d.ts +6 -3
  16. package/dist/client/container-card-caches.js +11 -6
  17. package/dist/client/container-card-controller.d.ts +16 -18
  18. package/dist/client/container-card-controller.js +92 -40
  19. package/dist/client/container-card-create-modal.d.ts +2 -2
  20. package/dist/client/container-card-create-modal.js +13 -5
  21. package/dist/client/container-card-default-env.d.ts +10 -0
  22. package/dist/client/container-card-default-env.js +69 -0
  23. package/dist/client/container-card-directory.d.ts +2 -2
  24. package/dist/client/container-card-directory.js +12 -4
  25. package/dist/client/container-card-editors.d.ts +3 -3
  26. package/dist/client/container-card-editors.js +104 -32
  27. package/dist/client/container-card-images.d.ts +4 -4
  28. package/dist/client/container-card-images.js +19 -6
  29. package/dist/client/container-card-paths.d.ts +3 -3
  30. package/dist/client/container-card-paths.js +18 -11
  31. package/dist/client/container-card-row.d.ts +10 -6
  32. package/dist/client/container-card-row.js +82 -13
  33. package/dist/client/container-card-secrets.d.ts +3 -3
  34. package/dist/client/container-card-secrets.js +8 -6
  35. package/dist/client/container-card-shared.d.ts +4 -17
  36. package/dist/client/container-card-shared.js +9 -25
  37. package/dist/client/container-card-styles.d.ts +2 -8
  38. package/dist/client/container-card-styles.js +14 -52
  39. package/dist/client/container-card-volumes.d.ts +2 -2
  40. package/dist/client/container-card-volumes.js +9 -5
  41. package/dist/client/container-card-workspace.d.ts +8 -6
  42. package/dist/client/container-card-workspace.js +9 -3
  43. package/dist/client/directory-picker.js +2 -1
  44. package/dist/client/index.js +13141 -2036
  45. package/dist/client/locales.d.ts +6 -1
  46. package/dist/client/locales.js +106 -38
  47. package/dist/client/podman-terminal-guide.d.ts +11 -0
  48. package/dist/client/podman-terminal-guide.js +208 -0
  49. package/dist/client/podman-terminal-title.d.ts +7 -0
  50. package/dist/client/podman-terminal-title.js +30 -0
  51. package/dist/client/podman-terminal.d.ts +26 -0
  52. package/dist/client/podman-terminal.js +412 -0
  53. package/dist/client/read-only-approval.d.ts +2 -2
  54. package/dist/client/read-only-approval.js +13 -3
  55. package/dist/client/slot-contract.d.ts +0 -11
  56. package/dist/client/terminal-preference.d.ts +26 -0
  57. package/dist/client/terminal-preference.js +67 -0
  58. package/dist/client/terminal-protocol.d.ts +85 -0
  59. package/dist/client/terminal-protocol.js +14 -0
  60. package/dist/client/terminal-tab.d.ts +60 -0
  61. package/dist/client/terminal-tab.js +35 -0
  62. package/dist/client/terminal-targets.d.ts +33 -0
  63. package/dist/client/terminal-targets.js +65 -0
  64. package/dist/client/terminal-titles.d.ts +23 -0
  65. package/dist/client/terminal-titles.js +52 -0
  66. package/dist/client/terminal-transport.d.ts +63 -0
  67. package/dist/client/terminal-transport.js +207 -0
  68. package/dist/client/tool-views.js +215 -49
  69. package/dist/container-env.d.ts +11 -0
  70. package/dist/container-env.js +96 -0
  71. package/dist/container-env.test.d.ts +1 -0
  72. package/dist/container-env.test.js +93 -0
  73. package/dist/containers.test.js +196 -18
  74. package/dist/daemons.test.js +18 -4
  75. package/dist/dsh-version.d.ts +1 -0
  76. package/dist/dsh-version.js +46 -0
  77. package/dist/env-rows.d.ts +14 -0
  78. package/dist/env-rows.js +52 -0
  79. package/dist/env-rows.test.d.ts +1 -0
  80. package/dist/env-rows.test.js +43 -0
  81. package/dist/fs-provider.d.ts +1 -0
  82. package/dist/fs-provider.js +26 -12
  83. package/dist/fs-provider.test.js +51 -10
  84. package/dist/generated/version.d.ts +2 -2
  85. package/dist/generated/version.js +2 -2
  86. package/dist/grpc/proto/dshctl/v1/control.proto +11 -1
  87. package/dist/grpc/proto/dshguest/v1/guest.proto +5 -0
  88. package/dist/grpc/runtime-client.d.ts +3 -1
  89. package/dist/grpc/runtime-client.js +90 -17
  90. package/dist/guest-rpc.d.ts +15 -19
  91. package/dist/guest-rpc.js +289 -70
  92. package/dist/guest-rpc.test.d.ts +1 -0
  93. package/dist/guest-rpc.test.js +241 -0
  94. package/dist/guest-terminal.d.ts +30 -0
  95. package/dist/guest-terminal.js +196 -0
  96. package/dist/images.test.js +10 -3
  97. package/dist/index.d.ts +11 -16
  98. package/dist/index.js +42 -36
  99. package/dist/locales.test.d.ts +1 -0
  100. package/dist/locales.test.js +34 -0
  101. package/dist/misc.test.js +38 -10
  102. package/dist/mount-enums.js +2 -1
  103. package/dist/mount-input.d.ts +2 -0
  104. package/dist/mount-input.js +38 -22
  105. package/dist/mounts.test.js +81 -11
  106. package/dist/naming.test.d.ts +1 -0
  107. package/dist/naming.test.js +28 -0
  108. package/dist/output-reader.js +36 -4
  109. package/dist/package-deps.test.d.ts +1 -0
  110. package/dist/package-deps.test.js +49 -0
  111. package/dist/paths.test.js +4 -2
  112. package/dist/plugin-meta.test.d.ts +1 -0
  113. package/dist/plugin-meta.test.js +29 -0
  114. package/dist/project-path.js +33 -7
  115. package/dist/project-path.test.js +14 -0
  116. package/dist/prompts.d.ts +0 -4
  117. package/dist/prompts.js +15 -84
  118. package/dist/read-only-shell.js +3 -1
  119. package/dist/read-only-shell.test.js +80 -25
  120. package/dist/runtime-client.test.d.ts +1 -0
  121. package/dist/runtime-client.test.js +53 -0
  122. package/dist/secrets.test.js +21 -6
  123. package/dist/settings-commands.test.js +61 -14
  124. package/dist/settings-create.test.js +32 -10
  125. package/dist/settings-default-env.test.d.ts +1 -0
  126. package/dist/settings-default-env.test.js +175 -0
  127. package/dist/settings-mounts.test.js +46 -7
  128. package/dist/settings-schema.d.ts +6 -11
  129. package/dist/settings-schema.js +4 -8
  130. package/dist/settings-schema.test.d.ts +1 -0
  131. package/dist/settings-schema.test.js +35 -0
  132. package/dist/settings-workspaces.test.js +38 -63
  133. package/dist/spill-store.d.ts +1 -0
  134. package/dist/spill-store.js +17 -4
  135. package/dist/spill-store.test.js +31 -8
  136. package/dist/subprocess.d.ts +4 -0
  137. package/dist/subprocess.js +148 -162
  138. package/dist/subprocess.test.js +118 -8
  139. package/dist/terminal-preference.test.d.ts +1 -0
  140. package/dist/terminal-preference.test.js +62 -0
  141. package/dist/terminal-route.d.ts +10 -0
  142. package/dist/terminal-route.js +270 -0
  143. package/dist/terminal-route.test.d.ts +1 -0
  144. package/dist/terminal-route.test.js +265 -0
  145. package/dist/terminal-sessions.d.ts +69 -0
  146. package/dist/terminal-sessions.js +252 -0
  147. package/dist/terminal-shells.d.ts +22 -0
  148. package/dist/terminal-shells.js +99 -0
  149. package/dist/terminal-tab.test.d.ts +1 -0
  150. package/dist/terminal-tab.test.js +58 -0
  151. package/dist/terminal-targets.test.d.ts +1 -0
  152. package/dist/terminal-targets.test.js +43 -0
  153. package/dist/terminal-titles.test.d.ts +1 -0
  154. package/dist/terminal-titles.test.js +36 -0
  155. package/dist/test-support.d.ts +35 -5
  156. package/dist/test-support.js +91 -29
  157. package/dist/tool-defs.js +19 -4
  158. package/dist/tool-handlers.js +80 -28
  159. package/dist/tool-params.js +19 -5
  160. package/dist/version-compat.d.ts +4 -0
  161. package/dist/version-compat.js +29 -0
  162. package/dist/version-compat.test.d.ts +1 -0
  163. package/dist/version-compat.test.js +27 -0
  164. package/dist/volumes.test.js +1 -1
  165. package/dist/workspace-binding.d.ts +19 -10
  166. package/dist/workspace-binding.js +252 -78
  167. package/dist/workspace-binding.test.js +202 -27
  168. package/docs/architecture.md +71 -20
  169. package/docs/architecture.zh.md +62 -12
  170. package/docs/configuration.md +61 -20
  171. package/docs/configuration.zh.md +53 -19
  172. package/docs/development-prompt.md +72 -0
  173. package/docs/development-prompt.zh.md +69 -0
  174. package/docs/development.md +184 -29
  175. package/docs/development.zh.md +165 -26
  176. package/docs/install-dsh-and-dsh-podman.md +24 -21
  177. package/docs/install-dsh-and-dsh-podman.zh.md +20 -21
  178. package/docs/uninstall.md +83 -0
  179. package/docs/uninstall.zh.md +79 -0
  180. package/docs/update.md +120 -0
  181. package/docs/update.zh.md +114 -0
  182. package/docs/usage.md +108 -61
  183. package/docs/usage.zh.md +76 -38
  184. package/locale/en.json +6 -0
  185. package/locale/zh.json +6 -0
  186. package/package.json +58 -25
  187. package/quadlet/dsh-podman-orchestrator.container +46 -0
  188. package/quadlet/dsh.container +35 -0
  189. package/LICENSE.pkg +0 -17103
  190. package/dist/preferences.d.ts +0 -6
  191. package/dist/preferences.js +0 -27
@@ -0,0 +1,60 @@
1
+ /** Page kind `openTab` names; unique among the tab types in force. */
2
+ export declare const PODMAN_TERMINAL_KIND = "podman-terminal";
3
+ /** Implementation id the body, title and guide card register under. */
4
+ export declare const PODMAN_TERMINAL_TAB_ID = "@exagone313/dsh-podman";
5
+ declare module "@deepseek-ai/dsh-client-ui-sidebar-right/client" {
6
+ interface SidebarRightTabParamsMap {
7
+ "podman-terminal": PodmanTerminalParams;
8
+ }
9
+ }
10
+ /** Navigation parameters of one Podman terminal page. */
11
+ export interface PodmanTerminalParams {
12
+ /** `""`/`"default"` for the default container, or a named one. */
13
+ readonly container?: string;
14
+ /** Absolute shell path returned by the shells route. */
15
+ readonly shell?: string;
16
+ }
17
+ export declare const PODMAN_TERMINAL_SHORTCUT_DEFAULTS: {
18
+ "desktop:macos": {
19
+ code: string;
20
+ modifiers: "control"[];
21
+ };
22
+ "desktop:windows": {
23
+ code: string;
24
+ modifiers: "control"[];
25
+ };
26
+ "desktop:linux": {
27
+ code: string;
28
+ modifiers: "control"[];
29
+ };
30
+ "web:macos": {
31
+ code: string;
32
+ modifiers: "control"[];
33
+ };
34
+ "web:windows": {
35
+ code: string;
36
+ modifiers: "control"[];
37
+ };
38
+ };
39
+ export declare const PODMAN_TERMINAL_SHORTCUT_FALLBACK_DEFAULTS: {
40
+ "desktop:macos": {
41
+ code: string;
42
+ modifiers: ("control" | "shift")[];
43
+ };
44
+ "desktop:windows": {
45
+ code: string;
46
+ modifiers: ("control" | "shift")[];
47
+ };
48
+ "desktop:linux": {
49
+ code: string;
50
+ modifiers: ("control" | "shift")[];
51
+ };
52
+ "web:macos": {
53
+ code: string;
54
+ modifiers: ("meta" | "shift")[];
55
+ };
56
+ "web:windows": {
57
+ code: string;
58
+ modifiers: ("control" | "shift")[];
59
+ };
60
+ };
@@ -0,0 +1,35 @@
1
+ // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+ /** Page kind `openTab` names; unique among the tab types in force. */
5
+ export const PODMAN_TERMINAL_KIND = "podman-terminal";
6
+ /** Implementation id the body, title and guide card register under. */
7
+ export const PODMAN_TERMINAL_TAB_ID = "@exagone313/dsh-podman";
8
+ // Keyboard profiles for opening a Podman terminal. The shortcuts registry
9
+ // validates every declared default and THROWS on one it does not admit, inside
10
+ // apply(), which fails the whole client entry and the web boot — so these
11
+ // profiles are load-bearing, not cosmetic:
12
+ // - Linux Web admits only Ctrl+/, Ctrl+Shift+, and Ctrl+Shift+. (the browser and
13
+ // system reservation rules), so no `web:linux` default can exist.
14
+ // - macOS Web admits Ctrl+` and Meta+Shift+`, but not Ctrl+Shift+`; Windows Web
15
+ // admits both Ctrl+` and Ctrl+Shift+`.
16
+ // - Desktop accepts Ctrl+` on every platform.
17
+ //
18
+ // The preferred binding is the built-in terminal's own Ctrl+`; this plugin's
19
+ // bundle patch disables that client UI, which is what frees it.
20
+ export const PODMAN_TERMINAL_SHORTCUT_DEFAULTS = {
21
+ "desktop:macos": { code: "Backquote", modifiers: ["control"] },
22
+ "desktop:windows": { code: "Backquote", modifiers: ["control"] },
23
+ "desktop:linux": { code: "Backquote", modifiers: ["control"] },
24
+ "web:macos": { code: "Backquote", modifiers: ["control"] },
25
+ "web:windows": { code: "Backquote", modifiers: ["control"] },
26
+ };
27
+ // Registered when the preferred binding is refused — the built-in terminal UI
28
+ // is enabled again and still owns Ctrl+`, or another command claimed it.
29
+ export const PODMAN_TERMINAL_SHORTCUT_FALLBACK_DEFAULTS = {
30
+ "desktop:macos": { code: "Backquote", modifiers: ["control", "shift"] },
31
+ "desktop:windows": { code: "Backquote", modifiers: ["control", "shift"] },
32
+ "desktop:linux": { code: "Backquote", modifiers: ["control", "shift"] },
33
+ "web:macos": { code: "Backquote", modifiers: ["meta", "shift"] },
34
+ "web:windows": { code: "Backquote", modifiers: ["control", "shift"] },
35
+ };
@@ -0,0 +1,33 @@
1
+ import type { ContainerView } from "./card-protocol.js";
2
+ import type { TerminalShellView } from "./terminal-protocol.js";
3
+ /**
4
+ * The named containers selectable for one workspace, sorted.
5
+ *
6
+ * The empty value is not repeated here: the picker's own placeholder already
7
+ * means "the workspace's default container".
8
+ * @param containers - container rows from the card snapshot.
9
+ * @param workspaceSlug - the selected workspace's slug.
10
+ * @returns logical container names, excluding the default.
11
+ */
12
+ export declare function containerOptions(containers: readonly ContainerView[], workspaceSlug: string | undefined): string[];
13
+ /**
14
+ * The remembered container, but only while the workspace still offers it.
15
+ * @param remembered - the container name the card started last time.
16
+ * @param options - the workspace's selectable container names.
17
+ * @returns the remembered name, or `""` for the workspace's default container.
18
+ */
19
+ export declare function validContainer(remembered: string | undefined, options: readonly string[]): string;
20
+ /**
21
+ * The remembered shell, but only while the chosen container still offers it —
22
+ * the same container may have been recreated from another image.
23
+ * @param remembered - the absolute shell path the card started last time.
24
+ * @param shells - the shells the host discovered in the chosen container.
25
+ * @returns the remembered path, or `undefined` when it is gone.
26
+ */
27
+ export declare function validShell(remembered: string | undefined, shells: readonly TerminalShellView[]): string | undefined;
28
+ /**
29
+ * The program name of a shell path.
30
+ * @param path - an absolute shell path such as `/usr/bin/bash`, or absent.
31
+ * @returns the basename, or `undefined` when there is nothing to name.
32
+ */
33
+ export declare function shellName(path: string | undefined): string | undefined;
@@ -0,0 +1,65 @@
1
+ // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+ /**
5
+ * The named containers selectable for one workspace, sorted.
6
+ *
7
+ * The empty value is not repeated here: the picker's own placeholder already
8
+ * means "the workspace's default container".
9
+ * @param containers - container rows from the card snapshot.
10
+ * @param workspaceSlug - the selected workspace's slug.
11
+ * @returns logical container names, excluding the default.
12
+ */
13
+ export function containerOptions(containers, workspaceSlug) {
14
+ if (workspaceSlug === undefined || workspaceSlug === "")
15
+ return [];
16
+ const names = new Set();
17
+ for (const container of containers) {
18
+ if (container.workspaceSlug !== workspaceSlug)
19
+ continue;
20
+ if (container.containerName === "" || container.containerName === "default") {
21
+ continue;
22
+ }
23
+ // A podman name is an internal identifier the API rejects; never offer one.
24
+ if (container.containerName.startsWith("dsh-podman-"))
25
+ continue;
26
+ names.add(container.containerName);
27
+ }
28
+ return [...names].sort((left, right) => left.localeCompare(right));
29
+ }
30
+ /**
31
+ * The remembered container, but only while the workspace still offers it.
32
+ * @param remembered - the container name the card started last time.
33
+ * @param options - the workspace's selectable container names.
34
+ * @returns the remembered name, or `""` for the workspace's default container.
35
+ */
36
+ export function validContainer(remembered, options) {
37
+ if (remembered === undefined || remembered === "")
38
+ return "";
39
+ return options.includes(remembered) ? remembered : "";
40
+ }
41
+ /**
42
+ * The remembered shell, but only while the chosen container still offers it —
43
+ * the same container may have been recreated from another image.
44
+ * @param remembered - the absolute shell path the card started last time.
45
+ * @param shells - the shells the host discovered in the chosen container.
46
+ * @returns the remembered path, or `undefined` when it is gone.
47
+ */
48
+ export function validShell(remembered, shells) {
49
+ if (remembered === undefined || remembered === "")
50
+ return undefined;
51
+ return shells.some((shell) => shell.path === remembered)
52
+ ? remembered
53
+ : undefined;
54
+ }
55
+ /**
56
+ * The program name of a shell path.
57
+ * @param path - an absolute shell path such as `/usr/bin/bash`, or absent.
58
+ * @returns the basename, or `undefined` when there is nothing to name.
59
+ */
60
+ export function shellName(path) {
61
+ if (path === undefined || path === "")
62
+ return undefined;
63
+ const name = path.slice(path.lastIndexOf("/") + 1);
64
+ return name === "" ? undefined : name;
65
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Publish the name the host reported for one terminal.
3
+ * @param key - `${sessionId}:${tabId}`, the same composite the host retains by.
4
+ * @param title - the running shell's name.
5
+ */
6
+ export declare function publishTerminalTitle(key: string, title: string): void;
7
+ /**
8
+ * Drop one terminal's title when its body goes away.
9
+ * @param key - the same composite {@link publishTerminalTitle} used.
10
+ */
11
+ export declare function forgetTerminalTitle(key: string): void;
12
+ /**
13
+ * Read the published title.
14
+ * @param key - the same composite {@link publishTerminalTitle} used.
15
+ * @returns the name, or `undefined` while nothing has been published.
16
+ */
17
+ export declare function terminalTitle(key: string): string | undefined;
18
+ /**
19
+ * Subscribe to title changes.
20
+ * @param listener - called after every publish or forget.
21
+ * @returns the unsubscriber.
22
+ */
23
+ export declare function subscribeTerminalTitles(listener: () => void): () => void;
@@ -0,0 +1,52 @@
1
+ // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+ // Live tab titles for the Podman terminal, the browser-side peer of the built-in
5
+ // terminal's live name: the host names the running shell in the open stream's
6
+ // `ready` (and `title`) frames, the tab body publishes it here, and the tab chip
7
+ // renders it. The store is module-level and DOM-free so it can be unit-tested
8
+ // from the host program.
9
+ const titles = new Map();
10
+ const listeners = new Set();
11
+ function emit() {
12
+ for (const listener of listeners)
13
+ listener();
14
+ }
15
+ /**
16
+ * Publish the name the host reported for one terminal.
17
+ * @param key - `${sessionId}:${tabId}`, the same composite the host retains by.
18
+ * @param title - the running shell's name.
19
+ */
20
+ export function publishTerminalTitle(key, title) {
21
+ if (title === "" || titles.get(key) === title)
22
+ return;
23
+ titles.set(key, title);
24
+ emit();
25
+ }
26
+ /**
27
+ * Drop one terminal's title when its body goes away.
28
+ * @param key - the same composite {@link publishTerminalTitle} used.
29
+ */
30
+ export function forgetTerminalTitle(key) {
31
+ if (titles.delete(key))
32
+ emit();
33
+ }
34
+ /**
35
+ * Read the published title.
36
+ * @param key - the same composite {@link publishTerminalTitle} used.
37
+ * @returns the name, or `undefined` while nothing has been published.
38
+ */
39
+ export function terminalTitle(key) {
40
+ return titles.get(key);
41
+ }
42
+ /**
43
+ * Subscribe to title changes.
44
+ * @param listener - called after every publish or forget.
45
+ * @returns the unsubscriber.
46
+ */
47
+ export function subscribeTerminalTitles(listener) {
48
+ listeners.add(listener);
49
+ return () => {
50
+ listeners.delete(listener);
51
+ };
52
+ }
@@ -0,0 +1,63 @@
1
+ import { type TerminalControl, type TerminalFrame, type TerminalOpenQuery, type TerminalRetainedView, type TerminalShellView, type TerminalTargetView } from "./terminal-protocol.js";
2
+ export type TerminalStreamQuery = TerminalOpenQuery & {
3
+ readonly reopen?: boolean;
4
+ };
5
+ /**
6
+ * Open or reattach one terminal and deliver every frame to `onFrame`.
7
+ *
8
+ * The route answers one JSON frame per line (`application/x-ndjson`), so the
9
+ * body is decoded incrementally and split on newlines; a partial trailing line
10
+ * is flushed once the stream ends. Aborting `signal` rejects the returned
11
+ * promise with an `AbortError`, which callers treat as a closed stream rather
12
+ * than a failure.
13
+ * @param query - session, tab, target shell and initial geometry.
14
+ * @param onFrame - per-frame callback, in arrival order.
15
+ * @param signal - lifetime of this attachment.
16
+ */
17
+ export declare function openTerminalStream(query: TerminalStreamQuery, onFrame: (frame: TerminalFrame) => void, signal: AbortSignal): Promise<void>;
18
+ /**
19
+ * Send one control request (input, resize, rename or close).
20
+ * @param control - the control to apply.
21
+ * @throws when the host refuses it: an unknown terminal, or a malformed body.
22
+ */
23
+ export declare function sendTerminalControl(control: TerminalControl): Promise<void>;
24
+ /**
25
+ * List the shells a container really provides, `sh`/`bash` first.
26
+ * @param workspace - the workspace project name, or `""` for the session cwd.
27
+ * @param container - `""`/`"default"` for the default container, or a named one.
28
+ * @param sessionId - owning session, used by the host when `workspace` is empty.
29
+ * @param signal - cancellation of the probe.
30
+ * @returns the verified shells, in host order.
31
+ */
32
+ export declare function fetchTerminalShells(workspace: string, container: string, sessionId: string, signal?: AbortSignal): Promise<TerminalShellView[]>;
33
+ /**
34
+ * The workspace the host resolved for one session.
35
+ *
36
+ * A Session belongs to exactly one workspace, whose side panes — and terminals —
37
+ * are its own, so the browser never asks for a workspace: the host answers from
38
+ * the session itself and reports when it cannot place it.
39
+ * @param sessionId - owning session.
40
+ * @param signal - cancellation of the read.
41
+ * @returns the resolved workspace and its harness workspace slug.
42
+ * @throws when the host cannot determine the session's workspace.
43
+ */
44
+ export declare function fetchTerminalTarget(sessionId: string, signal?: AbortSignal): Promise<TerminalTargetView>;
45
+ /**
46
+ * List the terminals this session still has retained on the host.
47
+ * @param sessionId - owning session.
48
+ * @returns the retained terminals, oldest first.
49
+ */
50
+ export declare function fetchRetainedTerminals(sessionId: string): Promise<TerminalRetainedView[]>;
51
+ /**
52
+ * Encode bytes for the wire. Control frames carry binary keyboard input, so the
53
+ * conversion is byte-exact and never goes through a UTF-8 string round trip.
54
+ * @param bytes - raw bytes.
55
+ * @returns standard base64.
56
+ */
57
+ export declare function bytesToBase64(bytes: Uint8Array): string;
58
+ /**
59
+ * Decode wire bytes into a `Uint8Array` xterm can write unchanged.
60
+ * @param data - standard base64.
61
+ * @returns the decoded bytes.
62
+ */
63
+ export declare function base64ToBytes(data: string): Uint8Array;
@@ -0,0 +1,207 @@
1
+ // SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+ // The terminal tab's browser half of the host routes. Everything travels over
5
+ // same-origin `fetch` on the harness API channel, which carries the browser
6
+ // session the harness issued for it; no service is injected here so the module
7
+ // stays testable and usable from any component.
8
+ import { TERMINAL_PATH, TERMINAL_RETAINED_PATH, TERMINAL_SHELLS_PATH, TERMINAL_TARGET_PATH, } from "./terminal-protocol.js";
9
+ async function failure(response) {
10
+ try {
11
+ const body = (await response.json());
12
+ if (typeof body.error === "string" && body.error !== "") {
13
+ return new Error(body.error);
14
+ }
15
+ }
16
+ catch {
17
+ // A non-JSON body falls through to the status text.
18
+ }
19
+ return new Error(`terminal request failed (${response.status})`);
20
+ }
21
+ function endpoint(path) {
22
+ return new URL(path, globalThis.location.origin);
23
+ }
24
+ function trimmed(value) {
25
+ return typeof value === "string" ? value : "";
26
+ }
27
+ /**
28
+ * Open or reattach one terminal and deliver every frame to `onFrame`.
29
+ *
30
+ * The route answers one JSON frame per line (`application/x-ndjson`), so the
31
+ * body is decoded incrementally and split on newlines; a partial trailing line
32
+ * is flushed once the stream ends. Aborting `signal` rejects the returned
33
+ * promise with an `AbortError`, which callers treat as a closed stream rather
34
+ * than a failure.
35
+ * @param query - session, tab, target shell and initial geometry.
36
+ * @param onFrame - per-frame callback, in arrival order.
37
+ * @param signal - lifetime of this attachment.
38
+ */
39
+ export async function openTerminalStream(query, onFrame, signal) {
40
+ const url = endpoint(TERMINAL_PATH);
41
+ url.searchParams.set("sessionId", query.sessionId);
42
+ url.searchParams.set("tabId", query.tabId);
43
+ url.searchParams.set("workspace", query.workspace);
44
+ url.searchParams.set("container", query.container);
45
+ url.searchParams.set("shell", query.shell);
46
+ url.searchParams.set("cols", String(query.cols));
47
+ url.searchParams.set("rows", String(query.rows));
48
+ if (query.reopen === true)
49
+ url.searchParams.set("reopen", "1");
50
+ const response = await fetch(url, {
51
+ headers: { accept: "application/x-ndjson" },
52
+ signal,
53
+ });
54
+ if (!response.ok)
55
+ throw await failure(response);
56
+ if (response.body === null)
57
+ throw new Error("terminal stream has no body");
58
+ const reader = response.body.getReader();
59
+ const decoder = new TextDecoder();
60
+ let buffered = "";
61
+ const emit = (line) => {
62
+ const text = line.trim();
63
+ if (text === "")
64
+ return;
65
+ onFrame(JSON.parse(text));
66
+ };
67
+ for (;;) {
68
+ const { done, value } = await reader.read();
69
+ if (done)
70
+ break;
71
+ buffered += decoder.decode(value, { stream: true });
72
+ let newline = buffered.indexOf("\n");
73
+ while (newline !== -1) {
74
+ emit(buffered.slice(0, newline));
75
+ buffered = buffered.slice(newline + 1);
76
+ newline = buffered.indexOf("\n");
77
+ }
78
+ }
79
+ // A multi-byte character split across the last chunk survives this flush.
80
+ buffered += decoder.decode();
81
+ if (buffered !== "")
82
+ emit(buffered);
83
+ }
84
+ /**
85
+ * Send one control request (input, resize, rename or close).
86
+ * @param control - the control to apply.
87
+ * @throws when the host refuses it: an unknown terminal, or a malformed body.
88
+ */
89
+ export async function sendTerminalControl(control) {
90
+ const response = await fetch(endpoint(TERMINAL_PATH), {
91
+ method: "POST",
92
+ headers: {
93
+ accept: "application/json",
94
+ "content-type": "application/json",
95
+ },
96
+ body: JSON.stringify(control),
97
+ });
98
+ if (!response.ok)
99
+ throw await failure(response);
100
+ }
101
+ /**
102
+ * List the shells a container really provides, `sh`/`bash` first.
103
+ * @param workspace - the workspace project name, or `""` for the session cwd.
104
+ * @param container - `""`/`"default"` for the default container, or a named one.
105
+ * @param sessionId - owning session, used by the host when `workspace` is empty.
106
+ * @param signal - cancellation of the probe.
107
+ * @returns the verified shells, in host order.
108
+ */
109
+ export async function fetchTerminalShells(workspace, container, sessionId, signal) {
110
+ const url = endpoint(TERMINAL_SHELLS_PATH);
111
+ url.searchParams.set("workspace", workspace);
112
+ url.searchParams.set("container", container);
113
+ url.searchParams.set("sessionId", sessionId);
114
+ const response = await fetch(url, {
115
+ headers: { accept: "application/json" },
116
+ signal,
117
+ });
118
+ if (!response.ok)
119
+ throw await failure(response);
120
+ const body = (await response.json());
121
+ if (!Array.isArray(body.shells))
122
+ return [];
123
+ return body.shells.map((shell) => ({
124
+ name: trimmed(shell.name),
125
+ path: trimmed(shell.path),
126
+ }));
127
+ }
128
+ /**
129
+ * The workspace the host resolved for one session.
130
+ *
131
+ * A Session belongs to exactly one workspace, whose side panes — and terminals —
132
+ * are its own, so the browser never asks for a workspace: the host answers from
133
+ * the session itself and reports when it cannot place it.
134
+ * @param sessionId - owning session.
135
+ * @param signal - cancellation of the read.
136
+ * @returns the resolved workspace and its harness workspace slug.
137
+ * @throws when the host cannot determine the session's workspace.
138
+ */
139
+ export async function fetchTerminalTarget(sessionId, signal) {
140
+ const url = endpoint(TERMINAL_TARGET_PATH);
141
+ url.searchParams.set("sessionId", sessionId);
142
+ const response = await fetch(url, {
143
+ headers: { accept: "application/json" },
144
+ signal,
145
+ });
146
+ if (!response.ok)
147
+ throw await failure(response);
148
+ const body = (await response.json());
149
+ return {
150
+ workspace: trimmed(body.workspace),
151
+ workspaceSlug: trimmed(body.workspaceSlug),
152
+ };
153
+ }
154
+ /**
155
+ * List the terminals this session still has retained on the host.
156
+ * @param sessionId - owning session.
157
+ * @returns the retained terminals, oldest first.
158
+ */
159
+ export async function fetchRetainedTerminals(sessionId) {
160
+ const url = endpoint(TERMINAL_RETAINED_PATH);
161
+ url.searchParams.set("sessionId", sessionId);
162
+ const response = await fetch(url, {
163
+ headers: { accept: "application/json" },
164
+ });
165
+ if (!response.ok)
166
+ throw await failure(response);
167
+ const body = (await response.json());
168
+ if (!Array.isArray(body.terminals))
169
+ return [];
170
+ return body.terminals.map((terminal) => {
171
+ const view = terminal;
172
+ return {
173
+ tabId: trimmed(view.tabId),
174
+ terminalId: trimmed(view.terminalId),
175
+ title: trimmed(view.title),
176
+ workspace: trimmed(view.workspace),
177
+ container: trimmed(view.container),
178
+ shell: trimmed(view.shell),
179
+ };
180
+ });
181
+ }
182
+ /**
183
+ * Encode bytes for the wire. Control frames carry binary keyboard input, so the
184
+ * conversion is byte-exact and never goes through a UTF-8 string round trip.
185
+ * @param bytes - raw bytes.
186
+ * @returns standard base64.
187
+ */
188
+ export function bytesToBase64(bytes) {
189
+ let binary = "";
190
+ for (let index = 0; index < bytes.length; index++) {
191
+ binary += String.fromCharCode(bytes[index]);
192
+ }
193
+ return btoa(binary);
194
+ }
195
+ /**
196
+ * Decode wire bytes into a `Uint8Array` xterm can write unchanged.
197
+ * @param data - standard base64.
198
+ * @returns the decoded bytes.
199
+ */
200
+ export function base64ToBytes(data) {
201
+ const binary = atob(data);
202
+ const bytes = new Uint8Array(binary.length);
203
+ for (let index = 0; index < binary.length; index++) {
204
+ bytes[index] = binary.charCodeAt(index);
205
+ }
206
+ return bytes;
207
+ }