<!-- 文档分类: IMPL-RPT-019 | 阶段: 实现 | 原路径: docs/指南/终端提示符泄漏与交付空行修复-2026-05-31.md -->
# 终端提示符泄漏与交付空行修复报告

> ⚠️ **部分已过期**：本报告中针对 `backend/src/cli/liteRepl.js` 的修复随该文件于 2026-06-05 迁移官方 Ink 运行时（`backend/src/cli/tui/ink-components/`）一并失效；`repl.js` 相关部分仍有效。本文仅作历史修复记录保留，最新 TUI 行为请参阅 [tui-inquirer闪退修复-2026-06-05.md](%5BIMPL-RPT-003%5D%20tui-inquirer闪退修复-2026-06-05.md)。

> 日期: 2026-05-31
> 涉及文件: `backend/src/cli/repl.js` · `backend/src/cli/liteRepl.js` · `backend/tests/cli/repl.busyPromptRendering.test.js` · `backend/tests/cli/repl.deliverySpacing.test.js`
> 状态: 已实施，定向测试通过，真实 PTY 验证通过

---

## 一、问题背景

本轮修复聚焦两个终端显示缺陷：

1. **忙碌提示符泄漏** — 在 KHY 执行工具、打印状态或开始交付正文时，临时输入提示 `❯` 会残留到输出前缀，看起来像每段输出都带了一个提示符
2. **执行过程与交付正文粘连** — 工具结果、状态行或 `AI 回复` 头部后，正文直接紧贴输出，阅读层次不清晰

用户的预期很明确：

- `❯` 只应出现在输入态
- 一旦系统开始输出正式内容，`❯` 必须消失
- 执行过程和最终交付之间应有一行空白分隔

---

## 二、问题现象

### 2.1 提示符污染正文

典型异常表现：

```text
❯  Read 已读取目标文件...
❯  第一段交付正文...
❯  第二段交付正文...
```

这里的 `❯` 并不是正文的一部分，而是忙碌期间临时重绘出来的输入提示没有被清除。

### 2.2 交付正文贴在执行输出后

典型异常表现：

```text
● Read 已读取目标文件，定位到执行输出与正文交付边界
第一段交付正文：...
```

工具执行结束后直接进入正文，没有视觉缓冲，用户很难一眼区分“过程”和“结论”。

---

## 三、根因分析

### 3.1 `❯` 泄漏根因

`repl.js` 和 `liteRepl.js` 都有“忙碌时允许用户插话”的临时 prompt 机制。这个 prompt 会在 AI 工作中重绘到底部，提示用户可以：

- 直接输入排队
- `/s` 补充修正
- `/i` 发起中断

问题不在于 prompt 被显示，而在于：

1. busy prompt 已经画到终端上
2. 后续状态行、工具输出、流式正文直接开始打印
3. 打印前没有先清掉这条临时 prompt

于是终端 scrollback 里就留下了一个看似属于输出内容的 `❯` 前缀。

### 3.2 正文粘连根因

交付正文的来源有多条路径：

- full REPL 流式正文
- full REPL 非流式最终回填
- lite REPL 的 `AI 回复`
- full/lite 的 one-shot 单次查询

这些路径此前没有统一的“交付边界”处理。有的直接 `console.log(body)`，有的从工具输出后直接进入首段正文，因此版式不稳定。

---

## 四、修复目标

修复后必须满足以下规则：

1. busy prompt 只能存在于“等待用户输入”的瞬间
2. 任何正式输出前，都必须先清理可见的 busy prompt
3. 执行过程和交付正文之间固定插入一个空行
4. full REPL、lite REPL、one-shot 路径行为一致
5. 用回归测试锁死，避免后续改动重新引入

---

## 五、实施改动

### 5.1 busy prompt 可见性状态化

在 `repl.js` 与 `liteRepl.js` 中都引入了可见性状态：

```js
let _busyPromptVisible = false;
```

同时补充统一清理函数：

- `repl.js`：`_clearVisibleBusyPromptLine()`
- `liteRepl.js`：`_clearVisibleBusyPromptLine()`

设计原则是：

- prompt 重绘时标记为可见
- 用户提交输入、终端换边界、正式输出开始前，将其清除并重置状态

### 5.2 正式输出前先清 prompt

以下输出入口都改成“先清 prompt，再打印内容”：

- 动态状态行
- `console.log` 驱动的普通状态输出
- 首段流式正文
- 工具执行过程输出

这使得 `❯` 不再混入：

- 状态行前缀
- 工具结果前缀
- 最终正文前缀

### 5.3 full REPL 增加“交付空行”边界

在 `backend/src/cli/repl.js` 中新增了交付分隔状态：

- `_deliveryGapPending`
- `_markDeliveryGapPending()`
- `_flushDeliveryGap()`

逻辑如下：

1. 当状态输出、工具输出、完成面板输出结束时，标记“下一段如果进入正文，需要先补空行”
2. 首段流式正文开始前执行 `_flushDeliveryGap()`
3. 非流式最终正文回填前也执行 `_flushDeliveryGap()`

这样不管正文来自流式还是最终回填，都能稳定与前面的执行信息分隔。

### 5.4 lite REPL 在 `AI 回复` 后补空行

`backend/src/cli/liteRepl.js` 的主要交付路径更直接：

1. `renderer.printStepLine('success', 'AI 回复', ...)`
2. 输出 token/meta
3. 渲染正文

本次在步骤头和正文之间显式加入：

```js
console.log('');
```

确保 `AI 回复` 头部和正文始终隔开。

### 5.5 one-shot 路径统一

此前 one-shot 路径也存在同样问题：

- `startRepl({ oneShot: true })`
- `startLiteRepl({ oneShot: true })`

本次同步修复，在 `AI 回复` 步骤行后先插入一行空白，再渲染正文。

---

## 六、回归测试

### 6.1 忙碌提示符泄漏回归

新增测试文件：

- `backend/tests/cli/repl.busyPromptRendering.test.js`

覆盖点：

1. full REPL 动态状态分支在直接写终端前会清理 busy prompt
2. full REPL 首段流式正文开始前会清理 busy prompt
3. lite REPL 在 busy 状态日志打印前后会清理并恢复 prompt

### 6.2 交付空行回归

新增测试文件：

- `backend/tests/cli/repl.deliverySpacing.test.js`

覆盖点：

1. full REPL：状态输出后接非流式正文
2. full REPL：首段流式正文前必须先执行交付分隔
3. lite REPL：`AI 回复` 头后接正文
4. full REPL one-shot：`AI 回复` 后接正文
5. lite REPL one-shot：`AI 回复` 后接正文

这些测试共同锁住了“提示符清理”和“交付空行”两个边界。

---

## 七、验证结果

### 7.1 定向测试

执行：

```bash
npm --prefix backend test -- --runInBand \
  backend/tests/cli/repl.deliverySpacing.test.js \
  backend/tests/cli/repl.busyPromptRendering.test.js \
  backend/tests/cli/liteRepl.rendering.test.js \
  backend/tests/cli/repl.tasks.interaction.test.js
```

结果：

- 4 个 test suite 通过
- 13/13 tests 通过

one-shot 补充后再次执行：

```bash
npm --prefix backend test -- --runInBand \
  backend/tests/cli/repl.deliverySpacing.test.js \
  backend/tests/cli/repl.busyPromptRendering.test.js
```

结果：

- 2 个 test suite 通过
- 8/8 tests 通过

### 7.2 规则检查

执行：

```bash
node scripts/check-agent-rules.js --changed
```

结果：

- 通过，无规则违规

### 7.3 真实 PTY 验证

使用真实 PTY 会话分别验证：

1. full REPL 交互路径
2. lite REPL 交互路径

通过可控 mock 流量触发：

- 状态输出
- `tool_use`
- `tool_result`
- 正文交付

实际观察到的输出满足：

- 工具结果后先空一行，再进入正文
- `AI 回复` 头后先空一行，再进入正文
- 正文前没有可见 `❯` 泄漏

---

## 八、影响范围

本次修复覆盖以下主路径：

1. full REPL 流式交付
2. full REPL 非流式最终回填
3. full REPL one-shot
4. lite REPL 交互式回复
5. lite REPL one-shot

因此当前 KHY 终端的主要 AI 交付入口都已统一到同一套显示规则。

---

## 九、经验总结

### 9.1 临时 UI 不得混入正式输出

像 `❯` 这种 busy prompt，本质上是**可擦除的临时 UI**，不能被当成输出历史的一部分。任何 CLI 只要存在：

- prompt repaint
- inline status
- streaming text

就必须明确区分：

- 输入态临时内容
- 正式输出内容

### 9.2 交付边界必须显式建模

“执行过程”和“交付正文”不是自然就能分开的，它们必须有一个统一边界。否则不同分支会各自 `console.log()`，最终表现一定不一致。

本次引入 `_deliveryGapPending` 的核心价值不只是补一行空白，而是把“进入交付正文之前的版式切换”变成了显式状态机。

### 9.3 真实 PTY 验证不可省

这类问题单靠源码阅读不够，单靠单元测试也不够。因为：

- scrollback 污染是终端行为
- busy prompt 泄漏依赖真实 TTY 重绘
- 交付空行是否稳定，需要看真实输出顺序

所以这类终端渲染修复，必须同时具备：

1. 源码级保护
2. 单元测试回归
3. 真实 PTY smoke test

---

## 十、后续约束

后续任何涉及终端渲染的改动，都应检查以下三项：

1. 新输出路径是否会在打印前清理可见 busy prompt
2. 新交付路径是否在过程输出和正文之间补出明确边界
3. 新分支是否被纳入 `repl.busyPromptRendering.test.js` 或 `repl.deliverySpacing.test.js`

如果这三项没有同时满足，就有较高概率再次引入同类回归。
