# 终端体验：语音、多语言与视觉

> MiMo 在 TUI 里新增的两种语音输入模式与 voice_input 协议、七份界面词典与按时区猜语言、星空与背景图、侧栏插件和一批新斜杠命令。

- 作者：David（道雾轩）
- 专栏：MiMo Code 源码解读（https://daiw.org/manual/mimo-code.md）
- 最后更新：2026-09-17
- 原文：https://daiw.org/manual/mimo-code/tui
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

MiMo Code 的终端界面仍在 `packages/opencode/src/cli/cmd/tui`，和底座 v1.14.19 同一个位置。OpenCode 后来把界面拆成了独立的 `packages/tui` 包，[终端界面](https://daiw.org/manual/opencode/tui)一篇写的是拆包之后的 v1.18.30，文件路径对不上，但骨架相同：OpenTUI 渲染、SolidJS 组件、KV 里存本地偏好、内置功能用插件插槽拼装。本篇只讲 MiMo 在这套骨架上加的东西。

## 改动分布

与 v1.14.19 的同名目录相比，TUI 新增 54 个文件、修改 105 个、原样保留 40 个，删除 1 个（主题 `opencode.json`）。新增文件里有一个 283349 字节的 `ten_vad.wasm`，其余 53 个文本文件合计 8711 行。按子目录看（修改文件的新增行是逐行比对的粗估）：

| 子目录 | 新增文件（行） | 修改文件（新增行） | 主要内容 |
| --- | --- | --- | --- |
| `i18n` | 10（4399） | 无 | 七种语言的界面词典 |
| `component` | 17（1996） | 16（+1605） | 登录、模态、背景图等对话框，星空背景 |
| `util` | 11（1442） | 7（+247） | 语音、VAD、系统语言检测、拼音 |
| `routes` | 3（39） | 10（+2204） | 会话页新增约 1850 行，现有 3826 行 |
| `context` | 4（413） | 42（+968） | 语言、视觉模式、MiMo 主题；修改的文件里 32 个是主题 JSON，只改了 `$schema` 地址 |
| 根目录 | 无 | 5（+964） | `app.tsx` 注册的新命令，`thread.ts` 与 `worker.ts` 的启动与监听 |
| `feature-plugins` | 5（302） | 10（+322） | 侧栏插件 |
| 其他 | 4（120 行与 wasm） | 15（+336） | 语音资源、按压交互、插件 API |

输入框 `component/prompt/index.tsx` 新增约 880 行，现有 2105 行，语音输入的状态机就在这里。上游 TUI 没有词典，界面文字直接写在代码里；MiMo 的 TUI 源码里有 305 处以 `tui.` 为前缀查词典的调用，分布在 30 个文件中，这也是修改文件多的原因之一。

## 语音输入

### 怎么用

- `/voice` 开关语音输入，打开后输入框右侧出现一个 Voice 按钮，点一下开始录音、再点一下停止（`packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx:813`）。
- `/voice-control` 在“快速输入”与“智能编辑”两种模式间切换，`/voice-send` 允许在智能编辑模式里说“发送”来提交（`prompt/index.tsx:848`、`prompt/index.tsx:831`）。这三个开关都存在本地 KV 里，默认全关。
- 模型由配置的 `voice.asr_model` 与 `voice.control_model` 指定，缺省分别是 `xiaomi/mimo-v2.5-asr` 与 `xiaomi/mimo-v2.5`（`packages/opencode/src/config/config.ts:393`）。换成别的提供方时，只要它有 API key 并能解析出 `baseURL` 即可：先取提供方的 `options.baseURL`，再取该提供方第一个模型的 API 地址，`xiaomi` 最后兜底为 `https://api.xiaomimimo.com/v1`（`packages/opencode/src/cli/cmd/tui/util/voice.ts:21`）。
- 需要命令行录音工具：macOS 用 `sox` 或 `rec`，Linux 用 `arecord` 或 `sox`，Windows 只认 `sox`，统一录成 16 kHz、单声道、16 位的原始 PCM（`voice.ts:59`）。

语音不走[内置 LLM 服务](https://daiw.org/manual/mimo-code/llm-server)的 `/v1` 接口，而是 TUI 直接带着提供方的密钥请求 `<baseURL>/chat/completions`，并加上 `X-Mimo-Source` 头。

### 录音与切段

录音进程的标准输出按 16 位样本对齐后喂给 `RealtimeVAD`（`voice.ts:116`）。VAD 用的是 Agora 开源的 TEN VAD，编译成 WebAssembly 随包分发，许可证为 Apache 2.0（`packages/opencode/src/cli/cmd/tui/asset/TEN_VAD_LICENSE`）。它每 256 个样本（16 kHz 下 16 毫秒）算一次语音概率（`packages/opencode/src/cli/cmd/tui/util/vad.ts:4`），参数缺省为：进入说话的阈值 0.8、维持说话的阈值 0.7、向前补 0.6 秒、静默 0.8 秒切段、单段最长 60 秒（`vad.ts:91`）。智能编辑模式把静默时长放宽到 1.2 秒（`prompt/index.tsx:308`）。

切段判定在 `vad.ts:202`：

```ts
if (prob >= this.endThreshold) {
  this.silenceStartS = null
  this.sumPositiveS += hopS
} else if (this.silenceStartS === null) {
  this.silenceStartS = chunkOffsetS
}

const shouldCut =
  (this.silenceStartS !== null && chunkOffsetS - this.silenceStartS >= this.minSilenceS) ||
  (chunkOffsetS - this.activeStartS >= this.maxSegmentS)
```

短于 0.2 秒的片段直接丢弃（`vad.ts:216`），初始化时先喂 25 帧静音（`vad.ts:108`）。每切出一段就立即发请求，不必等录音结束：快速输入的各段并发转写，智能编辑的各段排队依次处理（`prompt/index.tsx:324`）。录音期间按钮在等待说话、说话中（带计时）与处理中三种状态之间切换（`prompt/index.tsx:1890`）。

### 两种模式

```mermaid
flowchart TD
  A["sox 或 arecord 录音"] --> B["TEN VAD 按静默切段"]
  B --> C{"智能编辑是否开启"}
  C -- "否，快速输入" --> D["ASR 模型转写"]
  D --> E["在光标处插入文字"]
  C -- "是" --> F["多模态模型加 voice_input 工具"]
  F --> G{"恰好一次合法调用"}
  G -- "否，最多重试 2 次" --> F
  G -- "是" --> H["插入、改写或选中，可选发送"]
```

**快速输入**调用 ASR 模型，请求里只有一段 `input_audio`（WAV 编成 data URL）和 `asr_options`，语言设为自动，超时 30 秒（`voice.ts:202`）。转写结果插到这段录音送去转写时光标所在的位置，有选区就替换选区；插在文末时，若原文以英文句点、问号或叹号结尾就先补一个空格（`packages/opencode/src/cli/cmd/tui/util/voice-edit.ts:81`）。快速输入没有发送命令，说“发送”也只是插入这两个字。

**智能编辑**让多模态模型理解意图。请求由系统提示词、一段描述输入框现状的 JSON 和录音组成，并登记唯一的工具 `voice_input`（`voice.ts:367`）。输入框现状有两种写法：有可靠光标时拆成 `before_cursor`、`selection`、`after_cursor` 三段，输入框没有焦点时只给整段文本。工具参数是一个可选的 `operation` 加一个可选的 `send`，`operation` 三选一（`voice.ts:303`）：

| 动作 | 含义 |
| --- | --- |
| `insert` | 在光标处插入片段，或替换选区 |
| `set` | 给出完整的新文本，光标放到末尾 |
| `set_with_cursor` | 给出三段式的完整新文本，用来移动光标或保留选区 |

系统提示词 `voice-input.txt` 的规则用英文写成，例子以中文口语为主。核心约束在第 3 行（`packages/opencode/src/cli/cmd/tui/util/voice-input.txt:3`）：

> Your ONLY valid reply is a `voice_input` tool call. Never write text in the content field. Never put JSON in the content field. Your entire response must be exactly one tool call — no preamble, no explanation, no JSON object as text.

它还反复强调，用户说出的绝大多数话是要交给编码 Agent 的内容，应当转写后插入，模型不要自己去执行；只有明确针对输入框的删改、清空、移动光标和发送才算命令。

回复不合规（没有调用工具、调用了多次、工具名不对、参数不是合法 JSON 或不符合 schema）时，把模型上一条回复与错误说明附在对话后面重发，最多重试 2 次，即一段录音最多发 3 次请求（`voice.ts:490`）；有工具调用时错误说明以 tool 角色回复，否则以 user 角色补一条（`voice.ts:401`）。`send` 只在打开了 `/voice-send` 时生效，关着时直接丢弃，不触发重试。

结果回来后，若输入框的内容在请求期间被用户改过，这次编辑整个作废并提示“输入内容已变化，语音编辑已跳过”（`prompt/index.tsx:342`）。快速输入则退而求其次，把转写结果追加到当前文本末尾。内容没变时的插入走编辑器自己的 `insertText`，粘贴块、文件引用这类标记得以保留；`set` 与 `set_with_cursor` 整段重写，这些标记随之清空（`prompt/index.tsx:204`）。

### 与设计文档对照

仓库里的 `docs/compose/spec/voice-control-tool-protocol.md` 记录了这次从旧 JSON 协议迁到 `voice_input` 工具协议的设计，状态为已交付。对照代码，唯一的工具、不设 `tool_choice`、协议错误最多重试 2 次、两种模式的静默时长、输入被改过时丢弃编辑、ASR 请求格式不变，都与文档一致。有一处细节不同：文档正文写的是协议出错时追加 assistant 消息和一条 user 角色的错误说明（`docs/compose/spec/voice-control-tool-protocol.md:73`），代码在有工具调用时改用 tool 角色回复，文档开头的交付记录里把它列为评审后的修改（`voice-control-tool-protocol.md:15`）。

## 多语言

v1.14.19 的 TUI 文字都是写死的英文。MiMo 加了 `i18n` 目录，放 en、zh、zht、ja、ru、fr、es 七份界面词典，每份五六百行；界面组件包 `@mimo-ai/ui` 另有 17 种语言的词典（`packages/ui/src/i18n`）。加载时两者合并，组件包的词条优先，缺失的键回落到英文（`packages/opencode/src/cli/cmd/tui/context/language.tsx:15`）。韩语、德语等十种语言只有组件包词典，TUI 自己的文字仍显示英文。

语言偏好存在 KV 的 `locale` 里，缺省为 `auto`（`language.tsx:54`），用 `/language`（别名 `/lang`）切换（`packages/opencode/src/cli/cmd/tui/app.tsx:1122`）。`auto` 的判定顺序值得注意：先看时区，再看环境变量（`packages/opencode/src/cli/cmd/tui/util/system-locale.ts:190`）：

```ts
export function detectSystemLocale(): Locale {
  const tz = detectTimezoneLocale()
  if (tz) return tz
  for (const env of ["LC_ALL", "LC_MESSAGES", "LANG", "LANGUAGE"] as const) {
    const value = process.env[env]
    if (!value) continue
    for (const raw of value.split(":")) {
      const cleaned = raw.replace(/[.@].*$/, "").replace(/_/g, "-").toLowerCase()
      if (!cleaned || cleaned === "c" || cleaned === "posix") continue
      const match = matchers.find((m) => m.test(cleaned))
      if (match) return match.locale
    }
  }
  try {
    const intl = Intl.DateTimeFormat().resolvedOptions().locale.toLowerCase()
    const match = matchers.find((m) => m.test(intl))
    if (match) return match.locale
  } catch {}
  return "en"
}
```

时区表只覆盖有 TUI 词典的语言：`Asia/Shanghai` 等内地时区判为简体中文，香港、澳门、台北判为繁体中文，此外还有日语、俄语、法语、西班牙语与英语的时区列表（`system-locale.ts:175`）。所以系统语言是英文、时区在上海的用户，默认看到的是中文界面，需要手动 `/language` 改回来。

命令行里 TUI 之外的提示（比如登录流程）走另一个更简单的翻译函数，只带 en、zh、ja、fr、ru 五份词典，不读 KV 偏好，只看系统检测结果（`packages/opencode/src/cli/i18n.ts:8`）。内置技能与 `init`、`review`、`dream`、`distill`、`goal`、`deep-research` 这几个内置命令的描述也会翻译（`packages/opencode/src/cli/cmd/tui/i18n/slash-command.ts:1`），内置技能还可以通过词典登记中文斜杠别名（`packages/opencode/src/cli/cmd/tui/i18n/skill.ts:11`）。

还有一个方便中文用户的细节：通用列表选择框 `DialogSelect` 的模糊搜索多了一个拼音键，“切换会话”可以用 `qiehuanhuihua`、`qie huan hui hua` 或首字母 `qhhh` 搜到，不必切换输入法（`packages/opencode/src/cli/cmd/tui/util/pinyin.ts:11`、`packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx:104`）。

## 视觉

- **丰富与极简**。`/vivid` 在 `vivid` 与 `minimal` 两种视觉模式间切换，缺省为 `vivid`；动效只在 `vivid` 且 KV 的 `animations_enabled` 为真时播放（`packages/opencode/src/cli/cmd/tui/context/visual.ts:14`）。受它控制的有星空背景、流星、Logo 扫光和进行中标记的动画。
- **星空背景**。首页默认铺一层星空：每个字符格以 0.00394 的概率放一颗星，每 200 毫秒随机改变一批星的亮度，每 8 秒划过一颗持续 3.6 秒的流星（`packages/opencode/src/cli/cmd/tui/component/starry-background.tsx:5`）。流星用盲文字符的 2×4 点阵画出亚字符精度的斜线与圆头（`starry-background.tsx:61`）。纯文本终端里不画。
- **背景图**。`/background` 列出配置目录下 `backgrounds/` 里的 PNG 与 JPEG，也能输入路径导入，光标移动时即时预览，取消则恢复原设置（`packages/opencode/src/cli/cmd/tui/component/dialog-image-list.tsx:13`）。在 kitty、Ghostty、WezTerm 里 PNG 走 kitty 图形协议，交给终端按整屏尺寸绘制（`packages/opencode/src/cli/cmd/tui/util/image-protocol.ts:3`）；其他情况用上半块字符 `▀` 的前景色和背景色各画半行像素，按铺满屏幕的方式裁切，图片颜色以 45% 的比例混进主题背景色（`packages/opencode/src/cli/cmd/tui/component/background-image.tsx:117`）。
- **Logo 与主题**。`/logo` 切换首页 Logo 的样式（`app.tsx:928`）。默认主题从上游的 `opencode` 换成 `mimocode`（`packages/opencode/src/cli/cmd/tui/context/theme.tsx:194`），主色由上游暗色的 `#fab283`、亮色的 `#3b7dd8` 改成暗色 `#FF6A00`、亮色 `#C45200` 的橙色（`packages/opencode/src/cli/cmd/tui/context/theme/mimocode.json:12`）。

## 侧栏插件

侧栏内容仍由内置插件按 `order` 往插槽里填。MiMo 新增了四个，并改了上游的上下文面板（`packages/opencode/src/cli/cmd/tui/plugin/internal.ts:21`）：

| 插件 | order | 显示 |
| --- | --- | --- |
| `internal:sidebar-context` | 100 | 上游已有。MiMo 加了生效的上下文上限与生成速度 |
| `internal:sidebar-cwd` | 125 | 当前工作目录，主目录缩写成 `~` |
| `internal:sidebar-instructions` | 150 | 已加载的指令文件，如 `AGENTS.md`、`CLAUDE.md` |
| `internal:sidebar-goal` | 350 | 目标条件与裁判的最新结论，见 [Goal](https://daiw.org/manual/mimo-code/goal) |
| `internal:sidebar-task` | 400 | 进行中、待办、受阻的任务，外加最近完成的 3 个，见[树状任务](https://daiw.org/manual/mimo-code/tasks) |

生成速度由 `packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/tps.ts` 计算：流式输出期间，把已收到的正文与推理文本按每 4 个字符一个 token 估算，除以从消息创建到现在的秒数，不足 0.5 秒不显示，每秒刷新；输出完成后改用提供方报告的输出与推理 token 数，除以消息从创建到完成的时长（`packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx:38`）。两种算法的起点都是消息创建时间，包含等待首个 token 的时间。上下文上限一行显示生效的上限，若上限来自配置（比如用 `/context-limit` 设过），还会附上模型窗口的大小（`context.tsx:97`）。新插件里 cwd 与指令文件两块走了词典，Goal 与 Tasks 两块的标题仍是写死的英文。

## 其他新命令与对话框

与 v1.14.19 相比，TUI 在命令面板里登记的斜杠命令从 26 个增加到 49 个（服务端的 `/goal`、`/dream` 这类命令不在此列）。除了上文的语音、语言、视觉命令，以及[模型、提供方与提示词路由](https://daiw.org/manual/mimo-code/providers)里讲过的 `/login`、`/logout`、`/modalities`，还有：

| 命令 | 作用 | 出处 |
| --- | --- | --- |
| `/context-limit` | 为当前模型设一个比窗口小的上下文上限，档位 200K、300K、500K、1M 或自定义，写进 `compaction.max_context`，见[压缩、修剪与请求前缀](https://daiw.org/manual/mimo-code/compaction-and-prune) | `app.tsx:715` |
| `/permission-timeout` | 权限询问的等待时限：不限、30 秒、1、2、5、10 分钟，超时自动拒绝 | `app.tsx:763` |
| `/skip-permissions`、`/never-ask` | 自动放行权限请求；让 Agent 不再提问、自行决策（权限请求除外） | `app.tsx:747`、`app.tsx:728` |
| `/force-agent` | 绕过会话中的锁定强制切换模式 | `app.tsx:693` |
| `/worktree` | 列出、创建并切换到 git 工作树，目标在服务端工作目录之外时报错而不退出 | `app.tsx:893` |
| `/workflows` | 查看当前会话的 Workflow 运行，每秒刷新计数，选中后进入全屏详情页，见 [Workflow 运行时](https://daiw.org/manual/mimo-code/workflow-runtime) | `app.tsx:599` |
| `/btw` | 向正在工作的会话提一个旁问，不打断它 | `packages/opencode/src/cli/cmd/tui/routes/session/index.tsx:698` |
| `/recover` | 继续一个被中断的回合 | `routes/session/index.tsx:541` |
| `/revoke-consent` | 撤销免费模型的用户协议同意 | `prompt/index.tsx:796` |
| `/doc`、`/dark`、`/light` | 打开文档；直接切换到深色或浅色 | `app.tsx:1006`、`app.tsx:961` |

下一篇：[回顾](https://daiw.org/manual/mimo-code/recap)——回顾整本专栏，把前面各篇讲到的机制串起来。
