dsh-ssh-tui 0.3.4 → 0.3.6
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.en.md +64 -17
- package/README.md +60 -21
- package/lib/index.js +35 -4
- package/lib/index.js.map +1 -1
- package/lib/route-memory.js +55 -0
- package/lib/route-memory.js.map +1 -0
- package/lib/session-lock.js +113 -0
- package/lib/session-lock.js.map +1 -0
- package/lib/tui.js +700 -255
- package/lib/tui.js.map +1 -1
- package/lib/types/route-memory.d.ts +35 -0
- package/lib/types/session-lock.d.ts +26 -0
- package/lib/types/tui.d.ts +104 -8
- package/lib/types/update-check.d.ts +4 -0
- package/lib/update-check.js +49 -0
- package/lib/update-check.js.map +1 -0
- package/package.json +4 -2
package/README.en.md
CHANGED
|
@@ -9,6 +9,17 @@ If you mostly work over SSH — a jump host, a test box, a keyboard-only
|
|
|
9
9
|
session — start here. A local desktop terminal with themes and layout
|
|
10
10
|
you already like can stay as it is.
|
|
11
11
|
|
|
12
|
+
Install with the official CLI (no clone):
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
dsh plugin --profile tui add dsh-ssh-tui
|
|
16
|
+
dsh --profile tui
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Current `dsh` requires `--profile` (`dsh plugin add …` errors without it).
|
|
20
|
+
Swap `tui` for another profile name. The same command updates. Remove with
|
|
21
|
+
`dsh plugin --profile tui remove dsh-ssh-tui`.
|
|
22
|
+
|
|
12
23
|
## Official headless vs this TUI
|
|
13
24
|
|
|
14
25
|
There is no shipped TUI. The default terminal entry on a remote box is
|
|
@@ -49,11 +60,14 @@ Reproducible, no model in the loop: `npm run screenshots:slow` writes
|
|
|
49
60
|
|
|
50
61
|
## Install
|
|
51
62
|
|
|
52
|
-
|
|
63
|
+
The recommended install is the command at the top of this README:
|
|
64
|
+
`dsh plugin --profile tui add dsh-ssh-tui`. The CLI pulls npm, writes the
|
|
65
|
+
profile dependency, and appends this package to `dsh.profile.bundles`
|
|
66
|
+
because the manifest declares `dsh.bundle`.
|
|
67
|
+
|
|
68
|
+
Optional: reuse a SuperGrok / grok-bridge token already on the machine:
|
|
53
69
|
|
|
54
70
|
```sh
|
|
55
|
-
dsh plugin --profile tui add dsh-ssh-tui
|
|
56
|
-
# optional: reuse a SuperGrok / grok-bridge token already on the machine
|
|
57
71
|
dsh plugin --profile tui add github:cyjyyd/dsh-llm-xai-oauth
|
|
58
72
|
dsh plugin --profile headless add github:cyjyyd/dsh-llm-xai-oauth
|
|
59
73
|
```
|
|
@@ -67,6 +81,31 @@ dsh --profile tui
|
|
|
67
81
|
|
|
68
82
|
From a checkout: `bash scripts/smoke-headless.sh` (prints an outcome summary, never a token).
|
|
69
83
|
|
|
84
|
+
### Survive an SSH drop (tmux)
|
|
85
|
+
|
|
86
|
+
A slow-link-friendly painter is not the same as surviving a disconnect.
|
|
87
|
+
Closing the laptop or an idle jump host kills the TTY process. Put the TUI
|
|
88
|
+
in tmux so SSH is only the display:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
tmux new -s dsh -- dsh --profile tui
|
|
92
|
+
# after reconnecting
|
|
93
|
+
tmux attach -t dsh
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
A second TUI on the same `sessionId` is refused (it would steal stdin and
|
|
97
|
+
approvals) and prints `tmux attach`. Locks live under `$DSH_HOME/tui-locks/`;
|
|
98
|
+
a leftover file from a crash is stolen if the pid is dead.
|
|
99
|
+
`DSH_TUI_NO_SESSION_LOCK=1` skips this.
|
|
100
|
+
|
|
101
|
+
On start the TUI checks npm for a newer `dsh-ssh-tui` and **notifies only**:
|
|
102
|
+
|
|
103
|
+
```text
|
|
104
|
+
发现新版本 dsh-ssh-tui 0.3.5(当前 0.3.4)。更新:dsh plugin --profile tui add dsh-ssh-tui
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Set `DSH_TUI_NO_UPDATE_CHECK=1` to skip. `/status` also shows the plugin version.
|
|
108
|
+
|
|
70
109
|
From git:
|
|
71
110
|
|
|
72
111
|
```sh
|
|
@@ -187,17 +226,18 @@ The plugin runs on Linux, macOS, and Windows (Node ≥ 22.19):
|
|
|
187
226
|
| `1..9` + `Enter` | answer an `ask_user_question` dialog |
|
|
188
227
|
|
|
189
228
|
Type `/` to see slash-command suggestions — the panel merges the TUI's own
|
|
190
|
-
commands (`/find`, `/model`, `/help`, ...) with every command the harness
|
|
229
|
+
commands (`/find`, `/model`, `/provider`, `/help`, ...) with every command the harness
|
|
191
230
|
registers (`/goal`, `/plan`, `/compact`, `/permission`, `/feedback`, ...).
|
|
192
231
|
`/compact` shows a spinning 「压缩上下文」 card and footer until it
|
|
193
232
|
finishes, then the tokens recovered. `Tab` completes, `Enter` runs.
|
|
194
233
|
`/help` lists everything.
|
|
195
234
|
|
|
196
|
-
`/model` lists models for the **current** provider
|
|
197
|
-
is `grok-4.6` / `grok-4.5` plus reasoning effort (`xhigh` on 4.6).
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
235
|
+
`/model` lists models for the **current** provider only. On SuperGrok that
|
|
236
|
+
is `grok-4.6` / `grok-4.5` plus reasoning effort (`xhigh` on 4.6). `/provider`
|
|
237
|
+
switches provider, then model; it takes effect on the **next request** — no
|
|
238
|
+
restart. Each provider’s last model and effort is remembered. `/setup` adds
|
|
239
|
+
or updates one API-key provider without wiping the others. SuperGrok / X
|
|
240
|
+
Premium uses local OAuth and does not need a key.
|
|
201
241
|
|
|
202
242
|
For OpenCode and other third-party providers, `/model` queries the provider's
|
|
203
243
|
endpoint (`GET {baseURL}/models`) for a live model list, falling back to the
|
|
@@ -236,17 +276,20 @@ Interrupted streaming output keeps the already-generated prefix and is marked
|
|
|
236
276
|
system messages. Harness slash commands that accept image attachments are
|
|
237
277
|
labelled `(images ok)` in the command list and completion hints.
|
|
238
278
|
|
|
239
|
-
`/usage` (alias `/quota`) follows the **current** provider:
|
|
279
|
+
`/usage` (alias `/balance`; `/quota` still works) follows the **current** provider:
|
|
240
280
|
|
|
281
|
+
- **DeepSeek official** `GET {baseURL}/user/balance`;
|
|
282
|
+
- **OpenAI Completions gateways** probe `/user/balance` and `credit_grants`;
|
|
241
283
|
- **SuperGrok** reads `GET cli-chat-proxy.grok.com/v1/billing` (weekly remaining %);
|
|
242
284
|
- **OpenCode Go** reads the official `/v1/usage` windows (5-hour / week / month);
|
|
243
285
|
- **OpenCode Zen** is metered — the TUI points at `https://opencode.ai/zen`.
|
|
244
286
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
287
|
+
OpenCode Go / SuperGrok quota is fetched silently at start and on a
|
|
288
|
+
window-based cadence; the footer shows remaining %. A ⚠ transcript line
|
|
289
|
+
appears only when remaining crosses 50% / 25% / 10% / 5%. `/usage` or
|
|
290
|
+
`/balance` still prints the full snapshot. Cadence: every 10 turns for a
|
|
291
|
+
5-hour cap (every 4 when near), every 50 for weekly (every 10 when near),
|
|
292
|
+
every 80 for monthly (every 20 when near).
|
|
250
293
|
|
|
251
294
|
The startup screen shows the official DeepSeek whale logo (rendered from the
|
|
252
295
|
harness favicon) in the DeepSeek brand color, with the wordmark below it. The
|
|
@@ -344,8 +387,12 @@ switching while running.
|
|
|
344
387
|
Each paint is one `stdout.write` of dirty rows only, so a jump host or
|
|
345
388
|
corporate proxy does not see one SSH packet per line. Local ttys use 80 ms.
|
|
346
389
|
Over SSH the TUI probes CSI 6n once and picks 80 / 160 / 250 / 400 ms from
|
|
347
|
-
the round-trip. `DSH_TUI_PAINT_MS` always wins (40–1000).
|
|
348
|
-
|
|
390
|
+
the round-trip. `DSH_TUI_PAINT_MS` always wins (40–1000). The stats line
|
|
391
|
+
starts with `SSH ●●●○ 90ms` (1 pip red, 2 yellow, 3+ green). The probe
|
|
392
|
+
does not write into the transcript.
|
|
393
|
+
|
|
394
|
+
SSH disconnect still kills a TUI that is not inside tmux. Do not start a
|
|
395
|
+
second copy of the same session from another window.
|
|
349
396
|
|
|
350
397
|
## Development
|
|
351
398
|
|
package/README.md
CHANGED
|
@@ -10,6 +10,15 @@ English: [README.en.md](README.en.md)
|
|
|
10
10
|
|
|
11
11
|
SuperGrok / X Premium 订阅走配套插件 [dsh-llm-xai-oauth](https://github.com/cyjyyd/dsh-llm-xai-oauth),复用本机 grok-bridge token,不需要 xAI API Key。
|
|
12
12
|
|
|
13
|
+
安装(官方 CLI,无需 clone):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
dsh plugin --profile tui add dsh-ssh-tui
|
|
17
|
+
dsh --profile tui
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
当前 `dsh` 必须带 `--profile`(`dsh plugin add …` 会报缺选项)。装进别的 profile 把 `tui` 换成那个名字即可。更新同一条命令。卸载:`dsh plugin --profile tui remove dsh-ssh-tui`。
|
|
21
|
+
|
|
13
22
|
## 官方 headless 和这个 TUI
|
|
14
23
|
|
|
15
24
|
官方没有预置 TUI。远程机器上的默认终端入口是 `dsh --profile headless`:跑完一个任务,把**最后一条助手回复**打到 stdout 就退出。思考、工具调用、子代理、计划都在会话日志里,终端上看不到。
|
|
@@ -42,8 +51,8 @@ SuperGrok / X Premium 订阅走配套插件 [dsh-llm-xai-oauth](https://github.c
|
|
|
42
51
|
编辑工具 git 风格 diff(`-` 浅红底 / `+` 浅绿底 / 文件统计,编辑卡片默认展开且
|
|
43
52
|
diff 内容不截断)、JSON 参数与结果自动转可读内容;
|
|
44
53
|
- 转录区滚动回看(`PgUp`/`PgDn`、鼠标滚轮),点击思考/工具标题行直接展开收起;
|
|
45
|
-
-
|
|
46
|
-
|
|
54
|
+
- 输入框下方两行底栏:第一行链路芯片 + 按宽度丢组的会话数字(轮次、入/出 token、速度);
|
|
55
|
+
第二行只留一个活动词(运行中 / 工具 N / 子代理 N / 压缩中…),身份收到右侧;
|
|
47
56
|
- 历史会话启动选择器:`dsh --profile tui --resume`(或 `resume`)先选会话再进入;
|
|
48
57
|
- 终端窗口标题栏:运行中旋转图标 + `运行中 · 工具 N`,完成后 `✓ 已完成`,并响
|
|
49
58
|
一声终端铃(`DSH_TUI_NO_BELL=1` 关闭);
|
|
@@ -61,11 +70,11 @@ SuperGrok / X Premium 订阅走配套插件 [dsh-llm-xai-oauth](https://github.c
|
|
|
61
70
|
|
|
62
71
|
## 部署指南
|
|
63
72
|
|
|
64
|
-
|
|
73
|
+
推荐安装就是文首那条 `dsh plugin --profile tui add dsh-ssh-tui`。CLI 会从 npm 拉包、写入 profile 依赖,并把本插件加入 `dsh.profile.bundles`(因为包内声明了 `dsh.bundle`)。
|
|
74
|
+
|
|
75
|
+
可选:本机已有 SuperGrok / grok-bridge token 时再装配套 OAuth:
|
|
65
76
|
|
|
66
77
|
```bash
|
|
67
|
-
dsh plugin --profile tui add dsh-ssh-tui
|
|
68
|
-
# 可选:本机已有 SuperGrok / grok-bridge token 时
|
|
69
78
|
dsh plugin --profile tui add github:cyjyyd/dsh-llm-xai-oauth
|
|
70
79
|
dsh plugin --profile headless add github:cyjyyd/dsh-llm-xai-oauth
|
|
71
80
|
```
|
|
@@ -77,6 +86,29 @@ dsh --profile headless "Reply with exactly: tui-install-ok. Do not use tools."
|
|
|
77
86
|
dsh --profile tui # 必须在真实终端 / SSH 会话里
|
|
78
87
|
```
|
|
79
88
|
|
|
89
|
+
### SSH 断线后续跑(tmux)
|
|
90
|
+
|
|
91
|
+
弱网友好不等于断线后还能跑。合盖、跳板 idle、换网会杀掉当前 TTY 上的进程。
|
|
92
|
+
把 TUI 放进 tmux,断 SSH 只丢显示器,不丢 Agent:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
tmux new -s dsh -- dsh --profile tui
|
|
96
|
+
# 断线后重新 SSH 进来
|
|
97
|
+
tmux attach -t dsh
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
同一 `sessionId` 不能开第二份 TUI(会抢 stdin 和审批)。第二份启动会退出并提示
|
|
101
|
+
`tmux attach`。锁在 `$DSH_HOME/tui-locks/`;进程异常退出后残留锁会在下次启动时
|
|
102
|
+
核对 pid,已死则自动接管。调试可设 `DSH_TUI_NO_SESSION_LOCK=1`。
|
|
103
|
+
|
|
104
|
+
启动时会查一次 npm 上的 `dsh-ssh-tui` 最新版,有更新只提示、不自动升级:
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
发现新版本 dsh-ssh-tui 0.3.5(当前 0.3.4)。更新:dsh plugin --profile tui add dsh-ssh-tui
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`DSH_TUI_NO_UPDATE_CHECK=1` 可关掉。`/status` 里也能看到当前插件版本。
|
|
111
|
+
|
|
80
112
|
仓库内也可:`bash scripts/smoke-headless.sh`(记录出口摘要,不打印 token)。
|
|
81
113
|
|
|
82
114
|
### 方式一:从 git clone 安装
|
|
@@ -105,15 +137,14 @@ npm run build
|
|
|
105
137
|
dsh plugin --profile tui add "link:$(pwd)"
|
|
106
138
|
```
|
|
107
139
|
|
|
108
|
-
### 方式三:指定其它 profile
|
|
140
|
+
### 方式三:指定其它 profile
|
|
109
141
|
|
|
110
142
|
```bash
|
|
143
|
+
dsh plugin --profile work add dsh-ssh-tui
|
|
144
|
+
# 或仓库脚本
|
|
111
145
|
bash scripts/install-npm.sh work
|
|
112
146
|
```
|
|
113
147
|
|
|
114
|
-
`dsh plugin add` 会从 npm 拉取包、写入 profile 依赖,并自动把 `dsh-ssh-tui`
|
|
115
|
-
加入该 profile 的 `dsh.profile.bundles`。
|
|
116
|
-
|
|
117
148
|
### 智能路由模式(dsh-routing-suite)
|
|
118
149
|
|
|
119
150
|
需要“智能路由模式”时,安装 `dsh-routing-suite` 并注册其 preset:
|
|
@@ -163,18 +194,18 @@ dsh --profile tui --no-color
|
|
|
163
194
|
| `Ctrl+D` | 退出 |
|
|
164
195
|
| `Ctrl+L` | 重绘整个画面 |
|
|
165
196
|
|
|
166
|
-
斜杠命令:`/help`、`/find`、`/model`、`/submodel`、`/subeffort`、`/mode`、`/resume`、
|
|
167
|
-
`/status`、`/subagents`、`/usage`(`/
|
|
197
|
+
斜杠命令:`/help`、`/find`、`/model`、`/provider`、`/submodel`、`/subeffort`、`/mode`、`/resume`、
|
|
198
|
+
`/status`、`/subagents`、`/usage`(`/balance` 同义)、`/setup`、`/clear`,
|
|
168
199
|
以及 harness 自带命令(`/goal`、`/plan`、`/compact` 等)。`/compact` 进行中会显示
|
|
169
200
|
「压缩上下文」卡片和底栏转圈,结束时写出回收的 token 数。模型请求失败会显示重试
|
|
170
201
|
进度;会话标题由模型生成后写到窗口标题。harness 命令若声明
|
|
171
202
|
支持图片附件,会在命令列表和补全提示中标注“可附图”。
|
|
172
203
|
|
|
173
|
-
`/model`
|
|
174
|
-
`grok-4.6` / `grok-4.5
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
204
|
+
`/model` 只换**当前提供商**的模型和思考强度。已经在 SuperGrok 时,直接选
|
|
205
|
+
`grok-4.6` / `grok-4.5`(`grok-4.6` 含 `xhigh`)。要换 DeepSeek / OpenCode /
|
|
206
|
+
其它路由用 `/provider`:先选提供商,再选模型,**下一步请求生效,不用重启**。
|
|
207
|
+
每个提供商上次的模型和思考强度会分开记住。`/setup` 只新增或更新当前这条
|
|
208
|
+
API Key 提供商,不会冲掉其它路由。SuperGrok / X Premium 走本机 OAuth,不需要填 Key。
|
|
178
209
|
|
|
179
210
|
子代理默认跟随父会话的提供方,并尽量选同一家的轻量模型:DeepSeek 用
|
|
180
211
|
`deepseek-v4-flash` / `deepseek-v4-flash-vision-exp`,xAI 用 `grok-4.5`。
|
|
@@ -202,13 +233,15 @@ SuperGrok / X Premium 走本机 OAuth,不需要填 Key。`/status` 和底栏
|
|
|
202
233
|
`提问用户` 卡片。`/goal` 是折叠的 `目标` 卡片。`/find 思考 padAnsi` 或 `Alt+1..4`
|
|
203
234
|
可跳到对应类别的最新卡片。
|
|
204
235
|
|
|
205
|
-
`/usage`(`/quota`
|
|
236
|
+
`/usage`(`/balance` 同义,旧名 `/quota` 仍可用)按**当前提供商**查额度或余额:
|
|
206
237
|
|
|
238
|
+
- **DeepSeek 官方**:`GET {baseURL}/user/balance`(文档接口),显示可用/赠送/充值余额;
|
|
239
|
+
- **OpenAI Completions 兼容网关**:按配置的 base URL 探测 `/user/balance`、`/dashboard/billing/credit_grants` 等;
|
|
207
240
|
- **SuperGrok**:`GET cli-chat-proxy.grok.com/v1/billing`,显示本周剩余%;
|
|
208
241
|
- **OpenCode Go**:官方 `/v1/usage`,滚动 5 小时 / 本周 / 本月剩余%;
|
|
209
242
|
- **OpenCode Zen**:按量计费、没有固定额度,提示到 `https://opencode.ai/zen`。
|
|
210
243
|
|
|
211
|
-
|
|
244
|
+
OpenCode Go / SuperGrok 启动和运行中都静默查询,底栏只显示剩余%;跨过 50% / 25% / 10% / 5% 才往工作区打 ⚠。`/usage` 或 `/balance` 仍打印完整结果。查询节奏按最紧窗口:5 小时额度每 10 轮(接近改 4 轮),周额度每 50 轮(接近改 10 轮),月额度每 80 轮(接近改 20 轮)。
|
|
212
245
|
|
|
213
246
|
## 配置
|
|
214
247
|
|
|
@@ -228,7 +261,9 @@ ssh-tui-subagent:
|
|
|
228
261
|
```
|
|
229
262
|
|
|
230
263
|
`/model`、`/mode`、`/submodel` 与 `/subeffort` 的修改会写回这里,
|
|
231
|
-
web 端与 TUI
|
|
264
|
+
web 端与 TUI 共用同一份设置。`/model` 换提供商后**下一步请求生效**,不必重启。
|
|
265
|
+
每个提供商上次的模型和思考强度记在 `ssh-tui-routes` 里,切回 SuperGrok / 官方 / Go 时会预填。
|
|
266
|
+
`/setup` 只更新当前这条提供商(模型列表会合并),不会删掉其它路由的 Key 和模型。
|
|
232
267
|
|
|
233
268
|
对 OpenCode 和其他第三方提供商,`/model` 会先调用提供商的端点
|
|
234
269
|
(`GET {baseURL}/models`)获取实时模型列表;端点不可达时回退到已配置的
|
|
@@ -278,10 +313,12 @@ bash scripts/uninstall.sh work # 指定 profile
|
|
|
278
313
|
## 开发与目录结构
|
|
279
314
|
|
|
280
315
|
```text
|
|
281
|
-
src/index.ts
|
|
316
|
+
src/index.ts 插件入口:启动选择器、会话创建/恢复/切换、session lock
|
|
282
317
|
src/startup.ts 命令行参数解析(--resume / --new / --model ...)
|
|
283
318
|
src/picker.ts 启动历史会话选择器
|
|
284
319
|
src/session-list.ts 历史会话扫描与标签(共享给 /resume)
|
|
320
|
+
src/session-lock.ts 同会话防双开
|
|
321
|
+
src/update-check.ts npm 最新版提示(不自动升级)
|
|
285
322
|
src/tui.ts 终端渲染、交互、统计、标题/铃声
|
|
286
323
|
cordis.patch.yml dsh bundle patch(挂载 TUI 与 agent-presets)
|
|
287
324
|
scripts/ 安装 / 卸载 / 验证脚本
|
|
@@ -301,7 +338,9 @@ npm run build
|
|
|
301
338
|
- **标题栏或铃声不生效**:确认终端支持 OSC 0 与 BEL;铃声可用
|
|
302
339
|
`DSH_TUI_NO_BELL=1` 关闭。
|
|
303
340
|
- **滚轮误触取消**:已加入转义序列缓冲,网络拆包也不会把 `ESC` 当取消。
|
|
304
|
-
- **跳板机 / 多层代理 SSH 发画**:每一帧只发脏行,并且拼成一次 `stdout.write`。本机 80 ms;SSH 启动时用 CSI 6n 测往返,按 RTT 选 80/160/250/400 ms。`DSH_TUI_PAINT_MS` 始终优先(40–1000
|
|
341
|
+
- **跳板机 / 多层代理 SSH 发画**:每一帧只发脏行,并且拼成一次 `stdout.write`。本机 80 ms;SSH 启动时用 CSI 6n 测往返,按 RTT 选 80/160/250/400 ms。`DSH_TUI_PAINT_MS` 始终优先(40–1000)。统计行最左是 `SSH ●●●○ 90ms`(一格红、两格黄、三格及以上绿)。探测不写进转录。
|
|
342
|
+
- **SSH 断了任务也没了**:用 tmux 开 TUI(见上文)。不要在另一个 SSH 窗口再开同一会话。
|
|
343
|
+
- **提示会话已在 pid 运行**:那份 TUI 还活着。`tmux attach -t dsh`;确认进程已死再删 `$DSH_HOME/tui-locks/` 对应文件。
|
|
305
344
|
|
|
306
345
|
## License
|
|
307
346
|
|
package/lib/index.js
CHANGED
|
@@ -12,6 +12,8 @@ import { showSessionPicker } from './picker.js';
|
|
|
12
12
|
import { mountTui } from './tui.js';
|
|
13
13
|
import { defaultReasoningEffort } from './reasoning.js';
|
|
14
14
|
import { createSubagentSelection } from './subagent-model.js';
|
|
15
|
+
import { acquireSessionLock, releaseSessionLock, sessionLockDisabled } from './session-lock.js';
|
|
16
|
+
import { installRouteMemory } from './route-memory.js';
|
|
15
17
|
export const name = 'ssh-tui';
|
|
16
18
|
/** Core services required before the terminal channel can drive an agent. */
|
|
17
19
|
export const inject = ['agents', 'agentDefaultModel'];
|
|
@@ -26,11 +28,27 @@ export function apply(ctx, config) {
|
|
|
26
28
|
throw new Error('dsh-ssh-tui: both stdin and stdout must be TTYs; use a terminal/SSH session');
|
|
27
29
|
}
|
|
28
30
|
const subagentSelection = createSubagentSelection(ctx);
|
|
31
|
+
installRouteMemory(ctx);
|
|
29
32
|
ctx.effect(() => {
|
|
30
33
|
let disposed = false;
|
|
31
34
|
let switching = false;
|
|
32
35
|
let handle;
|
|
33
36
|
let controller;
|
|
37
|
+
let sessionLockPathHeld;
|
|
38
|
+
const dropSessionLock = async () => {
|
|
39
|
+
const path = sessionLockPathHeld;
|
|
40
|
+
sessionLockPathHeld = undefined;
|
|
41
|
+
if (path !== undefined)
|
|
42
|
+
await releaseSessionLock(path);
|
|
43
|
+
};
|
|
44
|
+
const takeSessionLock = async (sessionId) => {
|
|
45
|
+
if (sessionLockDisabled())
|
|
46
|
+
return;
|
|
47
|
+
const { path } = await acquireSessionLock(sessionId, {
|
|
48
|
+
tty: process.env.SSH_TTY ?? process.env.TTY,
|
|
49
|
+
});
|
|
50
|
+
sessionLockPathHeld = path;
|
|
51
|
+
};
|
|
34
52
|
// An explicit in-process change (/setup or /model) wins over launch-time
|
|
35
53
|
// CLI overrides for every session created or resumed later in this process.
|
|
36
54
|
let liveSelection;
|
|
@@ -106,11 +124,19 @@ export function apply(ctx, config) {
|
|
|
106
124
|
installModelSelection(agentCtx, selectionRef);
|
|
107
125
|
await agentPresets?.mount(agentCtx);
|
|
108
126
|
};
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
127
|
+
await takeSessionLock(String(sessionId));
|
|
128
|
+
try {
|
|
129
|
+
handle = resume
|
|
130
|
+
? await agents.resume({ resumeSessionId: sessionId, agentOptions, setup })
|
|
131
|
+
: await agents.create({ sessionId, meta: { cwd: process.cwd() }, agentOptions, setup });
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
await dropSessionLock();
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
112
137
|
if (disposed) {
|
|
113
138
|
await handle.dispose();
|
|
139
|
+
await dropSessionLock();
|
|
114
140
|
return;
|
|
115
141
|
}
|
|
116
142
|
const presetId = agentPresets?.composedPreset(handle.agent.ctx) ?? agentPresets?.defaultId;
|
|
@@ -157,6 +183,7 @@ export function apply(ctx, config) {
|
|
|
157
183
|
await handle.dispose();
|
|
158
184
|
handle = undefined;
|
|
159
185
|
}
|
|
186
|
+
await dropSessionLock();
|
|
160
187
|
await start(SessionId(target), true);
|
|
161
188
|
}
|
|
162
189
|
catch (error) {
|
|
@@ -206,7 +233,10 @@ export function apply(ctx, config) {
|
|
|
206
233
|
await start(SessionId(config.sessionId), config.resume === true);
|
|
207
234
|
};
|
|
208
235
|
void boot().catch((error) => {
|
|
209
|
-
|
|
236
|
+
const detail = error instanceof Error && error.name === 'SessionLockHeldError'
|
|
237
|
+
? error.message
|
|
238
|
+
: errorChain(error);
|
|
239
|
+
process.stderr.write(`dsh-ssh-tui: session "${bootingSessionId}" failed to start:\n${detail}\n`);
|
|
210
240
|
const exit = ctx.get('appExit');
|
|
211
241
|
if (exit !== undefined)
|
|
212
242
|
exit(1);
|
|
@@ -218,6 +248,7 @@ export function apply(ctx, config) {
|
|
|
218
248
|
pickerAbort.abort();
|
|
219
249
|
await controller?.dispose();
|
|
220
250
|
await handle?.dispose();
|
|
251
|
+
await dropSessionLock();
|
|
221
252
|
};
|
|
222
253
|
}, 'ssh-tui');
|
|
223
254
|
}
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,qBAAqB,EAAiE,MAAM,wBAAwB,CAAA;AAG7H,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEjD,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,QAAQ,EAAsB,MAAM,UAAU,CAAA;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AACvD,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,qBAAqB,EAAiE,MAAM,wBAAwB,CAAA;AAG7H,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEjD,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,QAAQ,EAAsB,MAAM,UAAU,CAAA;AACvD,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AACvD,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;AAC7D,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAC/F,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAEtD,MAAM,CAAC,MAAM,IAAI,GAAG,SAAS,CAAA;AAE7B,6EAA6E;AAC7E,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,QAAQ,EAAE,mBAAmB,CAAC,CAAA;AAqBrD;;;;;GAKG;AACH,MAAM,UAAU,KAAK,CAAC,GAAY,EAAE,MAAc;IAChD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CAAC,6EAA6E,CAAC,CAAA;IAChG,CAAC;IACD,MAAM,iBAAiB,GAAG,uBAAuB,CAAC,GAAG,CAAC,CAAA;IACtD,kBAAkB,CAAC,GAAG,CAAC,CAAA;IACvB,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE;QACd,IAAI,QAAQ,GAAG,KAAK,CAAA;QACpB,IAAI,SAAS,GAAG,KAAK,CAAA;QACrB,IAAI,MAA+B,CAAA;QACnC,IAAI,UAAqC,CAAA;QACzC,IAAI,mBAAuC,CAAA;QAE3C,MAAM,eAAe,GAAG,KAAK,IAAmB,EAAE;YAChD,MAAM,IAAI,GAAG,mBAAmB,CAAA;YAChC,mBAAmB,GAAG,SAAS,CAAA;YAC/B,IAAI,IAAI,KAAK,SAAS;gBAAE,MAAM,kBAAkB,CAAC,IAAI,CAAC,CAAA;QACxD,CAAC,CAAA;QAED,MAAM,eAAe,GAAG,KAAK,EAAE,SAAiB,EAAiB,EAAE;YACjE,IAAI,mBAAmB,EAAE;gBAAE,OAAM;YACjC,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,kBAAkB,CAAC,SAAS,EAAE;gBACnD,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG;aAC5C,CAAC,CAAA;YACF,mBAAmB,GAAG,IAAI,CAAA;QAC5B,CAAC,CAAA;QACD,yEAAyE;QACzE,4EAA4E;QAC5E,IAAI,aAAyC,CAAA;QAE7C,iEAAiE;QACjE,MAAM,UAAU,GAAG,CAAC,SAAiB,EAAU,EAAE;YAC/C,MAAM,QAAQ,GAAG,GAAG,CAAC,GAAG,CAAC,mBAAmB,CAAuB,CAAA;YACnE,MAAM,MAAM,GAAG,WAAW,CAAA;YAC1B,MAAM,WAAW,GAAG,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;YACvD,IAAI,QAAQ,KAAK,SAAS,IAAI,WAAW,KAAK,CAAC,CAAC,EAAE,CAAC;gBACjD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;gBACxD,IAAI,IAAI,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;oBACrC,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,SAAS,EAAE,CAAA;gBACxE,CAAC;YACH,CAAC;YACD,OAAO,sDAAsD,SAAS,EAAE,CAAA;QAC1E,CAAC,CAAA;QAED,MAAM,KAAK,GAAG,KAAK,EAAE,SAAoB,EAAE,MAAe,EAAiB,EAAE;YAC3E,MAAM,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAA;YAChC,IAAI,QAAQ;gBAAE,OAAM;YACpB,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;YAChC,IAAI,MAAM,KAAK,SAAS;gBAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAA;YACvF,MAAM,YAAY,GAAG,GAAG,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAA;YACjD,MAAM,cAAc,GAAG,YAAY,EAAE,gBAAgB,EAAE,CAAA;YAEvD,yEAAyE;YACzE,4EAA4E;YAC5E,MAAM,QAAQ,GAAG,aAAa,EAAE,QAAQ;mBACnC,MAAM,CAAC,QAAQ;mBACf,cAAc,EAAE,QAAQ;mBACxB,mBAAmB,CAAA;YACxB,MAAM,KAAK,GAAG,aAAa,EAAE,KAAK;mBAC7B,MAAM,CAAC,KAAK;mBACZ,cAAc,EAAE,KAAK;mBACrB,mBAAmB,CAAA;YAExB,sEAAsE;YACtE,0EAA0E;YAC1E,qCAAqC;YACrC,IAAI,eAAe,GAAG,aAAa,EAAE,eAAe,CAAA;YACpD,IAAI,eAAe,KAAK,SAAS,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;gBACjE,MAAM,cAAc,GAAG,cAAc,KAAK,SAAS;uBAC9C,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,KAAK,cAAc,CAAC,QAAQ,CAAC;uBAC9E,CAAC,MAAM,CAAC,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,KAAK,KAAK,cAAc,CAAC,KAAK,CAAC,CAAA;gBAC1E,IAAI,cAAc;oBAAE,eAAe,GAAG,cAAc,CAAC,eAAe,CAAA;YACtE,CAAC;YACD,mEAAmE;YACnE,0EAA0E;YAC1E,wEAAwE;YACxE,iDAAiD;YACjD,IAAI,eAAe,KAAK,SAAS,IAAI,QAAQ,KAAK,mBAAmB,EAAE,CAAC;gBACtE,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;gBAC1B,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;oBACtB,eAAe,GAAG,MAAM,sBAAsB,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAA;gBACtE,CAAC;YACH,CAAC;YAED,MAAM,kBAAkB,GAAmB;gBACzC,QAAQ;gBACR,KAAK;gBACL,GAAG,CAAC,eAAe,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC;aAC9D,CAAA;YACD,MAAM,YAAY,GAAsB;gBACtC,OAAO,EAAE,kBAAkB;gBAC3B,SAAS,EAAE,SAAS;aACrB,CAAA;YACD,MAAM,YAAY,GAAG,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;YAC5C,MAAM,YAAY,GAAG;gBACnB,QAAQ;gBACR,KAAK;gBACL,GAAG,CAAC,eAAe,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC;aAC9D,CAAA;YACD,MAAM,KAAK,GAAG,KAAK,EAAE,QAAiB,EAAiB,EAAE;gBACvD,qBAAqB,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAA;gBAC7C,MAAM,YAAY,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAA;YACrC,CAAC,CAAA;YACD,MAAM,eAAe,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAA;YACxC,IAAI,CAAC;gBACH,MAAM,GAAG,MAAM;oBACb,CAAC,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,EAAE,eAAe,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;oBAC1E,CAAC,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,CAAA;YAC3F,CAAC;YAAC,OAAO,KAAc,EAAE,CAAC;gBACxB,MAAM,eAAe,EAAE,CAAA;gBACvB,MAAM,KAAK,CAAA;YACb,CAAC;YACD,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,MAAM,CAAC,OAAO,EAAE,CAAA;gBACtB,MAAM,eAAe,EAAE,CAAA;gBACvB,OAAM;YACR,CAAC;YACD,MAAM,QAAQ,GAAG,YAAY,EAAE,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,YAAY,EAAE,SAAS,CAAA;YAC1F,IAAI,UAAU,GAAG,QAAQ,CAAA;YACzB,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,IAAI,CAAC;oBACH,MAAM,MAAM,GAAG,MAAM,YAAY,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;oBACpD,IAAI,MAAM,EAAE,IAAI,KAAK,SAAS;wBAAE,UAAU,GAAG,MAAM,CAAC,IAAI,CAAA;gBAC1D,CAAC;gBAAC,MAAM,CAAC;oBACP,uBAAuB;gBACzB,CAAC;YACH,CAAC;YACD,UAAU,GAAG,QAAQ,CAAC,GAAG,EAAE;gBACzB,GAAG,MAAM;gBACT,YAAY,EAAE,KAAK;gBACnB,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC;gBAC5B,MAAM;gBACN,QAAQ;gBACR,KAAK;gBACL,YAAY;gBACZ,iBAAiB;gBACjB,QAAQ;gBACR,UAAU;gBACV,OAAO,EAAE,UAAU,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;gBACtC,eAAe,EAAE,QAAQ;gBACzB,kBAAkB,EAAE,CAAC,IAAI,EAAE,EAAE;oBAC3B,aAAa,GAAG,IAAI,CAAA;gBACtB,CAAC;aACF,CAAC,CAAA;QACJ,CAAC,CAAA;QAED,6EAA6E;QAC7E,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAc,EAAiB,EAAE;YACvD,IAAI,SAAS,IAAI,QAAQ;gBAAE,OAAM;YACjC,SAAS,GAAG,IAAI,CAAA;YAChB,IAAI,CAAC;gBACH,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;oBAC7B,MAAM,UAAU,CAAC,OAAO,EAAE,CAAA;oBAC1B,UAAU,GAAG,SAAS,CAAA;gBACxB,CAAC;gBACD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;oBACzB,MAAM,MAAM,CAAC,OAAO,EAAE,CAAA;oBACtB,MAAM,GAAG,SAAS,CAAA;gBACpB,CAAC;gBACD,MAAM,eAAe,EAAE,CAAA;gBACvB,MAAM,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAA;YACtC,CAAC;YAAC,OAAO,KAAc,EAAE,CAAC;gBACxB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6CAA6C,MAAM,MAAM,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;gBACpG,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;gBAC/B,IAAI,IAAI,KAAK,SAAS;oBAAE,IAAI,CAAC,CAAC,CAAC,CAAA;;oBAC1B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACtB,CAAC;oBAAS,CAAC;gBACT,SAAS,GAAG,KAAK,CAAA;YACnB,CAAC;QACH,CAAC,CAAA;QAED,MAAM,WAAW,GAAG,IAAI,eAAe,EAAE,CAAA;QACzC,IAAI,gBAAgB,GAAG,MAAM,CAAC,SAAS,CAAA;QACvC,MAAM,IAAI,GAAG,KAAK,IAAmB,EAAE;YACrC,IAAI,MAAM,CAAC,YAAY,KAAK,IAAI,EAAE,CAAC;gBACjC,MAAM,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAA;gBAChC,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,KAAK,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;gBACvF,IAAI,QAAQ;oBAAE,OAAM;gBACpB,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;oBACpB,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;oBAC/B,IAAI,IAAI,KAAK,SAAS;wBAAE,IAAI,CAAC,CAAC,CAAC,CAAA;;wBAC1B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;oBACpB,OAAM;gBACR,CAAC;gBACD,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;oBAC7B,gBAAgB,GAAG,MAAM,CAAC,EAAE,CAAA;oBAC5B,MAAM,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAA;gBACzC,CAAC;qBAAM,CAAC;oBACN,oEAAoE;oBACpE,mEAAmE;oBACnE,sBAAsB;oBACtB,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,KAAK,EAAE;wBACvC,CAAC,CAAC,gBAAgB,UAAU,EAAE,EAAE;wBAChC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAA;oBACpB,gBAAgB,GAAG,SAAS,CAAA;oBAC5B,MAAM,KAAK,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,CAAA;gBAC1C,CAAC;gBACD,OAAM;YACR,CAAC;YACD,MAAM,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC,CAAA;QAClE,CAAC,CAAA;QAED,KAAK,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;YACnC,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,sBAAsB;gBAC5E,CAAC,CAAC,KAAK,CAAC,OAAO;gBACf,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,CAAA;YACrB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB,gBAAgB,uBAAuB,MAAM,IAAI,CAAC,CAAA;YAChG,MAAM,IAAI,GAAG,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;YAC/B,IAAI,IAAI,KAAK,SAAS;gBAAE,IAAI,CAAC,CAAC,CAAC,CAAA;;gBAC1B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACtB,CAAC,CAAC,CAAA;QAEF,OAAO,KAAK,IAAmB,EAAE;YAC/B,QAAQ,GAAG,IAAI,CAAA;YACf,WAAW,CAAC,KAAK,EAAE,CAAA;YACnB,MAAM,UAAU,EAAE,OAAO,EAAE,CAAA;YAC3B,MAAM,MAAM,EAAE,OAAO,EAAE,CAAA;YACvB,MAAM,eAAe,EAAE,CAAA;QACzB,CAAC,CAAA;IACH,CAAC,EAAE,SAAS,CAAC,CAAA;AACf,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remember the last model/effort per provider so /model and /setup can switch
|
|
3
|
+
* routes without wiping the previous provider's choice.
|
|
4
|
+
*/
|
|
5
|
+
import z from '@deepseek-ai/schemastery';
|
|
6
|
+
import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
7
|
+
export const ROUTE_MEMORY_NAMESPACE = settingsNamespace('ssh-tui-routes');
|
|
8
|
+
/** Settings schema for `$DSH_HOME/settings.yaml` under ssh-tui-routes. */
|
|
9
|
+
export const ROUTE_MEMORY_SCHEMA = z.object({
|
|
10
|
+
providers: z.dict(z.object({
|
|
11
|
+
model: z.string(),
|
|
12
|
+
reasoningEffort: z.string().default(''),
|
|
13
|
+
})).default({}),
|
|
14
|
+
});
|
|
15
|
+
export function parseRouteMemory(raw) {
|
|
16
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
17
|
+
return {};
|
|
18
|
+
const out = {};
|
|
19
|
+
for (const [provider, value] of Object.entries(raw)) {
|
|
20
|
+
const id = provider.trim();
|
|
21
|
+
if (id === '' || value === null || typeof value !== 'object' || Array.isArray(value))
|
|
22
|
+
continue;
|
|
23
|
+
const row = value;
|
|
24
|
+
const model = typeof row.model === 'string' ? row.model.trim() : '';
|
|
25
|
+
if (model === '')
|
|
26
|
+
continue;
|
|
27
|
+
const effort = typeof row.reasoningEffort === 'string' ? row.reasoningEffort.trim() : '';
|
|
28
|
+
out[id] = effort === '' ? { model } : { model, reasoningEffort: effort };
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
export function rememberedRouteFor(memory, provider) {
|
|
33
|
+
return memory[provider.trim()];
|
|
34
|
+
}
|
|
35
|
+
/** Register the route-memory settings section so /model and /provider can persist. */
|
|
36
|
+
export function installRouteMemory(ctx) {
|
|
37
|
+
installSettingsSection(ctx, ROUTE_MEMORY_NAMESPACE, ROUTE_MEMORY_SCHEMA, {
|
|
38
|
+
providers: {},
|
|
39
|
+
}, {
|
|
40
|
+
setSource: () => { },
|
|
41
|
+
onChange: () => { },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export function upsertRememberedRoute(memory, provider, route) {
|
|
45
|
+
const id = provider.trim();
|
|
46
|
+
if (id === '' || route.model.trim() === '')
|
|
47
|
+
return memory;
|
|
48
|
+
return {
|
|
49
|
+
...memory,
|
|
50
|
+
[id]: route.reasoningEffort === undefined || route.reasoningEffort === ''
|
|
51
|
+
? { model: route.model }
|
|
52
|
+
: { model: route.model, reasoningEffort: route.reasoningEffort },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=route-memory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-memory.js","sourceRoot":"","sources":["../src/route-memory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exclusive lock so two TUI processes do not drive the same session.
|
|
3
|
+
* Stale locks (dead pid) are stolen. Live locks tell the user to attach tmux.
|
|
4
|
+
*/
|
|
5
|
+
import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
export class SessionLockHeldError extends Error {
|
|
9
|
+
lock;
|
|
10
|
+
path;
|
|
11
|
+
constructor(lock, path) {
|
|
12
|
+
super(formatLockHeldMessage(lock));
|
|
13
|
+
this.name = 'SessionLockHeldError';
|
|
14
|
+
this.lock = lock;
|
|
15
|
+
this.path = path;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function sessionLockPath(sessionId, dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh')) {
|
|
19
|
+
const safe = sessionId.replaceAll(/[^A-Za-z0-9._-]/g, '_');
|
|
20
|
+
return join(dshHome, 'tui-locks', `${safe}.json`);
|
|
21
|
+
}
|
|
22
|
+
export function parseSessionLock(raw) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(raw);
|
|
25
|
+
const pid = typeof parsed.pid === 'number' && Number.isInteger(parsed.pid) ? parsed.pid : undefined;
|
|
26
|
+
const sessionId = typeof parsed.sessionId === 'string' ? parsed.sessionId : undefined;
|
|
27
|
+
if (pid === undefined || pid <= 0 || sessionId === undefined || sessionId === '')
|
|
28
|
+
return undefined;
|
|
29
|
+
return {
|
|
30
|
+
pid,
|
|
31
|
+
sessionId,
|
|
32
|
+
startedAt: typeof parsed.startedAt === 'string' ? parsed.startedAt : '',
|
|
33
|
+
...(typeof parsed.tty === 'string' && parsed.tty !== '' ? { tty: parsed.tty } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** True when `pid` still exists on this machine (best-effort). */
|
|
41
|
+
export function processIsAlive(pid) {
|
|
42
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
43
|
+
return false;
|
|
44
|
+
try {
|
|
45
|
+
process.kill(pid, 0);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return error.code === 'EPERM';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function formatLockHeldMessage(lock) {
|
|
53
|
+
const tty = lock.tty === undefined ? '' : ` · ${lock.tty}`;
|
|
54
|
+
return [
|
|
55
|
+
`会话 ${lock.sessionId} 已在 pid ${lock.pid}${tty} 运行。`,
|
|
56
|
+
'不要再开第二份 TUI。若 SSH 断过:tmux attach -t dsh',
|
|
57
|
+
'确认进程已死后,删除 $DSH_HOME/tui-locks/ 里对应的锁再启动。',
|
|
58
|
+
].join('\n');
|
|
59
|
+
}
|
|
60
|
+
export function sessionLockDisabled(env = process.env) {
|
|
61
|
+
return env.DSH_TUI_NO_SESSION_LOCK === '1' || env.DSH_TUI_NO_SESSION_LOCK === 'true';
|
|
62
|
+
}
|
|
63
|
+
export async function acquireSessionLock(sessionId, options = {}) {
|
|
64
|
+
const path = sessionLockPath(sessionId, options.dshHome);
|
|
65
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
66
|
+
const info = {
|
|
67
|
+
pid: options.pid ?? process.pid,
|
|
68
|
+
sessionId,
|
|
69
|
+
startedAt: new Date().toISOString(),
|
|
70
|
+
...(options.tty ? { tty: options.tty } : {}),
|
|
71
|
+
};
|
|
72
|
+
const payload = `${JSON.stringify(info, null, 2)}\n`;
|
|
73
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
74
|
+
try {
|
|
75
|
+
await writeFile(path, payload, { flag: 'wx', mode: 0o600 });
|
|
76
|
+
return { path, info };
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error.code !== 'EEXIST')
|
|
80
|
+
throw error;
|
|
81
|
+
let existing;
|
|
82
|
+
try {
|
|
83
|
+
existing = parseSessionLock(await readFile(path, 'utf8'));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
existing = undefined;
|
|
87
|
+
}
|
|
88
|
+
const ours = options.pid ?? process.pid;
|
|
89
|
+
if (existing !== undefined && processIsAlive(existing.pid) && existing.pid !== ours) {
|
|
90
|
+
throw new SessionLockHeldError(existing, path);
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
await unlink(path);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Raced with another unlock; retry exclusive create.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`无法占用会话锁 ${path}`);
|
|
101
|
+
}
|
|
102
|
+
export async function releaseSessionLock(path, pid = process.pid) {
|
|
103
|
+
try {
|
|
104
|
+
const current = parseSessionLock(await readFile(path, 'utf8'));
|
|
105
|
+
if (current !== undefined && current.pid !== pid)
|
|
106
|
+
return;
|
|
107
|
+
await unlink(path);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Missing lock is fine.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=session-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-lock.js","sourceRoot":"","sources":["../src/session-lock.ts"],"names":[],"mappings":"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"}
|