使用

September 3, 2026 · View on GitHub

English | 中文

开始一个会话

dshline在当前文件夹启动
dshline -C ~/code/api在另一个文件夹启动
dshline "run the tests"启动时发送第一条消息
dshline --resume浏览、搜索并重新打开过去的会话
dshline --resume <id>重新打开你知道 id 的会话
dshline --help本界面新增的所有命令行选项
dshline --version本包的版本号,提交问题报告时用
dshline --setup显式安装配置文件:脚本、重试或源码检出时用

首次运行时——还没有 dshline 配置文件——dshline 会询问一次是否允许 Harness 创建它并把本包安装进去,然后继续执行你输入的内容。没有可供询问的终端时它的行为,以及为什么已存在的配置文件从不被修复,见安装

dshline 是 Harness 自带启动器的一层轻量封装:它找到 dsh、加上 --profile dshline,并把会话固定在你运行它的文件夹。其余一切透传,因此 dshline <anything>dsh --profile dshline <anything> 行为一致。用你喜欢的那个即可。

-C(或 --cwd)设置会话工作的文件夹。它不改变命令本身从哪里运行。

--resume 重新打开会话会保留该会话创建时的文件夹,因为那个文件夹记录在会话文件中。因此恢复时 -C 被忽略,而不是把旧对话悄悄移到新文件夹。

dshline 已经打开你当前所在的文件夹,所以这个不需要别名。

如果你的 Harness 是源码检出而不是全局安装,请指定检出一次:

export DSH_HARNESS=~/path/to/deepseek-harness

检出没有可指向的 dsh 可执行文件——它的启动器是一个脚本——因此这命名文件夹、让 dshline 从那里读取该脚本。见 安装 → 故障排查

没有全局 dsh 也没有那个变量时,pnpm dsh 仍然有效——但只在 Harness 文件夹内部,因为该脚本属于 Harness 仓库。见 安装 → 故障排查

按键

enter发送。一轮正在运行时,排队或导向它——见排队还是导向
ctrl-enter一轮正在运行时以另一种方式发送,前提是你的终端能发出这个键
shift-enteralt-enter换行而不发送
tab接受高亮的建议
ctrl-c停止 agent;如果它没有运行则退出
ctrl-d从任何地方退出——包括选择器、提问或审批提示
ctrl-l清屏
ctrl-o检视最近一条被截断的工具输出,任意细节级别;否则循环切换显示多少工具输出:compact、full、hidden
ctrl-r搜索你本次会话发送过的内容;再按一次跳到下一条更早的匹配
ctrl-z撤销最后一次草稿编辑
ctrl-y重做上一次撤销的草稿编辑
在你更早的消息之间移动;在跨行换行的长提示内,先在其内部上下移动,然后 召回历史;建议列表打开时,改在列表内移动
enter esc确认或关闭一个框或列表

编辑按键: 移动,homeend(或 ctrl-actrl-e)到行的两端,backspacedelete,以及 ctrl-uctrl-kctrl-w 分别删除到行首、到行尾、按词删除。在跨行换行的提示中, 也穿过折行上下移动,在短行之间保持你瞄准的列。

ctrl-z 撤销最后一次草稿编辑,ctrl-y 重做。连续的输入合并为一步撤销;光标移动、接受补全、召回的历史行或已提交的提示,都会开启全新的一步——历史归历史,已发送的提示绝不会被撤销回来。

粘贴多行会全部插入,并作为一条消息发送。

输入历史

没有建议列表打开时, 一步步回到你本次会话发送过的行——提示与斜杠命令都一样—— 再向前。打到一半的行会为你保留:往回看更早的消息,再向前越过最新一条,就会精确恢复你未完成的行。

连续相同的提交只记一次,因此连跑三次 run tests 不会让历史里堆三份副本。

用 ctrl-r 搜索你的输入

一行行往回走,对最近几条够用。ctrl-r 在同一份历史上打开搜索,于是一小时前的提示只差几个字符,而不是按五十次键。

╭─ dshline ────────────────────────────────────── History 2/3 ─╮
│ ⌕ auth█                                                      │
│                                                              │
│   fix token refresh after auth retry                         │
│ ❯ investigate why auth state disappears after resume         │
│   ↳ include the subscription-provider case                   │
│                                                              │
│   /permission read-only                                      │
╰─ type to search · ctrl-r/↓ older · ↑ newer · ↵ recall · esc ─╯
ctrl-r打开搜索,再按则移到下一条更早的匹配
输入过滤,最新的匹配在最前
移到更新或更早的匹配
home end跳到最新或最早的匹配
backspacectrl-wctrl-u删一个字符、删最后一个词、清空整条查询
把选中的行放回输入框——不会发送
escctrl-c关闭搜索,一切保持原样

匹配就是不区分大小写的普通子串:你输入的内容按字面在行内任意位置查找,因此 auth 能找到 reauthorizeAUTHauth 找到同样的行。空格算数。没有模糊匹配,也没有排序——结果就是匹配到的行,最新的在前。

只召回该行而不发送,于是你可以先编辑,等真要发时再按 enter。被召回的行保留它在历史中的位置:从那里按 继续走到它之前的一行,按 向前走,最终恢复你搜索之前打到一半的行。esc 让输入框保持原样,光标位置也不变。

搜索只覆盖本次会话的输入:你的提示与斜杠命令,也就是 走过的那些行。它不搜索回复、工具输出或其他会话——要找过去的对话,去 /sessions

长提示或多行提示会围绕匹配到的那一行预览,而不是只显示第一行,于是你能看出一条结果为什么在列表里。会话还在重新打开时按 ctrl-r 也没问题:搜索会说明历史仍在加载,你已经输入的内容会在历史到达的那一刻立即解析。

重新打开会话会恢复保存的日志记录下的历史:每一条提示与每一条输入被记录的已解决斜杠命令。本界面自己处理的命令(/model/reasoning/usage/timing/enter/new/clear/sessions/work/todos/skills/exit/quit)与打错的命令在会话打开期间被记住,但不会写入会话日志,因此恢复后不会重现。

排队还是导向

在 agent 工作期间按 enter,可能发生两件事,而它们确实不同:

Queue(排队)把它当作单独的后续轮次处理,等当前这一轮结束
Steer(导向)把它交给正在运行的这一轮,在它下一个可用步骤边界

排队面向下一件事——“然后更新 changelog”。导向面向你眼前这一轮——“别再用那个文件了”。导向会加入已经在进行的推理,这既是它强大的地方,也是你不希望误触发它的原因。

普通 enter 默认排队。 /enter 可以改:

/enter          ask, with both described
/enter queue    plain enter queues while a turn runs
/enter steer    plain enter steers while a turn runs

这只改变一轮正在运行时enter。没有任何东西在运行时就没有选择可做——没有步骤供导向抵达——所以 enter 就是发送。

空的输入框会说明当前生效的是哪一个,你不必记住:

› type to queue
› type to steer

ctrl-enter 为单条消息以另一种方式发送,而不改变设置。它被放在最后,因为它是这个界面里唯一无法承诺的键:除非终端支持下文所述的额外键盘模式,否则终端为 ctrl-enter 发送的字节与 enter 完全相同,而且无法查询它到底支持哪种。在不支持的终端上,ctrl-enterenter 完全一致——即你的偏好——这也是输入框从不宣传它的原因。node tools/keyprobe.mjs 会告诉你终端是否发送它。

这个选择与你的其他设置一起存储,因此能在重开会话和重启后保留。在没有设置提供者的 profile 上,它在会话运行期间仍然生效,而 /enter 会说明未能存储。

关于 shift-enter

默认情况下,终端为 shift-enter 发送的字节与 enter 完全相同,因此没有程序能区分它们。为让差异可见,本界面启动时会向你的终端请求一个额外的键盘特性:kitty 键盘协议的最低选项,名为转义码消歧(disambiguate escape codes)。支持它的终端(kitty、Ghostty、WezTerm、foot、较新的 iTerm2 与 Alacritty、Konsole)随后把修改过的 enter 报告为自己的序列。

那个请求有一个值得知道的副作用。在支持它的终端上,escaltctrl 组合也不再以旧形式到达:ctrl-c 变成序列 CSI 99 ; 5 u,而不是单字节 0x03。本项目两种形式都读,因此上表中每个快捷键两种方式都有效。细节见 设计 → 键盘输入以两种格式读取

在忽略该请求的终端上,shift-enter 仍然发送消息。这就是为什么状态行建议 alt-enter 代替:alt-enter 处处有效。界面退出时额外模式被关闭,因此下一个程序正常读取你的键盘。

如果某个按键没有反应,node tools/keyprobe.mjs 会显示你的终端发送了什么、本项目如何读取它。那个输出正是缺陷报告需要的。

命令

输入 / 看你的 agent 实际拥有的命令。它们来自两个地方。

由本界面处理:

/model更改模型。接受一个名字(/model deepseek-v4-pro)或打开一个可输入的选择器
/reasoning更改模型思考的强度。接受一个级别(/reasoning max)或打开选择器
/connect配置并认证 Harness 可以对话的提供方。接受路由名(/connect openai)以按它过滤打开
/plugins浏览、搜索并定制运行中 agent 的 Harness 预设组合
/profiles浏览 Harness 配置文件及各自组合的 bundle;安装、更新或移除其中之一
/usage检视本会话消耗了什么。costtokensoff 设置状态行报告什么;不带参数时打开检视器
/timingonoff 控制常驻的实时轮次计时面板;裸命令翻转它
/enter一轮正在运行时普通 enter 的行为:queuesteer;裸命令则询问。见排队还是导向
/theme选择颜色配色。接受一个名字(/theme ember)或打开选择器
/work打开活动 Harness 工作流、subagent 与任务的有界实时视图
/context打开一个有界视图,显示是什么在占用模型的上下文,以及其中最大的条目
/new在当前工作区开始一个全新会话;当前激活的 Harness 配置文件提供会话持久化时,上一个会话仍可重新打开
/clear清屏并在当前工作区开始一个全新会话,如同 /new;当前激活的 Harness 配置文件提供会话持久化时,上一个会话仍可重新打开
/sessions不离开窗口浏览、搜索并重新打开过去的会话
/todos打开当前 Harness Todo 列表的有界只读视图
/skills浏览运行中 agent 可用的技能,并把其中之一放进提示
/exit/quit退出,与 ctrl-d 相同

前三个每一个都一样工作:**给出值就更改,只打命令就询问。**你很少需要凭记忆做其中任何一个,因为命令名后一出现空格,建议列表就提供取值:

› /reasoning
    › /reasoning off      no thinking at all
      /reasoning high     the usual level
      /reasoning max      as hard as it goes
      /reasoning default  whatever the provider does when nothing is set
      tab complete · esc dismiss

/rea 上按 tab 会补全名字并把光标留在空格后,取值无需再按一键就出现在那里。当你想阅读描述时,选择器是后备,而不是唯一入口。

来自 Harness,因此列表取决于你的配置文件加载哪些插件。使用标准插件集时:

/compact总结更早的对话历史以腾出上下文
/plan/plan off进入或离开计划模式
/goal显示或设置长任务的目标
/permission更改权限预设(见下文)
/feedback记录关于本次会话的备注

每条命令的结果都打印进会话记录:正常输出是一条 · 行,失败则是一条 行。未匹配任何内容——既不是命令,也不是技能——的命令名会被报告,而不是发送给模型:

✗ unknown command: /help · type / to see what there is

开头的 /name 按一个固定顺序解析:先是本界面自己的命令,然后是 Harness 注册的命令,最后是你的 agent 能看到的技能同名时命令总是获胜。 如果 /review 既是注册命令又是技能,/review the diff 运行命令;该技能在 /skills 中仍然可见,但不带斜杠列出,因此列表绝不承诺它做不到的手势。

该检查使用 Harness 自己对命令行是什么的规则,因此名字必须要么结束该行、要么后跟一个空格。这意味着 /etc/hosts is missing 被当作普通消息原样到达模型,而 /tmp is full 被当作命令报告为未知。这个取舍是刻意的:打错的命令远比以文件夹名开头的消息常见。

Warning

**/goal <objective> 不只是记录一个目标。**它会启动 Harness 的目标驱动器,立即在最多 256 轮内、使用你文件夹中的工具,自行开始处理该目标。不带文本使用 /goal 只查看当前目标,/goal pause/goal clear 停止一个。开始前没有任何警告——但一旦开始,状态行会在其运行的整个期间点名说明。

**目标也可能在你不知情时启动。**Harness 给模型一个 create_goal 工具,并告诉它可以不要求你说出「goal」这个词、就从你要求的内容推断长期目标。状态行是你发现它的方式;/goal 完整显示它,/goal pause 停止它。见 本会话接下来要做什么

Connect

/model 在已存在的模型中选择。/connect 是模型如何得以存在的途径。

它打开一个有界浮层,列出 Harness 说可以配置的内容,分两个分区:

╭─ dshline ────────────────────────────────────────────────────────────── Connect ─╮
│ ⌕                                                          9 rows               │
│                                                                                  │
│ Provider routes                                                                  │
│ ❯ ● OpenAI  openai                        active · 41 models · key from          │
│       llm-pi-ai · providers.openai · credential field apiKeyEnv                  │
│   · Anthropic  anthropic                                        dormant           │
│   ● DeepSeek  deepseek-official     active · DEEPSEEK_API_KEY unset              │
│                                                                                  │
│ Sign-ins                                                                         │
│   · ChatGPT (Codex)                                     not signed in             │
╰─ ↑↓ move · ctrl-r refresh · ↵ configure · esc close ──────────────────────────────╯

输入以过滤、 查看 Harness 允许你对选中行做什么、esc 清空查询、再按 esc 关闭。/connect openai 在该过滤上打开——命名路由表示你指哪一个,补全列表在空格后提供每个路由名,方式与 /reasoning 提供级别相同。它不会对其操作:对路由做什么仍然是在存密钥、激活与移除之间的选择。ctrl-r 再次询问 Harness,这是你在手动编辑 settings.yaml 或从另一个窗口的 Web 界面存入密钥之后想要的。

Provider 路由是每个已挂载适配器声明可配置的所有路由,无论它是否存活。裸挂载的 llm-pi-ai 以这种方式发布它整个已安装目录,因此 OpenAI、Anthropic、Google、OpenRouter 等在任何东西被配置前就被列出。active 表示适配器已注册该路由,/model 已经可以提供它的模型;dormant 表示还没有为它配置任何东西。

**登录(Sign-ins)**是 Harness 已注册的授权流程——那些获取凭据而不是从配置读取的登录。它们被单独列出、刻意不并入提供方行:Harness 不发布流程凭据记录与提供方路由之间的关联,因此本界面两者都显示,把连接留给你,而不是断言它无法验证的东西。

行前的点是刻意安静的。绿色表示已确认存在某个具名凭据,红色表示已确认缺失某个具名凭据,其余一切不加标记——通过提供方自身发现来认证的路由,或没有可询问的凭据存储的部署,并不算配置错误。

提供什么

只有已挂载 seam 实际上会接受的,因此列表上没有会以拒绝应答的东西:

用 API 密钥连接通过 Harness 的凭据存储保存密钥,并把引用记录到提供方的设置配置文件中
激活此路由写入一个最小配置文件,让适配器注册该路由;目录路由继承其端点、协议与模型
遗忘已存储的 API 密钥清除值;引用保留,因此路由继续命名它的密钥属于哪里
从你的设置中移除该路由取消设置你的设置文档携带的配置文件,保留任何组合默认值
登录通过 Harness 的授权 seam 运行所属插件自己的流程
遗忘此登录删除本地凭据记录——见下方警告

打出的密钥永远不会进入 settings.yaml。它进入凭据存储,设置文档只记录引用——名为 openai 的路由是 OPENAI_API_KEY——这与 Web Models 页使用的约定相同,因此在这里存的密钥就是 Web 界面读到的那个。

路由一旦存活,/model 无需进一步步骤就能看到它的模型:Harness 在设置提交时重新注册路由,浏览器重新读取自身。

关闭浏览器会撤消它启动的登录,包括在屏幕上没有提问、等待浏览器回调的那个。被撤消尝试的任何东西之后都不会出现;会话记录说它被撤消,那就到此为止。

Warning

**「遗忘此登录」是本地的。**它删除本机上的已存储凭据记录。Harness 没有办法让提供方声明服务端撤销,因此签发方永远不会被告知,授权在到期或你向提供方撤销之前一直有效。

声明与编辑一条路由

+ Add custom provider 通过 llm-pi-ai——今天唯一一个其设置 profile 能够描述一整条路由的配置域——声明适配器未附带任何内容的路由:私有网关、自托管服务器、本地的 OpenAI 兼容端点。它依次询问 provider id、端点、协议、可选的 API 密钥、请求头与模型目录,并且在你于最终审查中选择 Create provider 之前,什么都不写。以这种方式声明过的路由会获得 Edit route,在已有内容上打开同一组字段。

请求头会随该路由发出的每一个请求一起发送,也正是一个用 API 密钥以外的方式做认证的网关能被访问到的唯一途径——租户 id、签名的代理令牌、Authorization bearer、你的出口网络要求的路由标签。请把值当作敏感内容对待:设置 seam 并不会把它们标记为凭据,因此即便你放进去的是一个令牌,它们也会作为普通配置被存储和显示。路由菜单只列出它们的名字,因此路过这个菜单永远不会把其中任何一个显示到屏幕上;值要在你打开 Request headers 并移动到该请求头自己那一行之后才显示——那是你唯一主动要求看它的地方。Harness 自己的归属信息在保留名称上优先。

Fetch available models 询问端点它对外声明了哪些模型,并让你选择采纳哪些;在你保存之前,取回的任何东西都不会被写入。对已经存在的路由,拥有它的适配器会自行解析该路由已存储的请求头与凭据,因此一个位于请求头认证之后的网关能够作答。对你仍在声明中的路由,还没有任何已存储的内容可供解析,于是这次取回不带它们发出,并会明说这一点——先手工添加模型、创建路由,然后重新打开 Edit route,在请求头已就位的情况下再取一次。

仍然属于设置工作的部分:compat、重试策略、超时与每个模型的推理设置留在 settings.yaml 里。编辑 /connect 显示的内容,永远不会扰动它没有渲染的字段。见 通过网关访问 DeepSeek

Plugins

/plugins 在运行中 agent 的 Harness 预设上打开一个有界浮层——该 agent 实际加入的那个由工具、 提示词分节与委派后端构成的具名组合,而不是本界面自己保存的一份固定清单:

╭─ dshline ───────────────────────────────────────────────────────────── Plugins ─╮
│ Preset: Standard mode                              default: Standard mode       │
│                                                                                 │
│ ⌕ codex                                                            1 row        │
│                                                                                 │
│ ❯ ○   tool-subagent-codex               @deepseek-ai/dsh-tool-subagent          │
╰─ ↑↓ navigate · / search · space toggle · p presets · d default · esc close ─────╯

输入 / 可按行 id 或包名搜索一个庞大的组合;在选中行上按 space 打开或关闭它。 **内置预设绝不就地编辑。**Harness 把那些文件设为只读,因此在内置预设上切换某一行会先提议把它 复制为一个本地编写的预设——与官方 Web 界面自己的预设设置所用的"复制,然后编辑副本"是同一条 路径——并在同一步里把切换应用到新副本上。p 打开完整名册(该部署实际拥有的那些预设,而不是 固定的四个),用来切换到另一个预设或为新会话设定默认值;d 直接把当前显示的这个设为默认值。

**这里的预设切换遵循运行中会话已有的同一条规则。**会话的组合在它产生过一轮之后就是既成事实, 而不是本界面事后可以改写的设置:为一个已经开始的会话选择预设会被拒绝,并改为作为下一个会话 的默认值提供——绝不是静默的空操作,也绝不是绕过那道锁。你切换的某一行同样如此:文件无论如何 都会写入,但只有仍然空白并且正在运行该预设的会话才会实时接手这次变更。其他情况都被报告为 一次等待下一个会话的定制,因此变更绝不会看起来在一段它并未触及的对话上生效了。

重新打开一个会话时,它按自己日志所记录的预设组合,而不是按今天的默认值。dshline 采纳预设之前 的会话没有记录预设;那些会话按随附的 standard 恢复,这个预设的含义正是它们最初运行时的那套 工具集。如果你的部署未提供可用的 standard,这样的会话仍然会打开——用你自己的默认值——并且 transcript(文本记录)会说明它的工具可能与这段历史产生时所用的不同。

Profiles

/profiles 打开 Harness 自己的配置文件名册——预设之上的那一层:

╭─ dshline ──────────────────────────────────────────────────────────── Profiles ─╮
│ Host: dshline                                                  3 profiles       │
│ /Users/you/.dsh/profiles                                                        │
│                                                                                 │
│ ⌕ / to search                                                     6 rows        │
│                                                                                 │
│ ❯ ● dshline                                                       current       │
│       Bundles                                                                   │
│   ✓   @deepseek-ai/dsh-base                       from the installation         │
│   ✓   @dshline/dshline                                             0.8.0        │
│   ○ web                                                                         │
╰─ ↑↓ navigate · a add · u update · U update all · n new · / search · esc close ──╯

配置文件是启动器启动的对象:dsh --profile <name> 读取 $DSH_HOME/profiles/<name>, 其 package.json 列出有序的 bundle,它们的补丁层组合成 Host。 标记本会话正在运行的配置 文件。每个配置文件下面是它的 bundle 层,凡是 pnpm 的状态已经记录了安装版本的地方都一并给出; from the installation 表示这是随 dsh 本身一同提供的内置 bundle,而不是该配置文件的依赖之一。

a 安装一个 bundle,u 更新选中的那个,U 更新每一个由依赖管理的 bundle,r 移除一个 (需要确认,因为它会让此后每个会话都少一项能力),n 创建一个配置文件。上述每一项都运行 Harness 自己的 dsh plugin --profile <name> …,那是一个薄薄的 pnpm 转发器,事后会协调 bundle 列表——本界面不添加任何自己的安装器、解析器或 lockfile 行为。U 明确点名各个 bundle,而不是 运行一个裸的 pnpm update,后者还会更新那些并非 bundle 层、这里也不显示的普通库。

启动器的查找方式与 dshline 自己相同的四种——DSH_BINDSH_HARNESS 源码检出、PATH 上的 dsh,然后是已安装的 @deepseek-ai/dsh 包——因此这些操作在本界面能工作的地方都能工作。四种 都找不到时,会指出确切命令,供你自己运行。如果失败输出重要,它的最后几行会被提交进 transcript (文本记录),而不是随浮层一起丢失;可能在 URL 中携带 token 的 spec 会被从那份记录中略去,而 不是保留在里面。

**操作运行期间,边框会说明这一点。**一次 pnpm 安装要花上几分钟,因此运行中的操作在整个运行期间 显示为 <profile>: <what>… 旁边一个旋转的转子,而不是一条会过期的消息——并且它一结束该行就 消失,因为在已完成的工作上留一个转子说的是与事实相反的话。一旦对你正在运行的配置文件的变更落地, ↻ restart required to pick up: <profile> 会一直留在屏幕上,直到你关闭浏览器——而关闭它不会 停止任何东西:仍在运行的工作,以及仍然欠下的重启,都会在退出时写入 transcript(文本记录)。 其他按键全程可用;只有针对同一个配置文件的第二次操作会被拒绝,并且它会说出来,而不是什么都 不做。

**Bundle、层、依赖。**三个词对应三件不同的事,而它们的区别决定了一次安装到底做了什么:

依赖配置文件 package.json 里的任何东西——已安装,仅此而已
bundle一个其自身清单声明了 dsh.bundle、指向它导出的某个 cordis.patch.yml 的包。这是的属性,由发布它的人决定
配置文件 dsh.profile.bundles 列表中的一项。启动器按顺序应用每个所列 bundle 的补丁,构建出 Host 组合

因此 bundle 是一个拥有可贡献补丁的包,而层是一个正在被应用的补丁。dsh plugin 让层列表与 已安装的内容保持同步:声明了 dsh.bundle 的依赖会被追加进去,不再声明它的依赖会被剔除。从不 声明的依赖被安装,并且什么也不组合——永远如此,而且这是对的。

这就是版本重要的原因。同一个包名可以在一个版本上是 bundle,在另一个版本上不是,因为那条声明是 在某个时点才加上的;旧副本是普通依赖,而更新它会让它成为一个层。

/profiles 把不是层的依赖列在 Installed, composes nothing 之下,每个旁边标注 not a bundle, 这样一个什么也没改变的包是可见的,而不是缺席的。标注了 ⚠ declares dsh.bundle 的那个才是值得 处理的情况:已安装的副本确实是一个 bundle,而层列表还没跟上,任何一次 dsh plugin 运行都会 协调它——每当 pnpm 以非零状态退出时,那次协调就被跳过,这个状态正是这么来的。r 移除一个非层 依赖的方式,与它移除 bundle 的方式相同。

**添加一个 bundle 不是搜索。**这个输入框接受确切的包名(或 pnpm add 接受的任何 spec)并原样 转发,因此一个不完整或记错的名字会得到一次失败的安装,而不是一份候选列表。失败时,pnpm 给出的 原因就是标题——名字不存在时是 ERR_PNPM_FETCH_404,这台机器访问不到的仓库是 ERR_PNPM_GIT_RESOLVE_FAILED 以及 git 自己的 fatal: 那一行——同时输出的最后几行会被提交进 transcript(文本记录)。那些是 pnpm 的错误,也是 pnpm 的修法:比如一个在这里需要 SSH 的 git 依赖,要靠你机器上的 git config url."git@github.com:".insteadOf,而不是本界面能决定的事。

其中一种值得知道,因为它会阻塞一个配置文件上的每一项操作,直到你回答它,因此 /profiles 会 在你按下任何键之前就发出警告:这样的配置文件被标记为 builds pending,选中它会指出那些包, 以及回答它们的文件。ERR_PNPM_IGNORED_BUILDS 表示某个依赖想运行构建脚本,而 pnpm 不会在无人 值守的情况下运行它;pnpm 会为每一个在该配置文件的 pnpm-workspace.yaml 中写入一个占位:

allowBuilds:
  '@google/genai': set this to true or false
  protobufjs: set this to true or false

把每一项设为 truefalse,操作就会继续。/profiles 在看到这个错误时指出那个文件,但绝不 编辑它:允许一个构建脚本就是运行来自依赖的、安装期的任意代码,这是你的决定,不是一个终端浏览器 的决定。Harness 也不替你回答——它在创建配置文件时写下基础的 pnpm-workspace.yaml,此后再也 不碰它。请注意,dsh plugin 自己在这里可能挂起而不是失败,因为 pnpm 试图交互式地询问; /profiles 不给它的子进程任何可供询问的终端,因此它改为报告这个错误。

**两件它刻意不做的事。**它不会移除或更新内置 bundle,因为 dsh plugin 也不会——那些来自安装 本身,把它们的行关掉属于配置文件自己的 cordis.patch.yml。它也不会切换配置文件。Host 在启动时 一次性组合它的插件,没有任何东西能重新链接一个运行中 Host 的 bundle 层,因此在另一个配置文件上 按 enter 会指出启动它的命令,而不是假装把它换进来。

**移除一个 bundle 不可能弄坏随附的配置文件。**只有这个配置文件依赖的 bundle 才能被移除或更新 ——随 dsh 本身一同提供的那些层会被拒绝,这也是 webheadless 里根本没有可移除内容的原因。 删除整个配置文件不提供:dsh plugin 转发 pnpm 参数,而 Harness 里没有任何东西会移除一个配置 文件,因此 enter 指出那个目录,把这件事留给你。

**重启边界是被明说的,而不是被暗示的。**安装、更新或移除一个 bundle,改变的是下一个 Host 组合什么。在你正在运行的配置文件上,结果说 restart required;在任何其他配置文件上,它指出会 接手它的命令。这里没有任何东西声称改变了你所在的这个会话。

Sessions

/sessions 打开一个有界浮层,列出 Harness 知道的会话,最新的在前。它与 --resume 在第一个 agent 存在前打开的浏览器相同,因此只有一个学习的地方、一套按键。

type边输入边按标题、工作区或 id 过滤列表
tab通过 Harness 自己的会话索引搜索会话说过的内容
移动;列表两端都回绕
home end跳到最新或最旧的行
重新打开选中的会话
显示选中会话的详情与它自己的操作;esc 返回
ctrl-f收窄语料库:工作区、来源、年龄
ctrl-w ctrl-u删除查询的最后一个词,或整个查询
esc清空查询;查询为空时再按一次关闭
ctrl-d退出,与别处一样

输入过滤你能看到的行。tab 是另一个问题:它把同样的词交给 ctx.sessionQuery 的全文接口,后者搜索每个会话日志的内容并显示匹配的摘录。编辑查询会掉回过滤,因为内容结果回答的是编辑前你打的词。会话查询后端未实现全文搜索的部署会说明这一点并继续过滤——那条路径受支持,而不是坏了。

一行就是一个标题加一个相对年龄,这是刻意的:清单回答的是哪一个会话,而在每一行重复的工作区是在与这个答案争夺注意力,而不是补充它。右侧唯一的例外是 open,标记本窗口已经在驱动的会话——也就是重新打开会拒绝的那一行。关于一个会话的其他一切都只差一次按键: 显示它的工作区、创建时间、上次活动时间、日志有多少事件、它是否是委派的、Harness 是把它作为存活的还是持久化的持有、它的 fork 或委派父级,以及它的 id。在你打开它之前那里什么都不读取,因此在清单里移动完全不花费任何会话日志读取。

重新打开使驱动当前会话的 agent 退役,并在同一个窗口与同一个终端中恢复你选的那个。你滚动缓冲区(scrollback)里已有的一切都留在那里:恢复的会话记录追加在它下面,与 --resume 在启动时绘制它的方式完全一致。

当重新打开意味着猜测时,它会拒绝,并说明适用的原因:

会话已在此打开无事可做
会话在本进程中存活恢复会与存活 id 冲突
没有持久化日志重新打开经由 Harness 会话持久化加载
一轮正在进行先结束或中断它(ctrl-c
有任务或 subagent 附着使它们的所有者退役不是 Harness 定义的生命周期

在这些事实之下,同一个界面提供能对那一个会话做的事,不提供任何针对整个语料库的东西:

Find in this session通过 searchEvents 搜索某一个会话说过的内容,有自己的查询行(tab 执行搜索)
Lineage通过 traceSession 浏览选中会话已知的父级与子级; 把列表焦点还给该会话
Rename通过 ctx.sessionTitle 重命名本窗口正在驱动的会话(open 行),仅在挂载了会话标题服务时提供

过滤是关于语料库的问题,因此它有自己的按键,而不是放在某一个会话的标题之下:ctrl-f 打开工作区(all/current)、来源(all/own/delegated)与年龄(all/today/7 days/30 days)。用 ctrl 手势,因为这里裸的字母是搜索输入。

工作区与年龄变成精确的 Harness 子句(cwd 匹配、created-at 闭区间窗口),因此收窄发生在 Harness 内部。来源只在呈现层应用,因为 Harness 不发布来源谓词;每一行的分类来自 Harness 为同一会话返回的权威观测头部——搜索后端自己的命中投影省略 origin 时,批量标题观测仍然给出不可变的头部,因此持久化委派子会话的命中不会被误标为 own。过滤生效时标题出现 · filtered,改变过滤会重新开始分页。

两个内容作用域(tab 语料库搜索与 Find in this session)都通过不透明的 Harness 游标分页。末尾的 Load more… 行追加下一页();当语料库在游标之下变动时出现 Refresh (results changed),计数器说明有多少结果(· more available· end)——绝不是一个页码,因为 Harness 不发布页码。

重命名会追加一个带显式 user 来源的 session/title 事件:它钉住会话的标题(自动生成停止),浏览器从日志重新读取它的标题观测。它从不重新打开会话——重命名只在已在本窗口打开的会话上提供,因为通用标题服务只操作活动会话对象,而重命名一个已关闭的持久会话需要先恢复它。

即使重新打开仍然失败——不可读的日志、不兼容的格式版本、没有持久化后端——窗口打印原因并重新打开浏览器,让你选别的。在那里按 esc 改为开始一个新会话。它从不结束进程,也从不悄悄替换你没要的会话。

Work

/work 打开一个临时有界浮层:dshline 对本会话中 Harness 正在运行什么的实时视图。配置文件挂载了通用 Harness ctx.jobsctx.subagents 能力时,它读取它们,再加上 workflow 工具写入本会话自己日志的工作流(workflow)记录;这两项能力都没有的配置文件仍然启动,浮层说明 Work 不可用。它绝不切换屏幕或重写会话记录,因此关闭它回到同一个原生终端滚动缓冲区。

工作流、subagent 与任务保持独立分区,因为它们是彼此独立的 Harness 权威,而 dshline 不猜测两条能力记录描述同一个操作。它确实展示的那一条关系是发布出来的,而不是猜的:工作流成员携带它启动的 subagent 的 childId,因此那个子级出现在它的工作流之下,而不是在扁平的 Subagents 分区里第二次出现。任务仅限检视/状态;取消仍通过 Harness job_kill 供模型使用。工作流运行在这里完全没有控制手段,因为 ctx.workflowEngine 只把运行句柄交给启动它的调用方。状态行的 work 片段统计的正是这个浮层所显示的内容,因此呈现在工作流之下的子级不会在那里再被算作一个游离的 subagent。

行的标记说明 dshline 对它到底知道多少:

标记含义
◜◠◝◞◟◡观察到的执行:Harness 表明正在运行的存活进程内子级 Agent
活动的生命周期,但其内部不可观察
存在一条后台任务记录
任务正在停止
已完成、失败、已取消

只有弧线转子会动,而且它就是状态行使用的那一个。整条规则就是这样:动画意味着有正在计算的证据。处于 running 的任务是一条注册表记录而不是一次观察,所以它保持安静——提供方没有发布进程内子级的 subagent 运行同样如此。像 Codex 或 Claude Code 这样的外部提供方自己管理其模型与工具流量,并不通过通用 subagent seam 暴露它们,因此 dshline 显示该运行的生命周期与已用时间,不为它编造任何活动。

存活的进程内子级确实携带语义活动词——waitingthinkingrespondingreadingsearchingfetchingeditingrunningworking——并且当运行中的工具自己的呈现给它取了标题时,还有如 overlay.ts 这样的简短操作。两者都由状态行读取的同一组 Harness 会话事件与工具呈现折叠而来;绝不根据工具名猜测。

subagent 行的构造首先回答一个问题——这个工作者正在做什么——因此它先读任务,再读活动,再读究竟是哪个 LLM 在为它提供算力:

◜ Fix OAuth flow · reading route-editor.ts · openai-codex/gpt-x 18s
● Native review · codex 18s

子级的持久标签居首且绝不让位;终端变窄时先丢掉时钟,然后是模型路由,然后是操作对象,然后是活动词,且始终整条事实一起丢。后端——拥有该生命周期的 ctx.subagents 提供方,例如 spawncodex——只为其工作不可观察的子级占用总览空间,因为在那里它正是解释这份沉默的事实。

后端与模型是两个不同的权威,绝不合成一个词:一个 spawn 子级可以由任何已注册的 LLM 路由提供算力。子级发出过请求之后,模型来自它自己最新记录的请求信封;在那之前来自它被创建时的选项;路由变更只是一个更晚的信封。没有进程内子级的运行完全不会有模型这一行。

工作流行给出运行名、它最新的 phase(...) 叙述、有多少成员仍未结束,以及它已经启动了多少个。没有分母:meta.phases 声明的是进度词汇,而不是脚本会启动多少个 subagent,所以没有可信的总数可以拿来做除法;脚本尚未发出的调用也不会被列为待定。

打开选中行。总览之下的一切同样是列表: 在详情视图的各条事实之间移动高亮,homeend 跳到它的两端,视图会滚动以跟随光标。被聚焦的行不必是可操作的——在一条普通事实上按 什么也不做,而不是编造一个动作——而子级仍然存活的工作流成员会打开该子级自己的 subagent 视图。esc 恰好返回一层,因此从工作流进入的成员会回到那个工作流;在总览按 esc 关闭 Work,会话记录保持原样。

工作流视图显示存活引擎发布过的运行描述、它的状态、它最新的叙述,以及按每个成员被记录时的确切阶段分组的成员。这些阶段来自成员记录,而不是脚本声明的 meta.phases:一个没有任何成员进入过的声明阶段会读作待定工作,而这样的工作并未被发布。子级仍然存活的成员会显示该子级自己的活动与模型路由,来自子级的状态,而不是来自脚本对这个阶段所声明的任何内容。subagent 视图以子级在做什么、对什么做开头,然后是其路由携带时的模型与推理强度、它的后端、它已经工作了多久、该数字可归属于它时的 token 总数,以及模式(continuable/one-shot),接着是这一关系具有权威时的工作流、阶段与成员标签,最后才是报告需要的各种标识:持久会话 id、存活 Agent 状态、会话驻留状态、子会话、生命周期 run id,以及本次运行是否发布了进程内子级。任务视图显示它的状态、种类、生产者细节、已用时间、所有者与 job id——不会有任何一行去宣告它并不具备的动作。

tokens 是 Harness 的 tokenUsage 投影,它在子级的完整日志上折叠提供方报告的用量。它只为其 Session 不携带 fork 继承历史的子级出现,因为只有那时整个投影才可归属于该工作者:fork 出来的子级日志以其父级已完成的各轮开头,而那些用量就在同一个数字里。这样的子级仍然可以显示 active time,因为 subagentTiming 在子级自己的 descriptor 处重置,而 tokenUsage 没有对应的重置。

时钟带标签,因为有不止一个诚实的答案。active time 是 Harness 自己的 subagentTiming 投影:子级已完成的各轮加上一个未结束的轮次,只在子级确实正在运行时前进,不运行时冻结在投影最后折叠到的位置。elapsed 是较弱的回退——这一生命周期纪元已经打开了多久——为配置文件不投影其计时的子级显示。两者只会出现其中一个。

可续的 subagent 可能提供 k interrupt,它请求 Harness 中断该子级的当前这一轮——保留它的对话、收件箱与后代。一次性 subagent 没有。中断失败——包括授权失败——会短暂显示在浮层中,而不是被丢弃。

当 Harness 表明工作流已经结束时,它离开视图:引擎的停止原因出现在行上,而行本身在运行及其子级静默之后、工具关闭其持久记录时消失。/work 是一个实时界面而不是工作流历史——持久记录留在会话日志里,重新打开的会话会在那里重放它们。

Todos

/todos 打开当前 Todo 投影的临时只读视图。列表由 Harness 的 dsh-tool-todo 能力拥有、持久化并清空;终端只呈现它的当前快照。 是已完成, 是进行中, 是待定。关闭浮层让原生滚动缓冲区不变。没有会话投影或 Todo 投影的配置文件仍然可用,并说明哪种读数不可用。

技能

技能是你的 agent 可以加载的一组可复用的、面向具体任务的指令。它整个属于 Harness——技能从哪里来、同名时哪一个获胜、谁被允许调用它、加载一个会做什么。本界面只展示你的 agent 实际能看到的那些,并帮你把调用其中之一的那一行打出来。

调用一个技能就是打字。 一条以斜杠加技能名开头的消息就调用它:

› /review-pr inspect PR #126, especially lifecycle cleanup

整行按你写的样子作为你的消息发送。Harness 在它自己的边界识别 /review-pr 这个引用,并把该技能的指令放进同一步,因此模型在回答之前就已经拥有它们。这里不会改写这一行的任何部分,会话记录显示的就是你的提示。

它们在 / 列表里。 可以这样调用的技能会与命令并列出现在建议列表中,并标记为技能:

› /plugins       Browse the running agent's preset composition
    /review-pr     skill · Review a pull request
    /security      skill · Review code for security issues
    /sessions      Browse past sessions
    /skills        Browse available skills

接受其中之一会插入 /review-pr 并把光标留在空格之后。它不发送任何东西:名字之后你写的才是请求,准备好时由你发送。

/skills 是浏览器。 它列出运行中 agent 能看到的每一个技能,包括只有模型可以加载的那些,并说明每一个是做什么的:

Skills · 12 available

  /api-review       Review API changes and compatibility
  /debug-ci         Investigate failing CI
› /review-pr        Review a pull request
  /review-tests     Review test coverage
  architecture      Architecture decision guidance
  internal-router   Internal routing guidance
    … 6 more

review-pr
Review pull requests for correctness, regressions, architecture violations,
and missing tests.

Available to   you + model
Source         project
When to use    Before approving or merging a meaningful code change

斜杠是一个承诺,所以它只出现在它有效的地方:上面的 architecture 是模型自行加载的那种,而名字已被某个命令占用的技能同样不带斜杠显示。↑↓ 选择,打字过滤,enter 把选中技能的名字放进提示——它从不发送,也从不加载任何东西。esc 清除过滤,再按一次 esc 关闭,与 /connect/sessions 中一样。

没有组合任何技能注册表的配置文件会明说,而不是显示一个空列表;尚未完成的发现也会明说,而不是宣称你的 agent 一个技能都没有。如果某个技能提供方在你工作时失败,最后一份完整的列表会留在屏幕上,而不是闪成空的。

Note

在自定义的 Harness 组合中,一个技能可能在目录里可见,而用 /name 直接调用它却没有启用。Harness 没有为此暴露单独的信号,因此这个列表无法提醒你;标准预设启用了它,而 /plugins 显示运行中 agent 组合了什么。

Context

四个命令回答四个不同的问题,其中没有哪一个只是另一个的啰嗦说法:

/context此刻是什么在占用模型的上下文
/usage本会话累计消耗了什么
/timing这一轮的时间花在了哪里
/compact缩减当前上下文

/context 是其中第一个。状态行只放得下一个数字;这里放得下答案。

╭─ dshline ──────────────────────────────────────── Context ─╮
│                                                            │
│  184k / 1.0M · 18% · projected                             │
│  ████▎░░░░░░░░░░░░░░░░░░░░░░░░░░░░                         │
│                                                            │
│  Composition · estimated                                   │
│      system      ~12k  ━───────────   7%                   │
│      tools       ~48k  ━━━━────────  26%                   │
│      messages   ~124k  ━━━━━━━━━━━─  67%                   │
│                                                            │
│  Largest entries · estimated · 5 of 128                    │
│  ❯   ~42k  22%  tool result · run_shell_command            │
│      ~28k  15%  tool result · read_file                    │
│      ~18k  10%  assistant reply                            │
│      ~14k   8%  your message                               │
│       ~9k   5%  injected context · instructions            │
│                                                            │
╰─ ↑↓ select · ↵ inspect · c compact · esc close ────────────╯

顶部那个数字是下一次请求的提示词用量,不是会话总计,而 projected(预测) 就是它的准确定名:提供方自己对上一次收到的提示词的计数,加上此后对话增减部分的 估算。它是一种确定类型的数字,只标注一次,而不是一个时开时关的记号——多处变动可以 互相抵消到净估算为零,而对话其实已经动过,所以一个不带记号的数字有时会声称它并不 具备的精度。如果没有任何路线声明过上下文窗口,就既没有百分比也没有条形图——因为没有 一个如实可用的分母可以去除。

**组成(composition)全程是估算,而且它是一个组成而不是总计。**Harness 用一套固定 的密度估算给系统提示词、工具 schema 与对话定价;该估算会系统性地低估 CJK 文本与 JSON schema,这正是上面那个占用数字改为锚定到提供方的原因。所以这三项份额相除的是 它们自己的和,它们不会加总成顶部那个数字。这是诚实的安排,不是舍入误差。

**最大条目才是让这里不只是一根进度条的东西。**Harness 给模型当前携带的每一个条目 定价,dshline 从会话日志中对它们排序并命名:工具结果按 call id 与它自己的调用配对, 所以名字是 Harness 真正运行过的那个工具,而不是并行批次中恰好挨着它的那次调用。 这些价格同样是估算——Harness 的逐条目计量是按路线定价或启发式的,绝不是提供方的 分词器——所以每一个都带 ~,而它们的百分比相除的是当前上下文的已测量总量。

**这份列表是模型当前的上下文,不是会话的历史。**被压缩(compaction)替换掉的一次 交流不在其中:代表它的摘要在,名为 compaction summary——而且只有当它携带压缩自身 对写下它的那次事务的记录时才这样命名,因为 Harness 允许任何插件替换对话的一部分, 而 dshline 不会去猜是哪一个做的。其他任何替代了较早历史的东西只说出这一点: replaced。无论哪种情况都值得知道:你滚动缓冲区里的那张卡片仍然显示当时的内容, 而模型已经看不到它了。

打开一个条目:它是哪一类上下文、占了对话的多少、来自会话的什么位置,以及模型 实际携带内容的有界预览。share 写的是 of message context(占消息上下文),而且 就是这个意思:分母只是对话本身,因为那正是逐条目计量所定价的范围。系统提示词与工具 schema 由上面那套另外的估算统计,而把两种不同的估算加在一起去得出一个整体上下文 百分比,等于是在编造一个两者都没有给出的数字。

╭─ dshline ────────────────────────────────── Context entry ─╮
│                                                            │
│  type       tool result                                    │
│  tool       run_shell_command                              │
│  context    ~42.0k estimated                               │
│  share      22% of message context                         │
│  position   41 of 128                                      │
│  turn       31 · step 4                                    │
│  log entry  seq 418                                        │
│                                                            │
│  Preview                                                   │
│  PASS packages/dshline/tests/context-model.spec.ts         │
│  PASS packages/renderer/tests/rendered.spec.ts             │
│  …                                                         │
│                                                            │
╰─ ↑↓ scroll · esc back ─────────────────────────────────────╯

c 执行压缩,前提是你的 agent(智能体)拥有 /compact 命令——它运行的是那个命令, 而不是它的私有副本,所以只有该命令确实存在时底栏才提供这个按键。压缩落地后各项数字 随即刷新。

没有会话投影、没有 token 计量器、或者没有压缩后端的配置文件仍然可以打开 /context: 它说明哪一项读数不可用,并显示其余部分。绝不为填补空缺而编造任何东西。

Compaction

/compact 属于 Harness,而不属于本界面。它不接受参数,摘要什么、摘要何时算够好、 会话是否足够空闲可以尝试,都是 Harness 的决定。dshline 负责派发它并呈现结果。

它打印的内容来自压缩自己的持久记录,而不是来自命令的那句话——这意味着自动压缩 (agent 因为上下文将满而自行运行的那种)也会说出来:

› /compact
· compacted 27 entries · ~95k replaced

· context compacted automatically · 27 entries · ~95k replaced

又是 ~:被替换的数量是 Harness 对它所遮蔽内容的估算。两行都是普通的会话记录历史, 提交一次且永不重写,因此重新打开会话时它们出现在当时发生的位置。

三个值得知道的后果,按它们影响到你的顺序:

  1. **较旧的对话被一段摘要替换。**模型无法再引用它已经没有的内容。如果某件事重要, 值得重述一遍。
  2. 上下文压力下降,这正是目的所在——/context 立即显示新的数字。
  3. **从第一个被替换的 token 起,提示词缓存复用失效。**下一次请求要为该位置之后的 全部内容付一次缓存未命中,因此紧接压缩之后的那一轮比它的体积看起来更贵,随后的 几轮则更便宜。

超大的工具输出是另一套机制:Harness 就地缩短某一条结果,不触碰它周围的对话。那不会 得到自己的会话记录行——它没有改变任何你能读到的交流——而是在 /context 中作为一个 replaced 条目出现。用 replaced 而不是「已缩短」:会话日志记录的是该结果被替代, 而不是替代物更小,本文件不比日志声称更多。

主题

/theme 选择本窗口绘制所用的配色。指定名字即可切换,或不带参数运行以获得一个带说明的列表:

defaultdshline 一直发布的那套配色
high-contrast完全避开 dim 与灰色的明亮十六色配色
ember面向深色终端的暖色配色
tide面向深色终端的冷色配色
paper面向浅色终端,此时亮黑与 dim 不再是一回事

**主题只影响新的行。**完成的输出会被提交到你真实的终端滚动缓冲区并且永不重写,因此输入框上方的一切都保留它被打印时的颜色。正是这条规则让你能够正常滚动、选择与复制,主题无法豁免于它。应用一套配色会以一行用新配色绘制的确认作为回应;输入框、状态行以及其余仍在实时重绘的部分都会随之改变。

后三套以 24 位色编写。在无法显示它的终端上,每一套都回退到其作者选定的十六色形式,而不是某种近似,并且命令会说明你正在看的是哪一种回退,而不是让你困惑于它为何像你刚离开的那套配色。

NO_COLOR 完全禁用颜色,无论其取值为何,TERMdumb 时同样如此。FORCE_COLOR 覆盖两者:1 表示十六色,2 表示 256 色,3 表示 24 位色。

你选择的主题保存在 Harness 自己的设置文档中,位于本前端注册的 dshline 命名空间下:

# ~/.dsh/settings.yaml
dshline:
  theme: ember

分层由 Harness 负责,因此主题有两个来源,更具体的那个胜出:部署方在 ~/.dsh/cordis.patch.ymldshline 行中组合一个默认值,而你自己的 settings.yaml 覆盖它。/theme 只写入后者。

**它是实时生效的。**在会话运行期间手工编辑该小节会重绘窗口——你不需要重新打开任何东西。已提交的行保留它们被打印时的颜色,正如一切已提交的内容那样。

没有任何随包主题使用的名字会被设置模式拒绝而不是被存储,因此会话不会恢复到一套并不存在的配色上。没有挂载设置提供方的配置文件仍然以其组合出的配色运行;只是保存不可用,并且 /theme 会说明这一点。

主题就是以上五套。配色是针对一套内部的角色词汇编写的——一段文本是什么,而不是它应该是什么颜色——而该词汇尚未公开,因此无法添加你自己的配色。

工具输出

工具卡片显示工具产出的前几行,并带一个说明藏了多少的标记。命令是例外:它的卡片保留最后几行,把标记放在它们上方,因为你运行 pnpm test 想知道的失败与总结在底部,而不是顶部的横幅。

ctrl-o 打开被隐藏的行。在新近完成的工具卡片被截断时,它在那个卡片上打开一个检视器——相同的呈现、可滚动、预算远大于卡片本身——并在 esc 关闭,你的滚动缓冲区保持原样。这在 compactfull 下都有效。没有这样的卡片等待时,ctrl-o 改为循环切换每个未来卡片显示多少:compactfullhidden。已打印的卡片绝不会重绘,这是保持正常终端选择与复制的代价。

在检视器内部, 移到较旧的保留卡片, 移到较新的卡片;/ 滚动当前卡片,home/end 跳到其顶部或底部,esc 关闭。ctrl-o 在这里仍可用作移到较旧卡片的快捷键。标题会数出你所在的位置(Tool output 2/6),导航在两端停下,而不是绕回。最近十二张被截断的卡片就这样保持可达,因此你滚动越过的某个结果,不会被其后的工具调用夺走。每张卡片只提供一次:在最新的那张未看过的卡片之后,ctrl-o 回到细节循环,这正是让那个开关始终只差一次按键的原因。状态行在一轮进行中列出 ctrl-o output

计划审查

exit_plan_mode 提交计划时,审查是一个需要做出的决定,而不是要在选择器里通读的文档:它显示计划的标题、批准或继续规划的选项,以及计划开头的一段有限预览——足以认出它,而非全部内容。

ctrl-o 会以完整、连续、可滚动的文档形式打开整份计划,与工具输出检视器完全一样:/ 逐行滚动,home/end 跳到顶部或底部,ctrl-oesc 返回审查界面。返回不会改变待定的决定——之前选中的选项仍然选中——只有在审查界面本身按下 esc/ctrl-c 才会关闭它,转而让你对模型说话。只有当预览尚未显示整份计划并且完整阅读界面本身也能装入当前终端时,才会提供 ctrl-o——终端刚好够显示精简审查的高度或宽度,仍可能比阅读界面所需的还差一两行。

本会话接下来要做什么

有两样东西改变一轮会话什么,而不是什么,两者在会话记录中都不可见——设置它们的命令打印一行然后滚走,之后的一切看起来像普通会话。因此状态行携带它们:

plan计划模式生效。agent 会提议而不是行动
goal armed · ship the release已设置目标并将自行继续。尚未取任何一轮
goal 3/256 · ship the release已取三轮,上限 256
goal idle · ship the release已设置目标,但本次会话不会继续它。/goal resume 武装它
goal pausedgoal blockedgoal complete一个未在运行的目标,以及原因

目标会出现在那里,因为**目标不总是你设置的。**Harness 把 create_goal 发布为模型自己可以调用的工具,其自身描述说模型可以在没有被要求创建任何东西的情况下推断请求是长期的。因此会话可以获得自行继续的权威,而状态行就是它变得可见的地方。/goal 显示完整目标;/goal pause 停止它。

256 是部署对自动续跑轮数的上限,而不是目标——这就是为什么计数只在实际取了一轮之后才出现。goal 0/256 读起来像卡在零上的仪表;goal armed 真实地说出同一件事。

idle 是每个重新打开的会话对活动目标显示的内容。进程是否可以继续一个目标刻意不与目标一起保存,因此恢复对话不会重新启动你留下的运行——目标还在那里,而再次捡起它是你要求的事。

两种模式都不会在终端变窄时被放弃。它们只在模型名、总计、条形与上下文读数都消失之后才被丢弃,而运行中的目标是最后离开的——在按键提示之后。模式整块丢弃而非缩短:goal 12/25 不是比 goal 12/256 更小的真相,它是不同的一个。目标文本是唯一的例外,而且只因为它是散文:缩短的目标仍然是目标,因此它在关于目标的任何其他内容之前自行被放弃。

推理级别

/reasoning 列出你当前提供方实际接受的级别,而不是固定集合——DeepSeek 适配器是 offhighmax,而配置为关闭思考的部署只提供 off。还有一个 default 选择,它不是级别:它清除你的选择,让提供方在未设置任何东西时做它做的事。

变更从下一步生效,因此在一轮进行中按下不会把请求劈成两种设置,而且它会被记住——见下文。

状态行在模型名旁边命名级别,但只在它不同于你的设置默认值时——否则它每帧花几列在一个你没选择的事实上。

从长列表中选择

网关路由宣传网关服务的任何东西。OpenRouter 与 opencode 提供数百个模型,因此 /model 打开一个没有终端能一次显示的列表——而且你不该滚动浏览它。

选择器开窗到终端,一旦有多于一屏要选就长出一个查询框:

╭─ dshline ─────────────────────────────────────────────────────────────────────────────── Model ─╮
│ Select a model                                                                                  │
│ ⌕ sonnet                                                    6 of 412                            │
│ current: deepseek-official/deepseek-v4-flash                                                    │
│                                                                                                 │
│ ❯ openrouter/anthropic/claude-sonnet-4                                                          │
│   openrouter/anthropic/claude-sonnet-4-thinking                                                 │
│   opencode/claude-sonnet-4                                                                      │
╰─ ↑↓ move · type to filter · enter confirm · esc clear ──────────────────────────────────────────╯

每一行都按 /model 接受它的方式拼写——provider/model——因此你过滤的东西就是可以在命令后输入的,提供方自己的显示名位于选择下方,在不作为你必须匹配的文本的情况下消歧两个相似模型。esc 清空查询,再按 esc 关闭选择器,home/end 跳到任一端。

短列表不变:审批或 /reasoning 无可过滤,因此它不花一行放搜索框,打出的字符在那里也保持无意义。

你在这里选的,就是 Web 界面打开时用的

/model/reasoning 都把选择写入 ~/.dsh/settings.yaml,位于 Web Models 页读写的那同一个 agent-default-model 节。因此它们是同一个设置的两种视图:在终端切换模型,Web 界面就打开在它上;在那里切换,你的下一次终端会话就启动在它上。

这值得你在用 /model 为一个问题尝试什么之前知道,因为它不是会话作用域的试验——下一次会话从你留下它的地方开始。会话记录在发生时这样说:

· model set to deepseek-official / deepseek-v4-pro · also the default for new sessions

两者按那个顺序独立:运行中的会话先切换、绝不回滚,因此如果设置文件写不进去会告诉你,而即将运行的那一轮仍使用你要的模型。

整个选择一起存储——路由与推理级别——因为那一节只放一个选择。只保存级别而丢下模型,会让级别适用于下一次会话恰好打开的模型。

一轮进行中

◜ working 14m 26s · run_shell_command +2 calls · x-preview-f-free · ↑2.3M ↓21k · ▌░░░░░░░ 68k/1.0M · goal armed · todo 5/11 · ctrl-c interrupt

在已用时间旁边是这一轮等待的工具。长轮旁边没有名字时,无论命令在运行还是会话已停止响应,读起来都一样,因此名字是等待与担心之间的区别。它是终端变窄时第一个被放弃的东西。

+2 calls 表示还有两个工具在并行运行——Harness 调度可以安全并行运行的调用,因此几个可以同时未完成。名字是其中最新启动的一个。

时间是这一轮的,不是那个工具的。这里没有任何内容声称某个调用跑了多久,因为 Harness 不发布它。

在一轮运行期间提交的文本会被 agent 接受并停放,直到能够被取用——长轮中这可能要等上一会儿,而在此之前没有任何东西确认它。所以状态行替你说出来,用一个片段,其中的词表明等待的是两者中的哪一个:

1 queued一个后续轮次,等这一轮结束
1 steering等待正在运行这一轮的下一个步骤
2 pending两者都有

agent 取用之后计数就会离开。enter 给你的是哪一个,见排队还是导向

ctrl-c 中断这一轮,同时丢弃仍在等待的内容——停止就是停止,而不是让一条排队的提示自行把 agent 再次启动。它会说明丢弃了多少条,而 可以把其中任何一条取回。

Token 与成本

状态行携带会话的运行总计:

● ready · deepseek-v4-flash · ↑8.8k ↓1.6k \$0.018 · CR 99.8% · ▏░░░░░░░ 14k/1.0M

是发送的每一个提示 token,无论是否缓存; 是生成的每一个 token,含思考。两者都来自提供方自己的核算,因此是你被计费的量而不是估算,重新打开会话会带回它的总计。

CR 是提示词中来自缓存的比例,保留一位小数——99.8% 就是 99.8%,而不是 100%。 它读取的是 Harness 自己对本会话模型请求的累计记账,那与旁边 / 总计并不是 同一次折算(见下),因此它是那份记账的比例,而不是那两个数字的拆分。在端点与这一位 小数之间,它给出界而不是挪动数值:不完全是全部时为 CR >99.9%,不完全是零时为 CR <0.1%,而 CR 100% 只在整个提示词确实都是缓存读取时出现。

它是便利信息而非状态事实,因此终端变窄时它是这一行最先放弃的一段,而且是整段放弃而 不是缩短:CR 99 是另一个数字,而不是更小的那个真相。没有可说的真话时它什么也不 说——第一个提示 token 之前、未挂载 Harness 用量记账的 profile 上,以及 /usage off 已把这一行留给上下文读数时。

/usage 选择显示其中多少——cost(成本)、tokens(只要计数不要金额),或 off。点名其中一个即刻生效;你已经知道的那个词面前不会挡着一个菜单。

不带参数的 /usage 改为检视,因为那正是这个命令的名字所提的问题:

╭─ dshline ────────────────────────────────────────── Usage ─╮
│                                                            │
│  Usage                                                     │
│  input               2.3M                                  │
│    uncached          317k                                  │
│    cache read        2.0M                                  │
│    cache write        13k                                  │
│                                                            │
│  output               42k                                  │
│  cache read share   85.7%                                  │
│                                                            │
│  cost               \$0.84                                  │
│                                                            │
│  Performance                                               │
│  turns                  7                                  │
│  steps                 19                                  │
│  avg first token    640ms                                  │
│  avg output tok/s    42.3                                  │
│  model time        4m 12s                                  │
│  tool time         1m 03s                                  │
│                                                            │
│  status line         cost                                  │
│                                                            │
╰─ s status display · esc close ─────────────────────────────╯

那四个 token 数字是 Harness 自己的记账,按提供方报告它们时所用的分桶,是会话中 模型请求的累计——包括那些其消息此后已被压缩替换、但你仍然付过费的请求。有一样 东西特意在此范围之外:压缩为写出摘要而额外发起的那次提供方调用,在会话日志中单独 报告,而 Harness 的记账不把它折算进来,因此这里也不折算。cache read share 同样 特意不叫命中率:它是一个分桶除以提示词总量,而这里没有任何提供方发布过可供比较的 命中率。它保留一位小数,因为有意思的区间在顶端:按整数百分比,一个长期复用同一提示 的会话无论实际错失多少都读作 100%

金额是本界面自己的估算,按下面的费率,并且是单独折算出来的——取自每个请求最终定稿的 assistant 消息,因为只有那里能读到路由和它运行的时刻。两次折算的范围并不相同,也不 以彼此的拆分形式呈现:Harness 还会计入一次被重试的尝试所报告的用量样本,而那是任何 定稿消息都没有记录的,因此遇到过重试的会话,上方的提示 token 可能多于下方金额所据以 计价的量。这里不会把其中之一除进另一个。

s 打开的正是 /usage 过去单独打开的那个三选显示选择器。

它们下面的性能数字来自另一个 Harness 投影,覆盖的是整个会话日志,而不是还留在屏幕上 的那一部分——重新打开一个会话,它的计数与时间会一起回来。turnssteps 是 Harness 从会话自己的步骤边界数出来的:一个在产出任何东西之前就失败的步骤依然发生过,被你打断的 那个也一样。avg first tokenavg output tok/s 是在 Harness 真正计了时的那些步骤上取 的平均值,这也正是它们被标为平均值而不是实时速率的原因。model time 是从每个步骤开始到它 组装出回复的那段请求墙钟时间,tool time 是每次工具调用与其结果配对起来的墙钟时间。 两者分别测量,并不是同一个整体的两个部分。

你看不到的那一行是没有被测量,而这跟为零是两回事。Harness 只为组装出了回复的步骤累加 模型时间,只为结果确实返回的调用累加工具时间,所以一次被打断的回复与一次没有回应的工具 调用都可能真的花了时间,却仍然什么也没贡献。/usage 会把那些行直接省掉,而不是打印 0ms——那会宣称一次从未做过的测量。turnssteps 是例外,始终显示,包括零,因为 计数为零是一个真实的计数。

只有当 profile 挂载了 Harness 的会话统计插件时,整个这一小节才会出现——随附的 dshline profile 就挂载了它。一份手工搭建、把它漏掉的 profile 得到的 /usage 在其他方面毫无变化 ——每一个 token 数字、缓存拆分与金额都还在——外加一行说明本 profile 未挂载它们。不会有 任何东西被估算出来顶替它。

$ 的含义取决于路由。在按量付费路由上它估算你花了多少;在 OpenCode Go 上——你按订阅付费——它是按订阅额度计数的、以美元计量的用量,而不是单独一张账单。

它使用哪些费率

本界面针对其构建的路由——DeepSeek 自家的与 OpenCode 的(Zen 与 Go)——开箱即按已发布费率定价,每条消息按它运行时适用的费率计费,而不是按现在有效的费率。这很重要,因为标准价大约是折扣价的两倍:

cache hitcache missoutput
deepseek-v4-flashoff-peak$0.007$0.22$0.66
peak$0.014$0.44$1.32
deepseek-v4-prooff-peak$0.022$0.66$1.98
peak$0.044$1.32$3.96

每百万 token 美元。高峰是 01:00–04:00 与 06:00–10:00 UTC;其余每小时都是非高峰,也就是一天的大部分时间。

三条路由按这种方式定价:deepseek-official 加上 opencodeopencode-go——分别对应 OpenCode Zen 与 OpenCode Go,即已安装目录携带的两条 OpenCode 路由——也就是本界面构建来运行所针对的路由。OpenCode 数字镜像 DeepSeek 自己的列表,包括高峰时间表,这是 OpenCode 对这些模型应用的核算。把它们当作你可以修正的起点;计费不同的路由是一条配置项。

费率会变,而本文件不会。价格与高峰窗口都可以在 ~/.dsh/cordis.patch.yml 中覆盖,你写的条目替换该路由随附的条目而不是合并进它——修正一个价格不应让其余停留在发布构建时的值:

- id: dshline
  config:
    pricing:
      # Keyed provider/model. The bare fields apply off-peak; `peak` is the
      # exception, because it is the narrower window.
      deepseek-official/deepseek-v4-flash:
        input: 0.22          # cache miss
        cachedInput: 0.007   # cache hit
        output: 0.66
        peak:
          input: 0.44
          cachedInput: 0.014
          output: 1.32
      # A model id on its own covers whatever route serves it, which is how you
      # price one model the same way everywhere.
      deepseek-v4-pro:
        input: 0.66
        cachedInput: 0.022
        output: 1.98
    peakHoursUtc:
      - { from: '01:00', to: '04:00' }
      - { from: '06:00', to: '10:00' }

**除非你要求,否则没有任何东西仅按模型 id 定价。**同一个模型经网关由网关按其自身条款计费,因此随附费率钉在 deepseek-official 路由上。没有条目的路由上的模型被计数但不定价——你得到 token 而没有 $,这是诚实的读数——而缺失部分会话的总计被标记 ~,以免被误认为完整账单。

通过网关访问 DeepSeek

这里的重点是模型,而不是通往它们的路由,而通过 OpenAI 兼容网关到达它们是配置而不是代码变更——Harness 的 llm-pi-ai 适配器接受手动声明的路由。本界面无需为它添加任何东西:/model 列出路由宣传的任何内容,/reasoning 提供它声明的任何级别,用量计数器随之而行。

对于 opencode 的 Go 端点,把你的密钥放入环境变量 OPENCODE_API_KEY,并把路由加入 ~/.dsh/settings.yaml

llm-pi-ai:
  providers:
    opencode:
      displayName: opencode
      apiKeyEnv: OPENCODE_API_KEY
      api: openai-completions
      # The chat-completions path is appended by the protocol, so the route
      # stops at /v1.
      baseURL: https://opencode.ai/zen/go/v1
      # The endpoint speaks DeepSeek's thinking dialect but its URL does not say
      # so, so the format has to be named or /reasoning has nothing to send.
      compat:
        thinkingFormat: deepseek
      models:
        # Keys are the levels offered, values their wire spelling; `off` is the
        # one that may be left empty, meaning "supported, send nothing".
        - id: deepseek-v4-flash
          name: DeepSeek V4 Flash
          contextWindow: 1000000
          reasoningEfforts:
            off:
            high: high
            max: max
        - id: deepseek-v4-pro
          name: DeepSeek V4 Pro
          contextWindow: 1000000
          reasoningEfforts:
            off:
            high: high
            max: max

两个细节值得知道。apiKeyEnv 是一个引用,每次请求解析,因此密钥本身绝不进入文件。而它进入 settings.yaml 而不是 cordis.patch.yml——设置文档是适配器监听的层,因此路由随你保存出现与消失,无需重启。价格是反过来的:它们从 cordis.patch.yml 中的 dshline 行读取。本前端确实注册了一个设置节——dshline,用于保存主题——但价格属于组合期的部署事实,而不是会话内需要修改的东西,因此它们留在本行其余配置所在的地方。

两个模型与直接路由服务的 id 相同,这使两条路由都挂载后 /model deepseek-v4-pro 有歧义——裸 id 解析到先被发现的路由。当你指某一个时,说 /model opencode/deepseek-v4-pro——或 opencode-go/deepseek-v4-pro,如果那是网关注册的 id——选择器反正会给每行标上它的提供方。

成本在 OpenCode 路由上开箱按 DeepSeek 费率报告(见上文)。如果某条路由计费不同,一条条目修正它,它替换随附数字而不是合并:

- id: dshline
  config:
    pricing:
      opencode/deepseek-v4-pro:
        input: 0.66
        cachedInput: 0.022
        output: 1.98

任何其他网关在你另行指定前都不定价:只有本界面命名的路由带费率,因为经转售商到达的模型由转售商计费,而悄悄继承别人的价目表是值得排除的唯一失败。

一轮的时间去了哪里

/timing 在状态行上方打开一个常驻的实时分解:

  timing · turn 14 · 42.8s · live
  reasoning  ━━━━━━━━━━━━━━ 18.2s
  bash       ━━━━━━━━━━━━━─ 16.4s
  edit       ━━────────────  3.1s
  output     ━━────────────  2.1s

agent 工作时它留在那里,空闲时也留在那里。轮次时钟与进行中的工具调用实时推进;reasoning 与 output 随它们流式事件的到达而增长。一轮结束时,同一个面板保持它的最终测量——不会向滚动缓冲区(scrollback)添加任何东西——直到下一轮取代它。在你看着的时候出现的跨度,会在接下来的几次工作心跳里把它的条形缓入;它旁边的时长从第一帧起就是真实的测量值。工具密集的轮次被限制在一个小的固定高度,并以一行省略行结尾,该行计数被隐藏的内容并点名其中最长的调用(… +3 more · max 6.2s——是最长的,不是总和,因为这些跨度彼此重叠);在窄终端上这个数字会被整个放弃,而不是被切成一个残缺的时长,这发生在"挤占 composer"规则整行拿走之前。

在短到装不下全部内容的终端上,面板比输入行先降级:先是它的跨度行,然后是它的标题,只有在只剩寥寥几行的终端上它才完全消失——绝不会为了让图表可见而把输入行挤出屏幕。composer 遵循同一条规则,先舍弃它边框上方的那一行空行,然后才动用面板已获承诺的行。

条形按最长行缩放,而不是按这一轮。它们是跨度,不是份额:一个步骤内的工具调用彼此同时运行,因此它们的长度加起来可以超过这一轮,而差异不是空闲时间。标题里的挂钟时间是这一轮;条形只把行与行互相比较。

它默认关闭,关闭期间它完全不贡献任何活动行。裸 /timing 翻转它——只有两个状态,列一份两项的清单反而是多余的一步——而 /timing on/timing off 直接设置。在一轮进行中启用它,会显示已经在进行的测量。重新打开一个已保存的会话时以 no turn measured yet 开始:历史重放刻意省略了做出诚实分解所需的流式分块,因此面板不会用不完整的数据编造一个。

它以前叫 /profile,那是一个迟早要发生的命名冲突:Harness 的 **profile(配置文件)**是启动器启动的那个组合,而 /profiles 浏览的正是它们。这条命令是一只秒表,现在它这么说了。

权限与沙箱

在把会话指向你在意的代码之前,请阅读本节。

在标准设置中,**agent 的普通工具调用在运行前不会显示给你审批。**它可以在你的工作文件夹内创建、编辑、删除文件,并在那里运行 shell 命令。

这是 Harness 标准插件集的属性,不是本界面做的决定。审批提示在这里实现,而且它确实会出现——但只在有东西明确要求审批时,标准插件集中只有一种情况:模型要求在沙箱外工作时。文件夹内的普通调用只是被允许。文件夹外的操作被直接拒绝,而不是变成一个提问。

如果你希望普通工具调用先询问,添加一个做那个决定的插件——@deepseek-ai/dsh-hooks-claude-code,或你自己的 tools/pre-execute 策略。哪些调用需要审批是关于你如何部署 Harness 的决定,因此本界面不替你决定。

/permission 为当前会话打开一个有界选择器。它从 Harness 部署的 permissions 投影读取当前值、名称、描述与顺序,然后通过 Harness 正常的 /permission <preset> 命令把选中的值送回去。已经知道部署定义名称时, /permission <preset> 仍然可用。

标准 dsh-base 部署目前提供 read-onlyworkspace-writedanger-full-access

  • read-only — agent 可以读取与搜索,但不能改动任何东西。你只问问题时用它。
  • workspace-write — 默认。agent 可以更改你打开的文件夹内的文件。
  • danger-full-access — 没有沙箱。这个名字名副其实。

这些是 Harness 预设,而不是 dshline 的枚举:另一个部署可以发布不同的表、 标签、描述与顺序。如果实际沙箱与审批策略不匹配任何具名预设,选择器会把 custom 显示为当前状态,但不会把它作为目标提供。

从选择器选择 danger-full-access 时,会在运行 Harness 命令前要求明确确认。 这一步安全措施只适用于选择器:直接使用 /permission danger-full-access 命令时, 仍保持 Harness 既有语义。

会话

当前激活的配置文件提供 Harness 会话持久化时,对话可以在退出 dshline 后保留,并重新打开:

dsh --profile dshline --resume          # browse, search, and choose one
dsh --profile dshline --resume <id>     # reopen a session directly

重新打开的会话看起来与你看着发生的那场完全一样——推理、diff、工具输出等等——因为它已持久化的日志通过绘制实时会话的同一段代码重绘。没有会话持久化的配置文件仍然支持开始新的对话,只是对话结束后无法再次提供。

你不必在启动时决定。/sessions 从运行中的窗口内部打开同一个浏览器,并在原位重新打开会话;见 命令 → Sessions。一次驱动一个会话,每个的会话记录都留在你自己终端的滚动缓冲区中。

如果它拒绝启动

本界面在输入与输出上都需要真实终端。如果它的输入或输出被重定向到文件或另一个程序,它会报错退出,而不是空屏永远等待:

dshline: needs a terminal on stdin and stdout; for a piped or scripted run use --profile headless

一些封装脚本也会导致这种情况,因为它们不把终端透传给它们启动的程序。这种情况下请直接运行 Harness 命令,或为脚本使用 --profile headless