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

@itookit/dsht

v0.6.18

Published

Remote-first TUI for DeepSeek Harness with SSH-friendly mobile access and request-level cost tracking

Readme

DeepSeek Harness Terminal

English | 中文

第一次运行时,需要使用 DSH_URL 传入 npx @deepseek-ai/dsh web 启动时打印的 url:

export DSH_URL='http://127.0.0.1:3080/?token=<token>' && npx @itookit/dsht

以后运行只需要执行(当然必须确认npx @deepseek-ai/dsh web 已经启动):

npx @itookit/dsht

DeepSeek Harness Terminal(dsht)

dsht — 只要有终端,就能随时控制 DeepSeek Harness。

摘要

dsht 是一个面向远程使用场景设计的轻量级 DeepSeek Harness TUI 客户端。

它的核心目标是让 DeepSeek Harness 自然融入开发者已有的终端和 SSH 工作流:Harness 可以持续运行在远程工作站或服务器上,而你可以从笔记本、平板,甚至手机重新连入终端,继续查看状态、发送消息、转向任务、审批操作、回答问题、取消轮次或切换会话。

典型场景:

手机 / 平板 / 笔记本
        │
        │ SSH
        ▼
   跳板机 / Bastion
        │
        │ SSH
        ▼
     开发主机
        │
        ├── dsht
        │     │
        │     ▼
        │   dsh web
        │     │
        │     ▼
        └── DeepSeek Harness

dsht 本身不是 SSH 客户端。它运行在普通终端中,因此可以直接工作在 SSH、嵌套 SSH、ProxyJump/跳板机、tmux 等远程终端环境里。只要你的终端能够到达运行 dsht 的主机,就可以继续控制同一套 DeepSeek Harness 会话。

除了远程控制,dsht 还内置了面向成本控制的用量统计:它按请求记录 token 用量,区分未缓存输入、缓存读取、缓存写入和输出,并结合模型、时间、高峰/空闲价格及版本化价格表,计算当前会话与今日的人民币费用估算。

主要特点:

  • 远程优先:适合 SSH、嵌套 SSH、跳板机、ProxyJump、tmux 等远程开发环境。
  • 手机友好:只需要一个可用的移动端 SSH 客户端,就能在离开电脑后继续控制远程 Harness。
  • 工作区和会话选择器,通过 /ws、/resume 直接切换,并显式创建会话。
  • 流式回复、思考内容、精简工具名称、成功/失败状态以及分页对话历史。
  • 排队消息、转向输入、轮次取消、审批和自由文本问题回答。
  • 按服务端保存 cookie、自动重连和快照替换,方便断线后恢复控制。
  • 面向脚本的 JSON/制表符工作区与会话列表,以及可复用的 HTTP 客户端。
  • 成本感知:会话与今日人民币费用估算,支持版本化高峰/空闲价格和 /cost 汇总。

为什么使用 dsht?

为远程控制而设计

DeepSeek Harness 往往运行在性能更强、环境更完整的开发工作站或服务器上,而人并不总是在那台机器前。

dsht 将控制界面保持在纯终端中,因此无需给远程服务器安装桌面环境,也不要求手机运行完整的开发环境。你可以让 Harness 留在开发主机持续工作,需要查看或干预时,再通过已有的 SSH 链路进入主机运行 dsht。

最简单的方式:

笔记本 ───────── SSH ────────> 开发主机 ──> dsht

经过跳板机时:

手机 ── SSH ──> 跳板机 ── SSH ──> 开发主机 ──> dsht

这使得“手机在手,随时控制 Harness”成为实际可用的工作流:无需远程桌面,也无需在公网直接暴露 Harness 的 Web 服务。

SSH 隧道、ProxyJump、跳板机和访问控制由现有 SSH 环境负责;dsht 专注于 DeepSeek Harness 的终端交互与控制。

手机也能继续控制任务

在移动场景中,通常不适合长时间编辑代码,但非常适合进行控制和决策。

通过手机 SSH 进入运行 dsht 的远程终端后,可以:

  • 查看正在运行的任务和实时输出;
  • 阅读助手回复、思考内容和工具执行状态;
  • 发送新的 prompt,任务运行中自动作为转向输入;
  • /allow 或 /deny 审批操作;
  • 回答 Harness 提出的问题;
  • /cancel 停止当前轮次;
  • 切换工作区和会话;
  • 搜索历史记录;
  • 使用 /cost 查看当前任务和近期费用。

因此,即使离开电脑,也不必失去对长时间 Harness 任务的控制。

不只是日志查看器

dsht 是运行中 DeepSeek Harness 的交互式控制界面,而不是只读日志工具。

它能够发送输入、处理审批和问题、转向正在执行的任务、取消轮次、切换会话并在网络恢复后重新连接。Harness 的实际执行状态仍保留在服务端,客户端只负责通过终端呈现和控制。

成本感知

长时间的 AI 编码任务可能持续消耗大量 token,而仅看 token 总数很难直观判断实际费用。

dsht 按请求保存用量信息,并结合请求结算时间、模型身份和对应价格版本进行计算。费用统计区分:

未缓存输入
缓存读取
缓存写入
输出

/cost 可以查看:

当前会话
今日
今日 + 前两个自然日

状态栏还可以持续显示本会话费用与括号内的今日合计(¥: 1.23(5.00)),便于在任务执行过程中及时发现成本变化,而不是等到账单出现后才知道消耗了多少。

这让 dsht 同时承担两个角色:

  1. DeepSeek Harness 的远程终端控制界面
  2. 面向实际使用过程的成本监控工具

目录

启动

需要 Node.js 22.19 或更新版本,以及已经运行的 dsh web 服务;服务是单独的前置条件,本客户端不会启动它。默认连接本机 http://127.0.0.1:3080:

npx @itookit/dsht

仅在首次运行以及已保存的 cookie 过期后需要 token。可以单独导出它,也可以直接导出 dsh web 打印的完整地址,由客户端拆出其中的 ?token= 参数:

export DSH_TOKEN=<token> && npx @itookit/dsht
export DSH_URL='http://127.0.0.1:3080/?token=<token>' && npx @itookit/dsht

两者同时提供时 DSH_TOKEN 优先,--url 可覆盖单次运行的 DSH_URL。token 不会写入磁盘,只保存兑换得到的 cookie。上面两种 export 都会留在 shell 历史中,在意时改用 read -rs -p 'Host token: ' DSH_TOKEN。连接其他服务端需在 DSH_URL 中给出其 origin。

npx @itookit/dsht list workspaces --json 和 npx @itookit/dsht list sessions --json 供脚本获取工作区和会话列表,npm install -g @itookit/dsht 会安装 dsht 命令。这些 registry 命令要求包已发布。

若使用源码仓库,先安装依赖,再运行 TypeScript 入口:

npm ci --ignore-scripts
npm start

两种方式读取相同的 DSH_URL 和 DSH_TOKEN 变量。

使用 ↑/↓ 和 Enter 选择工作区,然后选择已有会话或 New session。All sessions 同时显示未归属注册工作区的会话。Add workspace (this directory) 直接注册 dsht 自身所在的目录:服务端尚未注册它时,它排在列表最前,因此也是默认选中项;Add workspace (host directory) 接收服务端已有目录的绝对路径,该路径可能与本机文件系统不同,按 Esc 可以退回选择器。在已注册的工作区目录里启动时,直接选中该工作区而不再显示选择器(会话列表里的 ← 仍可切到别的工作区);命令行给出会话 ID 时依旧直接打开。新建会话前必须选择工作区。

宿主不可达时,启动选择器显示的是原因与解决办法,而不是一个空列表:尚在进行的首次尝试写成"连接中";连不上宿主时给出启动它的 npx @deepseek-ai/dsh web 命令与首次运行所需的 DSH_URL 行;凭据被拒时要求该 URL 或 DSH_TOKEN。原始传输错误以暗色诊断信息留在下方,客户端自行持续重连。

首次登录通过 GET / 兑换 token,并按 HTTP origin 保存 cookie。后续启动和列表命令自动复用 cookie,无需再次提供 token。默认目录为 $XDG_STATE_HOME/dsht/auth,未设置时使用 ~/.local/state/dsht/auth;可通过 --auth-dir 或 DSHT_AUTH_DIR 覆盖。POSIX 下目录权限为 0700、cookie 文件为 0600;Windows 使用账户目录继承的访问控制。启动 token 永不保存。

Cookie 有效期由服务端决定。过期或被拒绝后,需要再次提供 token;已提供 token 时,HTTP 401 会自动触发重新认证。网络故障和 HTTP 403 不触发 token 兑换。损坏或权限不安全的 cookie 文件会明确报错。服务地址必须是不带路径、且除 token 外无其他查询参数的 origin,主机名须受服务端信任。

远程 SSH 工作流

dsht 最适合与现有 SSH 基础设施组合使用。它不要求 DeepSeek Harness 暴露到公网,也不要求客户端设备能够直接访问 dsh web。

直接 SSH 到开发主机

如果开发主机可以直接 SSH:

Laptop / Phone
      │
      │ SSH
      ▼
Development Host
      │
      ├── dsht
      └── dsh web

登录远程主机后直接运行:

dsht

或者无需全局安装:

npx @itookit/dsht

通过跳板机访问

如果开发主机只能通过跳板机访问:

Phone
  │
  │ SSH
  ▼
Jump Host
  │
  │ SSH / ProxyJump
  ▼
Development Host
  │
  ├── dsht
  └── dsh web

例如已有 OpenSSH ProxyJump 配置时,可以先正常 SSH 到目标开发主机,然后运行:

dsht

dsht 不需要理解这条 SSH 链路;从它的角度看,它只是运行在能够访问 dsh web 的终端环境中。

与 tmux 配合

远程环境中可以把 dsht 放在 tmux 会话中,以便网络中断后重新进入同一个终端环境:

tmux new -s dsht
dsht

之后重新 SSH 登录:

tmux attach -t dsht

即使不使用 tmux,Harness 会话状态仍然保留在服务端;重新启动 dsht 后可以重新选择原工作区和会话。tmux 的价值主要在于保留本地终端布局和当前 TUI 进程。

手机访问

任何能够正常使用 SSH 的手机终端都可以作为入口:

Mobile SSH Client
       │
       ▼
   Jump Host
       │
       ▼
Development Host
       │
       ▼
      dsht

实际体验取决于移动终端对 ANSI、Unicode、方向键、SGR mouse reports 等终端能力的支持。即使触摸鼠标能力有限,核心操作仍可以通过键盘和 slash 命令完成。

列出工作区和会话

npx @itookit/dsht list workspaces --json
npx @itookit/dsht list sessions --json
npx @itookit/dsht list sessions --workspace WORKSPACE_ID --json

在源码仓库中,可以通过 npm 或源码入口执行同样的命令;直接调用入口可以避免 npm 的脚本提示混入输出:

npm start -- list workspaces --json
npm start -- list sessions --json
node --import tsx src/cli/index.ts list workspaces --json
node --import tsx src/cli/index.ts list sessions --json

JSON 输出格式为 { "items": [...] };省略 --json 则输出制表符分隔的列表。工作区筛选使用服务端 sessionIds 成员关系。工作区列表读取 workspace/follow 的首个 baseline 后取消订阅,不会调用不存在的 workspace/list 端点。

回读状态迁移日志

npx @itookit/dsht trace
npx @itookit/dsht trace --json
npx @itookit/dsht trace --trace /path/to/trace.log

dsht trace 把 <state>/trace.log 读回并打印几行事实:执行了多少条命令、各自如何结束(含 kind);每个泳道与每个会话各有多少次写入;跑过多少次前台操作、其中多少被取消、哪一次最长;每个 loop run 发出了多少步、以什么结局(含 reason)结束;验证如何收尾(含 unavailable 的错误类别);以及所有 begin 没有配对 end 的 span——这些异常在 1443 行 JSONL 里是看不见的。--json 输出同一份汇总的结构化版本,--trace 读取指定文件而不是默认路径。

对话操作

Enter 提交消息:代理处于 Working 时自动作为下一个 step 的转向输入;空闲时开启新一轮。转向输入等待当前步骤及其工具执行完成,不会中断正在运行的工具。宿主只在下一次 step 开始时才把转向消息写进会话,因此提交后输入框上方会先出现一行 Sent · waiting for the host to record it(↳ 文本),让刚发出的内容立刻可见;宿主写入会话(或在支持队列的宿主上以 queue 帧上报)时,这一行在与宿主记录同一帧内消失,不会和对话里的同一条消息重复。输入框非空时,Ctrl+C 先清空输入;否则所选会话运行中时请求取消,只有空闲时才退出,连续按键会复用尚未完成的取消请求;一次没能退出的按键会为下一次“上膛”——五秒内再按一次 Ctrl+C 即可退出,即使 host 尚未确认停止,因此 host 永不报告空闲的 turn 也困不住客户端;退出前仍会请求 host 停止该 turn。取消会等待正在提交的消息完成接收,失败时保留客户端。聊天界面中 Esc 会发送取消请求,不受本地空闲状态判断限制。任务运行中时,Esc 关闭文件或历史/搜索菜单的同时请求取消;空闲菜单仅关闭。正在执行的历史/搜索/费用加载、服务端命令和导出请求优先被取消。Page Up/Down 滚动当前对话;/older 加载更早记录。所有退出路径(包括 /quit 和 SIGTERM)都会在关闭连接前停止所选任务,因此退出不会留下仍在运行的代理;会话空闲时不发送取消。取消当前任务会保留排队消息。

/copy、Ctrl+S 或普通聊天界面的鼠标左键单击冻结画面并关闭鼠标事件捕获,便于使用终端原生选择复制。Esc、Ctrl+S 或 Ctrl+C 退出复制模式并显示最新输出,退出复制模式不会取消代理。对话框和选择器只冻结其背后的对话内容——页眉与对话保持打开时的画面,直到对话框关闭——状态栏继续投影实时运行状态,因此面板打开期间计时不会停,打开着的 /status 面板也会即时显示新的取值。工作区选择、会话选择和主机路径输入界面的连接提示及状态栏持续刷新,复制模式除外。对话历史保留在输入框上方,鼠标滚轮及 PgUp/PgDn 可滚动历史,不会移动当前选项;帮助面板中的 PgUp/PgDn 用于帮助翻页。对话框中的鼠标单击不会进入复制模式,Ctrl+S 可冻结整个画面并释放鼠标捕获以进行原生选择。只有复制模式会停表:它为原生选择冻结整个画面,退出后立刻回到当前读数。另外有两个时刻会在你看别的窗口时找到你:开始等待回答,以及 turn 或 loop 结束。默认方式是 auto——在已知支持的终端(iTerm2、Ghostty、kitty、WezTerm、Warp)上发 OSC 9 桌面通知(ESC ] 9 ; … BEL),其余终端响铃;在 tmux 里 OSC 9 会被包进 DCS passthrough,而 tmux 只有在 set -gw allow-passthrough on 时才转发,铃则不需要任何设置,表现为 tmux 自己的窗口标记(window-status-bell-style,默认 reverse)。只有当客户端不确知当前窗格有焦点时才通知:它开启焦点上报(CSI ?1004h),一旦终端说窗格获得了焦点就安静下来。tmux 只有在 set -g focus-events on 时才转发这些上报,因此没有它时客户端无从得知,就照常通知——这是对一个否则永远无法满足的默认条件最诚实的处理。DSHT_NOTIFY=auto|bel|osc9|off 选方式,DSHT_NOTIFY_WHEN=unfocused|always 选条件。一次状态变化只通知一次:一直等待的问题不会反复响,工作开始保持安静,挂载时就已经在忙的会话也不会播报它启动时的状态。帮助/状态/费用面板不再定时消失。后台接收与内存回收继续运行,调整窗口大小仍可能重绘。

鼠标滚轮和 Page Up/Down 滚动对话;滚到顶部自动加载更早的一页。查看旧记录时,新输出保留阅读位置;向下滚动即可恢复跟随最新输出。TUI 挂载时启用鼠标报告,退出时关闭,需要终端支持 SGR 鼠标报告。加载历史或搜索期间,Esc 或 Ctrl+C 优先取消本地操作,不中断远程任务。

在 /ws 和 /resume 列表中,每一行只报告三种用户可见状态,按最需要处理者在前排序:? needs you(本客户端持有该会话未回答的审批或提问)、◐ working、● ready,其后是该会话最近活动时间。工作区行按同样顺序汇总其会话,不统计从未发过消息的会话;宽终端把状态写成文字并把目录放进右对齐列,窄终端只留 ?1 ◐2 ●6 并在标题下打印标记图例。选中工作区或会话后,输入框为空时按 d 或 Delete 查看移除确认页。已有草稿时 d 仍正常输入,Backspace 不会打开移除页。/ws --delete <名称或ID> 和 /resume --delete <标题或ID> 打开相同确认页,/resume --archive <标题或ID> 也可归档会话。移除会话前重新读取 session/list;服务端明确标记 blank: true、未运行,且没有已知排队任务或本地正在提交的提示词时,直接归档,不再确认。判定使用服务端空会话标记,不依赖当前已加载历史或标题。其他会话仍需确认,默认选中取消,Esc 关闭确认页。工作区移除调用 workspace/delete,只移除注册,保留目录和会话。会话移除调用 workspace/archiveSession,从工作区会话列表和 /resume all 隐藏,但保留历史,可通过 /resume ID 重开。当前服务端 API 提供归档,没有永久删除会话接口。0.2 起宿主会拒绝归档仍有工作在跑的会话,除非调用方同时要求停止这些工作,因此归档把它们一并停掉:会话不会一边继续运行一边被隐藏。归档当前会话会释放其 transcript 和排版缓存;操作被拒绝时保留列表及确认页,便于重试。

/search 对对话消息进行不区分大小写的字面文本匹配,包含旧页,排除纯工具行。搜索每次请求最多 80 条消息,扫描后释放临时页,只保留最多 200 条简短命中摘要,包含折叠的思考;结果截断时提示缩小查询范围。选择命中项只加载其序号附近的独立页面,/latest 释放该窗口。Esc 或 Ctrl+C 可取消搜索。稀有词或无匹配查询仍需通过 HTTP 扫描全部历史,此命令尚无服务端全文索引。/history 只列出已加载页面中自己的提示词,选择器显示的数字就是记录序号。/ssearch 与 /wsearch 调用 session/search,服务端搜索当前用户/助手消息内容,最多返回 20 个会话、摘要和截断标记,没有结果分页游标或命中记录序号。工作区筛选在全局数量限制之后进行,因此截断时可能漏掉工作区内的匹配会话;界面会提示结果不完整,可缩小查询范围。选择会话后加载其历史,再选择匹配消息跳转。所有操作均通过 HTTP 完成,不扫描服务端配置目录。

↑/↓ 或 Ctrl+P/N 回填之前提交的提示词和 slash 命令,按 Enter 才提交。一屏放得下的阅读面板会把方向键留给该历史,需要滚动的面板才接管方向键,而 Ctrl+P/N 在任何面板打开时都能回填。向下越过最新记录时恢复未发送草稿;编辑回填内容后开始新的草稿,切换会话会清空未发送的草稿。回填按当前会话保留其 user prompt,最多 2,000 条、约 512 KiB 文本,且不写入独立历史文件;切换会话即释放。它在记录到达时增量折叠这些提示词,因此覆盖本客户端连接之前的提示词,而不只是碰巧加载的那个窗口。打开会话后它还会在后台把更早的历史翻一遍,只提取提示词、不把这些页读进上方对话,因此从会话开始就持有整个会话的提示词列表。走到索引里最旧一条时,先用已加载的对话补回被预算淘汰的提示词,这一步不发请求;只有窗口也用尽才取回窗口之前的一页,并在同一次有界循环里跳过整页没有 User 消息的页,因此按键不会被工具页卡住;取回的那页也会留在输入框上方的对话里。于是淘汰只约束内存、不决定可达性:被淘汰的提示词要么仍在窗口内,要么仍在宿主上。连续重复输入合并,超大输入跳过,提问和审批回答不记入历史。由客户端自己组装的提示词——/loop 的 Brief 与后续短跟进——以"内部回合"发送,不记入该历史,因此上下箭头只保留你真正输入过的内容。提问选项与补全菜单优先使用箭头;工作区/会话列表在输入框为空时使用箭头选择,可用 Ctrl+P/N 调出输入历史。 !命令 在这台运行客户端的机器上执行——不是 agent 所在的宿主——并把命令行与输出内联打印在对话里、留在它发生的位置随历史一起滚走;不会发给模型,也不写入磁盘。每条命令的输出上限为 200 行与 64 KiB,只保留最近 20 条命令,被丢弃的行数会在块内注明。同一时刻只运行一条命令;Esc(或输入框为空时 Ctrl+C)会终止其整个进程组来停止它,因此管道与后台子进程一起结束;需要独占终端的命令(如 vim)无法工作。--no-shell 或 DSHT_NO_SHELL=1 关闭该前缀。每一次这样的运行同时是一个只读输出源:Ctrl+O 会打开最新的源——! 运行、fork 出来的 verifier 自己的会话,或 host 侧 subagent 子会话——用整屏视图实时跟随它,全程不选中它,因此“看”永远不会变成“写”。视图头部写明这是什么源、属于哪个会话;↑/↓、PgUp/PgDn 与滚轮滚动,Esc 关闭并把输入框还回来。verifier 的会话由循环创建并命名为 [dsht-verify] <记录名> · <轮>/<次>,运行停止后仍留在列表里;源列表本身只属于本次客户端运行,所以下次启动只能在 host 的会话列表里把它当作普通会话找到。

每条 User 消息之后,只在第一段助手正文或思考前显示 Assistant 标题;后续消息及流式输出沿用分组,工具结果和 Context 消息不重置分组。当前加载的历史窗口从自身起点建立可见分组,消息序号、工具状态、搜索和思考展开仍各自保留。

单击本地运行会打开它的整屏只读视图:/loop 运行留下的那一行里的 view 那一行(或该行的标题栏),或 ! 块的标题栏;其他位置的单击仍是进入复制模式,待画面冻结后再拖动选择。松开鼠标不会恢复刷新,需按 Esc、Ctrl+S 或 Ctrl+C。终端原生 Shift+拖选可能不向应用发送鼠标事件,此时请先按 Ctrl+S。对话框中可按 Ctrl+S 冻结整个画面并释放鼠标捕获,再进行原生选择。

Tab 补全开头的 slash 命令,多个候选时补到公共前缀;回车也会直接执行"首词唯一命名一条命令"的草稿,因此 /pro Save tests 等同于 /prompt Save tests,无需输入完整命令名。若首词匹配多条命令,则会拒绝执行并列出候选;/quit、/allow、/deny 必须输入完整命令名。输入框支持 Readline 风格编辑,并保留粘贴内容中的换行与制表符;制表符按制表位显示、原样发送。单词以空白分隔;光标移动和逐字符删除保持完整的 Unicode 组合字符。输入框只显示少量内容行,超出后内部滚动并保持光标可见,因此长草稿不会把对话区挤没;窗口高度只由终端行数决定,终端更宽只会减少折行。行数超过该窗口的多行草稿会把中间行折叠为 [N lines · X KB],首行与末行保持可见;←/→ 一次跨越整块,在其边界按 Backspace 或 Delete 删除整块,发送时仍为完整原文。空输入时 Ctrl+D 不退出;Ctrl+C 先清空非空输入,再取消仍在占用客户端的长操作,最后才停止或退出。导出、压缩等长操作占用客户端期间,输入框仍保持可编辑——下一句可以先写好——但需要占用客户端唯一执行位的行(普通提示词,以及自己发起操作的那些命令)会在领取该执行位时被拒绝并说明当前是哪个操作在占用;只读的视图类命令(如 /help)仍可打开;操作结束也不会自动替你发送。未处理的修饰键快捷键不会将控制字符插入消息。终端退格键的 BS 和 DEL 编码均向后删除;独立 Delete 键(CSI 3~)向前删除。

| 按键 | 编辑操作 | | --- | --- | | ↑ / ↓、Ctrl+P / Ctrl+N | 回填更早/较新的提交内容 | | Ctrl+A / Ctrl+E、Home / End | 移到开头/末尾 | | Ctrl+B / Ctrl+F、← / → | 移动一个字符 | | Alt+B / Alt+F、Ctrl+← / Ctrl+→ | 移动一个词 | | Ctrl+K / Ctrl+U | 删除光标至末尾/开头至光标 | | Ctrl+W、Alt+Backspace | 删除前一个词 | | Alt+D | 删除后一个词 | | Ctrl+Y | 在光标处恢复最近剪除的文本 | | Ctrl+H / Backspace、Ctrl+D / Delete | 删除前一个/后一个字符 |

| 命令 | 操作 | | --- | --- | | /ws | 显示所有工作区与两条添加工作区入口,选中后打开其会话列表 | | /ws TARGET | 按 ID、完整名称/路径或唯一 ID 前缀选择工作区 | | /resume | 显示当前工作区的会话;未选择工作区时先引导选择 | | /resume TARGET | 按 ID、完整标题或唯一 ID 前缀跨工作区打开会话 | | /resume all | 显示所有工作区的会话 | | /model [provider model [effort]] | 选择模型及其思考强度,也可直接提交精确路由 ID | | /new | 在所选工作区创建会话 | | /compact | 会话空闲时压缩旧历史,显示服务端结果 | | /cancel | 取消当前轮次,保留待处理队列 | | /queue | 查看待处理输入;方向键选择,Enter/d/Delete 删除 | | /plan [off|message] | 进入或退出服务端计划模式 | | /goal [objective|clear|edit text|pause|resume] | 查看、设置、编辑、暂停、恢复或清除目标 | | /permission [preset] | 查看或切换服务端沙箱与审批预设 | | /feedback TEXT | 记录当前会话反馈 | | /loop [name\|stop\|answer] [score] [tries] | 用列表选择循环记录(从配置目录自动创建并合并后的 loop.yaml 读取),修改它启动时用的输入与限制后再运行评分循环;stop/abort 结束当前运行,answer TEXT 补上验证者要的判断条件 | | /export [local.zip] | 下载会话日志 ZIP 到新的本地文件 | | /export-html [local.html] | 将已加载会话保存为包含图表和公式的离线 HTML | | /coredump [tag] | 在当前工作目录写出 V8 堆快照,用于内存诊断 | | /older | 加载更早的历史 | | /history [text] | 列出自己的提示词并可选筛选;Enter 跳到所选记录 | | /prompt [text] | 列出已保存的快捷提示词——Enter 使用、e 编辑、d 删除——或把 TEXT 保存为一条 | | /search <text> | 逐页搜索历史,选择命中项后打开其位置 | | /copy | 冻结画面便于终端选择,Esc 恢复 | | /latest | 返回实时输出并释放独立历史窗口 | | /ssearch <text> | 在服务端搜索结果中筛选当前工作区的会话 | | /wsearch <text> | 搜索服务端可见的所有工作区会话 | | /allow, /deny | 回复当前审批;批准仅限一次 | | /status | 展开或收起底部完整状态信息;↑/↓ 滚动、PgUp/PgDn 翻屏 | | /cost | 展开/收起会话与今日费用,并刷新用量 | | /think | 显示思考及用户 prompt 摘要列表;↑/↓ 选择、Enter 跳转并展开 | | /think SEQ | 切换一条已加载思考的展开状态;live 表示当前尝试 | | /help, /quit | 列出每条命令及其说明,或退出 |

Slash 命令在选择器和对话输入框中均可使用。输入 / 会显示匹配命令;命令名确定并跟一个空格后,输入框下方会显示该命令的用法和单行说明,因此参数不必靠记忆;/help 分页列出命令及其单行说明,PgUp/PgDn 翻页。/loop 还会在草稿仍在写记录名时,把 loop.yaml 的记录列表显示在输入框下方,因此运行一次循环不必逐个敲出旗标。/help、/cost、/status 面板保持打开,直到下一条命令或 Esc;/history 还会在十秒后自动关闭,避免遗留列表一直占用输入框。列表类菜单(工作区/会话列表、记录列表、@ 补全、参数表单等)的方向键是循环的:在首行继续向上回到末行,在末行继续向下回到首行,两端都不是死键。不读取自由文本的面板(/model、/prompts、/queue、/history、/think 与删除确认等)打开期间输入框停用,按键只归该面板,Esc 关闭后才恢复输入;启动列表与对话输入框不受影响,仍可直接输入 slash 命令。Esc 保留输入内容;当有长操作占用客户端时(搜索、导出、加载历史页),它先取消那个操作,其余情况才去取消命令打开的面板:选择器会回到打开它的那个对话(不重新加载),启动阶段还没有对话时,会话列表则退回工作区列表。/workspace、/workspaces 是 /ws 的别名;/session、/sessions 是 /resume 的别名。名称可以包含空格,完整目标两侧的引号可选。不带引号的目标 all 保留给 /resume all;打开标题为 all 的会话时,使用 /resume "all" 或其 ID。目标有歧义时必须提供完整 ID。切换工作区会打开其会话列表并解除旧对话订阅;切换会话会同步工作区标签。两种操作均不会取消远程代理。

/prompt 打开你保存的快捷提示词列表,/prompt TEXT 则把 TEXT 保存为一条;列表支持 ↑/↓ 选择、Enter 使用、e 编辑、d 或独立 Delete 键删除。选择一条提示词只会把文本放进输入框作为普通草稿,不会立即发送,因此还能继续修改后再按 Enter 发送;用 e 进入的编辑相反:Enter 把文本写回列表而绝不发送,Esc 放弃编辑并恢复此前的草稿。这些提示词是配置目录下的两个私有文件(0600),整个列表由所有工作区、所有宿主共用,因此一条快捷输入写一次、处处可用。prompt.json 是安装版本:首次启动会把随包发布的文件复制过去,之后每次启动都会让它与随包文件保持一致,因此新版本新增一条提示词、或给某条打上 top,都能到达已经存在的安装;旧版本遗留在该文件里、随包集合并不提供的条目会先被搬进你的本地文件,不会丢失。prompt.local.json 是你自己的:/prompt TEXT 写它;编辑一条内置提示词会在同一 id 下变成你自己的条目;删除会被它记住,所以被删掉的内置提示词不会自己回来。记录可以带 top: true,把它钉在所有条目之前;在你的文件里写 top: false 则把一条被钉住的内置记录降回原位。交接请求随包就是钉住的,因为它几乎每个会话都会用到;top 跟着记录的位置走,所以你自己那条同 id 的版本会显示在最前,用的是你的文本。除钉住的条目外,内置文件保持自己的顺序:你用来覆盖某条内置记录的条目就显示在那条记录的位置上(没被你动过的内置条目仍标注 · installed,因此没有这个标记就说明这行是你的),编辑默认值不会打乱列表;你自己新增、内置文件里没有对应记录的条目排在最前——那是只有你才有的东西。你的文件里 id 或文本相同的条目会隐藏对应的内置条目;而一条与内置条目完全相同的本地副本两者都不算:既不上浮也不遮蔽,因此该提示词的随包更新仍能进入列表。再次保存同一段文本即可找回被删掉的内置提示词。相同文本不会保存两次,单条上限 8 KiB,你的文件最多 500 条;某个文件读不出来只会让那一层为空并在面板里说明,不会让客户端起不来。分层之前的版本把提示词存在状态目录下的单个文件里;首次运行会把其中的条目搬到 prompt.local.json,旧文件保留不动;凡是安装版本已经以相同 id 或相同文本提供的条目都会被跳过,免得安装默认值反而把它们变成会遮蔽后续更新的本地副本。

交接请求现在作为第一条内置快捷提示词随包发布(带 top 钉在最前,正文为中文),用 /prompt 即可看到:回车把它放进输入框,内容要求 agent 在工作区根目录写出 HANDOFF.md(覆盖原有内容,使用本次对话的语言),并覆盖本次会话为何存在、目标、每个任务的状态——已完成、仍未完成、无法完成及原因——以及关键决策、改动的文件、如何验证当前状态与明确的下一步。它原先是一个 /handoff 命令,会先删掉本机那份文件;提示词用「覆盖」表达了同一件事,命令已删除。

在代理工作期间会写同一个对话的命令——/compact、/loop <name>——会被接受并持有而不是拒绝:turn(或 loop)一结束就按提交顺序运行;因为这一行已经提交,所以与"不替你发送草稿"无关。其余命令在代理工作期间照常可用,/cancel、/allow、/deny、/loop stop 永远不会被持有。

/loop [name|stop|answer] [score] [tries] 运行 loop.yaml 里的一条记录,是一条客户端驱动的循环,并且设计成"选"而不是"敲"。输入 /loop 会在输入框下方列出所有记录及其轮数、产出物、自带默认值和它指向的输入;↑/↓ 选择,Enter 确认高亮记录,Tab 则把记录名补进草稿以便继续加旗标。确认后会打开参数列表:先是该记录自己的输入(如 designdoc-review 的 path,即被审查的文档),再是共享的五个值(From、To、Pass、Tries、Auto compact),每一行都写出它数的单位(1 round、8 / 10、10 per round、150K tokens);最后一个的单位是千 token,本次运行的历史一旦超过它,就在发送下一轮之前先执行 /compact,0(也是默认值,显示为 off)表示关闭;选中某行直接输入即可覆盖——输入按自由文本,四个值按数字、与旗标同一套校验;用方向键或 Enter 离开某行即提交该行的内容,因此不需要在每个框里各按一次回车;Start run 开始运行,← Choose another record 返回记录列表;只有按下 Start 才会真正启动,因此每个默认值都在被花掉之前可见。改一条记录的输入只作用于本次运行、不改 loop.yaml,所以 designdoc-review 可以审查任意文档,而 design-review 不受影响。若命令行里写了任一旗标(/loop design-review 9 3、--from、--to、--score、--tries),则跳过表单、按所写的值直接运行,脚本与 headless 用法因此保持不变;名字不存在时会列出可用记录。轮次、每轮检查要点、附加标准、固定输入与默认值都由记录自带:本客户端发出本轮 Brief,由 dsht fork 出的独立进程在自己的 session 里验证并把 verdict 文件写回,客户端据此决定下一步——达到及格线进入下一轮,低于及格线消耗一次尝试,某轮用尽预算则停止。内置记录有 design-review(对模块设计做十轮审查)与 designdoc-review(同一循环,审查记录 vars.path 指定的文档,并一份文档一个审查文件、就放在它旁边(tui-design.md.review.md、loop.md.review.md),所以换文档重跑既不会读到也不会覆盖另一份文档的轮次;这个文件名在记录里是模板 {{path}}.review.md,且已被 .gitignore 覆盖)。status 为 blocked 时立即停止、不再消耗尝试预算;验证者无法判断时会要求人工介入并以同样方式停止(headless 退出码 3);只有客户端自己核对过「本轮小节确实写进了产出物文件」的轮次才算通过;--deadline <minutes> 约束整个 run。passed 只声称本次 run 覆盖的轮次并写明是哪些(rounds 1–3/10 · selected range);跑完整份记录时最后一轮是收束轮,它的验证者会拿到前面每一轮的检查要点逐轮复核,因此后来某一轮改坏了前序要求不会被放过。以"先验证"开始的一轮(starts: verify 且有 forked verifier)跑在独立验证进程自己的 session 里,因此当前会话的 history 不会出现内容,直到某一轮未通过、才要求 agent 去工作;验证进行期间进度行显示 verifying step N · attempt M,状态栏也如实显示同一件事(◐ 加本轮 review N/M)而不是 Ready;验证者拿不出 verdict 时会给出结构化原因——退出码 + 子进程自己报告的原因(回复里没有 JSON 判定、或回复在 turn 结束后仍未提交),而不是只给错误输出类别,更不是无声等待;同时验证者会拿到本次 run 自己的输入,因此不会去评审产出物里只属于更早那次 run 的文档,--trace-verbose 才会附上脱敏后的最后一行。输入框上方的进度行显示轮次、尝试与最高分;发送普通消息、/loop stop、/cancel、Esc、Ctrl+C、切换会话或断线都会停止循环,该状态不持久化:/loop stop 属于控制泳道,运行期间也允许提交;没有运行时它会直接说明而不是报错,且已结束运行的那行进度会保留可读——结束它的那一行仍然显示结果,而你运行下一行时它就被清掉。验证者无法判决时 run 会暂停而不是结束:进度行与状态栏显示 needs you,/loop answer TEXT 补上缺的判断条件、以新的 verification identity 重新判断当前产出物——不消耗尝试次数,也不给 agent 发任何东西;/loop abort(/loop stop 在暂停期的拼写)结束它。

运行时的 loop.yaml 位于配置目录($DSHT_CONFIG_DIR,默认 ~/.config/dsht)。首次启动时客户端把随包文件复制到该目录;以后每次启动,把新增或改过的内置记录合入该文件,同时保留你修改过的同名记录与默认值。同名记录作为整体保留,不做字段级拼接。上次合并的内置摘要放在同目录的 loop.yaml.seed.json。DSHT_LOOP_FILE 可改指另一个运行文件,同样会自动创建并合并。运行时文件旁边的 loop.local.yaml 属于你,并叠加在它之上:同名记录替换内置记录——内置那条根本不会被列出——你的记录排在前面,被你隐藏的名字显示在记录列表标题里;DSHT_LOOP_LOCAL_FILE 选择另一个本地文件。记录列表用 · yours 标注你的修改和新增项。运行文件非法时启动失败并指出路径和字段;随包文件不可读时回退到编译进包的记录。若内置记录更新但你的版本仍在使用,升级后的首次启动会提示这次更新没有进入你的副本。

文件引用仅在文本块中发送 @path。Harness 提示模型按需读取文件或列出目录;TUI 不读取本地文件、不上传字节,也不将文件内容展开进提示词。引用图片路径不会附带图片数据。尚未实现本地附件、图片上传/预览及 @ 会话引用。

待处理的普通消息显示在输入框内,最多预览两条。/queue 打开完整的待处理输入列表,↑/↓ 选择,Enter、d 或独立 Delete 键通过服务端删除。Esc 仅关闭此列表,不取消任务。消息已被领取后无法删除,服务端会提示该竞争情况,客户端不会重新发送。宿主负责这份列表,包括重连替换及消息领取后的移除,客户端不维护第二份发送队列:0.1.7 及以后的宿主把它发布为持久的 inbox 会话投影(下一个步骤或下一个回合将要领取的输入),session/updateQueue 按持久消息 id 修改它;更早宿主的 session/control 队列节仍然读取,两者都不上报时会话的计数保持未知(Queued ?)而不是声称队列为空。问题和审批优先于队列导航,其回答不会成为转向输入;slash 命令仍按各自语义执行。当前宿主不再发布活动任务计数,因此那里的 Jobs ? 才是诚实的读数,而不是零。

/plan、/goal、/permission 和 /feedback 直接调用服务端命令注册表。命令可用性、参数和忙碌限制由服务端加载的插件决定。结果和错误显示在本地,失败时保留输入;反馈内容不进入本地输入回填历史。/export 将经过认证的会话 ZIP 流式写入本地文件,默认在当前工作目录使用带时间戳的文件名;含空格路径可加引号。已有文件不会被覆盖,失败或取消时清理未完成的下载文件。

/compact 通过服务端命令接口执行,不作为普通消息排队。服务端要求代理空闲且没有即将执行的排队任务;忙碌、未提供此命令或执行失败时显示原因并保留输入。执行期间显示进度提示,Esc/Ctrl+C 取消请求;压缩请求不使用普通 RPC 的 15 秒超时,也不会自动重试。中断连接后不能仅凭客户端判断服务端是否已完成,请先检查会话状态。

终端少于 62 列时,最新流式思考也默认折叠为一行;宽屏保持流式展开、完成后折叠。/think live 可手动展开或折叠当前思考,新回复恢复默认行为。

审批提供 1. 允许一次、2. 拒绝、3. 停止当前轮次。输入框为空时,按 1–3 直接作答,或用 ↑/↓ 选中后按 Enter 确认;初始不选中任何操作,Esc 清除高亮,请求重放时重新回到未选中。已有草稿时仍按正常文字输入处理,也保留 /allow、/deny 和 /cancel 命令。

用户问题显示题目进度、编号选项和说明。选择列表、待答问题、审批和文件补全与文本输入共用一个输入框边框。待答问题和审批会在输入框上方保留最近对话历史,历史视口使用剩余高度;对话框打开时减少上下留白,并在手动滚动或视口高度变化时刷新;可见选项数量按终端高度调整,并跟随当前高亮项滚动。输入框为空时,↑/↓ 移动高亮、Enter 确认,单选题按 1–9 直接作答(数字键就是选择,不再需要再按一次回车),多选题用空格或 1–9 勾选/取消勾选、Enter 确认,超过九个选项仍可通过方向键访问。只有固定选项带编号;Other answer 是一个输入行,因此不编号,用方向键选中后回车进入。选择 Other answer 后可输入纯数字自由文本,也保留普通文本回答。已有草稿时按正常文字输入处理;Esc 可退出提问:选项模式下放弃整组问题(服务端记为取消),Other 模式下第一次 Esc 只返回选项。全部题目回答完成后,一次提交结构化选项标签及可选自定义文本;失败时保留答案以便重试。已识别的提问和审批事件在本次连接中按事件 ID 保留,包括早于会话选择到达的重放事件;只展示当前会话对应的请求。切换列表不会退回这些请求,不识别的 waterfall 仍通过 next 委托后续处理。存活服务端在客户端重连后重发待答事件;客户端重启不保留尚未提交的回答草稿。正常退出 TUI 会取消正在运行的轮次。调用已取消/失败或服务端已重启时,无法靠本地 UI 状态恢复原等待,需要发送新提示词要求重新提问。提交失败时保留输入;HTTP 响应中断可能导致投递状态不确定,手动重发前应检查会话记录。客户端不会自动重试修改请求。

标题固定在可滚动对话区域上方,输入框和状态栏保留在下方。底部不再常驻快捷键说明,完整快捷键放在 /help,选择器只显示当前需要的导航提示。顶部单行优先显示最新会话标题(无标题时回退到 ID),宽屏时在其后附上工作区名称,不再显示主机地址和连接状态。未选择会话时显示工作区名称或 All workspaces,下方为分隔线;/status 保留完整会话 ID。取消回执在后续历史消息到达时保持可见,直到服务端报告空闲;接受取消不表示工具进程已经退出。

Markdown、图表与公式

消息正文支持 GitHub 风格 Markdown:标题、强调、删除线、链接、列表、任务项、引用、代码和表格。表格按终端显示宽度对齐,单元格内换行;列宽过窄时改为逐条纵向展示。代码保留缩进,链接保留目标地址。思考和工具摘要继续使用原有纯文本展示,搜索保留原始 Markdown 源码。

闭合的 mermaid 代码块将支持的流程图、状态图、时序图、类图和 ER 图渲染为 Unicode 文本图。图形超出可用宽度、不支持的语法或未闭合的代码块显示源码。$...$、$$...$$、\(...\)、\[...\] 及 math / latex / tex / mathjax 代码块使用 MathJax 的基础与 AMS TeX 包解析。终端显示 Unicode 符号、带括号的分式、上下标和矩阵;不支持的记法或无效 TeX 保留源码。终端公式是数学排版的近似表达。

/export-html [local.html] 将当前已加载会话和实时尾部保存为包含 Mermaid SVG 图片及 MathJax 生成的 MathML 的页面。用浏览器打开文件可查看完整数学排版,不需要网络或脚本。尚未加载或已回收的消息不包含在内,工具行仍为摘要。默认文件名带时间戳并保存在当前工作目录,支持带引号的路径,不覆盖已有文件,取消时删除未完成输出。/export 仍下载服务端完整日志 ZIP。

断线期间仍可编辑草稿,保存、编辑或删除本地快捷提示词。需要服务端的操作会报告未连接并保留草稿。重连只刷新数据和恢复订阅,保留当前会话、选择器筛选或路径输入页面,不会自动发送未提交的草稿。

实时状态

底栏分组显示 ◐ Working · 8s · Ctrl+C Stop、● Ready,或本客户端还欠一个审批/回答时的 ? Needs you、模型与思考强度、本会话费用与「今天花费(历史总计)」、十格上下文进度条及百分比、会话轮次与累计 token 及缓存命中率。宽终端为运行状态预留固定宽度,完成后模型和指标保持对齐。窄终端依次回收留白、隐藏进度条、缩短模型名、省略次要指标,优先保留停止提示。/status 显示主机 URL、操作状态、工作区完整路径、完整供应商/模型、下次模型、各项用量、轮次、队列、后台任务和四位小数费用。! 表示有指标或模型目录错误,或计费覆盖不完整;详情中显示原因。运行中使用最近实际使用的模型,空闲时使用下次模型,新会话使用服务端模型目录默认值。服务端设置、凭据和适配器变更通知会刷新模型目录。

工作计时使用已加载日志的 turn/start 时间戳。缺少该时间戳时,详情面板中的 (observed) 表示从客户端观察到运行开始计时;重连可能重置此备用计时。服务端报告空闲后停止计时。运行状态涵盖模型生成、工具执行及审批等待,不仅是文本输出。断线时明确标注为最后已知状态。展开面板把相关值合并到一行,并用单行状态栏已有的紧凑计数(Context ~40% (400.6K/1M) · 229.7M tok),因此 24 行终端可以一屏看到全部详情;↑/↓ 逐行滚动,PgUp/PgDn 翻屏,页脚标出可见区间与总数。

单行状态栏按价值而不是按列来保留分组:◐ 6:18 · bash 1:08 · ^C │ v4-flash · high · ctx 30% · S¥2.49* · ¥: 5.00 (12.34) · 2 turns · 34.5M tok · hit 92%。当前阶段只报事实——推理时是 think 28s,工具运行时是工具名加已等待时长,回答流式输出时是 write 12s——绝不从静默推断异常,因为长时间推理与安静运行的工具都不是卡住。它显示的是当前事件的名字与年龄,并在下一段工作开始或回合关闭前保持不变:命令返回之后、下一次增量到达之前的静默期仍被算作这个回合的工作时间;● Ready 不显示阶段,因为只有宿主知道回合已经结束。两个费用分组互不替代:S¥2.49* 只报本会话,账本还没为它定价时如实写 S?;¥: 5.00 (12.34) 只报今天花费与历史总计。宽度不足时先丢价值最低的分组(缓存命中率、token、回合、effort、模型、当天花费,然后 ctx);本会话费用永不丢弃,只会挪到第二行;状态簇只在约二十列以下才让出阶段与停止提示。暂停的时钟会说明原因(⏸ copy、⏸ dialog、⏸ history)而不是无声冻结;! Offline 或具名降级会整体替换状态标记:实时控制流不可用或无法解码时是 ⚠ Metrics,模型目录读不到时是 ⚠ Models,preset 名册读不到时是 ⚠ Presets。名字说明是哪个子系统坏了,具体信息在 /status 里,而已经恢复的降级(又有帧能解码)会自行清除。本客户端还欠一个审批或回答时,? Needs you 会接管状态标记(对话框暂停时钟时也一样),因为需要动手的正是这个欠下的回答。

轮次数来自完整会话的 sessionStats.turns 投影。上下文占用标为 ~:Harness 将供应商用量与对话变化估算值、最新模型容量结合。Token 总量来自完整会话的 tokenUsage 投影,分别显示非缓存输入、输出、缓存读取和缓存写入;思考 token 已包含在输出中。缓存命中率是缓存读取占三个互斥提示侧桶(非缓存输入、缓存读取、缓存写入)之和的比例,遇到部分命中时增加小数位而不是报成 100%。本会话与当天费用来自账本按会话保存的切片,因此还没有切片的会话显示为未知,而不是当天的花费。总量随服务端用量投影更新,不按流式字符计数。缺失数据显示 unknown 或 ?。重连时控制流基线整体替换状态,每个投影键的序号防止旧 follow 快照覆盖较新的指标。

默认使用 Catppuccin Mocha 主题:❯ User 为蓝色,✦ Assistant 为绿色,思考为淡紫色,工具为天蓝色,成功为绿色,错误为红色。紧凑状态栏中 Ready 为绿色、Working 为黄色、离线为红色、暂停原因与分隔符为柔和灰色,模型/强度为淡紫色、费用为天蓝色、用量为柔和灰色。上下文占用达到 80% 时从绿色变黄,95% 时变红;这只是视觉阈值,不代表服务端压缩触发条件。各组在 ANSI 着色之前按优先级装填,因此无色终端显示同样的文字。语义配色独立放在 src/ui/theme/index.ts,应用可单独接收主题,消息不保存 ANSI 样式。Ink 根据终端能力输出颜色,无色终端仍保留角色标记。工具调用显示名称和描述;命令与描述不同时,下一行以 $ 显示命令第一行。没有描述时使用命令第一行、路径或查询作为摘要。各行按终端显示宽度截断;结果按调用 ID 在原条目上将 ⚙ 更新为 ✓ 或 ✗,不再重复新增结果条目,命令预览保留两格缩进。调用尚未加载时单独显示结果摘要,加载调用页后合并;嵌套结果正文保持隐藏。

思考生成时完整流式显示,思考块结束或正文/工具输出开始后自动折叠。/think 按从新到旧列出思考摘要、前一条已加载的用户 prompt,并包含当前尝试。↑/↓ 选择、Enter 跳到原消息并展开;/think SEQ 可切换该消息的折叠状态,/think live 控制当前尝试。列表在选择、Esc 或其他命令时关闭,不自动超时。打开列表只使用已加载记录,选择 Load older reasoning 才读取一页更早历史;prompt 在已加载窗口之前时明确提示,加载对应页面后补全。完整思考仍可被搜索。

/model 读取 session/modelCatalog,展示服务端公布的 provider/model 路由及思考强度。选择通过 session/selectModel 提交 { request: { sessionId, provider, model, reasoningEffort? } },省略强度时使用适配器默认值。服务端将选择用于后续请求、记录选择事件,并尝试保存为部署默认值;不会替换正在执行的请求。展示模型继续以 modelSelection.next 和 lastUsed 为准,调用失败保留原选择。部分 provider 目录失败会单独提示,不隐藏正常 provider。标题与网页的 Agent preset 标签一致:agentPreset 提供当前 ID,agentPresets/list 提供名称和信任来源。内置系统预设显示 Standard mode、PTC mode、Minimal mode、Creator mode;自定义预设保留其名称,目录缺失时回退显示 ID。可选目录按需读取并在本次连接中复用。Plan 是独立功能,不决定这里的模式名称。终端少于 62 列时,mode 可在 /status 查看,为会话标题留出空间。

历史分为语义消息块、prompt/思考摘要、独立折叠状态和行数索引。流式更新复用已提交历史的位置索引,只生成当前可见区域。每个会话的 LRU 最多保留 2,048 行已提交终端内容,移出缓存的行在回看时重建。已结束的旧版流式分片和不用展示的工具结果正文会释放,原始日志由服务端保存。服务端日志作为持久层,客户端作为可重载的内存层。实时历史默认以 2,000 条记录或 16 MiB 语义数据估算量为软限制(--history-records、--history-mb),触发回收后以限制的 75% 为目标,释放旧正文、摘要和排版缓存。切换会话会释放上一会话的 transcript。回看和思考导航期间保护已加载窗口;/latest 返回实时输出并恢复回收。离线历史、未结束流和最小近期尾部受保护,因此这些参数不是进程 RSS 硬上限。默认启用体积受限的运行时内存日志,位于 <state>/memory.log:每 30 秒一行 JSON,记录进程计数、保留记录数与字节数、是否处于回看保护状态、账本规模、布局行缓存、有界的数学与图表缓存,以及最近一次成本扫描重读的会话数、页数与事件数,用于区分真实增长与 V8 高水位。若运行时提供强制回收能力,样本还会记录回收后的堆;npm run start:profile 会以该能力加上堆快照信号启动,此时 kill -USR2 <pid> 即可写出快照。/coredump [tag] 无需信号或 profiling 运行时,即可把同样的 .heapsnapshot 产物写入客户端当前工作目录;tag 用于命名快照(默认 snapshot),V8 会同步序列化整个堆,因此客户端在文件写完前会暂停。快照请用 Chrome DevTools 打开分析,而不是终端。--memory-log <路径> 或 DSHT_MEMORY_LOG 可改路径,--no-memory-log 或 DSHT_MEMORY_LOG=off 可关闭。默认还启用体积受限的状态迁移日志,位于 <state>/trace.log:每行一个 JSON 事件,记录每次连接代际、选择器请求、本地工作区采用、会话解析、屏幕/选中项变化,每条被执行的命令都写成共享同一 commandId 的 begin/end 一对(副作用中途崩溃因此可见,end 带与用户看到的同一个 outcome/disposition 以及失败原因),每次会话写入的准入顺序(lane 是 normal 还是 control、是否等待过——行序就是 dispatch 顺序),客户端跑过的每个长操作(foreground 的 begin/end、kind 与是否被取消),以及每次 /loop 决策——列表选中了哪条记录、表单是否打开、运行是否开始且发了什么、因何被拒,以及每次 forked 验证的完整生命周期(begin/verified/retry/unavailable…,无法判决时带结构化原因:退出码与错误类别)——因此"屏幕自己动了"(例如重连把读者带回会话列表)、"/loop 没有启动"或"某一轮迟迟没有 verdict"都可以连同变化前后的取值一起回读,而不是靠猜。日志只记录标识与屏幕名,绝不记录 prompt、工具或会话正文。--trace <路径> 或 DSHT_TRACE 可改路径,--no-trace 或 DSHT_TRACE=off 可关闭;--trace-verbose(或 DSHT_TRACE_VERBOSE=1)才会在失败原因里引用验证子进程自己的最后一行,且先去掉路径与凭据。首次排版、改变终端宽度及展开特别大的单个内容块,仍需要处理对应全文。npm run bench:history 测量 500、2,000、10,000 条消息下的本地流式排版耗时,不含网络和模型时间。

费用估算

dsht 不只是显示 token 数,而是把 Harness 可见的逐请求用量转换为可追踪的人民币费用估算。它区分未缓存输入、缓存读取、缓存写入和输出,并结合模型、请求结算时间、价格版本以及高峰/空闲时段进行计算,便于在任务运行过程中及时了解当前会话和近期总成本。

这里的费用是基于 Harness 可见用量和本地价格配置得到的高精度估算,用于成本监控和控制;它不是供应商账户级账单,最终费用仍以供应商账单为准。

/cost 显示当前会话与今日的费用。日期使用 Asia/Shanghai。状态栏以两位小数显示本会话费用,括号内是今日合计(¥: 1.23(113.00));这两个数不表示预算。* 表示该小计并不精确:请求缺少时间戳、没有价格覆盖,或扫描尚未覆盖全部会话;/cost 会说明原因。每个服务端 origin 使用独立账本;总额覆盖 HTTP 可见会话及之前缓存的会话,不是供应商账户级账单。

客户端每次连接后、每 60 秒、任务结束及打开 /cost 时在后台通过 HTTP 读取完整历史;每个新连接都会把全部会话完整重读一遍,因为客户端不在时服务端仍在工作,已记录的更新时间无法说明这段空档里发生了什么;同一次连接内,服务端更新时间未变的空闲会话跳过扫描。计费不会发起模型请求。显式刷新时可按 Esc 或 Ctrl+C 取消。账本分别统计未缓存输入、缓存读/写和输出,思考 token 已包含在输出中。重试单独计费,同一次尝试的替换用量更新原记录,fork 继承历史不重复计费。没有可用用量、没有结算时间戳,或带官方价目表并不定价的缓存写入桶的请求只计入未计价而不猜测;没有条目覆盖的模型保持未计价,未列出的供应商不会套用官方价目。每个会话独立读取:某个会话不可达或被拒绝时只计为失败数量并继续扫描,不会中止整轮;子代理会话按其持久父级地址读取。扫描失败保留并标明部分缓存结果。

内置人民币价格于 2026-09-24 根据官方价格页核对。北京时间周一至周五(不含中国法定节假日)09:00–12:00、14:00–18:00 为高峰,其余时段半价。节假日日期来自安装的 workday-cn 包;国务院公布新年度安排后需要更新此依赖。Flash 高峰未命中输入/缓存命中输入/输出为每百万 token ¥2/¥0.04/¥8,Pro 为 ¥9/¥0.30/¥27;当前 Flash 模型名为 deepseek-flash,列出的旧 Flash 别名沿用同一费率。根据官方更新日志,9 月 14 日之后 V4 Pro 继续按自身费率计费。单列的缓存写入按未命中输入价计算。仅对价格表明确列出的模型与别名计价;其他供应商需要显式配置。

默认价格有效期从核对日期的北京时间零点开始,这是本地估算规则,不代表官方价格生效日期。更早用量需要补充历史价格版本。程序按助手请求结算记录的时间选择单价;官方未说明跨时段请求的归属,因此边界附近的估算可能与账单不同。图片使用供应商报告的 token 数。每次扫描都按当前加载的价格表重新折叠历史,因此落盘的总额是日志与价格表的投影:修改 prices.json 会在下一次扫描时重新计价它覆盖的请求,此前没有条目覆盖的请求则在出现覆盖后立即计价。

首次交互启动会创建 ~/.config/dsht/prices.json(或 $XDG_CONFIG_HOME/dsht/prices.json),可用 DSHT_CONFIG_DIR 覆盖目录。JSON 数组中的价格版本包含 id、provider、model、currency: "CNY"、source、包含起点的 from、可选且不含终点的 until、timezone、星期数字 weekdays(0 为周日)、日内分钟区间 windows,以及 peak/offPeak 下每百万 token 的 input、cacheRead、cacheWrite、output 单价。调价时用 until 结束旧区间,再添加唯一 ID 且 from 衔接的新版本;程序拒绝重叠区间。重启后读取配置修改;价格由用户维护,启动时不抓取网页价格。

用量文件位于 ~/.local/state/dsht/cost/<origin-hash>/,遵循 XDG_STATE_HOME,也可通过 DSHT_STATE_DIR 指定应用状态根目录。每个文件保存一个会话折叠后的总额:会话金额及其请求数与未计价数、该会话在保留窗口内每个自然日一个分桶、决策规则版本与该次折叠所用价格表的摘要,以及总额不精确的原因。文件不含提示词、工具正文、凭据、cookie,也不含任何逐请求信息。价格文件属于配置,这些用量文件属于状态,因此只有前者需要纳入设置备份。写入使用私有临时文件及原子替换;每个会话一个固定文件,写入前比较文件中记录的 cut 与规则版本,因此旧扫描无法覆盖较新的一次。缓存跨重启保留,不需要访问服务端配置目录。其他代数的文件会被忽略并由下一次扫描重建;无法解析的文件同样如此,因为切片是服务端日志的投影,而不是账本本身。

保留窗口是 60 个北京自然日。更早的日子在写入切片时被丢弃,最新一天已滑出窗口的切片会在启动时删除;任何账本文件——无论能否解析——只要文件时间早于窗口起点也会一并清掉,因此该目录不会随客户端见过的会话数无限增长;放掉一个切片只会让该会话被重扫一次,不会丢数据。/cost 显示会话、今天、本周(周一起算)与本自然月(1 日起算)四项,都统计到当前北京自然日;周与月两行只能回溯到保留窗口能覆盖的范围。扫描只在会话「可能在本窗口内产生过花费」时才翻它的历史——会话正在运行,或其服务端更新时间落在窗口内——外加当前打开的会话(面板总要报它自己的总额)。任何计费活动都会推进该更新时间,因此客户端离线期间产生的用量在下次连接时仍会被读到。

客户端接口

安装后的包通过 @itookit/dsht 导出 Client,通过 @itookit/dsht/auth 导出 login/CookieStore,并提供 TypeScript 声明。源码调用方可通过 TypeScript loader 从 src/transport/client.ts 导入,或构建后从 dist/index.js 导入。authenticate(token) 兑换凭据;connect() 打开一条多路复用连接;listWorkspaces(signal?) 和 listSessions(workspaceId?, signal?) 返回服务端列表的 Promise。call(endpoint, args, signal?) 将服务端错误保留为带有 code 和 details 的 RemoteError。务必在 finally 中等待 close();并发调用等待同一次关闭完成。关闭会永久结束该客户端,下一次使用需新建 Client。若只是对端断线而未调用 close(),可保留 Cookie 再次 connect()。库调用方可使用 src/transport/auth.ts 的 login(client, token, new CookieStore()) 启用持久化;Client.authenticate() 本身仅在内存中保留凭据。

会话和工作区命令在 args 内使用 { request: { ... } };会话列表使用 { _request: {} }。$events/result 直接使用具名参数。重连后的 follow 快照整体替换保留状态;持久消息与临时助手文本分别保存。读取器同时支持 event 记录和旧版 chunks 包装;后者包含 chunkrow/text-chunks、chunkrow/reasoning-chunks 或 chunkrow/tool-call-chunks。不提供 assistantStream 的服务端通过日志 chunk 传递实时文本;TUI 只重建尚未完成的尝试,并保留每条压缩记录的起始序号用于翻页。

发布到 npm

本仓库从 mushuanli/dsht 仓库发布一个公开包 @itookit/dsht。必须使用 scope,因为 npm 会以「与 dot、st 等现有短名过于相似」为由拒绝非 scope 的 dsht。下表中 package.json 是各字段的依据。

| 字段 | 值 | | --- | --- | | 名称 | @itookit/dsht | | 可执行命令 | dsht,不安装时用 npx @itookit/dsht;dsht --version 打印已发布版本 | | 库入口 | @itookit/dsht 和 @itookit/dsht/auth | | 作者 | [email protected] | | 许可证 | MIT,许可证正文位于 LICENSE | | 仓库与问题反馈 | mushuanli/dsht | | Node.js | 22.19 或更新版本 | | Registry 访问 | public,使用 @itookit scope | | 发布内容 | dist/、两份 README、它们的配对记录、截图和许可证 |

描述、关键词和依赖位于 package.json。以下命令属于维护者操作;创建本地安装包不会自动发布。

npm run test:package
npm login
npm publish --access public

test:package 构建 tarball,然后使用安装依赖时填充的缓存,在隔离的离线 npm-exec 安装中运行 CLI,并拒绝上述发布集合之外的打包路径。prepublishOnly 执行类型检查和测试;prepack 编译 JavaScript 与类型声明。包内不包含源码测试、录制数据和本地认证文件。

publishConfig.access 为 public;scoped 包需要它才能被公开安装,因此该设置放在包里而不是每次发布命令上。启用两步验证的账号需用即时验证码发布:npm publish --otp=<验证码>;验证码在最后一次请求时校验,此时类型检查、测试和构建均已执行完毕。

后续版本由 .github/workflows/publish.yml 发布:它以版本 tag 触发,使用 trusted publishing(OIDC)并生成 provenance,不保存任何发布 token。需在 npmjs.com → @itookit/dsht → Settings → Trusted Publisher → GitHub Actions 一次性配置:组织或用户 mushuanli、仓库 dsht、工作流文件名 publish.yml、允许动作 npm publish。Trusted publishing 无法创建包,因此最早的版本需手工发布;之后的版本推送对应 tag 即可发布,例如 npm version 0.2.3 && git push --follow-tags。

手动触发时该工作流只打包不发布,并拒绝与 package.json 不一致的 tag。参见官方 scoped 发布指南和 npx 文档。Registry 发布不属于本仓库已执行的本地验证。

开发与限制

npm test
npm run test:terminal
npm run build
npm run bench:input
node dist/cli/index.js --help

测试使用隔离的 HTTP/WebSocket 服务,驱动实际 Ink 选择器和输入框,在子进程中运行 CLI,并投影复制的 Harness v2 工作区编辑记录和 v0 压缩 chunk 记录。这些检查不需要模型凭据。记录和预期对话输出位于 tests/,不依赖父仓库。测试不覆盖真实模型供应商行为。

源码在 src/ 下按业务域组织:transport/ 负责服务端 wire 协议并把宿主帧归一化为语义事件,session/ 负责对话、历史、交互与会话运行态,cost/ 负责计费账本,catalog/ 负责模型与 preset,shell/ 负责本地 ! 命令,controller/ 是应用门面与事件路由,slash/ 负责命令语法,ui/ 承载全部 React 与 Ink,storage/ 负责全部文件系统操作,cli/ 是组装入口,根文件(state.ts、json.ts、text.ts、contracts.ts、session-title.ts、references.ts)是共享契约。内部跨域导入沿允许的依赖方向,可使用域入口或具体模块;UI 叶子组件通过 contracts.ts 读取领域类型。tests/architecture/dependencies.test.ts 使用 TypeScript 语法解析,检查九条禁止方向、纯类型契约、可解析的字面量模块路径,以及包含类型边在内的无环依赖。

npm test 渲染不带样式的帧,因为断言和 tests/expected/ 中的预期输出描述的是文本。从终端启动的测试运行器会向每个测试文件导出 FORCE_COLOR=1,使 Ink 在提示符与文本之间插入 SGR 转义序列;npm run test:terminal 在任何主机上复现该环境,prepublishOnly 也会运行它,因此从终端发布时验证的就是终端实际渲染的结果。 主题测试在独立子进程中分别渲染真彩色和纯文本,并隔离父进程中影响终端和 CI 颜色检测的环境设置。

输入期间复用历史投影和换行结果,直到对话版本或终端宽度变化;服务端更新和历史翻页会使缓存失效。bench:input 使用 20 条和 500 条合成消息,在预热后测量 30 次按键的本地输入至渲染耗时及历史投影读取次数。它排除网络/模型耗时,仅供诊断,不作为跨机器的延迟阈值。

界面显示纯文本、思考内容、工具调用和工具结果。尚未实现富插件卡片、文件上传、子代理导航和队列消息文本编辑;可通过 /queue 删除待处理消息。重连采用有上限的指数退避及抖动,并替换快照;列表命令直接报告失败而不重试。服务端的非稳定 API 更新后,需要同步本地报文适配和测试。