dsh-ssh-tui 0.1.0 → 0.1.2
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 +262 -0
- package/README.md +163 -219
- package/lib/tui.js +66 -2
- package/lib/tui.js.map +1 -1
- package/lib/types/tui.d.ts +5 -0
- package/package.json +2 -2
- package/README.zh-CN.md +0 -206
package/README.md
CHANGED
|
@@ -1,262 +1,206 @@
|
|
|
1
1
|
# dsh-ssh-tui
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
DeepSeek Harness(`dsh`)的 SSH 友好交互终端插件:纯 ANSI 聊天式转录、流式输出、
|
|
4
|
+
工具卡片、git 风格 diff、历史会话选择、滚动回看与鼠标点击展开,并带终端标题栏
|
|
5
|
+
进度与完成提示音。
|
|
6
|
+
|
|
7
|
+
English: [README.en.md](README.en.md)
|
|
8
|
+
|
|
9
|
+
## 功能一览
|
|
10
|
+
|
|
11
|
+
- 纯终端渲染,无需浏览器/鼠标/重量级终端框架,适合慢速或远程 SSH;
|
|
12
|
+
- 模型思考流默认折叠,显示 `▸ 思考中 ⠹ · N 字 · Ns` 动画;结束后折叠为
|
|
13
|
+
`▸ 已思考 · N 行`,可单独展开;思考过程中也能实时展开/收起查看原文;
|
|
14
|
+
- 工作区支持 markdown 渲染:多级标题(H1 放大/下划线、H2 下划线、H3 着色)、
|
|
15
|
+
粗体、斜体、行内代码、代码块、列表、引用与链接;模型最终回复以粗体白色显示;
|
|
16
|
+
- 工具调用卡片化:彩色状态点(运行黄 / 成功绿 / 失败红)、shell 命令友好展示、
|
|
17
|
+
编辑工具 git 风格 diff(`-` 浅红底 / `+` 浅绿底 / 文件统计,编辑卡片默认展开且
|
|
18
|
+
diff 内容不截断)、JSON 参数与结果自动转可读内容;
|
|
19
|
+
- 转录区滚动回看(`PgUp`/`PgDn`、鼠标滚轮),点击思考/工具标题行直接展开收起;
|
|
20
|
+
- 输入框下方会话统计行:轮次/步数、模型与工具耗时、TTFT、tok/s、缓存命中率、
|
|
21
|
+
输入/输出 token(与 web 端口径一致);
|
|
22
|
+
- 历史会话启动选择器:`dsh --profile tui --resume`(或 `resume`)先选会话再进入;
|
|
23
|
+
- 终端窗口标题栏:运行中旋转图标 + `运行中 · 工具 N`,完成后 `✓ 已完成`,并响
|
|
24
|
+
一声终端铃(`DSH_TUI_NO_BELL=1` 关闭);
|
|
25
|
+
- 审批、`ask_user_question`、子代理进度、`/mode` 模式切换、`/model` 模型切换、
|
|
26
|
+
`/resume` 会话切换等完整支持。
|
|
27
|
+
|
|
28
|
+
## 环境要求
|
|
29
|
+
|
|
30
|
+
- Node.js ≥ 22.19
|
|
31
|
+
- DeepSeek Harness CLI:`npm i -g @deepseek-ai/dsh`
|
|
32
|
+
- pnpm(`dsh plugin` 通过 pnpm 管理 profile 依赖)
|
|
33
|
+
- 支持 ANSI 的终端(推荐 SSH 直连;Windows 用 PowerShell / Windows Terminal)
|
|
34
|
+
|
|
35
|
+
## 部署指南
|
|
36
|
+
|
|
37
|
+
### 方式一:一键脚本(推荐)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/cyjyyd/dsh-ssh-tui.git
|
|
41
|
+
cd dsh-ssh-tui
|
|
42
|
+
bash scripts/install.sh # 默认安装到 tui profile
|
|
43
|
+
```
|
|
9
44
|
|
|
10
|
-
|
|
45
|
+
安装到其它 profile(例如自定义 `work` profile):
|
|
11
46
|
|
|
12
|
-
|
|
47
|
+
```bash
|
|
48
|
+
bash scripts/install.sh work
|
|
49
|
+
```
|
|
13
50
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- pnpm (used by `dsh plugin` to manage profile dependencies)
|
|
51
|
+
脚本会依次:安装依赖 → 构建 `lib/` → 通过 `dsh plugin --profile <name> add link:<repo>`
|
|
52
|
+
把插件链接进 profile,并自动把 `dsh-ssh-tui` 加入该 profile 的 `dsh.profile.bundles`。
|
|
17
53
|
|
|
18
|
-
|
|
54
|
+
### 方式二:手动安装
|
|
19
55
|
|
|
20
|
-
```
|
|
21
|
-
git clone https://github.com/cyjyyd/dsh-ssh-tui.git
|
|
56
|
+
```bash
|
|
22
57
|
cd dsh-ssh-tui
|
|
23
|
-
bash scripts/install.sh # installs into the `tui` profile
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Or manually:
|
|
27
|
-
|
|
28
|
-
```sh
|
|
29
58
|
npm install --no-audit --no-fund
|
|
30
59
|
npm run build
|
|
31
60
|
dsh plugin --profile tui add "link:$(pwd)"
|
|
32
61
|
```
|
|
33
62
|
|
|
34
|
-
|
|
35
|
-
|
|
63
|
+
### 方式三:npm 安装(发布后)
|
|
64
|
+
|
|
65
|
+
前置要求:已全局安装 `@deepseek-ai/dsh`(`npm i -g @deepseek-ai/dsh`)且有 pnpm。
|
|
36
66
|
|
|
37
|
-
```
|
|
67
|
+
```bash
|
|
38
68
|
dsh plugin --profile tui add dsh-ssh-tui
|
|
39
|
-
#
|
|
69
|
+
# 或在仓库内快捷执行:bash scripts/install-npm.sh
|
|
70
|
+
# 指定其它 profile:bash scripts/install-npm.sh work
|
|
40
71
|
```
|
|
41
72
|
|
|
42
|
-
|
|
43
|
-
|
|
73
|
+
`dsh plugin add` 会从 npm 拉取包、写入 profile 依赖,并自动把 `dsh-ssh-tui`
|
|
74
|
+
加入该 profile 的 `dsh.profile.bundles`。
|
|
44
75
|
|
|
45
|
-
|
|
46
|
-
|
|
76
|
+
## 启动与命令行参数
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
dsh --profile tui # 直接进入主界面(新建会话)
|
|
80
|
+
dsh --profile tui --resume # 打开历史会话选择器
|
|
81
|
+
dsh --profile tui resume # 同上(选择器)
|
|
82
|
+
dsh --profile tui --resume <session-id> # 直接恢复指定会话
|
|
83
|
+
dsh --profile tui resume <session-id> # 等价写法
|
|
84
|
+
dsh --profile tui --new # 显式新建会话(默认即新建,供脚本使用)
|
|
85
|
+
dsh --profile tui --model deepseek-v4-flash
|
|
86
|
+
dsh --profile tui --provider <id>
|
|
87
|
+
dsh --profile tui --no-color
|
|
47
88
|
```
|
|
48
89
|
|
|
49
|
-
|
|
90
|
+
选择器操作:`1-9` 选择历史会话;`0` / `Enter` 新建;`Esc` 取消退出。
|
|
50
91
|
|
|
51
|
-
|
|
52
|
-
bash scripts/verify.sh
|
|
53
|
-
bash scripts/uninstall.sh
|
|
54
|
-
```
|
|
92
|
+
## 交互与快捷键
|
|
55
93
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
| 键 | 作用 |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `Enter` | 发送;运行中则 steer 当前轮次;输入为空且有选中块时展开/收起 |
|
|
97
|
+
| `↑` / `↓` | 输入为空时在思考/工具块间移动选择;有输入时切换历史 |
|
|
98
|
+
| `Ctrl+N` / `Ctrl+P` | 在思考/工具块间移动选择 |
|
|
99
|
+
| `Ctrl+R` | 全部展开/全部收起(逐项展开请用 `↑`/`↓` 选中后按 `Enter`) |
|
|
100
|
+
| `Ctrl+T` | 折叠/展开输入框(折叠只影响显示,提交时仍为完整文本) |
|
|
101
|
+
| 鼠标左键 | 点击思考/工具标题行直接展开/收起 |
|
|
102
|
+
| `PgUp` / `PgDn`、滚轮 | 转录区滚动回看 |
|
|
103
|
+
| `Esc` | 取消选择 / 回到底部 / 取消当前轮次 |
|
|
104
|
+
| `Ctrl+C` | 中断当前轮次;空闲时退出 |
|
|
105
|
+
| `Ctrl+D` | 退出 |
|
|
106
|
+
| `Ctrl+L` | 重绘 |
|
|
107
|
+
|
|
108
|
+
斜杠命令:`/help`、`/model`、`/mode`、`/resume`、`/status`、`/subagents`、
|
|
109
|
+
`/usage`(`/quota` 同义)、`/setup`、`/clear`,以及 harness 自带命令
|
|
110
|
+
(`/goal`、`/plan`、`/compact` 等)。
|
|
111
|
+
|
|
112
|
+
`/usage` 在当前提供商为 OpenCode 源时可用,并区分两种计费方式:
|
|
113
|
+
|
|
114
|
+
- **OpenCode Go**:调用官方额度接口,显示滚动 5 小时 / 本周 / 本月用量
|
|
115
|
+
百分比、限流状态与重置时间;
|
|
116
|
+
- **OpenCode Zen**:按 API 账单计费、没有固定额度,TUI 不假装查询余额,
|
|
117
|
+
只提示到 `https://opencode.ai/zen` 查看,并附本会话已记录的 token 用量。
|
|
118
|
+
|
|
119
|
+
## 配置
|
|
120
|
+
|
|
121
|
+
### 模型默认值(`$DSH_HOME/settings.yaml`)
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
agent-default-model:
|
|
125
|
+
provider: opencode-go
|
|
126
|
+
model: deepseek-v4-pro
|
|
127
|
+
reasoningEffort: max
|
|
128
|
+
agent-presets:
|
|
129
|
+
default: standard
|
|
89
130
|
```
|
|
90
131
|
|
|
91
|
-
|
|
132
|
+
`/model` 与 `/mode` 的修改会写回这里,web 端与 TUI 共用同一份设置。
|
|
92
133
|
|
|
93
|
-
|
|
134
|
+
对 OpenCode 和其他第三方提供商,`/model` 会先调用提供商的端点
|
|
135
|
+
(`GET {baseURL}/models`)获取实时模型列表;端点不可达时回退到已配置的
|
|
136
|
+
模型列表。若选中的模型尚未写入提供商配置,会自动追加到
|
|
137
|
+
`llm-pi-ai.providers.<id>.models`,保证 Harness 可以正常调用。
|
|
94
138
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
```
|
|
139
|
+
首次配置向导的自定义/OpenCode 提供商步骤中,输入模型 ID 前可按
|
|
140
|
+
`Ctrl+F` 直接从端点拉取模型列表,免去手动输入。
|
|
98
141
|
|
|
99
|
-
|
|
142
|
+
### profile 用户层
|
|
100
143
|
|
|
101
|
-
|
|
144
|
+
每个 profile 的 `cordis.patch.yml` 是用户覆盖层,可覆盖插件 patch 的任何行;
|
|
145
|
+
`--patch <file>` 可临时叠加。
|
|
102
146
|
|
|
103
|
-
|
|
104
|
-
`~/.dsh/.credentials.yaml`, or `~/.dsh/env.sh` when a system-injected
|
|
105
|
-
environment variable must be overridden.
|
|
106
|
-
- Windows (PowerShell or Windows Terminal): install with the same npm/dsh
|
|
107
|
-
commands — `dsh` is on PATH via npm's global bin. The wizard stores the key
|
|
108
|
-
through the dsh credential store, or runs `setx` (plus `env.cmd`) when an
|
|
109
|
-
environment variable shadows the store. Agent shell tools automatically use
|
|
110
|
-
PowerShell on Windows (the harness disables bash there).
|
|
111
|
-
- Legacy Windows consoles without VT support: set `DSH_TUI_NO_ALT_SCREEN=1`
|
|
112
|
-
(and `--no-color` if needed) to skip the alternate-screen escape sequences.
|
|
113
|
-
- Keyboard input accepts both `\x7f` and `\x08` backspace, and both `\r` /
|
|
114
|
-
`\r\n` line endings.
|
|
147
|
+
## 验证
|
|
115
148
|
|
|
116
|
-
|
|
149
|
+
```bash
|
|
150
|
+
bash scripts/verify.sh # 检查 profile 组合与 CLI 语法
|
|
151
|
+
```
|
|
117
152
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
| `Esc` / `Ctrl+C` | cancel the running turn |
|
|
124
|
-
| `Ctrl+D` | exit |
|
|
125
|
-
| `Ctrl+L` | redraw |
|
|
126
|
-
| `Ctrl+T` | fold/unfold the input box (display-only; submission keeps the full text) |
|
|
127
|
-
| `↑` / `↓` | input history |
|
|
128
|
-
| `y` / `n` / `Esc` | answer an approval prompt |
|
|
129
|
-
| `1..9` + `Enter` | answer an `ask_user_question` dialog |
|
|
130
|
-
|
|
131
|
-
Type `/` to see slash-command suggestions — the panel merges the TUI's own
|
|
132
|
-
commands with every command the harness registers (`/goal`, `/plan`,
|
|
133
|
-
`/compact`, `/permission`, `/feedback`, ...). `Tab` completes, `Enter` runs.
|
|
134
|
-
`/help` lists everything.
|
|
135
|
-
|
|
136
|
-
`/model` opens a two-step selector: pick a model from the current provider's
|
|
137
|
-
catalog, then pick its reasoning effort (`off` / `high` / `max` when the
|
|
138
|
-
provider exposes them). The change applies to the next request without
|
|
139
|
-
changing the provider, updates the header/status line, and is remembered in
|
|
140
|
-
`agent-default-model` for future launches.
|
|
141
|
-
|
|
142
|
-
For OpenCode and other third-party providers, `/model` queries the provider's
|
|
143
|
-
endpoint (`GET {baseURL}/models`) for a live model list, falling back to the
|
|
144
|
-
configured catalog when the endpoint cannot be reached. Picking a model that
|
|
145
|
-
is not stored in the provider profile automatically adds it to
|
|
146
|
-
`llm-pi-ai.providers.<id>.models` so the harness can serve it.
|
|
147
|
-
|
|
148
|
-
`/usage` (alias `/quota`) works when the current provider is an OpenCode
|
|
149
|
-
source and keeps the two billing models distinct:
|
|
150
|
-
|
|
151
|
-
- **OpenCode Go** queries the official quota endpoint and shows rolling
|
|
152
|
-
5-hour / weekly / monthly usage percentages, limit state, and reset times;
|
|
153
|
-
- **OpenCode Zen** is metered per API bill and has no fixed quota, so the TUI
|
|
154
|
-
points to `https://opencode.ai/zen` for balance/billing and shows the
|
|
155
|
-
session token usage it has recorded instead of inventing a quota.
|
|
156
|
-
|
|
157
|
-
The startup screen shows the official DeepSeek whale logo (rendered from the
|
|
158
|
-
harness favicon) in the DeepSeek brand color, with the wordmark below it. The
|
|
159
|
-
logo scales to the terminal width — a 52-column variant on wide terminals,
|
|
160
|
-
down to a compact variant on narrow ones — so it never looks squeezed. A
|
|
161
|
-
horizontal rule separates the workspace (transcript, reasoning, tool cards)
|
|
162
|
-
from the input area.
|
|
163
|
-
|
|
164
|
-
Model reasoning blocks are collapsed by default: while thinking a compact
|
|
165
|
-
`▸ 思考中 ⠹ · N 字 · Ns` line with a spinner replaces the raw stream, and
|
|
166
|
-
after the turn each block collapses to a `▸ 已思考 · N 行` summary without
|
|
167
|
-
its content. The thinking block can be expanded live while streaming to watch
|
|
168
|
-
the raw reasoning as it arrives. Assistant replies render in bold white with
|
|
169
|
-
terminal markdown support: heading levels (H1 enlarged/underlined, H2
|
|
170
|
-
underlined, H3 colored), bold, italic, inline code, fenced code blocks,
|
|
171
|
-
lists, quotes, and links all get ANSI styling while remaining
|
|
172
|
-
width-wrapped for the terminal. Reasoning and tool cards are
|
|
173
|
-
each expandable/collapsible independently — `Ctrl+N` / `Ctrl+P` move the
|
|
174
|
-
selection highlight between them, `Ctrl+R` expands/collapses all blocks at once
|
|
175
|
-
(individual blocks use `↑`/`↓` + `Enter`), and `Esc` drops the selection. With
|
|
176
|
-
the input box empty, `↑`/`↓` move the selection and `Enter` toggles the
|
|
177
|
-
selected block directly. Clicking a reasoning or tool header in the transcript
|
|
178
|
-
also toggles it.
|
|
179
|
-
|
|
180
|
-
The transcript is scrollable: `PgUp`/`PgDn` or the mouse wheel move back
|
|
181
|
-
through earlier reasoning blocks and tool calls, a `↑ 已回看 N 行` indicator
|
|
182
|
-
shows the scroll position, and `Esc` (or sending a message) returns to the
|
|
183
|
-
live bottom.
|
|
184
|
-
|
|
185
|
-
The terminal window title mirrors the session state while unfocused: an
|
|
186
|
-
animated spinner plus `运行中 · 工具 N` while working, `✓ 已完成` for a few
|
|
187
|
-
seconds after completion, and `待命` when idle. A terminal bell rings on
|
|
188
|
-
completion (`DSH_TUI_NO_BELL=1` disables it).
|
|
189
|
-
|
|
190
|
-
Tool calls render as compact cards instead of raw argument JSON. A colored
|
|
191
|
-
dot leads each card — yellow while running, green on success, red on failure
|
|
192
|
-
(a shell command with a non-zero exit or signal also turns red, with a
|
|
193
|
-
`[退出码 N]` / `[信号 X]` suffix). Shell tools show the command as
|
|
194
|
-
`$ command`, file mutations (`edit` / `write` / `str_replace_editor`) render
|
|
195
|
-
the applied change git-style: a path header, `-` lines on a light-red
|
|
196
|
-
background, `+` lines on a light-green background, and a `└ +N -M · K file(s)`
|
|
197
|
-
footer. File-mutation diffs are shown in full (never collapsed to a `… more`
|
|
198
|
-
line) and their cards start expanded. Other tools show a short argument
|
|
199
|
-
summary and start collapsed to a single line (the command, truncated with
|
|
200
|
-
`…` when long); expand to reveal output or the result body. Expanded generic
|
|
201
|
-
calls convert their JSON arguments and JSON results into readable indented
|
|
202
|
-
content — key/value fields, bullet lists, and multiline blocks for
|
|
203
|
-
code/content — instead of raw JSON text.
|
|
204
|
-
|
|
205
|
-
A web-aligned session stats line sits below the input box: turn/step counts,
|
|
206
|
-
model and tool wall time, first-token latency, tokens/second, cache-hit
|
|
207
|
-
percentage, and billed input/output tokens (`输入 12.3K · 输出 1.2K`), updated
|
|
208
|
-
as the session progresses.
|
|
209
|
-
|
|
210
|
-
While a turn is waiting on the provider, the status line shows
|
|
211
|
-
`等待响应 Ns`; if nothing arrives for 60s a warning appears and `Esc` /
|
|
212
|
-
`Ctrl+C` cancels the turn. Follow-ups sent while a turn is running are
|
|
213
|
-
acknowledged immediately (`⚡ … 排队 N`) and take effect at the next step
|
|
214
|
-
boundary, so the UI never looks frozen. Long-running work is not
|
|
215
|
-
misclassified: while tools are executing the status shows `工具执行中 N`,
|
|
216
|
-
and while subagents are running it shows `子代理执行中 N` (no
|
|
217
|
-
`等待响应`/stall warning). Subagent start/end, child assistant output, child
|
|
218
|
-
tool calls/results, approvals, and `ask_user_question` prompts are all
|
|
219
|
-
rendered with a `[子代理 …]` label; `/subagents` lists active runs.
|
|
220
|
-
|
|
221
|
-
`/mode` opens the agent-mode picker backed by dsh's official preset roster:
|
|
222
|
-
标准模式 (standard), PTC 模式 (code), 极简模式 (minimal), 创造模式 (cordis),
|
|
223
|
-
plus any locally authored presets. On a session that has not produced work
|
|
224
|
-
the switch applies immediately; otherwise it is remembered as the default for
|
|
225
|
-
the next launch. The active mode is shown in the header/status line.
|
|
226
|
-
|
|
227
|
-
`/resume` switches the running TUI to a past session. With no argument it
|
|
228
|
-
opens a picker of recent sessions (excluding subagents), labeled by the user's
|
|
229
|
-
first message with a time/cwd description; `/resume <session-id>` switches
|
|
230
|
-
directly. Switching is refused while a turn is running.
|
|
231
|
-
|
|
232
|
-
```sh
|
|
233
|
-
dsh --profile tui --model deepseek-v4-flash
|
|
234
|
-
dsh --profile tui --no-color
|
|
235
|
-
dsh --profile tui --resume <session-id>
|
|
153
|
+
或手动:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
dsh --profile tui --dump-config | grep -A12 'id: ssh-tui'
|
|
157
|
+
dsh --profile tui --help
|
|
236
158
|
```
|
|
237
159
|
|
|
238
|
-
|
|
239
|
-
`dsh --profile tui --resume` (or `dsh --profile tui resume`) opens the
|
|
240
|
-
history-session picker before the main interface; `dsh --profile tui --resume
|
|
241
|
-
<session-id>` (or `dsh --profile tui resume <session-id>`) skips the picker
|
|
242
|
-
and resumes directly. `dsh --profile tui --new` explicitly starts fresh
|
|
243
|
-
without the picker. The in-app `/resume` command remains available for
|
|
244
|
-
switching while running.
|
|
160
|
+
## 卸载
|
|
245
161
|
|
|
246
|
-
|
|
162
|
+
```bash
|
|
163
|
+
bash scripts/uninstall.sh # 默认 tui profile
|
|
164
|
+
bash scripts/uninstall.sh work # 指定 profile
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
卸载只移除 profile 中的插件依赖与 bundle 层,不会删除会话数据。
|
|
168
|
+
|
|
169
|
+
## 隐私与上传安全
|
|
170
|
+
|
|
171
|
+
- 所有会话、凭据、设置都保存在 `$DSH_HOME`(默认 `~/.dsh`),**不落在本仓库**;
|
|
172
|
+
- `.gitignore` 已排除 `node_modules/`、`lib/`、`.env*`、`*.key`、`session*.jsonl*`、
|
|
173
|
+
`sessions/`、日志与临时文件;
|
|
174
|
+
- 上传前请自查:`find . -type f | grep -Ei 'credential|\.env|\.key|session'`;
|
|
175
|
+
- 插件本身不收集、不上传任何数据;会话日志仅按需读写于本机 `$DSH_HOME`。
|
|
176
|
+
|
|
177
|
+
## 开发与目录结构
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
src/index.ts 插件入口:启动选择器、会话创建/恢复/切换
|
|
181
|
+
src/startup.ts 命令行参数解析(--resume / --new / --model ...)
|
|
182
|
+
src/picker.ts 启动历史会话选择器
|
|
183
|
+
src/session-list.ts 历史会话扫描与标签(共享给 /resume)
|
|
184
|
+
src/tui.ts 终端渲染、交互、统计、标题/铃声
|
|
185
|
+
cordis.patch.yml dsh bundle patch(挂载 TUI 与 agent-presets)
|
|
186
|
+
scripts/ 安装 / 卸载 / 验证脚本
|
|
187
|
+
```
|
|
247
188
|
|
|
248
|
-
```
|
|
189
|
+
```bash
|
|
249
190
|
npm install
|
|
191
|
+
npm run typecheck
|
|
250
192
|
npm run build
|
|
251
193
|
```
|
|
252
194
|
|
|
253
|
-
##
|
|
195
|
+
## 常见问题
|
|
254
196
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
197
|
+
- **`dsh-ssh-tui: both stdin and stdout must be TTYs`**:必须从真实终端/SSH 会话启动。
|
|
198
|
+
- **pnpm 拒绝 git 依赖的构建脚本**:git 安装的插件需要把 pnpm 打印的 key 加入
|
|
199
|
+
profile 的 `pnpm-workspace.yaml` 的 `allowBuilds`。
|
|
200
|
+
- **标题栏或铃声不生效**:确认终端支持 OSC 0 与 BEL;铃声可用
|
|
201
|
+
`DSH_TUI_NO_BELL=1` 关闭。
|
|
202
|
+
- **滚轮误触取消**:已加入转义序列缓冲,网络拆包也不会把 `ESC` 当取消。
|
|
259
203
|
|
|
260
204
|
## License
|
|
261
205
|
|
|
262
|
-
MIT
|
|
206
|
+
MIT,见 [LICENSE](LICENSE)。
|
package/lib/tui.js
CHANGED
|
@@ -1040,6 +1040,7 @@ export class SshTui {
|
|
|
1040
1040
|
input = '';
|
|
1041
1041
|
cursor = 0;
|
|
1042
1042
|
inputFolded = false;
|
|
1043
|
+
inPaste = false;
|
|
1043
1044
|
history = [];
|
|
1044
1045
|
historyIndex = -1;
|
|
1045
1046
|
status = 'idle';
|
|
@@ -1136,7 +1137,7 @@ export class SshTui {
|
|
|
1136
1137
|
if (questions !== undefined) {
|
|
1137
1138
|
this.userQuestionDisposer = questions.registerProvider({ ask: this.handleUserQuestions });
|
|
1138
1139
|
}
|
|
1139
|
-
this.write(`${this.useAlternateScreen ? '\x1b[?1049h' : ''}\x1b[?1000h\x1b[?1006h\x1b[?25l`);
|
|
1140
|
+
this.write(`${this.useAlternateScreen ? '\x1b[?1049h' : ''}\x1b[?1000h\x1b[?1006h\x1b[?2004h\x1b[?25l`);
|
|
1140
1141
|
this.render();
|
|
1141
1142
|
this.updateTerminalTitle();
|
|
1142
1143
|
if (this.resumePicker) {
|
|
@@ -1288,7 +1289,7 @@ export class SshTui {
|
|
|
1288
1289
|
// In no-alternate-screen mode this removes the last painted frame that
|
|
1289
1290
|
// would otherwise stay behind the shell prompt after exit.
|
|
1290
1291
|
this.write('\x1b[0m\x1b[2J\x1b[3J\x1b[H');
|
|
1291
|
-
this.write(`\x1b[?1000l\x1b[?1006l\x1b[?25h${this.useAlternateScreen ? '\x1b[?1049l' : ''}`);
|
|
1292
|
+
this.write(`\x1b[?1000l\x1b[?1006l\x1b[?2004l\x1b[?25h${this.useAlternateScreen ? '\x1b[?1049l' : ''}`);
|
|
1292
1293
|
}
|
|
1293
1294
|
/** Human-facing exit with goodbye and flush; called from key handling. */
|
|
1294
1295
|
async requestExit(code) {
|
|
@@ -2710,6 +2711,13 @@ export class SshTui {
|
|
|
2710
2711
|
clearTimeout(this.escapeTimer);
|
|
2711
2712
|
this.escapeTimer = undefined;
|
|
2712
2713
|
}
|
|
2714
|
+
// Bracketed paste: terminals wrap pasted content in \x1b[200~ ... \x1b[201~.
|
|
2715
|
+
// While inside a paste, CR/LF are literal input characters rather than
|
|
2716
|
+
// submit, so copying a multi-line error message arrives as one message.
|
|
2717
|
+
if (this.inPaste || combined.includes('\x1b[200~') || combined.includes('\x1b[201~')) {
|
|
2718
|
+
this.processPasteChunk(combined);
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2713
2721
|
const escape = /^\x1b\[([A-D])$/u;
|
|
2714
2722
|
const match = combined.match(escape);
|
|
2715
2723
|
if (match !== null) {
|
|
@@ -2824,7 +2832,63 @@ export class SshTui {
|
|
|
2824
2832
|
}
|
|
2825
2833
|
this.handlePlainText(combined);
|
|
2826
2834
|
};
|
|
2835
|
+
/** Handle one data chunk that may contain bracketed-paste markers. */
|
|
2836
|
+
processPasteChunk(combined) {
|
|
2837
|
+
let index = 0;
|
|
2838
|
+
while (index < combined.length) {
|
|
2839
|
+
if (combined.startsWith('\x1b[200~', index)) {
|
|
2840
|
+
this.inPaste = true;
|
|
2841
|
+
index += 6;
|
|
2842
|
+
continue;
|
|
2843
|
+
}
|
|
2844
|
+
if (combined.startsWith('\x1b[201~', index)) {
|
|
2845
|
+
this.inPaste = false;
|
|
2846
|
+
index += 6;
|
|
2847
|
+
continue;
|
|
2848
|
+
}
|
|
2849
|
+
let end = index;
|
|
2850
|
+
while (end < combined.length
|
|
2851
|
+
&& !combined.startsWith('\x1b[200~', end)
|
|
2852
|
+
&& !combined.startsWith('\x1b[201~', end)) {
|
|
2853
|
+
end += 1;
|
|
2854
|
+
}
|
|
2855
|
+
if (end > index) {
|
|
2856
|
+
const part = combined.slice(index, end);
|
|
2857
|
+
if (this.inPaste)
|
|
2858
|
+
this.handlePasteText(part);
|
|
2859
|
+
else
|
|
2860
|
+
this.handlePlainText(part);
|
|
2861
|
+
index = end;
|
|
2862
|
+
}
|
|
2863
|
+
else {
|
|
2864
|
+
index += 1;
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
/** Insert pasted text into the input buffer; CR/LF are literal newlines. */
|
|
2869
|
+
handlePasteText(text) {
|
|
2870
|
+
const normalized = text.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
|
2871
|
+
if (normalized === '')
|
|
2872
|
+
return;
|
|
2873
|
+
this.input = `${this.input.slice(0, this.cursor)}${normalized}${this.input.slice(this.cursor)}`;
|
|
2874
|
+
this.cursor += normalized.length;
|
|
2875
|
+
this.markDirty();
|
|
2876
|
+
}
|
|
2827
2877
|
handlePlainText(text) {
|
|
2878
|
+
// Fallback for terminals without bracketed paste: a burst of multiple line
|
|
2879
|
+
// breaks in one chunk is a paste, not repeated Enter presses.
|
|
2880
|
+
let newlines = 0;
|
|
2881
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
2882
|
+
const char = text[index];
|
|
2883
|
+
if (char === '\r' && text[index + 1] !== '\n')
|
|
2884
|
+
newlines += 1;
|
|
2885
|
+
else if (char === '\n' && text[index - 1] !== '\r')
|
|
2886
|
+
newlines += 1;
|
|
2887
|
+
}
|
|
2888
|
+
if (newlines > 1) {
|
|
2889
|
+
this.handlePasteText(text);
|
|
2890
|
+
return;
|
|
2891
|
+
}
|
|
2828
2892
|
let previous = '';
|
|
2829
2893
|
for (const char of text) {
|
|
2830
2894
|
// Windows terminals may deliver Enter as CRLF; consume only the first half.
|