@deepseek-ai/dsh-api-terminal-controller 0.1.6-alpha.1 → 0.1.7-alpha.1
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.
- package/README.i18n.yaml +2 -2
- package/README.md +14 -5
- package/README.zh.md +14 -5
- package/lib/client.js +249 -19
- package/lib/index.js +263 -61
- package/lib/typert.host.js +215 -100
- package/lib/typert.remote-client.d.ts +7 -4
- package/lib/typert.remote-client.js +156 -80
- package/lib/types/client/bindings.d.ts +31 -0
- package/lib/types/client/bindings.js +71 -0
- package/lib/types/client/index.d.ts +22 -4
- package/lib/types/client/index.js +83 -8
- package/lib/types/client/model.d.ts +3 -1
- package/lib/types/client/model.js +20 -7
- package/lib/types/client/retention.d.ts +35 -0
- package/lib/types/client/retention.js +77 -0
- package/lib/types/index.d.ts +17 -3
- package/lib/types/index.js +49 -43
- package/lib/types/retention.d.ts +53 -0
- package/lib/types/retention.js +153 -0
- package/lib/types/terminal.d.ts +23 -1
- package/lib/types/terminal.js +40 -9
- package/lib/types/types.d.ts +6 -0
- package/package.json +21 -19
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/api/terminal-controller/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: dd93237a94f0cc9bdbd6e6d51f9a2762898399a7
|
|
6
|
+
README.zh.md: b3bda5f4040ab6f9bb9cd74a8fd32f48dbdb1362
|
package/README.md
CHANGED
|
@@ -25,9 +25,9 @@ Open the execution environment's default shell in a Session workspace from the W
|
|
|
25
25
|
<a id="use-this-package"></a>
|
|
26
26
|
## Use this package
|
|
27
27
|
|
|
28
|
-
The Web bundle mounts this package with the subprocess provider, sandbox policy and Typert Gateway. `remote.terminal` exposes `environment`, `shells`, `list`, `create`, `follow`, `write`, `resize`, `rename` and `close`; each operation is scoped by Session identity. Listing reads retained Host terminals directly, so viewing an offline Session neither activates an Agent nor produces a recovery error.
|
|
28
|
+
The Web bundle mounts this package with the subprocess provider, sandbox policy and Typert Gateway. Sandbox policy supplies only the fallback working directory for Sessions without a cwd. `remote.terminal` exposes `environment`, `shells`, `list`, `create`, `retain`, `follow`, `write`, `resize`, `rename` and `close`; each operation is scoped by Session identity. Listing reads retained Host terminals directly, so viewing an offline Session neither activates an Agent nor produces a recovery error.
|
|
29
29
|
|
|
30
|
-
Shell discovery lists the execution environment's declared default shell first. Only when the provider omits that default does resolution use `/bin/sh` on POSIX or `cmd.exe` on Windows. An optional `shell` profile overrides that choice with executable `path`, display `name` and `args` (default `[]`). The selector also probes `shellCandidates` through the execution provider and omits only confirmed lookup misses. Creation accepts a discovered `shellPath` and verifies it again; resolution or transport failure is reported without launching a different shell. Environment lookup returns the working directory and limits without resolving a shell, so an unavailable default does not prevent reattaching to an existing process. Automatic POSIX profiles start interactively, and PowerShell uses `-NoLogo`, so completion and startup configuration remain shell-owned. The Session workspace supplies the initial directory
|
|
30
|
+
Shell discovery lists the execution environment's declared default shell first. Only when the provider omits that default does resolution use `/bin/sh` on POSIX or `cmd.exe` on Windows. An optional `shell` profile overrides that choice with executable `path`, display `name` and `args` (default `[]`). The selector also probes `shellCandidates` through the execution provider and omits only confirmed lookup misses. Creation accepts a discovered `shellPath` and verifies it again; resolution or transport failure is reported without launching a different shell. Environment lookup returns the working directory and limits without resolving a shell, so an unavailable default does not prevent reattaching to an existing process. Automatic POSIX profiles start interactively, and PowerShell uses `-NoLogo`, so completion and startup configuration remain shell-owned. The Session workspace supplies the initial directory. User terminals run with the execution environment’s system-user permissions, independently of the Agent’s sandbox mode and approval policy. Operating-system and container restrictions still apply; DSH does not elevate the user. The subprocess provider retains its credential-environment scrubbing.
|
|
31
31
|
|
|
32
32
|
| Configuration | Default | Meaning |
|
|
33
33
|
|---|---|---|
|
|
@@ -39,6 +39,11 @@ Shell discovery lists the execution environment's declared default shell first.
|
|
|
39
39
|
| `maxBufferedBytes` | `2097152` | Output queued for one follower |
|
|
40
40
|
| `maxInputBytes` | `65536` | Maximum input request bytes |
|
|
41
41
|
| `disposeGraceMs` | `1000` | Provider termination grace in milliseconds |
|
|
42
|
+
| `unattendedTimeoutMs` | `7200000` | Continuous confirmed idle time without window holds before cleanup; `0` disables automatic reclamation |
|
|
43
|
+
| `activityPollIntervalMs` | `30000` | Interval between unattended activity observations |
|
|
44
|
+
| `cleanupRetryMs` | `60000` | Delay before retrying failed cleanup |
|
|
45
|
+
|
|
46
|
+
An open tab in any connected window retains its terminal, including hidden tabs and inactive Sessions. After the last hold disappears, only positively confirmed idle time counts toward reclamation. Running, stopped, input-waiting and background work remains protected; unknown activity clears the idle deadline. Completion starts a fresh full grace period. The Host uses a monotonic clock, rechecks current ownership and activity before cleanup, and resets evidence after an observation gap exceeding twice the polling interval. The timing fields use milliseconds and safe integers; polling and retry intervals must be positive.
|
|
42
47
|
|
|
43
48
|
<a id="understand-the-implementation"></a>
|
|
44
49
|
## Understand the implementation
|
|
@@ -48,13 +53,15 @@ Shell discovery lists the execution environment's declared default shell first.
|
|
|
48
53
|
|
|
49
54
|
The Host uses `ctx.subprocess.spawnTerminal` with `TERM=xterm-256color`; it never launches a desktop terminal application. Streaming UTF-8 decoding preserves split characters and leading BOMs, and replaces incomplete trailing bytes at EOF. Unary control uses the Gateway, and `follow` uses its multiplexed Remote stream transport. Headless xterm and its serializer produce each opening screen after all preceding output writes, then monotone output sequences identify subsequent frames. Slow followers fail explicitly; a new attachment restores the current screen.
|
|
50
55
|
|
|
51
|
-
The latest attachment owns input and resize. Detachment releases input control without killing the process. Explicit close awaits process cleanup and final output; cleanup failure retains the resource for retry. The Session remembers closed identities and rejects their delayed or repeated creation, including creation already in progress when close arrives. A new terminal uses a new identity. Pending allocations remain owned even if cancellation and cleanup both fail. Session owner disposal and controller disposal also terminate owned processes.
|
|
56
|
+
The latest attachment owns input and resize. Detachment releases input control without killing the process. Explicit close awaits process cleanup and final output; cleanup failure retains the resource for retry. The Session remembers closed identities and rejects their delayed or repeated creation, including creation already in progress when close arrives. A new terminal uses a new identity. Pending allocations remain owned even if cancellation and cleanup both fail. Session owner disposal and controller disposal also terminate owned processes. Changing the Session’s sandbox mode leaves user terminals running with the same permissions. Input or resize refused after control transfer or process exit leaves the output attachment intact and disables input; rejected input is not replayed.
|
|
57
|
+
|
|
58
|
+
The Client saves each Session/content-to-terminal association before allocation under its own `dsh.terminal.binding.v1.*` localStorage key. The content identity is globally unique; layout-local tab ids only identify live view occurrences. Independent record writes and deletes preserve other windows' bindings. Restored views reuse that identity; the sidebar terminal provider restores its views before querying unrepresented Host terminals. A new view may create a process, while a recovered view reports a missing target without creating a replacement. Explicit close removes the association after saving its cleanup request. The Host supplies current process metadata and screen contents; neither is saved in the browser. The Client model acknowledges screen writes after the browser emulator processes them, serializes input and ignores stale attachment responses. Client-owned errors carry locale keys. Plugin disposal awaits active and previously detached output streams without closing Host processes.
|
|
52
59
|
|
|
53
|
-
|
|
60
|
+
The separate `retain(sessionId, id, signal)` Remote stream acknowledges a window hold without Agent activation, screen output, input transfer or process creation. The terminal provider supplies the sidebar's complete open-tab inventory, and the Client intersects it with its own saved identities. Duplicate occurrences share one hold per window; stale associations alone retain nothing. Restored output attachments wait for an acknowledged current hold. Transport cancellation releases exactly that physical generation; plugin disposal joins all hold streams. Failed cleanup retains ownership and retries without reopening admission or restarting the idle grace.
|
|
54
61
|
|
|
55
62
|
New views start automatically, using an explicit guide selection or the remembered available shell. The last selected shell path is stored under `dsh.terminal.shell` in origin-scoped localStorage. Default launches verify the saved path through Host discovery and fall back to the current default when it is absent. The guide records a selection before opening its tab; each new tab retains its own chosen path and allocation identity. Storage failures do not prevent startup. Restoring existing terminals neither reads this preference nor discovers shells.
|
|
56
63
|
|
|
57
|
-
Closing saves an unfinished cleanup request before releasing the tab, then awaits Host cleanup in the background. A failure exposes a retry notification. Each request has its own terminal-ID localStorage key and is removed after successful cleanup or a definitive `session/not-found` response; startup retries saved requests. Transport failures retain the request.
|
|
64
|
+
Closing saves an unfinished cleanup request before releasing the tab, then awaits Host cleanup in the background. A failure exposes a retry notification. Each request has its own terminal-ID localStorage key and is removed after successful cleanup or a definitive `session/not-found` response; startup retries saved requests. Transport failures retain the request. Cleanup requests persist independently of tab associations and sidebar layout. If browser storage is unavailable, cleanup remains usable in memory but cannot be recovered after reload.
|
|
58
65
|
|
|
59
66
|
</details>
|
|
60
67
|
|
|
@@ -63,6 +70,7 @@ Closing saves an unfinished cleanup request before releasing the tab, then await
|
|
|
63
70
|
|
|
64
71
|
- [Subprocess](../../subprocess/subprocess/README.md)
|
|
65
72
|
- [Right Sidebar](../../client/ui-sidebar-right/README.md)
|
|
73
|
+
- [User-terminal permissions](../../../.agents/notes/implemented/architecture/2026-09-16-user-terminal-permissions.md)
|
|
66
74
|
- [Web terminal decision](../../../.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.md)
|
|
67
75
|
|
|
68
76
|
<a id="model-experience"></a>
|
|
@@ -80,6 +88,7 @@ None; terminal output travels only between the browser and Host.
|
|
|
80
88
|
|
|
81
89
|
- Processes and screens survive browser reloads, but not Host or Session owner disposal. There is no durable terminal restoration or automatic shell respawn. Exited terminals count toward `maxTerminals`; close unused tabs to release their screens and quota.
|
|
82
90
|
- The subprocess provider determines native PTY availability and process-tree cleanup guarantees. Finding an executable does not prove PTY allocation will succeed.
|
|
91
|
+
- Automatic reclamation depends on the provider's [supported shell activity observation](../../subprocess/subprocess-local/README.md#running-terminal-sessions). Unsupported shells, custom launch arguments and uncertain process observations may retain resources until explicit close or owner disposal. No maximum runtime kills busy commands.
|
|
83
92
|
- Screen recovery retains bounded history, not a complete transcript. Only one attachment at a time can write or resize.
|
|
84
93
|
|
|
85
94
|
<a id="dev-note"></a>
|
package/README.zh.md
CHANGED
|
@@ -25,9 +25,9 @@ kind: "package-reference"
|
|
|
25
25
|
<a id="use-this-package"></a>
|
|
26
26
|
## 使用此包
|
|
27
27
|
|
|
28
|
-
Web bundle 将此包与 subprocess provider、sandbox policy 和 Typert Gateway
|
|
28
|
+
Web bundle 将此包与 subprocess provider、sandbox policy 和 Typert Gateway 一起挂载。Sandbox policy 仅为没有 cwd 的 Session 提供默认工作目录。`remote.terminal` 提供 `environment`、`shells`、`list`、`create`、`retain`、`follow`、`write`、`resize`、`rename` 和 `close`;每个操作均按 Session 标识限定范围。列表直接读取 Host 保留的终端,因此查看离线 Session 不会激活 Agent,也不会产生恢复错误。
|
|
29
29
|
|
|
30
|
-
Shell 探测结果首先列出执行环境声明的默认 shell。仅当 provider 未声明默认值时,才在 POSIX 使用 `/bin/sh`,在 Windows 使用 `cmd.exe`。可选的 `shell` profile 通过可执行路径 `path`、显示名称 `name` 和参数 `args`(默认 `[]`)覆盖这一选择。选择器还会通过执行 provider 探测 `shellCandidates`,仅省略确定未找到的候选。创建请求接受探测返回的 `shellPath` 并再次验证;解析或传输失败会直接报告,不启动其他 shell。环境查询只返回工作目录和限制,不解析 shell,因此默认 shell 不可用时仍可重新连接已有进程。POSIX 自动 profile 以交互模式启动,PowerShell 使用 `-NoLogo`,补全和启动配置仍由 shell 提供。初始目录来自 Session
|
|
30
|
+
Shell 探测结果首先列出执行环境声明的默认 shell。仅当 provider 未声明默认值时,才在 POSIX 使用 `/bin/sh`,在 Windows 使用 `cmd.exe`。可选的 `shell` profile 通过可执行路径 `path`、显示名称 `name` 和参数 `args`(默认 `[]`)覆盖这一选择。选择器还会通过执行 provider 探测 `shellCandidates`,仅省略确定未找到的候选。创建请求接受探测返回的 `shellPath` 并再次验证;解析或传输失败会直接报告,不启动其他 shell。环境查询只返回工作目录和限制,不解析 shell,因此默认 shell 不可用时仍可重新连接已有进程。POSIX 自动 profile 以交互模式启动,PowerShell 使用 `-NoLogo`,补全和启动配置仍由 shell 提供。初始目录来自 Session 工作区。用户终端使用执行环境中系统用户的权限,独立于 Agent 的沙箱模式和审批策略。操作系统和容器的限制仍然生效;DSH 不提升用户权限。Subprocess provider 继续清除环境中的凭据变量。
|
|
31
31
|
|
|
32
32
|
| 配置 | 默认值 | 含义 |
|
|
33
33
|
|---|---|---|
|
|
@@ -39,6 +39,11 @@ Shell 探测结果首先列出执行环境声明的默认 shell。仅当 provide
|
|
|
39
39
|
| `maxBufferedBytes` | `2097152` | 单个订阅者的输出排队上限 |
|
|
40
40
|
| `maxInputBytes` | `65536` | 单次输入请求的字节上限 |
|
|
41
41
|
| `disposeGraceMs` | `1000` | provider 终止宽限期,单位毫秒 |
|
|
42
|
+
| `unattendedTimeoutMs` | `7200000` | 无窗口持有且持续确认空闲后开始清理的时长;`0` 禁用自动回收 |
|
|
43
|
+
| `activityPollIntervalMs` | `30000` | 无窗口持有时的活动观察间隔 |
|
|
44
|
+
| `cleanupRetryMs` | `60000` | 清理失败后的重试间隔 |
|
|
45
|
+
|
|
46
|
+
任何已连接窗口中的打开标签页都会持有其终端,包括隐藏标签和非当前 Session。最后一个持有关系消失后,只有明确确认空闲的时间才计入回收期限。运行中、停止、等待输入和后台执行的任务都会受到保护;活动状态未知时清除空闲截止时间。任务完成后重新给予完整宽限期。Host 使用单调时钟,在清理前重新检查持有关系和当前活动;观察间隔超过轮询周期的两倍时丢弃旧证据。时间字段使用毫秒和安全整数,轮询及重试间隔必须为正数。
|
|
42
47
|
|
|
43
48
|
<a id="understand-the-implementation"></a>
|
|
44
49
|
## 了解实现
|
|
@@ -48,13 +53,15 @@ Shell 探测结果首先列出执行环境声明的默认 shell。仅当 provide
|
|
|
48
53
|
|
|
49
54
|
Host 通过 `ctx.subprocess.spawnTerminal` 创建 `TERM=xterm-256color` 的终端,不启动桌面终端应用。流式 UTF-8 解码保留跨块字符和开头的 BOM,并在 EOF 将不完整的尾部字节替换为替代字符。控制请求走 Gateway,`follow` 使用其复用的 Remote stream。Headless xterm 和序列化 addon 在此前输出写入后生成初始屏幕,后续增量携带单调序号。过慢的订阅者明确失败;重新连接恢复当前屏幕。
|
|
50
55
|
|
|
51
|
-
最新连接持有输入和尺寸控制权。断开连接只释放输入权,不结束进程。显式关闭等待进程清理和最后输出;清理失败时保留资源以便重试。Session 记住已关闭的标识并拒绝迟到或重复的创建请求,包括关闭到达时仍在进行的创建。新终端使用新标识。取消创建且清理失败时,已分配的进程仍有所有者。Session owner 和 controller
|
|
56
|
+
最新连接持有输入和尺寸控制权。断开连接只释放输入权,不结束进程。显式关闭等待进程清理和最后输出;清理失败时保留资源以便重试。Session 记住已关闭的标识并拒绝迟到或重复的创建请求,包括关闭到达时仍在进行的创建。新终端使用新标识。取消创建且清理失败时,已分配的进程仍有所有者。Session owner 和 controller 卸载也会终止所拥有的进程。改变 Session 的沙箱模式时,用户终端继续以原有权限运行。控制权转移或进程退出后被拒绝的输入和尺寸请求保留输出连接并禁用输入,不重发被拒绝的输入。
|
|
57
|
+
|
|
58
|
+
Client 在分配前将每条 Session/内容与终端身份的关联保存到独立的 localStorage key `dsh.terminal.binding.v1.*`。内容身份全局唯一;布局内的 tab id 只标识活动视图 occurrence。逐条记录的写入和删除会保留其他窗口的关联。恢复视图复用该身份;侧栏 terminal provider 先恢复自己的视图,再查询尚无视图的 Host 终端。新视图可以创建进程,恢复视图在目标缺失时显示错误,不创建替代进程。显式关闭先保存清理请求,再删除关联。当前进程元数据和屏幕内容由 Host 提供,不保存在浏览器中。Client 模型在浏览器完成屏幕解析后确认帧,按序发送输入,并忽略旧连接迟到的响应。Client 自产错误携带本地化键。插件卸载等待活跃及先前断开的输出流结束,不关闭 Host 进程。
|
|
52
59
|
|
|
53
|
-
|
|
60
|
+
独立的 `retain(sessionId, id, signal)` Remote 流确认窗口持有关系,不激活 Agent、发送屏幕输出、转移输入权或创建进程。终端 provider 提供侧栏的完整打开标签清单,Client 将其与自己保存的身份取交集。同一窗口中的重复 occurrence 共用一个持有流;孤立的旧关联不会保活任何终端。恢复输出连接前必须等待当前持有关系确认。传输取消只释放对应的物理流代次;插件卸载等待全部持有流结束。清理失败时保留所有权并重试,不重新接受连接,也不重新计算空闲宽限期。
|
|
54
61
|
|
|
55
62
|
新视图自动启动,使用开始页明确选中的 shell,或上次选择且仍可用的 shell。上次选择的路径保存在当前站点 localStorage 的 `dsh.terminal.shell` 中。默认启动通过 Host 探测验证保存的路径,不可用时回到当前默认项。开始页在打开标签页前记录选择,每个新标签页保留自己的 shell 路径和分配身份。存储失败不影响启动。恢复已有终端既不读取这一偏好,也不探测 shell。
|
|
56
63
|
|
|
57
|
-
关闭时先保存未完成的清理请求并释放标签页,再在后台等待 Host 清理。失败时提供重试通知。每个请求使用独立的终端 ID localStorage key,清理成功或收到明确的 `session/not-found`
|
|
64
|
+
关闭时先保存未完成的清理请求并释放标签页,再在后台等待 Host 清理。失败时提供重试通知。每个请求使用独立的终端 ID localStorage key,清理成功或收到明确的 `session/not-found` 响应后删除;启动时重试已保存的请求。传输失败时保留请求。清理请求独立于标签关联和侧栏布局持久化。浏览器存储不可用时,内存中的清理仍可工作,但刷新后无法恢复该请求。
|
|
58
65
|
|
|
59
66
|
</details>
|
|
60
67
|
|
|
@@ -63,6 +70,7 @@ Client 视图只在内存中关联侧栏标签页与终端标识。恢复操作
|
|
|
63
70
|
|
|
64
71
|
- [Subprocess](../../subprocess/subprocess/README.zh.md)
|
|
65
72
|
- [Right Sidebar](../../client/ui-sidebar-right/README.zh.md)
|
|
73
|
+
- [用户终端权限](../../../.agents/notes/implemented/architecture/2026-09-16-user-terminal-permissions.zh.md)
|
|
66
74
|
- [Web terminal decision](../../../.agents/notes/implemented/feature/2026-09-09-web-sidebar-terminal.zh.md)
|
|
67
75
|
|
|
68
76
|
<a id="model-experience"></a>
|
|
@@ -80,6 +88,7 @@ Client 视图只在内存中关联侧栏标签页与终端标识。恢复操作
|
|
|
80
88
|
|
|
81
89
|
- 浏览器刷新保留进程和屏幕;Host 或 Session owner 卸载不保留。不提供持久终端恢复或自动重启 shell。 已退出的终端仍计入 `maxTerminals`;关闭不用的标签页可释放其屏幕和名额。
|
|
82
90
|
- 原生 PTY 可用性和进程树清理保证由 subprocess provider 决定。找到可执行文件并不保证 PTY 分配成功。
|
|
91
|
+
- 自动回收依赖 provider [支持的 shell 活动观察](../../subprocess/subprocess-local/README.zh.md#running-terminal-sessions)。不支持的 shell、自定义启动参数和不确定的进程观察可能让资源一直保留到明确关闭或 owner 卸载。运行中的命令没有强制最长执行时间。
|
|
83
92
|
- 屏幕恢复只保留有界历史,不保留完整记录。同一时刻只有一个连接可输入或调整尺寸。
|
|
84
93
|
|
|
85
94
|
<a id="dev-note"></a>
|
package/lib/client.js
CHANGED
|
@@ -97,6 +97,7 @@ window.__ModuleLoader__.load({
|
|
|
97
97
|
id;
|
|
98
98
|
createWhenMissing;
|
|
99
99
|
shellPath;
|
|
100
|
+
retain;
|
|
100
101
|
/** Observable controls, process metadata and the next screen update awaiting acknowledgement. */
|
|
101
102
|
state = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({
|
|
102
103
|
phase: "idle",
|
|
@@ -121,14 +122,16 @@ window.__ModuleLoader__.load({
|
|
|
121
122
|
* @param id - Host terminal identity, reused when recovering an item from its Session list.
|
|
122
123
|
* @param createWhenMissing - allow allocation only for a new tab, never a listed terminal.
|
|
123
124
|
* @param shellPath - explicit shell chosen at the guide; omission uses the remembered available shell.
|
|
125
|
+
* @param retain - window hold acknowledgement required before output attachment.
|
|
124
126
|
*/
|
|
125
|
-
constructor(sessionId, remote, gateway, id, createWhenMissing = true, shellPath) {
|
|
127
|
+
constructor(sessionId, remote, gateway, id, createWhenMissing = true, shellPath, retain) {
|
|
126
128
|
this.sessionId = sessionId;
|
|
127
129
|
this.remote = remote;
|
|
128
130
|
this.gateway = gateway;
|
|
129
131
|
this.id = id;
|
|
130
132
|
this.createWhenMissing = createWhenMissing;
|
|
131
133
|
this.shellPath = shellPath;
|
|
134
|
+
this.retain = retain;
|
|
132
135
|
}
|
|
133
136
|
/**
|
|
134
137
|
* Attach the DOM lifetime, starting the chosen shell or reconnecting the saved process.
|
|
@@ -210,7 +213,15 @@ window.__ModuleLoader__.load({
|
|
|
210
213
|
info,
|
|
211
214
|
title: info.title
|
|
212
215
|
});
|
|
213
|
-
if (this.
|
|
216
|
+
if (this.retain === void 0) {
|
|
217
|
+
if (this.mounted && this.closing === void 0) this.connect();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.retain(this.lifetime.signal).then(() => {
|
|
221
|
+
if (this.mounted && this.closing === void 0) this.connect();
|
|
222
|
+
}).catch((error) => {
|
|
223
|
+
if (!this.stopped()) this.fail(error);
|
|
224
|
+
});
|
|
214
225
|
}
|
|
215
226
|
/** Reattach with a fresh screen and regain input control. */
|
|
216
227
|
connect() {
|
|
@@ -219,11 +230,13 @@ window.__ModuleLoader__.load({
|
|
|
219
230
|
this.detach();
|
|
220
231
|
const stream = this.gateway.$stream({
|
|
221
232
|
name: "Browser terminal output",
|
|
222
|
-
open: (signal)
|
|
233
|
+
open: async function* (signal) {
|
|
234
|
+
await this.retain?.(signal);
|
|
235
|
+
signal.throwIfAborted();
|
|
223
236
|
const attachmentId = randomUUID();
|
|
224
237
|
this.attachmentId = attachmentId;
|
|
225
|
-
|
|
226
|
-
},
|
|
238
|
+
yield* this.remote.follow(this.sessionId, info.id, attachmentId, signal);
|
|
239
|
+
}.bind(this),
|
|
227
240
|
ended: () => new TerminalViewError("attachmentEnded"),
|
|
228
241
|
carrierFailed: () => {
|
|
229
242
|
if (this.stream === stream) this.patch({
|
|
@@ -437,7 +450,7 @@ window.__ModuleLoader__.load({
|
|
|
437
450
|
});
|
|
438
451
|
return;
|
|
439
452
|
}
|
|
440
|
-
const issue = failure?.code === "terminal/view" ? failure.details.issue : failure?.code === "terminal/limit-reached" ? "terminalLimit" : void 0;
|
|
453
|
+
const issue = failure?.code === "terminal/view" ? failure.details.issue : failure?.code === "terminal/limit-reached" ? "terminalLimit" : failure?.code === "terminal/unavailable" ? "missingTerminal" : void 0;
|
|
441
454
|
this.patch({
|
|
442
455
|
phase: error instanceof _deepseek_ai_dsh_api_gateway_client.RemoteStreamCarrierError ? "disconnected" : "failed",
|
|
443
456
|
writable: false,
|
|
@@ -452,7 +465,7 @@ window.__ModuleLoader__.load({
|
|
|
452
465
|
}
|
|
453
466
|
//#endregion
|
|
454
467
|
//#region lib/types/client/close-requests.js
|
|
455
|
-
const PREFIX = "dsh.terminal.close.v1.";
|
|
468
|
+
const PREFIX$1 = "dsh.terminal.close.v1.";
|
|
456
469
|
/** Each request has its own storage key, so other browser windows cannot overwrite its cleanup. */
|
|
457
470
|
var TerminalCloseRequests = class {
|
|
458
471
|
requests = /* @__PURE__ */ new Map();
|
|
@@ -461,7 +474,7 @@ window.__ModuleLoader__.load({
|
|
|
461
474
|
if (typeof localStorage === "undefined") return;
|
|
462
475
|
for (let index = 0; index < localStorage.length; index++) {
|
|
463
476
|
const key = localStorage.key(index);
|
|
464
|
-
if (key?.startsWith(PREFIX)) this.load(key);
|
|
477
|
+
if (key?.startsWith(PREFIX$1)) this.load(key);
|
|
465
478
|
}
|
|
466
479
|
} catch (error) {
|
|
467
480
|
console.error("Terminal cleanup recovery failed:", error);
|
|
@@ -481,7 +494,7 @@ window.__ModuleLoader__.load({
|
|
|
481
494
|
save(request) {
|
|
482
495
|
this.requests.set(request.id, request);
|
|
483
496
|
try {
|
|
484
|
-
if (typeof localStorage !== "undefined") localStorage.setItem(PREFIX + request.id, JSON.stringify(request));
|
|
497
|
+
if (typeof localStorage !== "undefined") localStorage.setItem(PREFIX$1 + request.id, JSON.stringify(request));
|
|
485
498
|
} catch (error) {
|
|
486
499
|
console.error("Terminal cleanup persistence failed:", error);
|
|
487
500
|
}
|
|
@@ -493,7 +506,7 @@ window.__ModuleLoader__.load({
|
|
|
493
506
|
remove(id) {
|
|
494
507
|
this.requests.delete(id);
|
|
495
508
|
try {
|
|
496
|
-
if (typeof localStorage !== "undefined") localStorage.removeItem(PREFIX + id);
|
|
509
|
+
if (typeof localStorage !== "undefined") localStorage.removeItem(PREFIX$1 + id);
|
|
497
510
|
} catch (error) {
|
|
498
511
|
console.error("Terminal cleanup persistence failed:", error);
|
|
499
512
|
}
|
|
@@ -503,7 +516,7 @@ window.__ModuleLoader__.load({
|
|
|
503
516
|
const raw = localStorage.getItem(key);
|
|
504
517
|
if (raw === null) return;
|
|
505
518
|
const parsed = JSON.parse(raw);
|
|
506
|
-
if (!isRequest(parsed) || key !== PREFIX + parsed.id) throw new Error("Invalid terminal cleanup request");
|
|
519
|
+
if (!isRequest(parsed) || key !== PREFIX$1 + parsed.id) throw new Error("Invalid terminal cleanup request");
|
|
507
520
|
this.requests.set(parsed.id, parsed);
|
|
508
521
|
} catch (error) {
|
|
509
522
|
console.error("Terminal cleanup recovery failed:", error);
|
|
@@ -516,6 +529,148 @@ window.__ModuleLoader__.load({
|
|
|
516
529
|
return typeof request.sessionId === "string" && request.sessionId.length > 0 && typeof request.id === "string" && /^[\w-]{1,128}$/u.test(request.id) && typeof request.title === "string";
|
|
517
530
|
}
|
|
518
531
|
//#endregion
|
|
532
|
+
//#region lib/types/client/retention.js
|
|
533
|
+
/** A stream acknowledgement gates output attachment for each physical connection. */
|
|
534
|
+
var TerminalWindowHold = class {
|
|
535
|
+
stream;
|
|
536
|
+
waiters = /* @__PURE__ */ new Set();
|
|
537
|
+
generation;
|
|
538
|
+
failure;
|
|
539
|
+
/**
|
|
540
|
+
* @param gateway - reconnecting stream owner.
|
|
541
|
+
* @param remote - typed terminal namespace.
|
|
542
|
+
* @param sessionId - saved layout's Session, without Agent activation.
|
|
543
|
+
* @param id - existing Host terminal.
|
|
544
|
+
*/
|
|
545
|
+
constructor(gateway, remote, sessionId, id) {
|
|
546
|
+
this.stream = gateway.$stream({
|
|
547
|
+
name: "Browser terminal window hold",
|
|
548
|
+
open: (signal) => remote.retain(sessionId, id, signal),
|
|
549
|
+
ended: () => new RemoteError("terminal/unavailable", "Terminal hold ended", {})
|
|
550
|
+
});
|
|
551
|
+
this.consume();
|
|
552
|
+
}
|
|
553
|
+
/** Whether a terminal-domain failure ended this hold, allowing an explicit retry. */
|
|
554
|
+
get failed() {
|
|
555
|
+
return this.failure !== void 0;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Wait for an acknowledged current physical hold before following its screen.
|
|
559
|
+
* @param signal - output request or view lifetime.
|
|
560
|
+
* @returns after acknowledgement, or rejects on cancellation/unavailability.
|
|
561
|
+
*/
|
|
562
|
+
async ready(signal) {
|
|
563
|
+
signal.throwIfAborted();
|
|
564
|
+
if (this.failure !== void 0) throw this.failure;
|
|
565
|
+
if (this.generation !== void 0 && !this.generation.aborted) return;
|
|
566
|
+
const waiting = Promise.withResolvers();
|
|
567
|
+
const abort = () => {
|
|
568
|
+
waiting.reject(signal.reason);
|
|
569
|
+
};
|
|
570
|
+
this.waiters.add(waiting);
|
|
571
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
572
|
+
try {
|
|
573
|
+
await waiting.promise;
|
|
574
|
+
} finally {
|
|
575
|
+
this.waiters.delete(waiting);
|
|
576
|
+
signal.removeEventListener("abort", abort);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Release this window's stream and all acknowledgement waiters.
|
|
581
|
+
* @returns after the stream consumer closes.
|
|
582
|
+
*/
|
|
583
|
+
dispose() {
|
|
584
|
+
this.reject(/* @__PURE__ */ new Error("Terminal window hold released"));
|
|
585
|
+
return this.stream.dispose();
|
|
586
|
+
}
|
|
587
|
+
async consume() {
|
|
588
|
+
try {
|
|
589
|
+
for await (const item of this.stream) {
|
|
590
|
+
item.accept();
|
|
591
|
+
this.generation = item.signal;
|
|
592
|
+
for (const waiter of this.waiters) waiter.resolve();
|
|
593
|
+
this.waiters.clear();
|
|
594
|
+
}
|
|
595
|
+
} catch (error) {
|
|
596
|
+
this.reject(error);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
reject(error) {
|
|
600
|
+
this.failure = error instanceof Error ? error : new Error("Terminal window hold failed", { cause: error });
|
|
601
|
+
this.generation = void 0;
|
|
602
|
+
for (const waiter of this.waiters) waiter.reject(error);
|
|
603
|
+
this.waiters.clear();
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
//#endregion
|
|
607
|
+
//#region lib/types/client/bindings.js
|
|
608
|
+
const PREFIX = "dsh.terminal.binding.v1.";
|
|
609
|
+
/** Saved recovery targets keyed by Session and globally unique terminal content identity. */
|
|
610
|
+
var TerminalBindings = class {
|
|
611
|
+
memory = /* @__PURE__ */ new Map();
|
|
612
|
+
/**
|
|
613
|
+
* Read a saved target, retaining this window's value when storage is unavailable.
|
|
614
|
+
* @param sessionId - owning Session.
|
|
615
|
+
* @param contentId - terminal content identity, shared only by deliberate copies.
|
|
616
|
+
* @returns the existing Host identity, if one has been saved.
|
|
617
|
+
*/
|
|
618
|
+
get(sessionId, contentId) {
|
|
619
|
+
const key = this.key(sessionId, contentId);
|
|
620
|
+
const known = this.memory.get(key);
|
|
621
|
+
if (known !== void 0) return known;
|
|
622
|
+
if (typeof localStorage === "undefined") return void 0;
|
|
623
|
+
try {
|
|
624
|
+
const raw = localStorage.getItem(key);
|
|
625
|
+
if (raw === null) return void 0;
|
|
626
|
+
const value = JSON.parse(raw);
|
|
627
|
+
if (typeof value !== "string" || !/^[\w-]{1,128}$/u.test(value)) return void 0;
|
|
628
|
+
this.memory.set(key, value);
|
|
629
|
+
return value;
|
|
630
|
+
} catch (_storageUnavailable) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Save an identity before its Host allocation begins.
|
|
636
|
+
* @param sessionId - owning Session.
|
|
637
|
+
* @param contentId - globally unique terminal content identity.
|
|
638
|
+
* @param id - existing or newly allocated Host identity.
|
|
639
|
+
*/
|
|
640
|
+
set(sessionId, contentId, id) {
|
|
641
|
+
const key = this.key(sessionId, contentId);
|
|
642
|
+
this.memory.set(key, id);
|
|
643
|
+
if (typeof localStorage === "undefined") return;
|
|
644
|
+
try {
|
|
645
|
+
localStorage.setItem(key, JSON.stringify(id));
|
|
646
|
+
} catch (error) {
|
|
647
|
+
console.error("Terminal binding persistence failed:", error);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Remove this content's target after its close intent has been saved.
|
|
652
|
+
* @param sessionId - owning Session.
|
|
653
|
+
* @param contentId - closing terminal content identity.
|
|
654
|
+
*/
|
|
655
|
+
delete(sessionId, contentId) {
|
|
656
|
+
const key = this.key(sessionId, contentId);
|
|
657
|
+
this.memory.delete(key);
|
|
658
|
+
if (typeof localStorage === "undefined") return;
|
|
659
|
+
try {
|
|
660
|
+
localStorage.removeItem(key);
|
|
661
|
+
} catch (error) {
|
|
662
|
+
console.error("Terminal binding cleanup failed:", error);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/** Release cached values when the Client service is disposed. */
|
|
666
|
+
clear() {
|
|
667
|
+
this.memory.clear();
|
|
668
|
+
}
|
|
669
|
+
key(sessionId, contentId) {
|
|
670
|
+
return PREFIX + JSON.stringify([sessionId, contentId]);
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
//#endregion
|
|
519
674
|
//#region lib/types/client/index.js
|
|
520
675
|
/** Client terminal model service; views are keyed independently from Host terminal identities. */
|
|
521
676
|
/** Session and occurrence lookup, independent tab and terminal identities and background cleanup. */
|
|
@@ -528,6 +683,10 @@ window.__ModuleLoader__.load({
|
|
|
528
683
|
closed = new Set(this.requests.pending().map((request) => request.id));
|
|
529
684
|
disposed = false;
|
|
530
685
|
views = /* @__PURE__ */ new Map();
|
|
686
|
+
bindings = new TerminalBindings();
|
|
687
|
+
holds = /* @__PURE__ */ new Map();
|
|
688
|
+
releasing = /* @__PURE__ */ new Set();
|
|
689
|
+
openTabs = [];
|
|
531
690
|
/**
|
|
532
691
|
* @param ctx - Client root Context with Gateway and terminal Remote namespace.
|
|
533
692
|
* @param remote - generated terminal namespace.
|
|
@@ -539,7 +698,15 @@ window.__ModuleLoader__.load({
|
|
|
539
698
|
this.disposed = true;
|
|
540
699
|
const detaching = [...this.views.values()].flatMap((views) => [...views.values()].map((view) => view.dispose()));
|
|
541
700
|
this.views.clear();
|
|
542
|
-
|
|
701
|
+
this.bindings.clear();
|
|
702
|
+
const holds = [...this.holds.values()].flatMap((holds) => [...holds.values()].map((hold) => hold.dispose()));
|
|
703
|
+
this.holds.clear();
|
|
704
|
+
await Promise.all([
|
|
705
|
+
...detaching,
|
|
706
|
+
...holds,
|
|
707
|
+
...this.releasing,
|
|
708
|
+
...this.closing.values()
|
|
709
|
+
]);
|
|
543
710
|
}, "terminal-controller.client.views");
|
|
544
711
|
for (const request of this.requests.pending()) this.cleanup(request);
|
|
545
712
|
}
|
|
@@ -547,11 +714,12 @@ window.__ModuleLoader__.load({
|
|
|
547
714
|
* Return the stable model for one sidebar occurrence.
|
|
548
715
|
* @param sessionId - owning Session.
|
|
549
716
|
* @param key - sidebar occurrence key.
|
|
550
|
-
* @param
|
|
717
|
+
* @param contentId - globally unique content identity; layout-local tab ids are not persistence keys.
|
|
718
|
+
* @param terminalId - existing Host identity when restoring a listed terminal; otherwise reuse the saved content identity.
|
|
551
719
|
* @param shellPath - explicit shell for a new terminal; restored terminals retain their own shell.
|
|
552
720
|
* @returns its observable state and terminal commands.
|
|
553
721
|
*/
|
|
554
|
-
view(sessionId, key, terminalId, shellPath) {
|
|
722
|
+
view(sessionId, key, contentId, terminalId, shellPath) {
|
|
555
723
|
let views = this.views.get(sessionId);
|
|
556
724
|
if (views === void 0) {
|
|
557
725
|
views = /* @__PURE__ */ new Map();
|
|
@@ -559,9 +727,12 @@ window.__ModuleLoader__.load({
|
|
|
559
727
|
}
|
|
560
728
|
let view = views.get(key);
|
|
561
729
|
if (view === void 0) {
|
|
562
|
-
const
|
|
563
|
-
|
|
730
|
+
const saved = terminalId ?? this.bindings.get(sessionId, contentId);
|
|
731
|
+
const id = saved ?? randomUUID();
|
|
732
|
+
this.bindings.set(sessionId, contentId, id);
|
|
733
|
+
view = new TerminalView(sessionId, this.remote, this.ctx.remote, id, saved === void 0, shellPath, (signal) => this.hold(sessionId, id).ready(signal));
|
|
564
734
|
views.set(key, view);
|
|
735
|
+
this.reconcileHolds();
|
|
565
736
|
view.refresh();
|
|
566
737
|
}
|
|
567
738
|
return view;
|
|
@@ -592,12 +763,13 @@ window.__ModuleLoader__.load({
|
|
|
592
763
|
* Save a close intent and release the tab immediately; cleanup outlives DOM unmount and reload.
|
|
593
764
|
* @param sessionId - owning Session.
|
|
594
765
|
* @param key - sidebar occurrence key, including an inactive restored tab.
|
|
766
|
+
* @param contentId - globally unique content identity whose binding is removed.
|
|
595
767
|
* @param terminalId - restored identity if the tab has no model yet.
|
|
596
768
|
*/
|
|
597
|
-
close(sessionId, key, terminalId) {
|
|
769
|
+
close(sessionId, key, contentId, terminalId) {
|
|
598
770
|
const views = this.views.get(sessionId);
|
|
599
771
|
const view = views?.get(key);
|
|
600
|
-
const id = view?.id ?? terminalId;
|
|
772
|
+
const id = view?.id ?? terminalId ?? this.bindings.get(sessionId, contentId);
|
|
601
773
|
if (id === void 0) return;
|
|
602
774
|
const request = {
|
|
603
775
|
sessionId,
|
|
@@ -606,12 +778,70 @@ window.__ModuleLoader__.load({
|
|
|
606
778
|
};
|
|
607
779
|
this.closed.add(id);
|
|
608
780
|
this.requests.save(request);
|
|
781
|
+
this.bindings.delete(sessionId, contentId);
|
|
609
782
|
views?.delete(key);
|
|
610
783
|
if (views?.size === 0) this.views.delete(sessionId);
|
|
611
784
|
this.cleanup(request, view);
|
|
785
|
+
this.reconcileHolds();
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Reconcile this window's open terminal occurrences, including dormant saved Sessions.
|
|
789
|
+
* @param tabs - terminal-kind membership supplied by the sidebar layout owner.
|
|
790
|
+
*/
|
|
791
|
+
retainTabs(tabs) {
|
|
792
|
+
this.openTabs = tabs;
|
|
793
|
+
this.reconcileHolds();
|
|
794
|
+
}
|
|
795
|
+
hold(sessionId, id) {
|
|
796
|
+
let holds = this.holds.get(sessionId);
|
|
797
|
+
if (holds === void 0) {
|
|
798
|
+
holds = /* @__PURE__ */ new Map();
|
|
799
|
+
this.holds.set(sessionId, holds);
|
|
800
|
+
}
|
|
801
|
+
let hold = holds.get(id);
|
|
802
|
+
if (hold === void 0 || hold.failed) {
|
|
803
|
+
if (hold !== void 0) this.release(hold);
|
|
804
|
+
hold = new TerminalWindowHold(this.ctx.remote, this.remote, sessionId, id);
|
|
805
|
+
holds.set(id, hold);
|
|
806
|
+
}
|
|
807
|
+
return hold;
|
|
808
|
+
}
|
|
809
|
+
reconcileHolds() {
|
|
810
|
+
if (this.disposed) return;
|
|
811
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
812
|
+
for (const tab of this.openTabs) {
|
|
813
|
+
const id = this.bindings.get(tab.sessionId, tab.contentId);
|
|
814
|
+
if (id === void 0 || this.closed.has(id)) continue;
|
|
815
|
+
let ids = wanted.get(tab.sessionId);
|
|
816
|
+
if (ids === void 0) {
|
|
817
|
+
ids = /* @__PURE__ */ new Set();
|
|
818
|
+
wanted.set(tab.sessionId, ids);
|
|
819
|
+
}
|
|
820
|
+
ids.add(id);
|
|
821
|
+
const view = this.views.get(tab.sessionId)?.get(tab.tabId);
|
|
822
|
+
if (view === void 0 || view.state.getSnapshot().info !== void 0) {
|
|
823
|
+
if (!this.holds.get(tab.sessionId)?.has(id)) this.hold(tab.sessionId, id);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
for (const [sessionId, holds] of this.holds) {
|
|
827
|
+
for (const [id, hold] of holds) if (!wanted.get(sessionId)?.has(id)) {
|
|
828
|
+
holds.delete(id);
|
|
829
|
+
this.release(hold);
|
|
830
|
+
}
|
|
831
|
+
if (holds.size === 0) this.holds.delete(sessionId);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
release(hold) {
|
|
835
|
+
const releasing = hold.dispose().finally(() => {
|
|
836
|
+
this.releasing.delete(releasing);
|
|
837
|
+
});
|
|
838
|
+
this.releasing.add(releasing);
|
|
839
|
+
releasing.catch((error) => {
|
|
840
|
+
this.ctx.logger.warn("Terminal hold release failed", error);
|
|
841
|
+
});
|
|
612
842
|
}
|
|
613
843
|
/**
|
|
614
|
-
* Query Host terminals
|
|
844
|
+
* Query Host terminals without a live view or unfinished close.
|
|
615
845
|
* @param sessionId - Session being displayed.
|
|
616
846
|
* @returns terminals available for opening as recovered tabs.
|
|
617
847
|
*/
|