dsh-ssh-tui 0.8.2 → 0.8.3
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/CHANGELOG.md +24 -0
- package/docs/release-notes-0.8.2.md +3 -1
- package/docs/release-notes-0.8.3.md +72 -0
- package/lib/tui.js +200 -28
- package/lib/tui.js.map +1 -1
- package/lib/types/tui.d.ts +32 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,30 @@
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## 0.8.3 — 2026-10-13(`latest` + `next`)
|
|
14
|
+
|
|
15
|
+
发版说明:[`docs/release-notes-0.8.3.md`](release-notes-0.8.3.md)。
|
|
16
|
+
**紧急修复**:三个缺陷都是 0.8.2 把首启向导改成 Screen 时引入的,全部在"首次安装 / `/setup`"这条路上。
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **首启向导 / `/setup` 的粘贴**:终端粘贴(Ctrl+V / Cmd+V / 右键,即括号粘贴)此前落到 Screen
|
|
21
|
+
*背后*的工作区草稿里——向导字段一个字都收不到,读者只能手敲 API Key。现在粘贴归字段所有:字段步骤
|
|
22
|
+
直接落进字段(行尾换行折叠掉,多行按分隔拼接成一行),没有字段的步骤(模型多选、确认)不接收,
|
|
23
|
+
两种情况下草稿都不再被改写。
|
|
24
|
+
- **走完向导没有退回工作区**:保存成功后 Screen 仍然占着屏,要随便按一个键才退得出去(那个键的兜底
|
|
25
|
+
才是真正的退场)。现在**保存本身就是退场**,与 Esc 对称;保存失败则**保留向导与错误**(此前错误写在
|
|
26
|
+
一个立刻被丢弃的状态上:Screen 留着、错误没了、填好的答案也没了)。完成语改为转写行,回到工作区
|
|
27
|
+
仍可读到。
|
|
28
|
+
- **一次读取里的多个按键**:`text\r` 这样的读取会收下文字却丢掉回车,步骤停住不动。现在按 composer
|
|
29
|
+
一直以来的规则拆开(转义序列完整、CRLF 一个回车、控制字节各自成键、可打印文本连成一段)。
|
|
30
|
+
- **Screen 打开时的 composer 编辑键**:`Enter` / `Backspace` / `Ctrl+H` / `Ctrl+U` / `Ctrl+K` /
|
|
31
|
+
`Ctrl+A` / `Ctrl+E` / `Ctrl+R` / `Ctrl+T` 不再作用于 `/diag`、`/status`、diff 全览等 Screen 背后
|
|
32
|
+
那个看不见的草稿(此前 `Enter` 会把草稿当消息发出去,`Ctrl+U` / `Ctrl+K` 会清空或截断它)。
|
|
33
|
+
- **Screen 里的 `Ctrl+L`**:任意 Screen(不只是向导)都能整屏重画,用于清理花屏的终端;此前只有向导认这个键。
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
13
37
|
## 0.8.2 — 2026-10-05(`latest` + `next`)
|
|
14
38
|
|
|
15
39
|
发版说明:[`docs/release-notes-0.8.2.md`](release-notes-0.8.2.md);候选版说明保留在
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
> **发布方式:`latest` + `next`。** 上一次发版:**0.8.1**(2026-09-30,`latest` + `next`)。
|
|
4
4
|
> 本版由 **0.8.2-rc.1**(2026-10-04 只上 `next`)沉淀一周后转正:候选版期间没有收到新的回归报告,
|
|
5
5
|
> CI 五条腿与全部真 PTY 探针持续全绿,Windows / ConPTY 实机人工验收也已完成(见
|
|
6
|
-
> [`checkpoints.md`](checkpoints.md)「Windows
|
|
6
|
+
> [`checkpoints.md`](checkpoints.md)「Windows 实机轮」)。**运行时内容与候选版逐字节相同**(转正后从 registry 分别取回两个 tarball,`lib/` 整目录
|
|
7
|
+
> `diff -r` 无差异);自候选版以来新增的只有 README 与文档(DSH Relay 软品牌那一批)以及
|
|
8
|
+
> `docs/screenshots/reconnect.gif`。
|
|
7
9
|
> 逐项说明见 [`release-notes-0.8.2-rc.1.md`](release-notes-0.8.2-rc.1.md)。
|
|
8
10
|
|
|
9
11
|
> `dsh-ssh-tui` is now presented as DSH Relay.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# 0.8.3 发版说明
|
|
2
|
+
|
|
3
|
+
> **发布方式:`latest` + `next`。** 上一次发版:**0.8.2**(2026-10-05,`latest` + `next`)。
|
|
4
|
+
> 本版是**紧急修复**:0.8.2 把首启向导改成 Screen 之后,向导的输入与退场出了三个缺陷,
|
|
5
|
+
> 都在"首次安装 / `/setup`"这条路上。没有协议变更、没有配置变更、没有数据损坏;
|
|
6
|
+
> 升级即修复,不需要任何迁移动作。
|
|
7
|
+
|
|
8
|
+
## 头等事:向导收得下粘贴,也走得掉
|
|
9
|
+
|
|
10
|
+
三个缺陷都是 0.8.2 引入的(0.8.1 的向导借工作区的输入行,所以既收得到粘贴、也不需要自己退场)。
|
|
11
|
+
|
|
12
|
+
### 1. 粘贴的 API Key 进不了字段,只能手敲
|
|
13
|
+
|
|
14
|
+
终端粘贴(Ctrl+V / Cmd+V / 右键,即括号粘贴)此前落到了向导 **Screen 背后**那个看不见的工作区草稿里:
|
|
15
|
+
向导字段一个字都收不到,而粘贴并没丢——它会留在草稿里,Esc 离开向导后在输入行现形,按回车就会被当成
|
|
16
|
+
一条消息发出去。受影响的是**所有做括号粘贴的终端**(Windows Terminal、iTerm2、GNOME Terminal、WezTerm、
|
|
17
|
+
kitty、VS Code 终端、tmux……),也就是几乎所有现代终端;受影响的是**每一个字段步骤**(提供商过滤、
|
|
18
|
+
Provider ID、Base URL、**API Key**、模型列表、上下文窗口)。
|
|
19
|
+
|
|
20
|
+
现在粘贴归字段所有:字段步骤直接落进字段(行尾换行折叠掉,多行按分隔拼成一行),没有字段的步骤
|
|
21
|
+
(模型多选、确认)不接收,两种情况都不再改写工作区草稿。老控制台不发括号粘贴时,多行 burst 也按粘贴处理。
|
|
22
|
+
|
|
23
|
+
### 2. 走完向导没有退回工作区
|
|
24
|
+
|
|
25
|
+
保存成功后向导仍然占着屏,读者要随便按一个键才退得出去(那个键的"Screen 背后没有向导 → 关掉它"兜底
|
|
26
|
+
才是真正的退场)。**保存本身就是退场**,现在与 Esc 一致:按 `y` 的同一帧,工作区回来。
|
|
27
|
+
|
|
28
|
+
同一处的失败路径也修了:保存失败时错误写在了一个立刻被丢弃的状态上——Screen 留着、错误没了、填好的答案
|
|
29
|
+
也没了,下一次按键还把这套流程当作没发生过。现在失败**保留向导与错误**,改完可以直接重试。
|
|
30
|
+
|
|
31
|
+
向导自己的提示行会随 Screen 消失,所以"配置完成,已记住 X / Y"现在落在**转写行**里(凭据那一行是
|
|
32
|
+
footer 回显,会过期),回到工作区后还读得到。
|
|
33
|
+
|
|
34
|
+
### 3. 一次读取里的多个按键(顺带修掉的同类)
|
|
35
|
+
|
|
36
|
+
一次终端读取里的多个按键此前被当成一个键:`text\r`(慢链路上的快速输入、终端合并写入)会收下文字却
|
|
37
|
+
**丢掉那个回车**,步骤停住不动——读者只能再按一次回车。现在一次读取按 composer 一直以来的规则拆开:
|
|
38
|
+
转义序列保持完整、CRLF 算一个回车、控制字节各自成键、可打印文本连成一段(所以没有括号粘贴的
|
|
39
|
+
"yes" 仍然不会替读者按下确认)。
|
|
40
|
+
|
|
41
|
+
## 兼容
|
|
42
|
+
|
|
43
|
+
上游兼容窗口与 0.8.2 相同:`0.2.0-rc.1` / `0.2.0-rc.2` / `0.1.7-rc.1` / `0.1.7-rc.2`。
|
|
44
|
+
**0.1.5 及更早的宿主不再支持。** 本版不改协议、不加命令、不动上游接口,也不改任何配置文件的形状。
|
|
45
|
+
|
|
46
|
+
## 安装 / 升级
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
dsh plugin --profile tui add dsh-ssh-tui@latest
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
装完退出再启动。`/status` 可确认当前版本;装不上先看 `/doctor`。
|
|
53
|
+
更新时请显式带 `@latest`,否则已有 lockfile 可能继续沿用旧版本。
|
|
54
|
+
|
|
55
|
+
## 实测数字(本版,100×20)
|
|
56
|
+
|
|
57
|
+
| 动作 | 毫秒(中位) | 字节 | 寻址行 | 全屏清屏 |
|
|
58
|
+
|---|---|---|---|---|
|
|
59
|
+
| 流式 delta | 1.67 | 501 | 3 | **0** |
|
|
60
|
+
| 等待卡时钟 | 1.12 | 465 | 3 | **0** |
|
|
61
|
+
| 报告屏翻页 | 1.21 | 2457 | 17 | **0** |
|
|
62
|
+
| 菜单光标移动 | 0.90 | 588 | 4 | **0** |
|
|
63
|
+
| 向导字段打字 | 0.19 | 140 | 1 | **0** |
|
|
64
|
+
|
|
65
|
+
复现:`npm run bench`;README 里的同一张表由 `npm run bench:report` 从这次真跑生成。
|
|
66
|
+
本版没有触碰绘制路径,数字与 0.8.2 同一量级。
|
|
67
|
+
|
|
68
|
+
## 已知问题
|
|
69
|
+
|
|
70
|
+
- 向导字段没有左右光标键,粘贴总是追加在末尾(掩码字段 + 中间光标这条组合目前不存在)。
|
|
71
|
+
- `未测` 那一格在 Windows 本机看不到(ConPTY 代答光标位置),由管道父进程形状的探针覆盖。
|
|
72
|
+
- 完整已知问题与人工验收清单见 [`checkpoints.md`](checkpoints.md) 的 0.8.2 一节。
|
package/lib/tui.js
CHANGED
|
@@ -451,6 +451,81 @@ const QUOTA_RETRY_MS = 15_000;
|
|
|
451
451
|
const SETUP_STEPS = ['provider', 'id', 'base-url', 'key', 'models', 'models-pick', 'model-default', 'context', 'confirm'];
|
|
452
452
|
/** The steps that show a text field (the others are pickers or the confirmation). */
|
|
453
453
|
const SETUP_FIELD_STEPS = new Set(['provider', 'id', 'base-url', 'key', 'models', 'context']);
|
|
454
|
+
/**
|
|
455
|
+
* Whether one data chunk is a paste rather than keystrokes.
|
|
456
|
+
*
|
|
457
|
+
* Bracketed paste says so outright, and that is the path every modern terminal
|
|
458
|
+
* takes. A console without it sends the content as one burst instead, and a burst
|
|
459
|
+
* carrying more than one line break is not a human pressing Enter twice — they
|
|
460
|
+
* cannot press it twice inside one read. This is the fallback's one rule, kept
|
|
461
|
+
* here so every surface that can take a paste applies the same one.
|
|
462
|
+
*/
|
|
463
|
+
function looksLikePasteBurst(text) {
|
|
464
|
+
let breaks = 0;
|
|
465
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
466
|
+
const char = text[index];
|
|
467
|
+
if (char === '\r' && text[index + 1] !== '\n')
|
|
468
|
+
breaks += 1;
|
|
469
|
+
else if (char === '\n' && text[index - 1] !== '\r')
|
|
470
|
+
breaks += 1;
|
|
471
|
+
if (breaks > 1)
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Split one read into the keys it carries.
|
|
478
|
+
*
|
|
479
|
+
* A terminal hands over whatever arrived together, which is not always one key: a
|
|
480
|
+
* fast typist on a slow link sends `text\r`, a console without bracketed paste sends
|
|
481
|
+
* a whole line, and a paste is followed by Enter inside the same read. The composer
|
|
482
|
+
* has always taken a read apart (`handlePlainText` types it character by character),
|
|
483
|
+
* and a Screen has to see the same keys — a control byte is only itself when it
|
|
484
|
+
* stands alone, so a read handed to one step is a read whose Enter is dropped on the
|
|
485
|
+
* floor. That is what this returns: escape sequences whole (an arrow is one key),
|
|
486
|
+
* control bytes one each with CRLF read as the one Enter it is, and a run of
|
|
487
|
+
* printable text kept together, because a step that filters on what it is given
|
|
488
|
+
* wants the run and a paste without markers still must not answer a confirmation.
|
|
489
|
+
*/
|
|
490
|
+
function screenKeys(text) {
|
|
491
|
+
const keys = [];
|
|
492
|
+
let index = 0;
|
|
493
|
+
while (index < text.length) {
|
|
494
|
+
const char = text[index];
|
|
495
|
+
if (char === '\x1b') {
|
|
496
|
+
// A CSI sequence (`\x1b[` params final), SS3 (`\x1bO` one char), or the lone
|
|
497
|
+
// Escape key itself when nothing follows it in this read.
|
|
498
|
+
const rest = text.slice(index);
|
|
499
|
+
const sequence = /^\x1b\[[0-9;?<]*[ -/]*[@-~]/u.exec(rest)?.[0] ?? /^\x1bO[@-~]/u.exec(rest)?.[0];
|
|
500
|
+
if (sequence !== undefined) {
|
|
501
|
+
keys.push(sequence);
|
|
502
|
+
index += sequence.length;
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
keys.push('\x1b');
|
|
506
|
+
index += 1;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (char === '\r' || char === '\n') {
|
|
510
|
+
// Windows terminals send Enter as CRLF; both halves are one key.
|
|
511
|
+
const next = text[index + 1];
|
|
512
|
+
keys.push('\r');
|
|
513
|
+
index += (char === '\r' && next === '\n') || (char === '\n' && next === '\r') ? 2 : 1;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (char < ' ' || char === '\x7f') {
|
|
517
|
+
keys.push(char);
|
|
518
|
+
index += 1;
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
let end = index;
|
|
522
|
+
while (end < text.length && text[end] >= ' ' && text[end] !== '\x7f')
|
|
523
|
+
end += 1;
|
|
524
|
+
keys.push(text.slice(index, end));
|
|
525
|
+
index = end;
|
|
526
|
+
}
|
|
527
|
+
return keys;
|
|
528
|
+
}
|
|
454
529
|
/**
|
|
455
530
|
* The durable event types the plan projection reads (B2.5).
|
|
456
531
|
*
|
|
@@ -11271,6 +11346,41 @@ export class SshTui {
|
|
|
11271
11346
|
const screen = this.screen;
|
|
11272
11347
|
if (screen === undefined)
|
|
11273
11348
|
return;
|
|
11349
|
+
// Ctrl+L repaints the frame and belongs to the terminal rather than to any
|
|
11350
|
+
// surface that happens to be up: a Screen replaces the workspace entirely, so a
|
|
11351
|
+
// reader with a garbled terminal would otherwise have to leave the Screen to
|
|
11352
|
+
// clear it. The next paint is a full one, exactly as the composer's Ctrl+L is.
|
|
11353
|
+
if (combined === '\x0c') {
|
|
11354
|
+
this.screenNeedsFullPaint = true;
|
|
11355
|
+
this.markDirty();
|
|
11356
|
+
return;
|
|
11357
|
+
}
|
|
11358
|
+
// A burst of line breaks in one read is a paste from a console that cannot say so
|
|
11359
|
+
// (`looksLikePasteBurst`): it belongs in the field as one edit, which is how a
|
|
11360
|
+
// pasted key or model list survives a terminal without bracketed paste. It is
|
|
11361
|
+
// asked *before* the split below, because splitting is what would turn one paste
|
|
11362
|
+
// into a step's worth of Enters.
|
|
11363
|
+
if (looksLikePasteBurst(combined)) {
|
|
11364
|
+
this.pasteIntoScreen(combined);
|
|
11365
|
+
return;
|
|
11366
|
+
}
|
|
11367
|
+
// A read is not a key (`screenKeys`). This is the only path a Screen's input
|
|
11368
|
+
// arrives on, so a read carrying several keys — `text\r` from a fast typist on a
|
|
11369
|
+
// slow link, an arrow batched with the key after it — has to be taken apart here
|
|
11370
|
+
// or the keys after the first are spent on a step that cannot read them. Each key
|
|
11371
|
+
// is handed on its own, and the rest of the read is dropped once the Screen it was
|
|
11372
|
+
// addressed to is gone: the keys after a dismissal belong to whatever comes next,
|
|
11373
|
+
// and guessing that here is how a paste ended up in the composer in the first
|
|
11374
|
+
// place.
|
|
11375
|
+
const keys = screenKeys(combined);
|
|
11376
|
+
if (keys.length > 1) {
|
|
11377
|
+
for (const key of keys) {
|
|
11378
|
+
if (this.screen === undefined)
|
|
11379
|
+
return;
|
|
11380
|
+
this.handleScreenInput(key, this.keymap.sequences.get(key));
|
|
11381
|
+
}
|
|
11382
|
+
return;
|
|
11383
|
+
}
|
|
11274
11384
|
// The setup Screen owns its own field, its own navigation and its own keys: it
|
|
11275
11385
|
// consumes everything while it is up, and nothing reaches the workspace's
|
|
11276
11386
|
// composer, history or transcript (B2.6 §4).
|
|
@@ -11683,14 +11793,23 @@ export class SshTui {
|
|
|
11683
11793
|
* A paste is text input, so it goes through the same ownership question ordinary
|
|
11684
11794
|
* typing does: into the composer's draft when the composer owns the keyboard, and
|
|
11685
11795
|
* into a borrowed field when a surface that has one does. A surface without a
|
|
11686
|
-
* text field (a confirmation,
|
|
11796
|
+
* text field (a confirmation, a menu, a report) takes no paste at all — the
|
|
11687
11797
|
* alternative was writing the composer's hidden draft behind it, which is how a
|
|
11688
11798
|
* paste into an approval dialog could silently rewrite the message being written.
|
|
11799
|
+
*
|
|
11800
|
+
* A **Screen** is that same question with a different answer, and it is asked
|
|
11801
|
+
* first because `textIsEditable()` only ever answers for the composer's field: the
|
|
11802
|
+
* setup Screen keeps a field of its own, which the composer's helpers cannot see
|
|
11803
|
+
* and must never be handed (B2.6 §4).
|
|
11689
11804
|
*/
|
|
11690
11805
|
handlePasteText(text) {
|
|
11691
11806
|
const normalized = text.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
|
|
11692
11807
|
if (normalized === '')
|
|
11693
11808
|
return;
|
|
11809
|
+
if (this.screen !== undefined) {
|
|
11810
|
+
this.pasteIntoScreen(normalized);
|
|
11811
|
+
return;
|
|
11812
|
+
}
|
|
11694
11813
|
if (!this.textIsEditable())
|
|
11695
11814
|
return;
|
|
11696
11815
|
this.leaveHistoryBrowse();
|
|
@@ -11705,29 +11824,52 @@ export class SshTui {
|
|
|
11705
11824
|
}
|
|
11706
11825
|
this.markDirty();
|
|
11707
11826
|
}
|
|
11827
|
+
/**
|
|
11828
|
+
* Give a paste to the field the Screen owns, or to nobody.
|
|
11829
|
+
*
|
|
11830
|
+
* The setup Screen is the only Screen with a field, and it is the one place a
|
|
11831
|
+
* paste matters most: an API key, a base URL and a model list are all copied from
|
|
11832
|
+
* somewhere else, and a first run that could only be typed by hand is the
|
|
11833
|
+
* difference between a working session and an empty one. A step without a field —
|
|
11834
|
+
* the model pickers, the confirmation — is not a text field, so the paste is
|
|
11835
|
+
* dropped exactly as a menu drops it: a paste is text, never an answer.
|
|
11836
|
+
*/
|
|
11837
|
+
pasteIntoScreen(text) {
|
|
11838
|
+
const state = this.onboarding;
|
|
11839
|
+
if (this.screen?.kind !== 'setup' || state === undefined || state.saving)
|
|
11840
|
+
return;
|
|
11841
|
+
if (!SETUP_FIELD_STEPS.has(state.step))
|
|
11842
|
+
return;
|
|
11843
|
+
// The field is drawn on one row, so it takes the value and not the line breaks
|
|
11844
|
+
// around it: a key copied off a web page carries the newline that ended its
|
|
11845
|
+
// line, and a pasted list or `.env` block means the breaks as separators. Both
|
|
11846
|
+
// are folded here rather than inserted literally, which would leave the field
|
|
11847
|
+
// holding text no step can parse and no row can draw.
|
|
11848
|
+
const flat = text.split('\n').map(part => part.trim()).filter(part => part !== '').join(' ');
|
|
11849
|
+
if (flat === '')
|
|
11850
|
+
return;
|
|
11851
|
+
this.setupInsert(state, flat);
|
|
11852
|
+
this.markDirty();
|
|
11853
|
+
}
|
|
11708
11854
|
/**
|
|
11709
11855
|
* Whether the keys that edit text belong to a field right now.
|
|
11710
11856
|
*
|
|
11711
11857
|
* The composer always has one; an open surface only has one when it borrowed it
|
|
11712
|
-
* (a free-text answer
|
|
11713
|
-
*
|
|
11714
|
-
* must do nothing rather than reach the draft behind the surface.
|
|
11858
|
+
* (a free-text answer). Everything else — a confirmation, a menu, the inspect
|
|
11859
|
+
* overlay, every Screen — owns the keyboard without owning the composer's text,
|
|
11860
|
+
* and those keys must do nothing rather than reach the draft behind the surface.
|
|
11861
|
+
* A Screen is included by name: the setup Screen has a field of its own, and it
|
|
11862
|
+
* is reached through the Screen's own keys, never through the composer's.
|
|
11715
11863
|
*/
|
|
11716
11864
|
textIsEditable() {
|
|
11865
|
+
if (this.screen !== undefined)
|
|
11866
|
+
return false;
|
|
11717
11867
|
return this.dialog === undefined || this.borrowedText !== undefined;
|
|
11718
11868
|
}
|
|
11719
11869
|
handlePlainText(text) {
|
|
11720
11870
|
// Fallback for terminals without bracketed paste: a burst of multiple line
|
|
11721
11871
|
// breaks in one chunk is a paste, not repeated Enter presses.
|
|
11722
|
-
|
|
11723
|
-
for (let index = 0; index < text.length; index += 1) {
|
|
11724
|
-
const char = text[index];
|
|
11725
|
-
if (char === '\r' && text[index + 1] !== '\n')
|
|
11726
|
-
newlines += 1;
|
|
11727
|
-
else if (char === '\n' && text[index - 1] !== '\r')
|
|
11728
|
-
newlines += 1;
|
|
11729
|
-
}
|
|
11730
|
-
if (newlines > 1) {
|
|
11872
|
+
if (looksLikePasteBurst(text)) {
|
|
11731
11873
|
this.handlePasteText(text);
|
|
11732
11874
|
return;
|
|
11733
11875
|
}
|
|
@@ -11747,11 +11889,15 @@ export class SshTui {
|
|
|
11747
11889
|
}
|
|
11748
11890
|
}
|
|
11749
11891
|
handleChar(char) {
|
|
11750
|
-
// A
|
|
11751
|
-
//
|
|
11752
|
-
//
|
|
11753
|
-
//
|
|
11754
|
-
|
|
11892
|
+
// A Screen owns every key outright, and the gate comes first for the same reason
|
|
11893
|
+
// `handleInputText` puts it first: the switch below spends its keys on the
|
|
11894
|
+
// composer — Enter submits the draft, Backspace/Ctrl+U/Ctrl+K edit it, Ctrl+R
|
|
11895
|
+
// folds the transcript behind the Screen. A Screen is not a dialog, so none of
|
|
11896
|
+
// the `dialog === undefined` guards in that switch ever saw it, and every one of
|
|
11897
|
+
// those keys used to reach a draft the reader could not see. The Screen decides
|
|
11898
|
+
// what it uses (Enter and Ctrl+C close it, Esc leaves it, the wizard answers with
|
|
11899
|
+
// them); anything it does not use is dropped, never forwarded (B2.6 §4).
|
|
11900
|
+
if (this.screen !== undefined) {
|
|
11755
11901
|
this.handleScreenInput(char, this.keymap.sequences.get(char));
|
|
11756
11902
|
return;
|
|
11757
11903
|
}
|
|
@@ -12249,13 +12395,6 @@ export class SshTui {
|
|
|
12249
12395
|
this.markDirty();
|
|
12250
12396
|
return;
|
|
12251
12397
|
}
|
|
12252
|
-
if (combined === '\x0c') {
|
|
12253
|
-
// Ctrl+L still repaints the whole frame: the field is a row like any other,
|
|
12254
|
-
// and a forced repaint is how a reader clears a garbled terminal.
|
|
12255
|
-
this.screenNeedsFullPaint = true;
|
|
12256
|
-
this.markDirty();
|
|
12257
|
-
return;
|
|
12258
|
-
}
|
|
12259
12398
|
if (combined === '\x15') {
|
|
12260
12399
|
// Ctrl+U clears the wizard's field, not the workspace's draft.
|
|
12261
12400
|
this.setupSetField(state, '', 0);
|
|
@@ -12725,7 +12864,7 @@ export class SshTui {
|
|
|
12725
12864
|
this.setupMessage(state, 'system', t('onboard.baseSaved', { path: displayDshPath('settings.yaml') }));
|
|
12726
12865
|
}
|
|
12727
12866
|
if (saved) {
|
|
12728
|
-
this.
|
|
12867
|
+
this.onboardDone(t('onboard.officialDone', { model }));
|
|
12729
12868
|
}
|
|
12730
12869
|
}
|
|
12731
12870
|
else {
|
|
@@ -12898,7 +13037,7 @@ export class SshTui {
|
|
|
12898
13037
|
this.onSelectionChanged?.(selection);
|
|
12899
13038
|
await this.rememberRoute(selection);
|
|
12900
13039
|
await this.syncSubagentToProvider(ownerId, state.models, previousParentProvider !== ownerId);
|
|
12901
|
-
this.
|
|
13040
|
+
this.onboardDone(t('onboard.customDone', { id: ownerId, model }));
|
|
12902
13041
|
}
|
|
12903
13042
|
}
|
|
12904
13043
|
}
|
|
@@ -12907,12 +13046,45 @@ export class SshTui {
|
|
|
12907
13046
|
this.setupMessage(state, 'error', t('onboard.saveFailed', { error: errorChain(error) }));
|
|
12908
13047
|
}
|
|
12909
13048
|
finally {
|
|
12910
|
-
this.onboarding = undefined;
|
|
12911
13049
|
state.resolve(saved);
|
|
13050
|
+
// The Screen's exit is the answer, and the answer is one of two things.
|
|
13051
|
+
//
|
|
13052
|
+
// **Saved**: the wizard has nothing left to ask, so it hands the workspace back
|
|
13053
|
+
// in the same frame. Cancelling always did this; saving did not, so a completed
|
|
13054
|
+
// setup left the reader on the wizard's own frame until they pressed a key —
|
|
13055
|
+
// and only that key's "no wizard behind this Screen" fallback closed it. Every
|
|
13056
|
+
// confirmation the wizard would have painted on its own row went with the
|
|
13057
|
+
// Screen, which is why the completion line is a transcript row (`onboardDone`).
|
|
13058
|
+
//
|
|
13059
|
+
// **Failed**: the wizard keeps the screen, because the row it just wrote is the
|
|
13060
|
+
// reason and the answer (an API key, a Base URL) has to stay editable to retry.
|
|
13061
|
+
// Dropping the state here is what used to leave a Screen with no wizard behind
|
|
13062
|
+
// it: the error was written and then thrown away with it, and the next keypress
|
|
13063
|
+
// dismissed the whole flow as if nothing had been asked.
|
|
13064
|
+
if (saved) {
|
|
13065
|
+
this.onboarding = undefined;
|
|
13066
|
+
this.closeScreen();
|
|
13067
|
+
}
|
|
13068
|
+
else {
|
|
13069
|
+
state.saving = false;
|
|
13070
|
+
}
|
|
12912
13071
|
this.showNextDialog();
|
|
12913
13072
|
this.markDirty();
|
|
12914
13073
|
}
|
|
12915
13074
|
}
|
|
13075
|
+
/**
|
|
13076
|
+
* Say the wizard is done, where the reader will still be able to read it.
|
|
13077
|
+
*
|
|
13078
|
+
* A saved wizard closes its Screen in the same frame, so its own notice row would
|
|
13079
|
+
* be painted for no frames at all — and the credential line that goes with it is a
|
|
13080
|
+
* footer echo (`command-feedback`), which expires. This is the durable half of the
|
|
13081
|
+
* answer: `command-status` routes to the transcript, exactly like the boot line
|
|
13082
|
+
* that asked for the setup in the first place.
|
|
13083
|
+
*/
|
|
13084
|
+
onboardDone(text) {
|
|
13085
|
+
this.pushRow(represent('command-status', { kind: 'system', text }));
|
|
13086
|
+
this.markDirty();
|
|
13087
|
+
}
|
|
12916
13088
|
/** Store one credential, falling back to a launch-environment override on shadow/absence. */
|
|
12917
13089
|
async saveCredential(credentials, envRef, key) {
|
|
12918
13090
|
const shadowing = process.env[envRef];
|