@hyzyn/dsh-tty 0.30.0 → 0.32.0
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 +43 -14
- package/README.md +37 -10
- package/client.js +145 -45
- package/lib/index.d.ts +61 -7
- package/lib/index.js +109 -13
- package/lib/index.js.map +1 -1
- package/lib/shell-integration.d.ts +10 -0
- package/lib/shell-integration.js +148 -7
- package/lib/shell-integration.js.map +1 -1
- package/package.json +1 -1
- package/scripts/preview/harness.js +140 -7
- package/scripts/preview/mock-host.js +19 -0
- package/scripts/preview.mjs +11 -1
- package/scripts/pty-kill-probe.cjs +123 -0
- package/scripts/pty-kill-repro-plugin.mjs +72 -0
- package/scripts/pty-kill-repro.mjs +101 -0
- package/scripts/windows-smoke.mjs +97 -2
package/lib/index.d.ts
CHANGED
|
@@ -51,9 +51,10 @@
|
|
|
51
51
|
* reconnectGraceSec(默认 120s,0 = 旧行为立即结束),等待新连接 attach
|
|
52
52
|
* 并回放 256KB 环形缓冲;到点由回收器清理。
|
|
53
53
|
*
|
|
54
|
-
* shell 集成(src/shell-integration.ts,0.4.0):spawn
|
|
55
|
-
*
|
|
56
|
-
*
|
|
54
|
+
* shell 集成(src/shell-integration.ts,0.4.0):spawn 时注入 OSC 133/7 钩子,
|
|
55
|
+
* 输出流解析出命令边界(tty_capture{last} / tty_expect 早停)与实时 cwd(tty_list)。
|
|
56
|
+
* 注入形态按平台分:POSIX 走 `-c` 包装层 + rc 桩(zsh ZDOTDIR 桩 / bash --rcfile 桩),
|
|
57
|
+
* Windows 走 PowerShell 的 `-File` 稳定桩(cmd 没有命令边界钩子,不注入)。
|
|
57
58
|
* 辅助路由:/api/dsh-tty/ssh-config(~/.ssh/config 导入候选)、
|
|
58
59
|
* /api/dsh-tty/credential-refs(凭据存储里已知的引用名,SSH 对话框选择器候选,只要名字)、
|
|
59
60
|
* /api/dsh-tty/env-vars(env 插件托管变量名;SSH 对话框已改从连接簿 + 凭据存储取候选,此路由保留兼容)、
|
|
@@ -223,10 +224,24 @@ interface PtyHandle {
|
|
|
223
224
|
terminal?: {
|
|
224
225
|
resize?(cols: number, rows: number): void;
|
|
225
226
|
kill?(signal: string): void;
|
|
227
|
+
/**
|
|
228
|
+
* node-pty 的 conout 代理(`WindowsPtyAgent`,**只有 Windows 的
|
|
229
|
+
* `WindowsTerminal` 有这个字段**)——D100 的一次性原生 kill 闸门装在这里。
|
|
230
|
+
* 它是 `_ptyNative.kill(id)` 唯一的调用点,所以也是唯一能拦住「同一个 pty 被杀两次」
|
|
231
|
+
* 的位置(见 installWindowsKillGate)。
|
|
232
|
+
*/
|
|
233
|
+
_agent?: {
|
|
234
|
+
kill?(): unknown;
|
|
235
|
+
/** 闸门已安装的标记(本包自己加的字段,node-pty 不认)。 */
|
|
236
|
+
__dshKillGated?: boolean;
|
|
237
|
+
};
|
|
226
238
|
};
|
|
227
239
|
}
|
|
228
|
-
/** 本地 PTY 包装成 TermHandle(resize/kill 仍是透传 node-pty 的内部耦合;防御性降级)。
|
|
229
|
-
|
|
240
|
+
/** 本地 PTY 包装成 TermHandle(resize/kill 仍是透传 node-pty 的内部耦合;防御性降级)。
|
|
241
|
+
*
|
|
242
|
+
* `platform` 只为单测注入(D100 的闸门是 win32-only,macOS/Linux 上要能断言两条分支)。
|
|
243
|
+
*/
|
|
244
|
+
export declare function wrapLocalPty(handle: PtyHandle, platform?: NodeJS.Platform): TermHandle;
|
|
230
245
|
/**
|
|
231
246
|
* 本地 PTY 顶层 shell 的 best-effort 强杀(D48)。
|
|
232
247
|
*
|
|
@@ -240,6 +255,45 @@ export declare function wrapLocalPty(handle: PtyHandle): TermHandle;
|
|
|
240
255
|
* 导出仅供单测(test/host-frames.test.ts):平台参数注入,两个分支都能在 macOS/Linux 断言。
|
|
241
256
|
*/
|
|
242
257
|
export declare function killLocalShellTerminal(terminal: unknown, platform?: NodeJS.Platform): void;
|
|
258
|
+
/**
|
|
259
|
+
* 一次性原生 kill 闸门(D100,2026-10-12 Windows 真机实测)。
|
|
260
|
+
*
|
|
261
|
+
* **为什么必须有**:node-pty 的 `WindowsTerminal.kill()` 落到 `WindowsPtyAgent.kill()`,
|
|
262
|
+
* 后者对同一个 `_pty` 调 `conpty.node` 的 `kill(id)`;而 `conpty.cc` 的 `PtyKill` **不幂等**——
|
|
263
|
+
* 它 `ClosePseudoConsole(handle->hpc)` 之后**不置空 `hpc`**,那条 baton 要等 shell 进程真的退出、
|
|
264
|
+
* 由 exit 线程从 `ptyHandles` 里 `erase_if` 摘掉才算消失。这段窗口里再调一次,同一个**已释放的
|
|
265
|
+
* HPCON** 会被第二次交给 `ClosePseudoConsole`。
|
|
266
|
+
*
|
|
267
|
+
* 而 Windows 上**两个调用方必然叠加**:
|
|
268
|
+
* 1. DSH 的 `LocalTerminalHandle.stopShellWindows()` 在 `rootIdentity === undefined` 时调
|
|
269
|
+
* `terminal.kill()`;而 Windows 上 rootIdentity **恒为 undefined**——node-pty 的 `pid` 要等
|
|
270
|
+
* conout 连接建立才有值,DSH 在 `new LocalTerminalHandle(...)` 那一刻就取 `terminal.pid`,
|
|
271
|
+
* 拿到的是 **0**(真机探针逐轮实测 `spawned pid=0`;真实 pid 直到 `connect` 才出现),
|
|
272
|
+
* 于是 `snapshot().tree(0)` 找不到自己、必然走那条兜底分支;
|
|
273
|
+
* 2. 本插件 D48 的预杀(`killSessionNow` 里的 `handle.forceKill?.()`)又调一次。
|
|
274
|
+
* 两次叠加 = 同一个 HPCON 被 ClosePseudoConsole 两次 = **STATUS_HEAP_CORRUPTION(0xC0000374)**,
|
|
275
|
+
* 被 Windows 打死的是**整个宿主进程**,不是某一个会话。
|
|
276
|
+
*
|
|
277
|
+
* **实测**(Win11 ARM64,同一 harness、同一 `kill+terminate` 顺序,各 150 轮,只差这一个变量):
|
|
278
|
+
* 无闸门 → 累计 94 次双重原生 kill,第 75 轮宿主崩(退出码 -1073740940);
|
|
279
|
+
* 装闸门 → 150/150 每 pty 恰好 1 次原生 kill、0 次双重,跑满 150 轮不崩;
|
|
280
|
+
* 单次 kill 的对照(只调 terminate)150 轮也不崩。
|
|
281
|
+
*
|
|
282
|
+
* **为什么装在 `_agent.kill`(执行点)而不是 `terminal.kill`(调用点)**:node-pty 在终端 ready
|
|
283
|
+
* 之前会把自己收到的 kill **排队**(`_deferNoArgs`),到第一次 data 才执行。装在调用点的话
|
|
284
|
+
* 「排队的那次」会先把闸门占掉、DSH 真到执行的那次被吞——万一排队的那次永远不执行(进程没产出
|
|
285
|
+
* 数据、deferred 从不跑),就变成**一个都没杀**(进程泄漏)。装在执行点则谁先真正动手谁生效、
|
|
286
|
+
* 后到的才被吞,两个方向的坏结果都不会出现。
|
|
287
|
+
*
|
|
288
|
+
* **边界**:只在 win32 装——POSIX 的 `stopShell` 本来就要 SIGTERM→SIGKILL **两次**,吞第二次是错的。
|
|
289
|
+
* `_agent` 是 node-pty 的私有字段(与 `terminal.resize` 同级的内部耦合):取不到时返回 false,
|
|
290
|
+
* 调用方记一条警告后照旧调用——退化的后果是回到「双重 kill」(今天的现状),不是新增故障(D79 同款)。
|
|
291
|
+
*
|
|
292
|
+
* 导出仅供单测(test/host-frames.test.ts):平台参数注入,两个分支都能在 macOS/Linux 断言。
|
|
293
|
+
*
|
|
294
|
+
* @returns 闸门是否已就位(非 win32 恒 true)
|
|
295
|
+
*/
|
|
296
|
+
export declare function installWindowsKillGate(handle: PtyHandle, platform?: NodeJS.Platform): boolean;
|
|
243
297
|
/**
|
|
244
298
|
* Windows 本地 PTY 的**输入归一化**(D74,2026-09-27 真机报告):conhost 把 Enter 当
|
|
245
299
|
* **CR**,裸 LF 只把光标下移一格、**不提交命令行**——于是 `tty_send` 按工具描述发
|
|
@@ -480,7 +534,7 @@ interface ShellIntegrationState {
|
|
|
480
534
|
carry: string;
|
|
481
535
|
/**
|
|
482
536
|
* 这个会话**见过至少一个 OSC 133 标记**(0.23.0)。tty_list 的 `running` 能不能
|
|
483
|
-
* 下结论全看它:没有标记 = 命令边界不可信(非持久 SSH / fish·csh / Windows
|
|
537
|
+
* 下结论全看它:没有标记 = 命令边界不可信(非持久 SSH / fish·csh / Windows 的 cmd /
|
|
484
538
|
* 集成被关 / tmux <3.3 吞了 DCS 信封),此时**必须报「未知」而不是「没在跑」**。
|
|
485
539
|
*/
|
|
486
540
|
sawMark: boolean;
|
|
@@ -767,7 +821,7 @@ export interface SessionSnapshot {
|
|
|
767
821
|
/**
|
|
768
822
|
* 有命令正在执行(0.23.0):true = 在跑;false = 命令已结束(或进程已退出);
|
|
769
823
|
* **省略 = 无法判断**——这个会话没有 shell 集成标记(非持久 SSH / fish·csh /
|
|
770
|
-
* Windows
|
|
824
|
+
* Windows 的 cmd / 集成被关 / tmux <3.3 吞了信封),或命令型会话尚未收到终局。
|
|
771
825
|
* 拿它当「现在可以往里发命令」的许可时要按三态处理:省略 ≠ 没在跑。
|
|
772
826
|
*/
|
|
773
827
|
running?: boolean;
|
package/lib/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
|
17
17
|
import { sanitizeJumpSpec, sanitizeProxyCommand, spawnSsh, sshTarget, expandHome, setCredentialResolver, setProxyCommandPolicy, validateJumpSpec, validateProxyCommand } from './ssh.js';
|
|
18
18
|
import { sharedGrantStore, auditLoadedGrants, attachTierGate, bindCapabilitySources, capabilityDeniedMessage, capabilityGrantAt, capabilityGrantVia, capabilityGranted, capabilityPaths, createElevationManager, } from '@hyzyn/dsh-kit';
|
|
19
19
|
import { probeSsh } from './probe.js';
|
|
20
|
-
import { buildCommandSpawn, buildShellSpawn, commandShellHint, defaultShellPath, tmuxSocketName } from './shell-integration.js';
|
|
20
|
+
import { buildCommandSpawn, buildShellSpawn, commandShellHint, defaultShellPath, shellIntegrationSupported, tmuxSocketName } from './shell-integration.js';
|
|
21
21
|
import { KEY_VOCABULARY, resolveKeys } from './keys.js';
|
|
22
22
|
import { parseSshConfigDetailed } from './ssh-config.js';
|
|
23
23
|
import { parseKnownHostsDetailed } from './known-hosts.js';
|
|
@@ -312,9 +312,18 @@ const TTY_GUIDANCE = '**先判断要不要用终端**:只有「需要跨调用
|
|
|
312
312
|
+ '标签栏「+」还能开 SSH 标签(ssh2 原生连接;连接簿、agent forwarding、主机指纹 TOFU、单跳跳板机都在设置卡片维护);面板内另有 SFTP 文件浏览(上传/下载/建目录/重命名/删除),以及由连接簿条目配置的端口转发隧道;设置卡片开「会话持久化(tmux)」后新标签由 tmux 托管,能跨宿主重启恢复,长任务建议开着它跑。'
|
|
313
313
|
+ '用户提到「开个终端 / 在终端里跑 / SSH 到某台机器 / 传文件 / 转发端口」时,引导他打开这个面板。'
|
|
314
314
|
+ '典型流程:tty_send 启动长任务 → tty_expect 等就绪标记 → tty_capture{last:true} 拿结果。';
|
|
315
|
-
/** 本地 PTY 包装成 TermHandle(resize/kill 仍是透传 node-pty 的内部耦合;防御性降级)。
|
|
316
|
-
|
|
315
|
+
/** 本地 PTY 包装成 TermHandle(resize/kill 仍是透传 node-pty 的内部耦合;防御性降级)。
|
|
316
|
+
*
|
|
317
|
+
* `platform` 只为单测注入(D100 的闸门是 win32-only,macOS/Linux 上要能断言两条分支)。
|
|
318
|
+
*/
|
|
319
|
+
export function wrapLocalPty(handle, platform = process.platform) {
|
|
317
320
|
let resizeWarned = false;
|
|
321
|
+
// D100:在 DSH 拿到这个 terminal 之前装好一次性原生 kill 闸门(D48 的预杀与 DSH 的
|
|
322
|
+
// stopShellWindows 兜底会调同一个 terminal.kill(),而这个原生调用不幂等)。
|
|
323
|
+
if (!installWindowsKillGate(handle, platform) && !killGateUnavailableLogged) {
|
|
324
|
+
killGateUnavailableLogged = true;
|
|
325
|
+
console.warn('[dsh-tty] 未能在 node-pty 的 conout 代理(terminal._agent)上安装一次性 kill 闸门(D100):node-pty 内部结构可能已变化。Windows 上「插件的预杀 + DSH 的兜底 kill」叠加时会把同一个 HPCON 交给 ClosePseudoConsole 两次,实测可打死宿主进程(0xC0000374)');
|
|
326
|
+
}
|
|
318
327
|
// D79:README 承诺「DSH 升级若改内部结构会警告一次并退化为固定尺寸」,但老写法是
|
|
319
328
|
// `handle.terminal?.resize?.(…)` —— `?.` 只在**抛错**时才进 catch,DSH 若把
|
|
320
329
|
// `handle.terminal` 改名 / 移除(而不是让 resize 抛错),这里既不 resize 也不警告,
|
|
@@ -376,6 +385,79 @@ export function killLocalShellTerminal(terminal, platform = process.platform) {
|
|
|
376
385
|
/* 已退出 */
|
|
377
386
|
}
|
|
378
387
|
}
|
|
388
|
+
/** 「闸门没装上」只在这一处记一次(每个会话 spawn 都记就是刷屏;见 D100)。 */
|
|
389
|
+
let killGateUnavailableLogged = false;
|
|
390
|
+
/** 「闸门吞掉了一次重复终止」只记一次(正常路径上每次 Windows kill 都会命中,逐次记就是刷屏)。 */
|
|
391
|
+
let killGateSwallowedLogged = false;
|
|
392
|
+
/**
|
|
393
|
+
* 一次性原生 kill 闸门(D100,2026-10-12 Windows 真机实测)。
|
|
394
|
+
*
|
|
395
|
+
* **为什么必须有**:node-pty 的 `WindowsTerminal.kill()` 落到 `WindowsPtyAgent.kill()`,
|
|
396
|
+
* 后者对同一个 `_pty` 调 `conpty.node` 的 `kill(id)`;而 `conpty.cc` 的 `PtyKill` **不幂等**——
|
|
397
|
+
* 它 `ClosePseudoConsole(handle->hpc)` 之后**不置空 `hpc`**,那条 baton 要等 shell 进程真的退出、
|
|
398
|
+
* 由 exit 线程从 `ptyHandles` 里 `erase_if` 摘掉才算消失。这段窗口里再调一次,同一个**已释放的
|
|
399
|
+
* HPCON** 会被第二次交给 `ClosePseudoConsole`。
|
|
400
|
+
*
|
|
401
|
+
* 而 Windows 上**两个调用方必然叠加**:
|
|
402
|
+
* 1. DSH 的 `LocalTerminalHandle.stopShellWindows()` 在 `rootIdentity === undefined` 时调
|
|
403
|
+
* `terminal.kill()`;而 Windows 上 rootIdentity **恒为 undefined**——node-pty 的 `pid` 要等
|
|
404
|
+
* conout 连接建立才有值,DSH 在 `new LocalTerminalHandle(...)` 那一刻就取 `terminal.pid`,
|
|
405
|
+
* 拿到的是 **0**(真机探针逐轮实测 `spawned pid=0`;真实 pid 直到 `connect` 才出现),
|
|
406
|
+
* 于是 `snapshot().tree(0)` 找不到自己、必然走那条兜底分支;
|
|
407
|
+
* 2. 本插件 D48 的预杀(`killSessionNow` 里的 `handle.forceKill?.()`)又调一次。
|
|
408
|
+
* 两次叠加 = 同一个 HPCON 被 ClosePseudoConsole 两次 = **STATUS_HEAP_CORRUPTION(0xC0000374)**,
|
|
409
|
+
* 被 Windows 打死的是**整个宿主进程**,不是某一个会话。
|
|
410
|
+
*
|
|
411
|
+
* **实测**(Win11 ARM64,同一 harness、同一 `kill+terminate` 顺序,各 150 轮,只差这一个变量):
|
|
412
|
+
* 无闸门 → 累计 94 次双重原生 kill,第 75 轮宿主崩(退出码 -1073740940);
|
|
413
|
+
* 装闸门 → 150/150 每 pty 恰好 1 次原生 kill、0 次双重,跑满 150 轮不崩;
|
|
414
|
+
* 单次 kill 的对照(只调 terminate)150 轮也不崩。
|
|
415
|
+
*
|
|
416
|
+
* **为什么装在 `_agent.kill`(执行点)而不是 `terminal.kill`(调用点)**:node-pty 在终端 ready
|
|
417
|
+
* 之前会把自己收到的 kill **排队**(`_deferNoArgs`),到第一次 data 才执行。装在调用点的话
|
|
418
|
+
* 「排队的那次」会先把闸门占掉、DSH 真到执行的那次被吞——万一排队的那次永远不执行(进程没产出
|
|
419
|
+
* 数据、deferred 从不跑),就变成**一个都没杀**(进程泄漏)。装在执行点则谁先真正动手谁生效、
|
|
420
|
+
* 后到的才被吞,两个方向的坏结果都不会出现。
|
|
421
|
+
*
|
|
422
|
+
* **边界**:只在 win32 装——POSIX 的 `stopShell` 本来就要 SIGTERM→SIGKILL **两次**,吞第二次是错的。
|
|
423
|
+
* `_agent` 是 node-pty 的私有字段(与 `terminal.resize` 同级的内部耦合):取不到时返回 false,
|
|
424
|
+
* 调用方记一条警告后照旧调用——退化的后果是回到「双重 kill」(今天的现状),不是新增故障(D79 同款)。
|
|
425
|
+
*
|
|
426
|
+
* 导出仅供单测(test/host-frames.test.ts):平台参数注入,两个分支都能在 macOS/Linux 断言。
|
|
427
|
+
*
|
|
428
|
+
* @returns 闸门是否已就位(非 win32 恒 true)
|
|
429
|
+
*/
|
|
430
|
+
export function installWindowsKillGate(handle, platform = process.platform) {
|
|
431
|
+
if (platform !== 'win32')
|
|
432
|
+
return true;
|
|
433
|
+
const terminal = handle?.terminal;
|
|
434
|
+
// 不是真 node-pty 终端(测试夹具 / 未来结构变化)不算「闸门装不上」:先看有没有 kill
|
|
435
|
+
if (terminal === undefined || typeof terminal.kill !== 'function')
|
|
436
|
+
return true;
|
|
437
|
+
const agent = terminal._agent;
|
|
438
|
+
if (agent === undefined || typeof agent.kill !== 'function')
|
|
439
|
+
return false;
|
|
440
|
+
if (agent.__dshKillGated === true)
|
|
441
|
+
return true;
|
|
442
|
+
agent.__dshKillGated = true;
|
|
443
|
+
const real = agent.kill;
|
|
444
|
+
let killed = false;
|
|
445
|
+
agent.kill = function () {
|
|
446
|
+
if (killed) {
|
|
447
|
+
// 这一行就是「闸门真的拦到了一次双重终止」的证据:排查 Windows 上的宿主崩溃时有它可查
|
|
448
|
+
if (!killGateSwallowedLogged) {
|
|
449
|
+
killGateSwallowedLogged = true;
|
|
450
|
+
console.warn('[dsh-tty] 原生 kill 闸门已生效:同一个 PTY 的重复终止被吞掉(node-pty 的产线 kill 不幂等,重复调用会把已释放的 HPCON 再交给 ClosePseudoConsole,实测会打死宿主进程;见 D100)');
|
|
451
|
+
}
|
|
452
|
+
return undefined;
|
|
453
|
+
}
|
|
454
|
+
// 先落标记再真调(失败也不放开):放开的代价是可能再来一次双重 kill → 宿主崩溃;
|
|
455
|
+
// 不放开的代价只是「这一次没杀掉」,而调用方还有 terminate 兜底。
|
|
456
|
+
killed = true;
|
|
457
|
+
return real.call(this);
|
|
458
|
+
};
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
379
461
|
/**
|
|
380
462
|
* Windows 本地 PTY 的**输入归一化**(D74,2026-09-27 真机报告):conhost 把 Enter 当
|
|
381
463
|
* **CR**,裸 LF 只把光标下移一格、**不提交命令行**——于是 `tty_send` 按工具描述发
|
|
@@ -701,7 +783,18 @@ function osc7Path(body) {
|
|
|
701
783
|
const url = new URL(body);
|
|
702
784
|
if (url.protocol !== 'file:')
|
|
703
785
|
return undefined;
|
|
704
|
-
|
|
786
|
+
let decoded = decodeURIComponent(url.pathname);
|
|
787
|
+
/*
|
|
788
|
+
* Windows 盘符:PowerShell 桩用 `[Uri]::AbsoluteUri` 上报,`D:\DSH\x` 于是变成
|
|
789
|
+
* `file:///D:/DSH/x`、`URL.pathname` 是 `/D:/DSH/x` —— 去掉盘符前那根多余的斜杠才是
|
|
790
|
+
* 可用的 Windows 路径(`/D:/x` 形态拿去 spawn 会被当成相对盘根的路径)。
|
|
791
|
+
*
|
|
792
|
+
* UNC(`\\server\share` → `file://server/share`)的服务器段住在 host 里,而本函数按
|
|
793
|
+
* 既有语义(POSIX shell 上报的 host 是本机名)忽略 host,所以 UNC cwd 只显示成
|
|
794
|
+
* `/share/…`:纯展示降级,不参与任何 spawn 决策。
|
|
795
|
+
*/
|
|
796
|
+
if (process.platform === 'win32' && /^\/[A-Za-z]:[\\/]/.test(decoded))
|
|
797
|
+
decoded = decoded.slice(1);
|
|
705
798
|
return decoded !== '' ? decoded : undefined;
|
|
706
799
|
}
|
|
707
800
|
catch {
|
|
@@ -1097,7 +1190,8 @@ function advanceReadMark(session) {
|
|
|
1097
1190
|
* 水位线之后的**未读**原始输出。下界再抬到「最后一个 B 标记之后」:回显落在
|
|
1098
1191
|
* A(prompt 开始)与 B(命令开始)之间,所以这样能零启发式地排除回显——按
|
|
1099
1192
|
* 文本比对「跳过与发送内容相同的行」会被折行 / ANSI / 多行粘贴打碎。
|
|
1100
|
-
* 没有 B 标记(未开 shell 集成 / Windows
|
|
1193
|
+
* 没有 B 标记(未开 shell 集成 / Windows 的 cmd / 远端未装集成 / 用户关了 PSReadLine)时
|
|
1194
|
+
* 无法按边界切片:
|
|
1101
1195
|
* D75 起改用「最近提交过的输入」这份回显候选把回显行从匹配窗口里剔掉(见 echoOnlyMatch),
|
|
1102
1196
|
* 不再直接拿回显当命中。
|
|
1103
1197
|
*/
|
|
@@ -3727,20 +3821,21 @@ const plugin = definePlugin({
|
|
|
3727
3821
|
const config = plainConfig((rawConfig ?? {}));
|
|
3728
3822
|
if (config?.enabled === false)
|
|
3729
3823
|
return;
|
|
3824
|
+
// 默认 shell 按平台取(Windows 没有 $SHELL,回落 /bin/zsh 会让本地终端一条都开不起来;
|
|
3825
|
+
// 见 defaultShellPath)。用户显式填了「Shell 路径」就照用。
|
|
3826
|
+
const configuredShell = config?.shell?.trim() || defaultShellPath();
|
|
3730
3827
|
const live = new LiveConfig({
|
|
3731
|
-
|
|
3732
|
-
// 见 defaultShellPath)。用户显式填了「Shell 路径」就照用。
|
|
3733
|
-
shell: config?.shell?.trim() || defaultShellPath(),
|
|
3828
|
+
shell: configuredShell,
|
|
3734
3829
|
term: config?.term?.trim() || 'xterm-256color',
|
|
3735
3830
|
colorTerm: config?.colorTerm?.trim() || 'truecolor',
|
|
3736
3831
|
cwd: config?.cwd?.trim() || process.cwd(),
|
|
3737
3832
|
reconnectGraceSec: typeof config?.reconnectGraceSec === 'number' && Number.isInteger(config.reconnectGraceSec) && config.reconnectGraceSec >= 0 ? config.reconnectGraceSec : DEFAULT_RECONNECT_GRACE_SEC,
|
|
3738
3833
|
sshHosts: Array.isArray(config?.sshHosts) ? config.sshHosts : [],
|
|
3739
3834
|
hostKeys: Array.isArray(config?.hostKeys) ? config.hostKeys : [],
|
|
3740
|
-
// shell 集成(OSC 133/7
|
|
3741
|
-
//
|
|
3742
|
-
//
|
|
3743
|
-
shellIntegration:
|
|
3835
|
+
// shell 集成(OSC 133/7):POSIX 走 rc 注入 + `-c` 包装层,Windows 走 PowerShell 的
|
|
3836
|
+
// `-File` 桩(见 buildWindowsShellSpawn)。Windows + cmd 仍是 false —— 没有命令开始/
|
|
3837
|
+
// 结束钩子,硬开只会让界面摆一个不生效的开关。
|
|
3838
|
+
shellIntegration: config?.shellIntegration !== false && shellIntegrationSupported(configuredShell),
|
|
3744
3839
|
tunnels: Array.isArray(config?.tunnels) ? config.tunnels : [],
|
|
3745
3840
|
persistence: config?.persistence === 'tmux' ? 'tmux' : 'off',
|
|
3746
3841
|
endOnPageClose: config?.endOnPageClose === true,
|
|
@@ -3839,6 +3934,7 @@ const plugin = definePlugin({
|
|
|
3839
3934
|
hostKeys: live.hostKeys,
|
|
3840
3935
|
tunnels: live.tunnels,
|
|
3841
3936
|
shellIntegration: live.shellIntegration,
|
|
3937
|
+
shellIntegrationSupported: shellIntegrationSupported(live.shell),
|
|
3842
3938
|
sftpStyle: stateRef.sftpStyle,
|
|
3843
3939
|
persistence: live.persistence,
|
|
3844
3940
|
endOnPageClose: live.endOnPageClose,
|
|
@@ -4978,7 +5074,7 @@ const plugin = definePlugin({
|
|
|
4978
5074
|
name: 'tty_list',
|
|
4979
5075
|
// 只读工具:与同轮其它工具并发执行(宿主默认把未声明的工具当独占,见项目级 ROADMAP 第 4 项)
|
|
4980
5076
|
isConcurrencySafe: () => true,
|
|
4981
|
-
description: '列出当前终端面板会话(sid / kind(local|ssh) / target / pid / cwd / 创建与最后活动时间),**含进程已退出但仍只读保留着的会话**(带 exited:true + 退出码/信号;按时间释放时另带 retainMs)。**running** 说明这个会话有没有命令在跑(true = 在跑,现在别往里发命令,那些字节会被正在跑的程序当输入吃掉;false = 命令已结束;**省略 = 无法判断**——该会话没有 shell 集成标记,例如非持久 SSH / fish·csh / Windows
|
|
5077
|
+
description: '列出当前终端面板会话(sid / kind(local|ssh) / target / pid / cwd / 创建与最后活动时间),**含进程已退出但仍只读保留着的会话**(带 exited:true + 退出码/信号;按时间释放时另带 retainMs)。**running** 说明这个会话有没有命令在跑(true = 在跑,现在别往里发命令,那些字节会被正在跑的程序当输入吃掉;false = 命令已结束;**省略 = 无法判断**——该会话没有 shell 集成标记,例如非持久 SSH / fish·csh / Windows 的 cmd / 集成被关,**不是**「没在跑」);同源的 lastExitCode / lastExitAt 是「上一条已完成命令」的退出码与结束时刻。用户开了终端面板后,用 tty_capture 读取某个 sid 的输出、用 tty_send 向该会话发送按键。已退出的会话只能读(写会报错),要接着操作请 tty_open 新开一条。',
|
|
4982
5078
|
parameters: {},
|
|
4983
5079
|
output: {
|
|
4984
5080
|
schema: {
|