终端体验 — 从能用到好用

功能再强,用起来不舒服,就等于没有

📝 本章目标

读完本章,你将:

  1. 用四个 Prompt 让 AI 帮你实现彩色输出进度反馈更多命令多行输入
  2. 理解终端颜色背后的 ANSI 转义序列——那些 \033[ 开头的神秘字符
  3. 掌握从 Markdown 到终端彩色输出的渲染管线
  4. 学会用事件驱动架构和命令注册表模式构建可扩展的终端界面

两辆车,同一款发动机,同样的马力。一辆的仪表盘清晰明了,方向盘手感细腻,座椅包裹感恰到好处;另一辆的仪表盘字太小看不清,方向盘硬邦邦,座椅坐久了腰疼。

你会选哪辆?

答案显而易见。发动机决定了车能不能跑,但仪表盘、方向盘、座椅决定了你想不想开

我们的 Agent 现在就像那辆”能跑但不想开”的车。功能全有——对话、工具调用、权限控制、钩子、MCP 集成——但交互体验还停留在”纯文本打印”的阶段。AI 的回复没有颜色区分,等待时没有任何反馈,输入长文本只能挤在一行里,想做什么操作得记住有限的几个命令。

这一章,我们给 Agent 装上一套舒适的仪表盘:彩色的输出让信息层次分明,旋转的进度指示器让等待不再焦虑,丰富的命令让操作随手可得,多行输入让编写长文本变得自然。

先动手造出来,再回头理解。

动手:用四个 Prompt 打造舒适的终端体验

继续在你的 harness 项目里工作。如果你跟着前面几章做了,现在应该有完整的工具系统、权限系统、钩子、技能和 MCP 集成。如果没有,去 GitHub 仓库 git checkout ch08-mcp 获取起点。

打开 Claude Code,确认你在项目根目录,然后跟着走。

Prompt 1:美化输出

我们的 Agent 现在所有输出都是白底黑字——用户输入、AI 回复、工具调用结果、错误信息全挤在一起,看起来像一面文字墙。第一步是给不同类型的内容上不同的颜色。

复制下面这段话,粘贴到 Claude Code 里:

帮我美化终端输出:

1. AI 的回复要渲染成好看的样子——
   标题加粗,代码块带边框和语法高亮,
   列表有缩进,链接有颜色,粗体和斜体也要能显示。

2. 不同类型的信息用不同颜色区分——
   用户输入用一种颜色,AI 回复用另一种,
   系统提示用灰色,错误信息用红色。

3. 工具调用的过程也要好看——
   调用了什么工具、传了什么参数、返回了什么结果,
   用缩进和颜色区分开来。

选一个现成的终端渲染库来做这件事,
不要自己从零造轮子。

等 AI 跑完,它会帮你引入终端渲染库(通常是 rich),重构输出逻辑。试一下:

$ harness
Harness v0.1.0 — AI Agent Runtime

You > 用 Python 写一个快速排序

Assistant >
┌─────────────────────────────────────────┐
│ 好的,这是一个 Python 快速排序实现:    │
│                                         │
│ ```python                               │
│ def quicksort(arr):                     │
│     if len(arr) <= 1:                   │
│         return arr                      │
│     pivot = arr[0]                      │
│     left = [x for x in arr if x < ...]  │
│     ...                                 │
│ ```                                     │
│                                         │
│ **时间复杂度**:平均 O(n log n)         │
└─────────────────────────────────────────┘

You > exit

颜色和格式出来了,但等 AI 回复的时候屏幕上什么都没有——用户不知道系统是在工作还是卡死了。

Prompt 2:进度反馈

帮我加上进度反馈:

1. 等 AI 回复时显示一个旋转的加载动画,
   旁边写上"思考中"之类的提示。

2. AI 回复如果很长,改成一个字一个字地显示出来,
   像打字一样,而不是等全部生成完才一次性蹦出来。

3. 工具执行的时候也要有提示——
   告诉用户正在执行什么,执行完了结果如何。

加载动画要优雅,不要闪烁太快也不要太慢。
$ harness
You > 帮我看看项目里有没有安全问题

⠋ 思考中...
Assistant > 我来检查一下项目的安全状况。

⚙ 执行工具:grep_search
  参数:pattern="password|secret|api_key"
  ✓ 完成(0.3s)— 找到 3 处匹配

⚙ 执行工具:file_read
  参数:path="config.py"
  ✓ 完成(0.1s)

⠋ 思考中...
Assistant > 发现了几个潜在的安全问题:
(AI 的回复一个字一个字地流式显示)

好看了,也有反馈了。但现在能用的命令只有 /help/skill——太少了。

Prompt 3:更多命令

帮我加一批实用命令:

/clear   — 清空当前对话,重新开始
/history — 显示对话历史
/model   — 查看或切换当前使用的 AI 模型
/cost    — 显示当前会话消耗了多少 token、花了多少钱
/export  — 把当前对话导出成一个文件
/compact — 手动压缩对话上下文
/tools   — 查看当前可用的所有工具

每个命令都要有简短的帮助说明。
/help 也要更新,列出所有新命令。

命令的实现方式要方便以后继续添加新命令——
加一个新命令应该只需要写一小段代码,
不用改动已有的代码。
$ harness
You > /help
Available commands:
  /help     — 显示此帮助信息
  /clear    — 清空对话,重新开始
  /history  — 显示对话历史
  /model    — 查看或切换 AI 模型
  /cost     — 显示 token 用量和费用
  /export   — 导出对话到文件
  /compact  — 手动压缩上下文
  /tools    — 查看可用工具列表
  /skill    — 查看或激活技能

You > /cost
Session cost:
  Input tokens:   1,234
  Output tokens:     567
  Total cost:    $0.0042

You > /model
Current model: claude-sonnet-4-20250514

功能齐全了。最后一个痛点——输入长文本。

Prompt 4:多行输入

帮我支持多行输入:
按回车不再直接发送,而是换行;
用一个特别的方式来表示"输入完了,发送"——
比如按两次回车、或者按某个快捷键。

另外也支持直接粘贴多行文本——
从别的地方复制一大段内容粘过来,
应该能正确识别为一整条消息,不会提前发送。

输入时要能看到行号,方便对照。
$ harness
You > 帮我审查下面这段代码:
...  1│ def process(data):
...  2│     result = eval(data["expr"])
...  3│     os.system(f"echo {result}")
...  4│     return result
...  5│
(按两次回车发送)

⠋ 思考中...
Assistant > 这段代码有严重的安全问题:

1. **`eval()` 注入** — 直接对用户数据调用 eval...
2. **命令注入** — 用 f-string 拼接 os.system...

💡 四个 Prompt 做了什么

  • Prompt 1 给了眼睛——彩色输出让信息层次分明
  • Prompt 2 给了脉搏——进度反馈让系统有了生命感
  • Prompt 3 给了双手——丰富的命令让操作随手可得
  • Prompt 4 给了笔记本——多行输入让表达不再受限

现在你手里有了一个用着舒服的 Agent。完整代码在 GitHub 仓库,对应 tag ch09-terminal

接下来我们回过头,理解你刚刚构建的东西。


深入理解

ANSI 转义序列

终端里的颜色不是魔法。每一种颜色、加粗、下划线,都是通过一段特殊字符序列实现的——ANSI 转义序列。

原理很简单:终端程序逐字符读取输出。当它读到 \033[(ESC 加上左方括号)时,就知道”接下来的字符不是要显示的文字,而是一条格式指令”。终端解析这条指令,改变后续文字的显示样式,直到遇到下一条指令或者重置命令 \033[0m

举个例子:

# ANSI 转义序列基础示例

# 红色文字
print("\033[31mThis is red\033[0m")

# 绿色加粗
print("\033[1;32mBold green\033[0m")

# 蓝色背景 + 白色文字
print("\033[44;37mWhite on blue\033[0m")

\033 是 ESC 字符(ASCII 27),[ 是控制序列引导符(CSI),后面的数字是具体指令,m 表示”这是一条样式指令”。多个指令用分号分隔。

常用的指令编号:

表 9-1:常用 ANSI 样式代码

代码效果代码效果
0重置所有样式1加粗
2变暗3斜体
4下划线7反色
30-37前景色(标准 8 色)40-47背景色(标准 8 色)
90-97前景色(高亮 8 色)100-107背景色(高亮 8 色)
38;5;N前景色(256 色)48;5;N背景色(256 色)
38;2;R;G;B前景色(真彩色)48;2;R;G;B背景色(真彩色)

从最早的 8 色,到 256 色,再到 RGB 真彩色——终端的色彩能力随着标准的演进越来越丰富。现代终端(iTerm2、Windows Terminal、Kitty)基本都支持真彩色,可以显示 1600 万种颜色。

为什么不直接手写 ANSI 序列?

原因有三:

  • 可读性差——\033[1;38;2;212;115;76m 谁能一眼看出这是什么颜色?
  • 终端兼容性——不是所有终端都支持真彩色。有些古老的终端只支持 8 色
  • 重复劳动——每次输出都要手动拼接重置码,漏了一个 \033[0m 后面全变色

所以我们用库来封装这些细节。Python 的 rich 库就是做这件事的——你告诉它”这段文字要红色加粗”,它自动生成对应的 ANSI 序列,还能检测终端能力、自动降级。

Markdown 渲染管线

Claude 的回复是 Markdown 格式——标题用 #,代码块用 ```,粗体用 **,列表用 -。要把这些 Markdown 变成终端里好看的彩色输出,需要一条渲染管线。

图 9-1:Markdown 到终端彩色输出的渲染管线

  1. 原始 Markdown → Claude API 返回的文本流
  2. 流式缓冲 → 按块累积,检测完整的 Markdown 元素
  3. Markdown 解析 → 识别标题、代码块、列表、粗体等
  4. 样式映射 → 每种元素映射到对应的终端样式
  5. ANSI 渲染 → 生成带转义序列的终端输出
  6. 终端显示 → 用户看到彩色、有格式的文字

管线中最有挑战的环节是流式缓冲。AI 的回复是一个 token 一个 token 流过来的——可能先来一个 ```,过一会儿来个 python,再过一会儿来代码内容。你不能一看到 ``` 就断定”代码块开始了”,因为也许下一个 token 会告诉你这其实是行内代码。

解决方案是贪心缓冲:遇到可能是多字符标记的起始时(如 ```**#),先把后续 token 缓冲起来,等看到足够的上下文再决定怎么渲染。这会引入微小的延迟,但保证了渲染的正确性。

Rich 库的核心架构

rich 不是简单地”在字符串里插入 ANSI 代码”。它有一套完整的渲染架构:

# harness/rendering.py — Rich 渲染管线
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.syntax import Syntax

console = Console()

def render_assistant_message(text: str):
    """把 AI 的 Markdown 回复渲染成彩色终端输出"""
    md = Markdown(text)
    console.print(Panel(
        md,
        title="Assistant",
        border_style="blue",
        padding=(1, 2),
    ))

def render_tool_call(name: str, args: dict, result: str):
    """渲染工具调用过程"""
    console.print(f"[dim]⚙ 执行工具:[bold]{name}[/bold][/dim]")
    for k, v in args.items():
        console.print(f"[dim]  {k}={v}[/dim]")
    console.print(f"[green]  ✓ 完成[/green]")

Console 是 Rich 的核心——它检测终端能力(宽度、色深、是否支持 Unicode),所有输出都通过它。Markdown 解析器把 Markdown 文本解析成内部的 RenderableType 树,然后 Console 把这棵树渲染成 ANSI 序列。

这里有一个重要的设计决策:不在 Markdown 上做字符串替换,而是先解析成结构化的树,再从树渲染到终端。这样换一种输出目标(比如 HTML)只需要换渲染器,不需要重写解析逻辑。

事件驱动架构

Prompt 2 加的”进度反馈”看起来简单,但它暴露了一个架构问题:谁来决定什么时候显示什么?

在最初的实现里,代码大概长这样:

# 旧方式:直接打印
def query_loop(state):
    while True:
        user_input = input("You > ")
        print("思考中...")
        response = call_api(state)
        print(response.text)
        for tool in response.tools:
            print(f"执行:{tool.name}")
            result = execute(tool)
            print(f"结果:{result}")

问题出在哪里?query_loop 既负责业务逻辑(调 API、执行工具),又负责界面展示(打印信息)。这两个职责纠缠在一起,导致:

  • 想改输出格式就得改业务逻辑代码
  • 想跑自动化测试就得处理一堆 print 输出
  • 想做一个 Web 界面就得把整个 query_loop 重写

解决方案是事件驱动——业务逻辑只负责产生事件,界面层订阅事件并决定怎么展示:

# 新方式:事件驱动
def query_loop(state):
    while True:
        user_input = input("You > ")
        yield Event("thinking_start")
        response = call_api(state)
        yield Event("thinking_end")
        yield Event("response", text=response.text)
        for tool in response.tools:
            yield Event("tool_start", name=tool.name)
            result = execute(tool)
            yield Event("tool_end", name=tool.name,
                       result=result)

业务逻辑通过 yield 发出事件,完全不关心这些事件怎么被展示。终端界面层(或 Web 界面层、或测试层)各自监听这些事件,做自己的事:

图 9-2:事件驱动 vs 直接打印

  1. 业务逻辑层 → yield Event(…) — 只产生事件,不做任何展示
  2. 事件分发器 → 把事件路由到所有已注册的监听器
  3. 终端渲染器 → 监听事件,用 Rich 渲染彩色输出
  4. 日志记录器 → 监听事件,写入日志文件
  5. Web 前端 → 监听事件,通过 WebSocket 推送到浏览器

同一套业务逻辑,接上不同的渲染器就能适配不同的界面。这就是关注点分离的威力。

💡 核心概念:Generator 模式与事件流

Python 的 yield 让函数变成一个生成器(Generator)——调用它不会一次性执行完,而是每次执行到 yield 就暂停,把值交给调用者,等调用者准备好了再继续。

这天然适合事件驱动架构:业务逻辑不断 yield 事件,调用者逐个消费这些事件。整个过程是惰性求值的——事件只有在被消费时才会驱动下一步执行。

对比回调(callback)模式:回调需要事先注册,逻辑分散在各个回调函数里,调试时调用栈跳来跳去。Generator 模式的逻辑是线性的,yield 之后的代码就是”事件被处理后的下一步”,读起来和写起来都更直觉。

命令系统设计

Prompt 3 要求”加一个新命令应该只需要写一小段代码,不用改动已有的代码”。这是一个典型的开闭原则需求——对扩展开放,对修改关闭。

实现方式是**命令注册表(CommandRegistry)**模式:

# harness/commands.py — 命令注册表
from dataclasses import dataclass
from typing import Callable

@dataclass
class Command:
    name: str
    description: str
    handler: Callable

class CommandRegistry:
    def __init__(self):
        self._commands: dict[str, Command] = {}

    def register(self, name: str, description: str,
                 handler: Callable):
        self._commands[name] = Command(name, description,
                                       handler)

    def execute(self, name: str, args: str, state):
        cmd = self._commands.get(name)
        if cmd is None:
            return f"Unknown command: /{name}"
        return cmd.handler(args, state)

    def list_all(self) -> list[Command]:
        return sorted(self._commands.values(),
                     key=lambda c: c.name)

注册一个新命令只需要一行:

# 注册命令示例
registry = CommandRegistry()

registry.register("clear", "清空对话,重新开始",
    lambda args, state: state.reset())

registry.register("cost", "显示 token 用量和费用",
    lambda args, state: format_cost(state.usage))

registry.register("model", "查看或切换 AI 模型",
    lambda args, state: switch_model(args, state))

更优雅的方式是用装饰器

# 用装饰器注册命令
def command(name: str, description: str):
    def decorator(func):
        registry.register(name, description, func)
        return func
    return decorator

@command("cost", "显示 token 用量和费用")
def cmd_cost(args: str, state):
    input_tokens = state.usage.input_tokens
    output_tokens = state.usage.output_tokens
    cost = calculate_cost(input_tokens, output_tokens)
    return (
        f"Input tokens:  {input_tokens:>8,}\n"
        f"Output tokens: {output_tokens:>8,}\n"
        f"Total cost:    ${cost:.4f}"
    )

现在所有已注册的命令一览:

表 9-2:Harness 命令列表

命令功能来源
/help显示帮助信息,列出所有可用命令内置
/clear清空当前对话上下文,重新开始内置
/history显示当前会话的对话历史内置
/model查看当前模型或切换到另一个模型内置
/cost显示本次会话的 token 用量和估算费用内置
/export把当前对话导出为 JSON 或 Markdown 文件内置
/compact手动触发上下文压缩内置
/tools列出当前可用的所有工具(含 MCP 工具)内置
/skill查看或激活一个技能包第 7 章

想扩展?在 .harness/commands/ 目录放一个 Python 文件,定义一个函数、用装饰器注册——启动时自动发现并加载。和技能系统的设计思路如出一辙:把扩展点暴露给用户,让他们不用改核心代码就能加功能

命令解析

用户输入 /model claude-sonnet-4-20250514 时,需要把它拆成命令名和参数:

# 命令解析逻辑
def parse_command(input_text: str):
    """解析斜杠命令,返回 (name, args) 或 None"""
    if not input_text.startswith("/"):
        return None
    parts = input_text[1:].split(maxsplit=1)
    name = parts[0]
    args = parts[1] if len(parts) > 1 else ""
    return (name, args)

简单到几乎不值一提——但这种”简单”是刻意设计的。命令系统的复杂度应该在命令本身,而不是在解析和分发逻辑上。解析器就做一件事:拆分命令名和参数。分发器就做一件事:查表调用。每个命令的 handler 各自负责自己的复杂逻辑。

Claude Code 的终端实现

我们用 Python + Rich 做终端渲染,功能够用,但和 Claude Code 的终端体验比起来还是有差距。来看看 Claude Code 做了什么不一样的事。

Ink:终端里的 React

Claude Code 的终端界面不是用传统的”逐行打印”方式构建的,而是用了一个叫 Ink 的框架——它让你用 React 组件的方式来构建终端 UI。

// Ink 组件示例(概念性)
import { render, Text, Box } from "ink";
import Spinner from "ink-spinner";

function ThinkingIndicator({ message }) {
  return (
    <Box>
      <Spinner type="dots" />
      <Text color="gray"> {message}</Text>
    </Box>
  );
}

function ToolCallDisplay({ name, status }) {
  return (
    <Box flexDirection="column" paddingLeft={2}>
      <Text>
        <Text color="yellow"></Text> {name}
      </Text>
      <Text color="green">  ✓ {status}</Text>
    </Box>
  );
}

这段代码看起来就像写 Web 前端——<Box> 对应 <div><Text> 对应 <span>flexDirection 是 Flexbox 布局。但它渲染的目标不是浏览器,而是终端。

Ink 在终端上模拟了一个虚拟 DOM

图 9-3:Ink 的虚拟 DOM 终端渲染流程

  1. React 组件树 → JSX 转为 Virtual DOM
  2. Diff 计算 → 新旧 DOM 差异
  3. Yoga 布局 → Flexbox 转为行列坐标
  4. ANSI 输出 → 只更新变化的区域

为什么要用这么”重”的方案?因为 Claude Code 的终端界面不是简单的”打印文本”——它有多个同时更新的区域:顶部是对话内容,底部是输入框,中间可能有工具调用的进度条,侧边可能有 token 计数。这些区域需要独立更新,互不干扰。传统的逐行打印做不到这一点——你打印了一行新内容,就没法回头修改上面的内容了。

Ink 通过接管整个终端屏幕,用光标定位和局部重绘来实现多区域更新。就像游戏引擎不是一行行画像素,而是管理一个帧缓冲区,每帧只更新变化的部分。

柔和的色彩系统

注意 Claude Code 的配色——不是鲜艳的纯色,而是柔和的低饱和色(pastel)。这不是随便选的,而是经过深思熟虑的设计决策:

  • 暗色主题下纯白文字太刺眼,Claude Code 用 #C0A090 这样的暖灰色做正文
  • 代码高亮用低饱和度的蓝、绿、橙——长时间看不累
  • 错误信息用暗红而不是亮红——严肃但不刺激
  • 系统信息用灰色——存在但不抢注意力

💡 终端配色的经验法则

好的终端配色遵循三条原则:

  1. 信息层级——最重要的信息用最醒目的颜色(通常是暖色:橙、赤),次要信息用冷色或灰色
  2. 对比度足够——在暗色背景上,文字颜色的亮度至少要达到 WCAG AA 标准(4.5:1 对比度)
  3. 颜色数量克制——全局不超过 5-6 种主色。颜色太多等于没有颜色——什么都强调就是什么都不强调

用户体验的细节

“好用”藏在细节里。几个值得深挖的 UX 细节:

Spinner:等待的艺术

等待时的旋转动画看起来是个小事,但有讲究:

表 9-3:常见 Spinner 样式对比

样式帧序列适用场景
dots⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏通用,最常见
line-|/简洁,ASCII 兼容
arc◜◠◝◞◡◟优雅,适合等待较长操作
bounce⠁⠂⠄⠂轻快,适合短操作

Claude Code 用的是 dots 系列的 Braille 点阵动画——8 帧循环,每帧间隔约 80ms。太快了会感觉焦躁(< 50ms),太慢了会感觉卡顿(> 150ms),80ms 刚好给人一种”正在流畅运转”的感觉。

流式输出:一个字一个字来

AI 回复的流式输出不仅是体验优化,更是感知性能优化。假设一次回复需要 3 秒生成完毕:

  • 非流式:等 3 秒,突然出现一大段文字。用户感觉”卡了 3 秒”
  • 流式:0.2 秒后开始逐字出现,3 秒后全部显示完。用户感觉”几乎立即就开始回答了”

实际等待时间一样,但感知完全不同。这就是**首字节时间(Time to First Token)**的价值。

流式输出的实现用的是 API 的 streaming 模式。后端每生成一个 token 就立即推送,前端收到就立即渲染。我们在第 2 章就实现了 streaming,现在只需要把”直接打印 token”改成”通过 Rich 渲染 token”。

多行输入的实现

多行输入看似简单,实际上要处理几种不同的情况:

键盘输入。 用户敲回车时,不发送,而是换行。双击回车(或 Ctrl+D)才发送。这需要用 prompt_toolkit 或类似的库来接管键盘输入——Python 内置的 input() 做不到这种级别的控制。

粘贴检测。 用户从外部粘贴多行文本时,终端会在极短时间内(< 5ms)收到大量回车。正常手动输入两次回车之间至少有几十毫秒。利用这个时间差可以区分”粘贴的换行”和”用户主动按的回车”。

行号显示。 多行模式下显示行号,用对齐的灰色数字,帮助用户对照代码的行数。

# 多行输入的核心逻辑(简化)
from prompt_toolkit import PromptSession
from prompt_toolkit.key_binding import KeyBindings

bindings = KeyBindings()

@bindings.add("enter", "enter")
def submit(event):
    event.current_buffer.validate_and_handle()

@bindings.add("enter")
def newline(event):
    event.current_buffer.insert_text("\n")

session = PromptSession(
    multiline=True,
    key_bindings=bindings,
    prompt_continuation="... ",
)
user_input = session.prompt("You > ")

prompt_toolkit 是 Python 终端输入的瑞士军刀——它能接管所有键盘事件,支持自定义按键绑定、语法高亮、自动补全。IPython 和很多 CLI 工具底层都用的它。

进度条与 Token 计数

长时间的工具执行(比如跑测试)应该有进度反馈。问题是——大多数工具执行时你不知道总量,所以没法显示”50% 完成”。

解决方案是脉冲式进度条(indeterminate progress bar)——不显示百分比,只显示一条来回滚动的光带,表示”正在执行中”。Rich 库自带这种进度条:

# 脉冲式进度条
from rich.progress import Progress, SpinnerColumn, TextColumn

with Progress(
    SpinnerColumn(),
    TextColumn("[bold blue]{task.description}"),
) as progress:
    task = progress.add_task("执行工具...", total=None)
    result = execute_tool(tool)
    progress.remove_task(task)

total=None 告诉 Rich “我不知道总量”,它就会自动切换到脉冲模式。

Token 计数则是另一种反馈——不是实时进度,而是累积统计。在输入框旁边或状态栏里显示 tokens: 1.2k / 200k,让用户时刻知道上下文窗口还剩多少空间。这个信息决定了”要不要手动 /compact”——如果快撑满了,压缩一下比”突然收到报错”友好得多。


延伸思考

在进入下一章之前,想想这几个问题:

  1. 我们的渲染层和业务逻辑通过事件解耦了。如果要做一个 Web 界面版本的 harness,事件系统需要怎么改?WebSocket 推送和终端打印在事件消费方式上有什么根本区别?
  2. Claude Code 用 Ink(React for Terminal)来构建 UI,我们用 Rich(Python 库)。两种方案的本质差异在哪里——是语言选择的结果,还是架构理念的不同?在什么场景下”组件化终端 UI”比”逐行渲染”更合适?
  3. 终端 UI 的信息密度是有限的——屏幕宽度通常只有 80-120 列。当 AI 一边流式输出回复、一边触发工具调用、工具又在后台跑——这三件事同时发生时,你会怎么安排终端的空间分配,才能让用户不迷失?

章节小结

  • 四个 Prompt 构建了舒适的终端体验:彩色输出 → 进度反馈 → 命令系统 → 多行输入
  • ANSI 转义序列是终端颜色的底层原理:\033[ 开头的控制序列改变文字样式,从 8 色到真彩色不断演进
  • Markdown 渲染管线把 AI 的回复变成彩色终端输出:流式缓冲 → Markdown 解析 → 样式映射 → ANSI 渲染
  • 事件驱动架构解耦了业务逻辑和界面展示:yield Event(...) 让同一套逻辑适配不同的界面
  • CommandRegistry 模式让命令系统可扩展:注册一个新命令只需要一行代码,不用改已有逻辑
  • Claude Code 用 Ink(React for Terminal)+ 虚拟 DOM 实现多区域独立更新的终端界面
  • 用户体验藏在细节里:Spinner 的帧率、流式输出的首字节时间、多行输入的粘贴检测、柔和的配色系统
  • 下一章我们给 Agent 加上测试和质量保障——确保这些功能不会在迭代中悄悄坏掉
English EN 简体中文 ZH