npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

deepseek-harness-tui

v0.3.0

Published

Interactive terminal UI for deepseek-harness, installed as a dsh profile bundle

Downloads

1,482

Readme

deepseek-harness-tui

npm license

English | 简体中文

deepseek-harness 的交互式终端 UI,以 dsh profile bundle 的形式安装。它直接渲染在终端主屏上——不用备用屏 (alternate screen),退出后对话仍留在终端回滚缓冲区里——并且与它驱动的 agent 运行在同一个进程内。

dsh TUI 演示:启动、提问、看着 agent 读文件并回答

亮点

  • 为 agent 而生的对话视图——流式回答、带实时输出的工具卡片;连续的只读调用 折叠成一行(Thought for 8s, searched for 3 patterns, read 2 files), Ctrl+O 随时展开回卡片。
  • 运行中随时介入——turn 进行时编辑器保持可用:Enter 追加引导(steer), Esc 或 Ctrl+C 取消,取消时排队中的输入会原样退还。agent 还没读到的那条会自己 说明(插入指令 · 待认领),提示行给出还排着几条,/status 列出它们。输入框 为空时按 Up 把最新一条排队消息取回来改;改完再发是排到队尾,所以连着按 Up 可以把整个队列过一遍。用 /config 可以把 Enter 改成「排到下一轮」, Ctrl+Enter 则对单条消息走相反的那一档(见 插话与排队)。
  • 一个键切模式——Shift+Tab 在 normal → auto-accept → plan 间循环;提示行 上方的徽标与 /permission/plan 永远一致,因为按键写的是同一套服务。
  • 会话可持久——/resume 恢复任意历史会话,/rewind fork 回到更早的 prompt 且原会话完好保留,/search 全文检索本会话所有消息。
  • 模型与 provider 就地管理——/model 选择路由和推理力度(可只对本会话生效, 也可存为默认);/login 存 key 前先对端点校验,密钥只进凭据存储。
  • 终端自适应——亮/暗/无色主题实时预览、中英文界面(/lang)、按键可重绑、 @ 文件引用通过 fd 尊重 .gitignore
  • 可脚本化——--print 无 UI 跑一个任务、答案输出到 stdout,模型、preset、 各项 flag 与交互模式含义完全一致。

一个正在流式输出的 turn:agent 的 read 调用渲染出工具卡片,随后是思考过程、 逐 token 到达的回答——下方编辑器仍然可用,随时等你引导(Enter)或取消 (Esc):

流式 turn:工具卡片、思考、部分回答、steer 提示

快速开始

需要先安装 dsh CLI——本包是 dsh 插件,不是独立程序:

npm install -g @deepseek-ai/dsh
dsh plugin --profile tui add deepseek-harness-tui
dsh --profile tui

plugin 命令会把本包装进一个新的 tui profile(dsh-base + deepseek-harness-tui),下次启动即生效。

欢迎界面:banner、模型路由、工作区和已加载插件

用法

dsh --profile tui                                      # 启动交互式 TUI
dsh --profile tui "fix the failing test"               # 启动并发送首条 prompt
dsh --profile tui --continue                           # 继续最近一个会话
dsh --profile tui --resume <sessionId>                 # 恢复指定会话
dsh --profile tui --preset code                        # 以 "code" agent preset 启动
dsh --profile tui -m deepseek-official/deepseek-v4-flash  # 覆盖模型
dsh --profile tui --print "run the tests"              # 跑一个任务,答案输出到 stdout

| Flag | 作用 | |---|---| | -m, --model <provider/model> | 本次运行的模型选择 | | --preset <id> | 新会话按此 agent preset 组装;恢复的会话保持它自己日志里记录的 preset | | -r, --resume <sessionId> | 按 id 恢复会话 | | -c, --continue | 继续本工作区最近的会话 | | -p, --print <task> | 无 UI 跑一个任务:答案写到 stdout,只有 turn 完整结束退出码才是 0;工具审批固定为 never,因为没有人可问 | | -h, --help | 显示帮助 | | [prompt...] | 首条 prompt,UI 就绪后发送 |

stdin 和 stdout 都必须是 TTY,否则拒绝启动。--print 是唯一例外——它不渲染 任何东西,所以可以跑在管道里,而管道也是它唯一有用的地方。其余 flag 与交互模式 含义完全一致:--print 同样可以作用于 --resume--continue 的会话, 模型和 preset 也听命令行其余部分的。

按键

下面每个键都是终端默认真实绑定的键。空输入框按 ?/hotkeys/help 打印的是同一份列表,从按键注册表生成——部署改了绑定,三处看到的都是新键。

| 按键 | 作用 | |---|---| | Enter | 发送;turn 运行中是插话还是排队,取决于 /config(见插话与排队) | | Ctrl+Enter | 对这一条走「运行中按 Enter」的相反档:Enter 插话时它排队,Enter 排队时它插话。需要终端能把它和 Enter 区分开——Kitty 键盘协议或 xterm 的 modifyOtherKeys 会把它编码成独立按键,其余终端物理上只发得出 \r,于是退化成普通 Enter 正常发送 | | Shift+Enter / Alt+Enter / Ctrl+J | 换行;行尾 \ 再回车效果相同,照顾发不出 Shift+Enter 的终端 | | Up / Down | 光标在输入框首行时翻 prompt 历史,其余行移动光标;输入框为空且还有排队消息时,Up 把最新那条取回编辑器修改 | | Tab | 接受补全 | | @ | 引用文件 | | / | 运行命令;/skill:<name> 加载技能 | | ? | 空输入框时显示快捷键帮助;不会打进草稿 | | Ctrl+R | 反向搜索 prompt 历史;历史跨进程保存在 $DSH_HOME/history.jsonlDSH_SKIP_PROMPT_HISTORY=1 停写(见 prompt 历史) | | Ctrl+G | 搜索本会话消息;Ctrl+F 保留给编辑器的前进一格 | | Shift+Tab | 循环模式:normal → auto-accept → plan → normal。normalauto-acceptworkspace-writeauto-accept 两个权限 preset(同一沙箱,问不问审批之差);plan 是计划模式,循环在 workspace-write 上进入。danger-full-access 不在循环里——它靠 /permission 进入,已处于其上的会话保持不变,按键只切计划模式 | | Ctrl+N | 展开/收起计划;Ctrl+Y 保留给编辑器的 kill-ring 粘贴 | | Ctrl+O | 循环工具卡片:预览、完整、隐藏 | | Ctrl+T | 显示/隐藏思考块——关闭时思考随所在 step 流式出现并消失;开启时每个 step 都保留,含历史。模型无论如何都在推理;showReasoning: false 连同此键一起关闭 | | Ctrl+X | 复制最后一条回答 | | Alt+E | 用 $EDITOR 编辑草稿,保存后取回;先看 $VISUAL,再看 $EDITOR,最后在 PATH 上找 nano/vim/vi。发不出 Alt 的终端用 /editor | | Ctrl+L | 重绘 | | Esc | 取消 turn(并退还排队的输入);有草稿时再按清空草稿;空输入框再按打开 Rewind | | Ctrl+C | 运行中取消(排队的输入照 Esc 的规矩退还),输入中清空草稿,空闲时连按两次退出;第三次按下直接离开无法取消的 turn | | Ctrl+D | 空输入框时退出 | | Shift+Ctrl+D | 会话调试面板——身份、生命周期、屏幕、按键解析结果 |

当某个界面持有键盘时

| 界面 | 按键 | |---|---| | 面板(/help/hotkeys/palette/status/mcp/doctor/subagents/jobs) | Up/Down 滚动 · PgUp/PgDn 翻页 · g/G 或 Home/End 到顶/到底 · Esc 或 Ctrl+C 关闭 | | 提问 | Up/Down 移动 · 1-9 直接作答 · Space 勾选(多选)· "Type something." 行输入自定义答案 · PgUp/PgDn 翻长详情 · Enter 提交 · Esc 或 Ctrl+C 取消 | | 权限审批 | Up/Down 移动 · 数字键直接作答该行 · Enter 确认 · Esc 或 Ctrl+C 拒绝。只有能被记住的授权才会多出第 5 行(命令行工具则打开规则供编辑) | | 历史搜索(Ctrl+R) | 输入即匹配 · Ctrl+R 跳上一条更旧的匹配 · Tab 或 Esc 取回编辑器 · Enter 直接发送 · Ctrl+C 或清空查询恢复草稿 | | 会话搜索(/search、Ctrl+G) | 输入即过滤 · Up/Down 移动 · PgUp/PgDn 翻页 · Enter 打开该消息 · Esc 依次退出消息、清空查询、关闭 | | 模型选择器(/model) | 输入即过滤 · Up/Down 移动 · Left/Right 或 Shift+Tab 调推理力度 · Enter 存为默认 · Ctrl+S 仅本会话生效 · Esc 先清过滤再关闭 | | 恢复选择器(/resume) | 输入即搜索 · Up/Down 移动 · PgUp/PgDn 翻页 · Tab 在本工作区/全部之间切换 · Enter 恢复 · Esc 先清搜索再关闭 | | Rewind(/rewind) | Up/Down 移动 · PgUp/PgDn 翻页 · Home/End 首/末 · Enter 回到那条 prompt · Esc 关闭 | | 插件(/plugins) | 输入即过滤 · Up/Down 移动 · PgUp/PgDn 翻页 · Enter 展开条目 · Esc 关闭 | | 技能(/skills) | 输入即过滤 · Up/Down 移动 · PgUp/PgDn 翻页 · Enter 阅读技能(Up/Down 滚动 · g/G 或 Home/End 到顶/到底)· Esc 依次退出技能、清空过滤、关闭 | | 设置(/config) | Up/Down 移动 · Enter 翻开关、步进选项或进子菜单 · Left/Right 步进选项 · Esc 关闭 | | 主题选择器(/theme) | Up/Down 逐个在背后屏幕上预览 · Enter 保留 · Esc 恢复打开时的主题 | | Provider 登录(/login/provider add) | Up/Down 移动 · Space 勾选模型 · Enter 继续 · Ctrl+U 清空输入 · Esc 取消整个流程 |

Ctrl+C 是唯一永不可重绑的键:它是离开终端的最后手段。其余绑定均可配置——见 下文 keybindings

命令

| 命令 | 作用 | |---|---| | /help | 快捷键与命令 | | /hotkeys | 只看快捷键 | | /model [[provider/]model] | 切换模型并存为默认;不带参数打开选择器,也可只对本会话生效 | | /preset [<preset> \| copy <preset> <new-id>] | 查看、切换或复制本会话的 agent preset | | /config | 本终端自己的设置——Ctrl+T 思考固定、会话打开时的工具卡片阶段、运行中按 Enter 是插话还是排队、主题——就地修改并保存到下次会话 | | /theme [auto\|light\|dark\|no-color] | 本终端的配色;不带参数打开选择器 | | /login [provider] | 给 provider 配 API key:选一条已配置的或适配器提供的路由,粘贴 key,先对端点校验再存储。key 进凭据存储;settings 只记录变量名 | | /provider [add] | 列出已配置的 provider 和 /login 可配置的;add 依次填写名称、端点、协议、key 和端点报告的模型 | | /copy [N] | 复制一条回答到系统剪贴板;不带参数是最后一条,/copy 2 是上一条 | | /editor | 在 $EDITOR 里编辑当前输入;编辑器运行期间终端交给它,退出后整屏重绘 | | /new | 在本工作区开一个空白会话;当前会话保留全部历史、仍可恢复 | | /clear | 清空对话视图;会话日志不变 | | /rename [名字] | 给本会话起个名字;起了名字就固定住,自动命名不再改它。不带参数则重新生成标题,并继续交给自动命名维护 | | /compact | 把更早的对话历史压缩成一段摘要;屏幕上的对话保留,模型侧只留摘要。不接受参数 | | /lang [en\|zh] | 查看或切换界面语言;选择会记住到下次会话 | | /palette | 本终端渲染的全部颜色与属性角色 | | /export [path \| clipboard] | 把本会话日志写入文件并报告路径(覆盖已有文件前会先确认);写 clipboard 则把会话以 Markdown 放到系统剪贴板(远程 SSH 下走一次 OSC 52 写入,超过 100000 个字符会截断,提示里会说明) | | /plugins | 搜索并查看 Loader 的插件条目 | | /search [query] | 搜索本会话消息;参数会预填面板查询框 | | /rewind | 回到本会话更早的 prompt | | /resume [session] | 列出本工作区可恢复的会话;参数会预填选择器搜索框 | | /skills | 搜索本会话的技能并完整阅读 | | /subagents | 本会话下的子代理树:标签、一次性还是可继续、运行中还是未运行,以及 /resume 要用的子会话 id。面板开着时会自动刷新;profile 没挂子代理注册表时会明说 | | /jobs | 后台任务:类型、标签、状态、生产方给的细节,以及各自跑了多久。面板开着时跟随注册表刷新;profile 没挂后台任务注册表时会明说 | | /status | 会话诊断、排队消息、系统提示词、已注册工具 | | /mcp | 本 agent 各工具来自哪个 MCP 服务器及其工具列表;profile 没挂 MCP 时告诉你怎么挂 | | /doctor | 检查 Node 版本、终端、模型路由,以及缺了会静默降级的服务 | | /exit/quit | 当前 turn 到达空闲后退出 | | /skill:<name> [instructions] | 把技能加载进对话 | | /reload | 实验性(开发用):重读 Loader 配置文件并应用差异,仅空闲时可用。仅在 experimentalCommands 开启时注册 |

以上是本 bundle 自己的命令。profile 挂载的其他插件会在其上注册各自的命令, /help 列出的才是当前会话真正可用的全集。

/details 已退役。它把两个不相关的开关塞进一套要背下来才能用的参数语法 ([collapsed|expanded|hidden] [reasoning [on|off]]),而且两个都不跨进程记忆。 它做的两件事各归各处:工具卡片阶段就是 Ctrl+O 当场循环的东西,思考显示是一项 长期偏好——现在都是 /config 里的行,旁边就是打开 /theme 的主题行。它的 detailsDialogWidth 更名为 settingsDialogWidth,参数补全是 /theme 的四个值。

/config/theme 的修改立即生效,并写入 harness 自己 settings 文档 ($DSH_HOME/settings.yaml)的 tui 段——用的正是 /model 保存默认模型的那个 可选 settings 服务。/config 的每一行都实时读值,面板开着时按 Ctrl+O,下面的 工具卡片行会跟着动。宿主没挂该服务时,所有开关本会话内照常工作,只是退出即忘。

/lang 切换的是本终端自己的界面元素——命令列表、各面板(/help/status/config/search/skills/subagents/jobs/mcp/doctor/plugins)、提示行与状态行、对话框及其按钮、这些界面写出的通知——在英文 (默认)与中文之间切换;对话内容永远不会被翻译。少数命令回执无论语言如何仍是英文:/model/preset/resume 打印它们自己的报告文本,对话视图折叠的 turn 结局通知("Turn cancelled."、"The model reached its output-token limit.")来自会话日志而非 消息表。

语言选择写入 Host 的 locale settings 段(有 settings provider 时,与 web 客户端读的是同一份偏好),否则写入 $DSH_HOME/tui-locale.json~/.dsh/tui-locale.json)。

插话与排队

turn 运行时编辑器保持可用,所以 Enter 永远是「发送」。运行中「发送」到底是什么 意思,由 /config 里的运行中按 Enter 决定:

  • 插话当前轮次(默认)把这条交给正在跑的 driver,它在下一个 step 边界读到 ——正在写的这段回答会被打断。提示行上方的 echo 带 插入指令 徽章。
  • 排到下一轮则是先寄存:正在跑的这一轮不受打扰地跑完,这条消息开启它之后 的新一轮。认领之前它的 echo 带 排队中 徽章,认领之后就是一条普通的 prompt 行——因为它确实变成了一轮的开场提问。

Ctrl+Enter 只对当前这一条走相反的那一档,不改设置。它需要终端能把它和 Enter 区分开——Kitty 键盘协议或 xterm 的 modifyOtherKeys;其它终端上 Ctrl+Enter 物理上就是一个 \r,谁也认不出来,于是按普通 Enter 正常发送。

两档都只是「排进队列」而不是「立刻被回答」,所以队列提供的一切对两者都成立: 提示行给出条数(2 条排队中)、/status 按认领边界列出内容、输入框为空时 Up 把它取回来编辑(见 prompt 历史)、取消 turn 时 Esc 或 Ctrl+C 把文本退还编辑器。这项选择按用户存在 $DSH_HOME/settings.yaml;harness 的 web 端有它自己的一份同名选择,默认是排队,而本终端默认插话——这是它一直以来 的行为。

prompt 历史

上方向键和 Ctrl+R 能取回本进程启动之前输入过的 prompt。每条提交的 prompt—— 以及被 Esc 或 Ctrl+C 清掉的草稿(含取消时退还回来的队列),那也是你可能想要 回来的东西——都会追加到 $DSH_HOME/history.jsonl~/.dsh/history.jsonl),挂载时读回:最新的在前, 本会话自己的排在其他会话之前,且只取在本工作区里输入过的。超过 1024 个字符的 prompt 在行内只留 200 字符预览,正文移到旁边的 history-cache/;两者都以 0600 写入。文件超过 1 MB 后压实一次,只保留最新 1000 条;没人再引用的正文 文件在一周后删除。

turn 运行时 Up 还有第二个用处。输入框为空、还有排队消息、且没有打开补全菜单 时,它把最新那条排队消息从 agent inbox 取出来放回编辑器——提示行上方那条 echo 随之消失,计数减一。取回来之后它就是一条普通草稿:Enter 重新发出去,落到队尾; Esc 丢掉,文本进 prompt 历史。想改更早的一条,就把手上这条发回去再按一次 Up, 这样能把整个队列过一遍。三个条件缺一个,Up 就还是原来的 Up:正在写的草稿不会 被覆盖,队列为空时照旧翻历史。

存的是发出去的原文,明文:粘在输入框里的密钥或客户名会一直留在磁盘上,直到 文件被删掉。浮层里的输入框不是 prompt,不会被记录——/login 的 API key 不会 进这个文件。设 DSH_SKIP_PROMPT_HISTORY=1(或 true/yes/on)可以只停写、 不停读;要清掉已经记下的,删掉 $DSH_HOME 下的 history.jsonlhistory-cache/

上下文压力

prompt 行上的 ${context} 报告本会话用掉了模型窗口的多少——已用 78% 上下文, 暗色——直到窗口变紧。剩 25% 起改报另一个数:黄色的 上下文剩 22%;剩 10% 及以下 转红色。一次测量同时喂给颜色和数字,所以两个数不会打架,屏幕上也只会出现其中一个。

transcript 里每档还会写一行,因为正在读长回答的人不会盯着 prompt 行:黄色的 上下文快满了——窗口还剩 25%,以及剩 10% 以下时红色的 上下文几乎用尽。每档最多 写一行,并且只有读数重新回到该阈值之上 3 个百分点才会重新武装,所以读数在边界上来 回跳不会把同一条警告重复贴出来。本会话的 preset 挂了压缩服务时,这一行让你执行 /compact,没挂时让你用 /new;而当前这一轮还在运行时,它会说等这一轮结束再压缩 ——/compact 需要会话空闲。

两个阈值是常量,不是配置项:25% 特意高于 @deepseek-ai/dsh-compaction-basic 默认 配置下自动压缩的 20%,所以黄行是你还能自己决定压不压的最后时刻;红行意味着自动压缩 这条路缺席、被关掉或者失败了。

权限授权

权限审批框的第 2 行("本会话内不再询问")只记在内存里,窗口关掉就没了。第 5 行 ("本项目内不再询问")写入 $DSH_HOME/approvals.json~/.dsh/approvals.json), 重启或 /resume 之后依然有效:

{
  "version": 1,
  "projects": {
    "/home/you/code/app": {
      "allow": ["edit", "bash(npm run:*)", "bash(git status)", "edit [danger-full-access]"]
    }
  }
}

规则按会话打开时的工作区分组,所以在一个仓库里给的授权不会花在另一个仓库上,也 不会有任何东西被写进仓库本身。规则有三种:裸工具名(edit——该工具的每次询问都 放行)、命令前缀(bash(npm run:*)——匹配 npm run buildnpm run test,但绝不 匹配 npm run-evil)、单条精确命令(bash(git status))。命令按词比较,模型多打 几个空格不会改变规则是否命中。规则永远不覆盖一行里跑多条命令的情况:只要带上 ;&&|、重定向、反引号、括号或换行,即使第一条命令被允许也照样弹框;命令 指定了项目目录之外的工作目录时同样弹框,审批框里会写明是哪个目录。想撤销授权,把 对应条目从文件里删掉即可——projects 下其余内容(包括本版本不写的键)原样保留。

规则末尾的 [mode] 是这条授权当时对应的 sandbox 权限。宿主在调用被沙箱拒绝后会带 着更高权限重问一次(「escalate sandbox to danger-full-access: …」),而一条规则只回 答与自己同类的请求:在 workspace-write 下存的规则不会替之后的 danger-full-access 作答,普通调用下存的规则也不会回答任何提权请求。第 5 行的文案会写明它将不再询问的 权限档位。

命令行工具问的是「这条命令能不能跑」,所以它的第 5 行给的是一条规则而不是整工具放 行:审批框会预填(npm run build 预填成 npm run:*git status 预填成 git status:*),Enter 保存,而先改一改正是它的用意——改成 npm:* 覆盖得更宽,改 成 npm run build 就只覆盖这一条。把输入框清空则只允许这一次、不存任何规则。这里 刻意没有「本项目内放行所有 shell 命令」这一行;遇到任何规则都匹配不上的命令——复合 命令,或 sudo/env/bash -c 这类裸包装——第 5 行直接不出现,只剩原来的四个选项。 终端看不清这次调用时也一样(后台命令、入参不是合法 JSON 等):持久授权只对审批框 真正展示过的内容开放,绝不只凭一个工具名发出去。

写文件类的工具会把这次改动直接画在审批框里:文件路径加 old→new 的 diff,内容取自 这次调用自己的入参(不读磁盘)。改动太长会被截断以保住下面的选项行——预算按实际占用 的屏幕行数计算(含长行折行与多文件),所以终端多宽都不会把选项挤掉;截断标记会写明 还有多少行没显示,调用真正跑起来之后,transcript 里的工具卡片有完整 diff。改动量 超过 maxDiffEditLength 时按整文件替换渲染并标注为近似;终端拿不到这次调用的内容 时,弹的就是原来那个不带 diff 的审批框。

@ 文件引用

宿主装了 fd(Debian/Ubuntu 上叫 fdfind)时,@ 通过它列出工作区,补全因此 尊重 .gitignore.ignore.fdignore。没有 fd 时由内置遍历器接管,按名 跳过构建产物——.gitnode_modulesdistbuildoutcoverage.cache.next.nuxt.turbo.venv__pycache__target——查询没 写扩展名时还会隐藏 *.log*.tsbuildinfofileSearchCommand 可固定二进制 路径、设 "" 强制用遍历器;fileSearchExcludedDirectories 改遍历器跳过的目录。

命令参数同样有补全:/model 提供所有已公布的 provider/model/preset 提供 roster 里的 preset 和 copy 动词,/theme 提供四个取值,/resume 提供本工作区 最近的会话。

界面

  • 对话——主视图:流式消息、工具卡片、计划、状态行,以及带上下文行的输入框。 连续的只读调用——read、grep、glob、ls/cat 型 shell 命令、MCP 查询——折叠 为一行(Thought for 8s, searched for 3 patterns, read 2 files)而不是一卡 一行;Ctrl+O 把这一段展开回卡片。写入的调用永远不会入组——cat a > b 写了 b,动词说什么都没用;失败的调用留在组里并把圆点染红,因为读者看不见的失败 比一行承认失败更糟。该行的每个片段在每种语言里都是完整短语,而不是渲染时拼接 的动词加名词,中文因此有自己的语序、量词和逗号。
  • 该行上的思考——这一段把思考作为首个分句报告在旁边(Thinking for 12s, read 2 files…),模型还在思考时随时钟递增。这是默认对话视图唯一陈述思考 时长的地方:思考块本身保持自己的规则,随写下它的 step 一起消失(Ctrl+T 固定它,Ctrl+O 展开时回来)。每个分句有自己的时态——思考还在进行时文件已经 读完——时长出现在哪一行就留在哪一行,所以一段以回答而非下一次工具调用收尾的 思考会原地落定,而不是从屏幕上消失。在这一段的第一个调用报出文件、模式或命令 之前,行下的 行显示思考的最新一行;showReasoning: false 和其他地方一样 不让这行出现,而时长——不引用任何内容——保留。
  • 工作流运行——一次 workflow 工具调用折叠成「运行 / 阶段 / 成员」三级: 运行行给出名称与成员数,每个阶段一行表头,每个成员一行状态与耗时。展开到哪一 级由运行状态决定而不是开关——只要某个阶段里还有成员不是「已完成」,这个阶段就 保留成员行;成员全部完成的运行收成一行,Ctrl+O 再展开。没有阶段的成员和阶段名 为空的成员是两个不同的组,因为它们在日志里本来就是两回事。turn 结束时仍未收到 结果的运行读作「已中断」,未结算的成员一同中断:它们不会再有结果,而一行还在 说「运行中」就是在说反话。
  • Rewind——/rewind,或空输入框连按 Esc:回到更早的 prompt。宿主能 fork 会话时对话随之移动、原会话仍可恢复;否则只是把那条 prompt 放回编辑器。文件 永远不会被恢复——dsh 不做文件快照。
  • 恢复——/resume [session]:挑选并恢复历史会话,范围是本工作区,Tab 切到 全部。每行是标题加上多久前碰过和日志多大(2 hours ago · 354.1KB);当前 所在的会话不列出,因为恢复到自己不是一个去处。id 可被搜索框匹配但不打印在 行上。什么都没输且无可列时,面板直说没有其他会话可恢复,而不是报告一次落空 的搜索——空列表就是答案,不是查询失败。离开终端时会打印找回刚离开会话的 命令,走的那一刻回来的路就在屏幕上。
  • 会话搜索——/search [query],或 Ctrl+G:本会话的每条消息,输入即过滤, 命中处就地展示,整条消息一个 Enter 即达。做成面板而不是跳转,是因为输入框 上方的对话属于终端的回滚缓冲区,任何程序都无法替你滚动它。
  • 插件——/plugins:搜索并查看 Loader 的条目。
  • 技能——/skills:搜索本会话组装的技能并阅读正文;/skill:<name> 再把它 加载进对话。
  • 设置——/config:本终端自己决定的偏好——思考固定、会话打开时的工具卡片 阶段、主题——外加语言和模型两行,它们只读、注明改它们该用哪条命令。
  • 主题——/theme,或 /config 里那一行:auto(跟随终端报告)、lightdarkno-color,移动时在选择器背后的屏幕上实时预览,Esc 离开则恢复原样。
  • Provider 登录——/login [provider]:给一条路由配 API key。列表包含 settings 已配置的和适配器目录自带的,后者正是一台 settings 空白的机器也能 连上 DeepSeek 官方端点的原因。key 永不回显——输入框画点——有端点可校验时 先校验再存:401 或 403 什么都不存;端点答不上来的 key 只有明确说"是"才存; 目录路由的端点在适配器内部、本终端看不见,直接存,因为本来就无从问起。回执 只把端点真正应答过的 key 称为已校验;其余存储的 key 一律报告为未校验而不是 可用。密钥进凭据存储自己的文件;settings 只记录变量名。/provider 列出同样 两组,/provider add 引导一条适配器没听说过的路由走完名称、端点、协议、凭据 变量、key 和端点报告的模型。
  • 子代理——/subagents:本会话下的委派树,每个子代理一行——标签、是一次性 委派还是可继续的会话、记录是活着还是只在持久化里,以及 /resume 要用的子会话 id。这些行来自子代理目录而不是对话视图,所以上一个进程里委派出去的子代理同样 在树里;subagent/startsubagent/end 只负责告诉打开着的面板「该重读目录 了」。刚刚 spawn 的子代理可能晚一次刷新才出现:目录从子代理写下自己的 descriptor 那一刻起才列出它。/status 用一行给出同一棵树。
  • 状态——/status:会话诊断、排队消息、系统提示词、已注册工具。排队消息 一节按 agent 将要读取的顺序列出它收到但还没读的输入,每条一行(过长省略), 并标出停在哪个边界:[插话] 打断正在写的这段回答,[下一轮] 单独占一轮。 它和提示行那个计数(2 条排队中)读的是同一个 agent inbox,所以别的 host 插进这个会话的输入也在其中。
  • 后台任务——/jobs:本会话丢到后台还在跑的活。带 run_in_backgroundbash 调用,或者发出去就不等的委派,都会立刻把控制权还给模型然后继续跑;它们 当前的状态只存在于后台任务注册表里,所以这些行就来自那里。每一行给出生产方 类型、生产方起的标签(命令本身、委派描述)、生命周期状态与随之而来的细节 (失败 · exit code: 1),以及已经跑了多久——运行中的排在前面且最早的在最上, 然后是已结束的,最近结束的在前。面板开着且确实有任务在跑时,运行中那行的秒表 才走;都结束了就停。提示行只带一个计数(2 个后台任务运行中),让后台有活这件 事一眼可见,而不用在屏幕上多放一块秒表;/status 用一行给出同样两个数。
  • MCP——/mcp:本会话每个工具来自哪个 MCP 服务器,从工具注册名 mcp__<server>__<tool> 反推出来,因为 harness 没有可查询的注册表。它天生 只读——终端没有连接、重启或认证服务器的句柄——profile 没有 MCP 行时告诉你 怎么挂一个,而不是给你看一张空列表。
  • Doctor——/doctor:会话跑在什么之上/status 描述的是会话本身)—— Node 版本、终端、模型路由,以及缺了会静默降级的服务。每项检查一行:结论、 观察到的事实、该做的那一件事。
  • 帮助——/help:按键与斜杠命令。

配置

bundle 行(tui-runner)上的值,全部可选。

| 键 | 默认 | 含义 | |---|---|---| | welcome | — | 启动 banner 下额外的一行暗色文字;完全不设时改为字标扫入动画 | | sessionId | main | 本终端驱动的共享 agent/会话标识 | | initialSkill | — | 作为会话第一个 turn 自动调用的技能,等同输入 /skill:<name>;供启动器设置,不面向人 | | initialDraft | — | 编辑器打开时预填的未发送文本;由 rewind handoff 设置 | | experimentalCommands | false | 注册开发者命令(目前是 /reload) | | showReasoning | true | 本对话视图是否允许渲染推理文本;false 在所有阶段隐藏思考块、不让折叠行的 提示引用模型原文(时长保留),并连同 Ctrl+T 和 /config 的思考显示行一起关闭 | | markdownRenderer | claude | claude(本 bundle 的渲染器)或 pi(pi-tui 的 Markdown);claude 渲染抛错后本进程余下时间回退到 pi | | maxToolOutputLines | 6 | 折叠工具卡片头/尾预览保留的正文行数 | | maxDiffEditLength | 1000 | 推导精确行级 diff 时探索的增删行数上限 | | maxPromptChars | 10000 | 单条提交输入的字符上限;超出的中段会被丢弃并留下 ... [N characters truncated] ... 标记与一条提示,0 表示不截断 | | maxQuestionOptions | 8 | 提问面板一次可见的选项数 | | maxModelOptions | 8 | 模型选择器一次可见的模型数 | | maxResumeOptions | 8 | 恢复选择器一次可见的会话数 | | resumeScanConcurrency | 4 | 一次恢复扫描的冷投影并发读数 | | questionDialogWidth | 200 | 提问面板宽度(列),受终端约束 | | questionDialogMaxHeight | 20 | 提问面板最大高度(行) | | modelDialogWidth | 76 | 模型选择器宽度(列) | | modelDialogMaxHeight | 20 | 模型选择器最大高度(行) | | settingsDialogWidth | 72 | /theme 选择器宽度(列) | | fileSearchMaxResults | 20 | 一次 @ 查询展示的模糊候选数 | | fileSearchMaxEntries | 10000 | 一个 @ 工作区索引保留的路径数 | | fileSearchExcludedDirectories | 见上文 | 遍历器跳过的目录名 | | fileSearchCommand | — | fd 的路径或名称;不设则在 PATH 上发现,"" 禁用 | | externalEditor | — | Alt+E/editor 交给哪个编辑器;不设则依次读 $VISUAL/$EDITOR 再在 PATH 上发现,"" 关闭该功能。GUI 编辑器需要自带等待参数(code -w),已知的那些会自动补上 | | showHardwareCursor | false | 在编辑器 IME 标记处显示终端硬件光标 | | updateCheck | true | 每天最多问一次 npm registry 有没有更新的版本,有则提示一条;从不自动安装,任何失败都不出声 | | title | DeepSeek Harness | UI 挂载期间的终端窗口标题 | | theme.color | true | 应用内置 ANSI 配色 | | theme.truecolor | 自动检测 | banner 的 24 位品牌渐变;不设时读 COLORTERM | | theme.leftPrompt | ${cwd}${git/worktree}${model}${token_meter/cache_hit_rate}${context} | 编辑器上方左对齐模板 | | theme.rightPrompt | ${queued}${jobs} | 编辑器上方右对齐模板 | | theme.inputPrompt | | 编辑器首行前缀 | | theme.inputPlaceholder | press enter to steer and esc to cancel | agent 运行时空编辑器的占位文本 | | keybindings | — | 按键覆盖,按 action id 键入 |

提示行模板以 ${name} 插值本 bundle 注册的值——cwdgit/worktreemodelcontexttoken_meter/cache_hit_rategoalqueuedjobssymbolindicator——某个值当前不可用时,挨着它的分隔符一并省去。context 报的是"已用" 还是"剩余"取决于窗口有多满,见上下文压力

除 Ctrl+C 外的绑定均可配置:在 bundle 行上设 keybindings{ "app.history.search": "alt+r" }),按 action id 键入,值为一个或多个 pi-tui 按键 id。本 bundle 的 id 有 app.mode.cycleapp.tools.cycleapp.history.searchapp.transcript.searchapp.todos.toggleapp.thinking.toggleapp.message.copyapp.screen.redrawapp.cancelapp.exit;pi-tui 编辑器自己的绑定也可以同样方式移动。Shift+Ctrl+D 会报告 每个 id 解析成了什么键、哪个键被两个 action 同时认领、哪个键被 app.* action 从 pi-tui 编辑器手里拿走——按键"没反应"时第一个该查的就是它。

有两个键故意不用读者可能期待的那个,因为 app.* 绑定在编辑器看到按键之前就被 应答:搜索是 Ctrl+G 而不是 Ctrl+F(pi-tui 的 tui.editor.cursorRight),计划 开关是 Ctrl+N 而不是 Ctrl+Y(pi-tui 的 tui.editor.yank)。把它们重绑到编辑器 的键上,编辑器的习惯就永远没了。

Shift+Tab 拿来做模式循环是安全的,理由相同:pi-tui 在这一族里只绑了 tab, 它的编辑器不认识 Shift+Tab。/model 选择器里的 Shift+Tab(步进推理力度)是 另一个作用域、照常工作——任何浮层持屏时,应用层监听器在第一个分支前就返回, 对话框先看到按键。

循环本身不写任何自己的状态:normalauto-accept 通过 ctx.permissionPresets 选择(auto-accept 条目由本 bundle 的 cordis.patch.yml 加进表里),计划模式通过 ctx.planMode——所以 /permission/plan、恢复的日志和按键报告的是同一个状态。开启的模式会在 输入框上方说明——⏸ plan mode on⏵⏵ auto-accept on——徽标旁标注循环它的 键。两个徽标可以同时在场,通过 /permission auto-accept/plan 而非按键 到达;此时提示只跟着最后一个,因为同一个键在两行叠着重复会被读成要按两个键。 没有组装 preset 表或计划模式的部署保留它有的档位:按键循环挂载了的东西, 无可循环时直说。

开发

pnpm install
pnpm typecheck
pnpm test
pnpm build
dsh plugin --profile tui add link:./path/to/dsh-tui  # 开发用 live-link
dsh --profile tui

tui-runner 行上设 experimentalCommands: true,编辑配置文件时可用 /reload

本插件是一个 bundle 里的两个 Cordis 插件:

  • dsh-tui/startup——解析 TUI 自己的命令行,提供 tuiStartup 服务。
  • dsh-tui——runner:持有 pi-tui 渲染循环、进程内 agent 会话、审批应答器和 用户提问 provider。

数据单向流动。dsh 总线上的事件(session/eventagent/status)由每会话的 读模型折叠成不可变节点列表;带 key 的 reconciler 把列表变成 pi-tui 组件,版本 已应用过的节点全部复用,一阵流式 chunk 只重绘一个 assistant step 而不是整个 对话。TUI 运行在进程内,直接调用 ctx.agents / ctx.approval / ctx.commands——没有 SDK、没有 ACP 传输、没有 React。

许可证

MIT