dsh-ssh-tui 0.3.4 → 0.3.5
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 +45 -3
- package/README.md +43 -8
- package/lib/index.js +33 -4
- package/lib/index.js.map +1 -1
- package/lib/session-lock.js +113 -0
- package/lib/session-lock.js.map +1 -0
- package/lib/tui.js +24 -1
- package/lib/tui.js.map +1 -1
- package/lib/types/session-lock.d.ts +26 -0
- package/lib/types/tui.d.ts +1 -0
- 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
|
|
@@ -347,6 +386,9 @@ Over SSH the TUI probes CSI 6n once and picks 80 / 160 / 250 / 400 ms from
|
|
|
347
386
|
the round-trip. `DSH_TUI_PAINT_MS` always wins (40–1000). `/status` and the
|
|
348
387
|
footer show the active tier.
|
|
349
388
|
|
|
389
|
+
SSH disconnect still kills a TUI that is not inside tmux. Do not start a
|
|
390
|
+
second copy of the same session from another window.
|
|
391
|
+
|
|
350
392
|
## Development
|
|
351
393
|
|
|
352
394
|
```sh
|
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 就退出。思考、工具调用、子代理、计划都在会话日志里,终端上看不到。
|
|
@@ -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:
|
|
@@ -278,10 +309,12 @@ bash scripts/uninstall.sh work # 指定 profile
|
|
|
278
309
|
## 开发与目录结构
|
|
279
310
|
|
|
280
311
|
```text
|
|
281
|
-
src/index.ts
|
|
312
|
+
src/index.ts 插件入口:启动选择器、会话创建/恢复/切换、session lock
|
|
282
313
|
src/startup.ts 命令行参数解析(--resume / --new / --model ...)
|
|
283
314
|
src/picker.ts 启动历史会话选择器
|
|
284
315
|
src/session-list.ts 历史会话扫描与标签(共享给 /resume)
|
|
316
|
+
src/session-lock.ts 同会话防双开
|
|
317
|
+
src/update-check.ts npm 最新版提示(不自动升级)
|
|
285
318
|
src/tui.ts 终端渲染、交互、统计、标题/铃声
|
|
286
319
|
cordis.patch.yml dsh bundle patch(挂载 TUI 与 agent-presets)
|
|
287
320
|
scripts/ 安装 / 卸载 / 验证脚本
|
|
@@ -302,6 +335,8 @@ npm run build
|
|
|
302
335
|
`DSH_TUI_NO_BELL=1` 关闭。
|
|
303
336
|
- **滚轮误触取消**:已加入转义序列缓冲,网络拆包也不会把 `ESC` 当取消。
|
|
304
337
|
- **跳板机 / 多层代理 SSH 发画**:每一帧只发脏行,并且拼成一次 `stdout.write`。本机 80 ms;SSH 启动时用 CSI 6n 测往返,按 RTT 选 80/160/250/400 ms。`DSH_TUI_PAINT_MS` 始终优先(40–1000)。`/status` 和底栏显示当前档。
|
|
338
|
+
- **SSH 断了任务也没了**:用 tmux 开 TUI(见上文)。不要在另一个 SSH 窗口再开同一会话。
|
|
339
|
+
- **提示会话已在 pid 运行**:那份 TUI 还活着。`tmux attach -t dsh`;确认进程已死再删 `$DSH_HOME/tui-locks/` 对应文件。
|
|
305
340
|
|
|
306
341
|
## License
|
|
307
342
|
|
package/lib/index.js
CHANGED
|
@@ -12,6 +12,7 @@ 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';
|
|
15
16
|
export const name = 'ssh-tui';
|
|
16
17
|
/** Core services required before the terminal channel can drive an agent. */
|
|
17
18
|
export const inject = ['agents', 'agentDefaultModel'];
|
|
@@ -31,6 +32,21 @@ export function apply(ctx, config) {
|
|
|
31
32
|
let switching = false;
|
|
32
33
|
let handle;
|
|
33
34
|
let controller;
|
|
35
|
+
let sessionLockPathHeld;
|
|
36
|
+
const dropSessionLock = async () => {
|
|
37
|
+
const path = sessionLockPathHeld;
|
|
38
|
+
sessionLockPathHeld = undefined;
|
|
39
|
+
if (path !== undefined)
|
|
40
|
+
await releaseSessionLock(path);
|
|
41
|
+
};
|
|
42
|
+
const takeSessionLock = async (sessionId) => {
|
|
43
|
+
if (sessionLockDisabled())
|
|
44
|
+
return;
|
|
45
|
+
const { path } = await acquireSessionLock(sessionId, {
|
|
46
|
+
tty: process.env.SSH_TTY ?? process.env.TTY,
|
|
47
|
+
});
|
|
48
|
+
sessionLockPathHeld = path;
|
|
49
|
+
};
|
|
34
50
|
// An explicit in-process change (/setup or /model) wins over launch-time
|
|
35
51
|
// CLI overrides for every session created or resumed later in this process.
|
|
36
52
|
let liveSelection;
|
|
@@ -106,11 +122,19 @@ export function apply(ctx, config) {
|
|
|
106
122
|
installModelSelection(agentCtx, selectionRef);
|
|
107
123
|
await agentPresets?.mount(agentCtx);
|
|
108
124
|
};
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
125
|
+
await takeSessionLock(String(sessionId));
|
|
126
|
+
try {
|
|
127
|
+
handle = resume
|
|
128
|
+
? await agents.resume({ resumeSessionId: sessionId, agentOptions, setup })
|
|
129
|
+
: await agents.create({ sessionId, meta: { cwd: process.cwd() }, agentOptions, setup });
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
await dropSessionLock();
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
112
135
|
if (disposed) {
|
|
113
136
|
await handle.dispose();
|
|
137
|
+
await dropSessionLock();
|
|
114
138
|
return;
|
|
115
139
|
}
|
|
116
140
|
const presetId = agentPresets?.composedPreset(handle.agent.ctx) ?? agentPresets?.defaultId;
|
|
@@ -157,6 +181,7 @@ export function apply(ctx, config) {
|
|
|
157
181
|
await handle.dispose();
|
|
158
182
|
handle = undefined;
|
|
159
183
|
}
|
|
184
|
+
await dropSessionLock();
|
|
160
185
|
await start(SessionId(target), true);
|
|
161
186
|
}
|
|
162
187
|
catch (error) {
|
|
@@ -206,7 +231,10 @@ export function apply(ctx, config) {
|
|
|
206
231
|
await start(SessionId(config.sessionId), config.resume === true);
|
|
207
232
|
};
|
|
208
233
|
void boot().catch((error) => {
|
|
209
|
-
|
|
234
|
+
const detail = error instanceof Error && error.name === 'SessionLockHeldError'
|
|
235
|
+
? error.message
|
|
236
|
+
: errorChain(error);
|
|
237
|
+
process.stderr.write(`dsh-ssh-tui: session "${bootingSessionId}" failed to start:\n${detail}\n`);
|
|
210
238
|
const exit = ctx.get('appExit');
|
|
211
239
|
if (exit !== undefined)
|
|
212
240
|
exit(1);
|
|
@@ -218,6 +246,7 @@ export function apply(ctx, config) {
|
|
|
218
246
|
pickerAbort.abort();
|
|
219
247
|
await controller?.dispose();
|
|
220
248
|
await handle?.dispose();
|
|
249
|
+
await dropSessionLock();
|
|
221
250
|
};
|
|
222
251
|
}, 'ssh-tui');
|
|
223
252
|
}
|
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":"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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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"}
|
package/lib/tui.js
CHANGED
|
@@ -15,6 +15,7 @@ import { existsSync } from 'node:fs';
|
|
|
15
15
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
16
16
|
import { homedir } from 'node:os';
|
|
17
17
|
import { dirname, join } from 'node:path';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
18
19
|
import { StringDecoder } from 'node:string_decoder';
|
|
19
20
|
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
20
21
|
import { createUserMessage, errorChain, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
@@ -22,6 +23,7 @@ import { SessionId } from '@deepseek-ai/dsh-session';
|
|
|
22
23
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
23
24
|
import { formatSessionTime, listResumableSessions } from './session-list.js';
|
|
24
25
|
import { defaultReasoningEffort } from './reasoning.js';
|
|
26
|
+
import { checkForPluginUpdate } from './update-check.js';
|
|
25
27
|
import { DEFAULT_SUBAGENT_MODEL, SUBAGENT_SETTINGS_NAMESPACE, defaultSubagentModelForProvider, subagentModelMatchesProvider, subagentSettingsValue, } from './subagent-model.js';
|
|
26
28
|
import { UserQuestionError, } from '@deepseek-ai/dsh-user-questions';
|
|
27
29
|
const PROVIDER_TEMPLATES = {
|
|
@@ -129,6 +131,16 @@ export function composePaintOutput(options) {
|
|
|
129
131
|
out += `\x1b[${cursorRow};${Math.max(1, options.cursorColumn)}H\x1b[?25h`;
|
|
130
132
|
return out;
|
|
131
133
|
}
|
|
134
|
+
const PLUGIN_VERSION = (() => {
|
|
135
|
+
try {
|
|
136
|
+
const require = createRequire(import.meta.url);
|
|
137
|
+
const parsed = require('../package.json');
|
|
138
|
+
return typeof parsed.version === 'string' ? parsed.version : '0.0.0';
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return '0.0.0';
|
|
142
|
+
}
|
|
143
|
+
})();
|
|
132
144
|
const STALL_WARNING_MS = 60000;
|
|
133
145
|
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
134
146
|
const QUESTION_OPTION_KEYS = '123456789abcdefghijklmnopqrstuvwxyz';
|
|
@@ -291,7 +303,7 @@ const LOCAL_COMMANDS = [
|
|
|
291
303
|
{ name: 'quit', description: 'exit the TUI' },
|
|
292
304
|
{ name: 'exit', description: 'exit the TUI' },
|
|
293
305
|
{ name: 'clear', description: 'clear the transcript view' },
|
|
294
|
-
{ name: 'status', description: 'show session, provider and
|
|
306
|
+
{ name: 'status', description: 'show session, provider, model, paint, and plugin version' },
|
|
295
307
|
{ name: 'usage', description: 'show remaining quota for the current provider (OpenCode Go / SuperGrok)' },
|
|
296
308
|
{ name: 'quota', description: 'alias of /usage' },
|
|
297
309
|
{ name: 'subagents', description: 'list active subagents; kill <id> to stop one' },
|
|
@@ -2133,6 +2145,16 @@ export class SshTui {
|
|
|
2133
2145
|
void this.refreshQuota({ reason: 'start', announce: true }).catch(() => {
|
|
2134
2146
|
// Start-up quota is best-effort; /usage still reports errors.
|
|
2135
2147
|
});
|
|
2148
|
+
void this.notifyPluginUpdate().catch(() => {
|
|
2149
|
+
// Update check is best-effort and never blocks the TUI.
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
async notifyPluginUpdate() {
|
|
2153
|
+
const notice = await checkForPluginUpdate(PLUGIN_VERSION);
|
|
2154
|
+
if (this.disposed || notice === undefined)
|
|
2155
|
+
return;
|
|
2156
|
+
this.pushRow({ kind: 'system', text: notice });
|
|
2157
|
+
this.markDirty();
|
|
2136
2158
|
}
|
|
2137
2159
|
startRenderTimer() {
|
|
2138
2160
|
if (this.renderTimer !== undefined) {
|
|
@@ -6199,6 +6221,7 @@ export class SshTui {
|
|
|
6199
6221
|
const effort = this.selectionRef?.current?.reasoningEffort;
|
|
6200
6222
|
const lines = [
|
|
6201
6223
|
`session: ${this.agent.id}`,
|
|
6224
|
+
`plugin: dsh-ssh-tui ${PLUGIN_VERSION}`,
|
|
6202
6225
|
`route: ${provider}/${model}${effort === undefined ? '' : ` (${effort})`}`,
|
|
6203
6226
|
`provider: ${route.kind}`,
|
|
6204
6227
|
`status: ${this.agent.status}`,
|