🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

终端聊天界面 rayafriandion/dsh-oc-tui

DeepSeek Harnessv0.1.5
rayafriandion15 天前有提交已提供中文文档扫描:含敏感能力

由于自定义 TUI 引擎无法支持高级视觉效果,也无法完整集成 Markdown 和 Mermaid,未来版本会引入…

37.8/100
综合分
9
GitHub 星标
339
npm 周下载
—/5
用户评分

数据截至 2026/10/20 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dsh-oc-tui
npm 源 · dsh-oc-tui 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:需留意
是什么
dsh 原生插件 · ui
装得上吗
实装验证未通过(runtime_boot_failed),装前请到仓库确认最近更新与 issue
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 15 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
⚠︎ 实装验证未通过(runtime_boot_failed · 2026/11/1) ——可能是验证环境差异,装前建议到 GitHub 仓库确认最近更新与 issue。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包dsh-oc-tui @ 0.1.5
✓Node 引擎要求 ^22.19 || >=24 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/31 03:46:44

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 76 处,展示前 68 条)
  • 读取本地文件
    lib/index.js:101
    roots = await readdir(sessionsRoot, { withFileTypes: true })
  • 读取本地文件
    lib/index.js:110
    entries = await readdir(rootPath, { withFileTypes: true })
  • 删除本地文件递归
    lib/index.js:117
    await rm(join(rootPath, sessionId), { recursive: true, force: false })
  • 读取本地文件
    lib/index.js:130
    const markerText = await readFile(marker, 'utf8')
  • 读取本地文件
    lib/index.js:133
    const head = (await readFile(join(gitDirectory, 'HEAD'), 'utf8')).trim()
  • 读取本地文件
    lib/index.js:137
    const head = (await readFile(join(marker, 'HEAD'), 'utf8')).trim()
  • 读取本地文件
    lib/index.js:282
    if (hostPath) return readFile(hostPath)
  • 发起网络请求
    lib/index.js:286
    const res = await fetch(src.url, { signal: AbortSignal.timeout(10000) })
  • 发起网络请求
    lib/index.js:2211
    const res = await fetch(url)
  • 读取本地文件
    bin/dsh-oc-tui.js:21
    const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
  • 读取本地文件
    bin/dsh-oc-tui.js:91
    if (!existsSync(manifestPath)) return 'missing'
  • 读取本地文件
    bin/dsh-oc-tui.js:93
    const profileManifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
  • 读取本地文件
    bin/dsh-oc-tui.js:101
    if (existsSync(patchPath) && readFileSync(patchPath, 'utf8').includes(PACKAGE_NAME)) return 'ready'
  • 读取本地文件
    bin/dsh-oc-tui.js:101
    if (existsSync(patchPath) && readFileSync(patchPath, 'utf8').includes(PACKAGE_NAME)) return 'ready'
  • 读取本地文件
    bin/dsh-oc-tui.js:129
    if (existsSync(full)) return full
  • 执行外部命令来自配置/变量
    bin/dsh-oc-tui.js:160
    if (entry !== undefined) return spawn(process.execPath, [entry, ...args], { stdio: 'inherit', env })
  • 执行外部命令固定命令
    bin/dsh-oc-tui.js:162
    return spawn('cmd.exe', ['/d', '/s', '/c', command, ...args], { stdio: 'inherit', env })
  • 执行外部命令来自配置/变量
    bin/dsh-oc-tui.js:165
    return spawn(command, args, { stdio: 'inherit', env })
  • 执行外部命令来自配置/变量
    lib/mermaid.js:41
    child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
  • 写入本地文件
    lib/mermaid.js:82
    await writeFile(src, code, 'utf8')
  • 读取本地文件
    lib/mermaid.js:85
    const svg = await readFile(out)
  • 删除本地文件递归
    lib/mermaid.js:94
    void rm(dir, { recursive: true, force: true }).catch(() => {})
  • 读取本地文件
    lib/mermaid.js:131
    const bytes = await readFile(cacheFile)
  • 写入本地文件
    lib/mermaid.js:149
    void mkdir(cacheDir, { recursive: true })
  • 写入本地文件
    lib/mermaid.js:150
    .then(() => writeFile(cacheFile, result.bytes))
  • 读取本地文件
    lib/mcp.js:251
    text = await readFile(file, 'utf8')
  • 写入本地文件
    lib/mcp.js:330
    await mkdir(directory, { recursive: true })
  • 写入本地文件
    lib/mcp.js:333
    await writeFile(temporary, serializeDocument(servers), 'utf8')
  • 写入本地文件
    lib/mcp.js:334
    await rename(temporary, file)
  • 删除本地文件
    lib/mcp.js:336
    await rm(temporary, { force: true }).catch(() => undefined)
  • 读取本地文件
    lib/mcp.js:343
    return await stat(path)
  • 读取本地文件
    lib/mcp-skill.js:58
    return parseSkillFile(readFileSync(SKILL_FILE, 'utf8'))
  • 读取本地文件
    lib/updates.js:21
    const TUI_MANIFEST = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8'))
  • 读取本地文件
    lib/updates.js:144
    if (existsSync(full)) return full
  • 读取本地文件
    lib/updates.js:156
    const manifest = JSON.parse(readFileSync(packagePath, 'utf8'))
  • 读取本地文件
    lib/updates.js:167
    if (existsSync(bundled)) return { command: process.execPath, args: [bundled] }
  • 执行外部命令来自配置/变量
    lib/updates.js:186
    child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
  • 读取本地文件
    lib/updates.js:483
    return new Set(readdirSync(updateCacheDir())
  • 发起网络请求
    lib/updates.js:503
    response = await fetch(url, { signal: AbortSignal.timeout(METADATA_TIMEOUT_MS) })
  • 发起网络请求
    lib/updates.js:529
    const response = await fetch(url, { signal })
  • 写入本地文件
    lib/updates.js:562
    mkdirSync(dirname(destPath), { recursive: true })
  • 写入本地文件
    lib/updates.js:563
    writeFileSync(destPath + '.part', data)
  • 写入本地文件
    lib/updates.js:564
    renameSync(destPath + '.part', destPath)
  • 读取本地文件
    lib/updates.js:578
    if (existsSync(cache)) {
  • 删除本地文件
    lib/updates.js:582
    else { try { unlinkSync(cache) } catch { } }
  • 删除本地文件
    lib/updates.js:589
    try { unlinkSync(cache) } catch { }
  • 执行外部命令来自配置/变量
    lib/updates.js:707
    const child = spawn(spec.command, spec.args, spec.options)
  • 执行外部命令固定命令
    lib/rewind.js:138
    const child = spawn('git', args, {
  • 写入本地文件
    lib/rewind.js:209
    await mkdir(dirname(target), { recursive: true })
  • 写入本地文件
    lib/rewind.js:210
    await copyFile(abs, target)
  • 读取本地文件
    lib/rewind.js:233
    if (existsSync(abs)) await backup(abs, rel)
  • 写入本地文件
    lib/rewind.js:234
    await mkdir(dirname(abs), { recursive: true })
  • 写入本地文件
    lib/rewind.js:235
    await writeFile(abs, head.stdoutBuffer)
  • 读取本地文件
    lib/rewind.js:242
    if (!existsSync(abs)) {
  • 删除本地文件
    lib/rewind.js:255
    await unlink(abs)
  • 执行外部命令固定命令
    lib/git-branch.js:21
    const child = spawn('git', args, {
  • 读取本地文件
    bin/apply-update.cjs:34
    const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
  • 读取本地文件
    bin/apply-update.cjs:47
    const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
  • 写入本地文件
    bin/apply-update.cjs:65
    fs.mkdirSync(path.dirname(plan.marker), { recursive: true })
  • 写入本地文件
    bin/apply-update.cjs:66
    fs.writeFileSync(plan.marker, JSON.stringify({
  • 删除本地文件
    bin/apply-update.cjs:80
    try { fs.unlinkSync(path.join(path.dirname(plan.tarball), 'staged.json')) } catch { }
  • 执行外部命令来自配置/变量
    bin/apply-update.cjs:89
    const result = cp.spawnSync(plan.npm.command, [...plan.npm.args, 'install', '-g', plan.tarball], {
  • 执行外部命令来自配置/变量
    bin/apply-update.cjs:101
    const detach = cp.spawnSync(plan.dsh.command, [
  • 执行外部命令来自配置/变量
    bin/apply-update.cjs:105
    const add = cp.spawnSync(plan.dsh.command, [
  • 执行外部命令来自配置/变量
    bin/apply-update.cjs:141
    const child = cp.spawn(target.command, target.args, {
  • 读取本地文件
    bin/apply-update.cjs:158
    plan = JSON.parse(fs.readFileSync(planFile, 'utf8'))
  • 发起网络请求
    bin/mcp-sse-bridge.mjs:328
    const res = await fetch(endpointValue, {
  • 发起网络请求
    bin/mcp-sse-bridge.mjs:387
    res = await fetch(target, {
扫描于 2026/11/3(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-agent@deepseek-ai/dsh-agent-loop@deepseek-ai/dsh-agent-presets@deepseek-ai/dsh-commands@deepseek-ai/dsh-cmdline@deepseek-ai/dsh-llm@deepseek-ai/dsh-session@deepseek-ai/dsh-session-persistence@deepseek-ai/dsh-user-approval@deepseek-ai/dsh-user-questions@deepseek-ai/schemastery
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 终端界面(TUI)

一个受 opencode 启发的终端界面(TUI),用于 DeepSeek Harness,以 dsh 配置档应用插件形式发布。

v0.2.02026-11-01
0.2.0 是视觉体验版本,也是这条线上第一个稳定版。直接安装不带标签的版本:latest 现在已指向它,这里不再需要 @next。 · 视觉体验 图片真正渲染出来了,走终端支持的图形协议:kitty、iTerm2、sixel,通用兜底是 ANSI 半块字符。Mermaid 代码块排成原生制表符画,而不是位图——lovely-mermaid 用纯 JavaScript 做布局,所以图表就是终端文本,能滚动、能选中、能复制,跟别的内容一样。引擎画不了的语法会退到 mmdc(已安装时离线运行),再退到 mermaid.ink。Markdown 有了真正的渲染器:标题、表格、语法高亮、链接。 对话记录是平铺的,不再套框;页头标出工作区和它的 git 分支;宽窗口下右侧栏显示模型、上下文和正在运行的构建。 · 不离开终端就能配置服务商 模型标签页以前只能编辑已存在的服务商,还把所有整段路由都藏了起来——DeepSeek 带的是 settingsPath: [],所以全新安装打开的是空页面,没地方填密钥。现在它构建的目录跟 WebUI 一样:已配置的路由逐条列出,可添加的路由放在搜索行后面,还有一个创建表单——目录或自定义端点——能在保存服务商之前采用已发现的模型列表。单个模型可以添加、编辑、删除。拉取失败会报出适配器自己的拒绝原因(…/models 返回 401;检查 API 密钥),而不是弹个空窗口。 · 分支切换 标题界面上按 Ctrl+B 列出本地和远程分支,可以新建或切换。工作区页头跟着变。 · 为 dsh 0.2.1-rc.5 构建 跟 rc.2 逐包对比过:核心没变,唯一一处显示变化在这里处理。有些端点会给推理块加前缀 …——这段不透明数据供后续请求回放,用来证明思维链是模型自己产出的。它是路由数据,不是正文,而且长到足以自己填满思考框,所以从显示里过滤掉。不携带其他内容的块完全不画行。 宿主输出现在也不再画进界面:备用屏幕开启时,插件写到 stdout/stderr 的内容会进 $DSH_HOME/tui-diagnostics.log,而不是盖在界面上,退出时再回放到终端。 · 修复 · Mermaid 磁盘缓存从来没缓存过任何东西——它写进一个从未创建的目录,失败还被吞掉了。现在首次写入时创建目录,读取放在任何服务商之前而不是之后(之前导致每个会话都重新渲染磁盘上已有的图表,还白白把图表文本发给公共服务),并按像素宽度做键。 · 图片渲染现在带上它构建时用的单元格网格,所以调整窗口大小会重新渲染,而不是按错误尺寸画旧位图。 · 折叠的思考框会预览第一行,而不是只显示「Thinking」。 · 调整大小的重绘合并到 40ms;以前是立即执行,拖一次窗口边缘每像素列就重建一次屏幕。 · 安装 dsh plugin --profile tui add -w dsh-oc-tui npm install -g dsh-oc-tui 两者都落在稳定通道。要锁定这个确切版本,加上 @0.2.0。 支持 dsh >=0.1.1-rc.2 <0.3.0-0,声明在 engines.dsh 里,已在 0.2.1-rc.2、0.2.0-rc.2 和 0.1.2-rc.1 上验证。
v0.2.0-pre.132026-10-18预发布
0.2.0 系列预发布版——用 dsh-oc-tui@next 安装。 · 界面本地化 TUI 绘制的每个字符串——外框、对话记录、对话框、浮层提示、命令帮助——现在都来自一份文案目录,整个界面跟随同一个设置:Ctrl+P → Language,可实时切换,无需重启。17 种语言加 System,每种都用自身文字标注:英语、中文(简体和繁体)、日语、韩语、德语、法语、西班牙语、葡萄牙语(巴西)、意大利语、俄语、波兰语、荷兰语、土耳其语、越南语、印尼语和阿拉伯语。 System 按顺序解析:DSH_TUI_LOCALE → LANGUAGE → LC_ALL / LC_MESSAGES / LANG → 操作系统区域设置 → 英语。取值可以是 POSIX(de_DE.UTF-8)、BCP 47(pt-BR)或裸语言码(zh、fr);未知语言回退到英语,而不是回退到它并不相似的邻近语言。 · 移除 · 印地语(hi)——Windows Terminal 不做印度语系字形整形(microsoft/terminal#17838,标记为 wontfix),界面上的元音符会脱离基辅音单独绘制,看起来像坏掉了。已保存的 hi 偏好现在改为回退到系统区域设置。 · 标题页上那条失效的标签页智能体提示——Tab 在输入框里唯一的绑定是循环切换思考强度,并不存在智能体相关功能。快捷键行现在只显示两个真实可用的功能:Ctrl+P 设置和 Tab 思考。 · 安装 dsh plugin --profile tui add -w dsh-oc-tui@next # 最新预发布版 npm install -g dsh-oc-tui@next 不带标签的安装仍落在稳定通道;latest 不受本次发布影响。
v0.2.0-pre.92026-10-05预发布内容较长已截断
· dsh-oc-tui v0.2.0-pre.9 Pre-release of the 0.2.0 line. This build targets dsh 0.2.0. It is not on npm's latest tag — install from the asset below, or dsh plugin --profile tui add -w dsh-oc-tui@next once it has been published under the next tag. · New — the shell is OpenCode-shaped · Title screen: the DeepSeek brand mark is one line tall (blue→white gradient text, no ASCII-art logo), followed by the workspace and its git branch, and a shortcut row where Ctrl+P (settings) and Tab (thinking intensity) stay ahead of the cosmetic hints. · Mermaid fences are drawn as native box-drawing art in the terminal (lovely-mermaid), before the image provider chain (mmdc → mermaid.ink → disk cache) ever sees the diagram. Width is verified in both directions: every art line's display width is compared cell by cell against the engine's own measurement. · From 140 columns a right-hand status rail takes over the status figures, OpenCode-style: the session title pins to the top, scrollable sections fill the middle (MODEL, THINKING, CONTEXT, SESSION, GOAL, SUBAGENTS — sections with no data drop out whole), and the running builds pin to the bottom. Shift+↑/↓ or the wheel over the rail scrolls it; clicking CONTEXT opens the session stats window. · New — a flat transcript · The rounded chat bubbles and tool cards are gone. Assistant output is flat rows (a dsh · time label row plus full-width text at a two-space indent), tool actions are one-line rows — status mark, accented verb, muted target, a ⎿ result gutter, and click-to-expand for the full result — user turns use a brand-blue ❯ pointer with a hanging indent and a trailing timestamp, and thinking/note/system markers keep their own flat rows in their own colours. · Header: the model name no longer repeats in the top-right corner (the status row and the rail both already carry it); the workspace path and its git branch take that slot behind a yielding budget ladder (label → branch, clipped at 20 columns → whole block),
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。

README

由 DeepSeek 最新模型翻译生成
dsh-oc-tui 由于自定义 TUI 引擎无法支持高级视觉效果,也无法完整集成 Markdown 和 Mermaid,未来版本会引入 chafa、mmdc、mdcat 等依赖,提升视觉体验。本次更新暂定于 0.2.0 版本;请注意,这可能导致 0.1.x 与 0.2.x 系列不兼容。我们计划先发布预发布版本,收集反馈。如果你对具体视觉功能有建议,欢迎在本 issue 下留言,或新开一个 issue,标题带上 Visual Experience Suggestion _感谢你对 dsh-oc-tui 插件的支持!_ DeepSeek Harness 的终端 UI —— 一个受 opencode 启发的聊天客户端,作为配置档应用插件在 dsh 进程内启动。 dsh-oc-tui 在终端里渲染 harness 的持久事件流 —— 流式回复、工具卡片、待办列表、思考块 —— 并把你输入的内容送回智能体。模型路由、工具执行、审批、命令、持久会话、凭据仍由 DSH 掌管;本包只负责终端输入与呈现。 已发布到 npm:dsh-oc-tui,并收录于 awesome-dsh-plugin 市场。
展开完整文档(README 剩余 1 节)
dsh-oc-tui

由于自定义 TUI 引擎无法支持高级视觉效果,也无法完整集成 Markdown 和 Mermaid,未来版本会引入 chafa、mmdc、mdcat 等依赖,提升视觉体验。本次更新暂定于 0.2.0 版本;请注意,这可能导致 0.1.x 与 0.2.x 系列不兼容。我们计划先发布预发布版本,收集反馈。如果你对具体视觉功能有建议,欢迎在本 issue 下留言,或新开一个 issue,标题带上 Visual Experience Suggestion

_感谢你对 dsh-oc-tui 插件的支持!_

DeepSeek Harness 的终端 UI —— 一个受 opencode 启发的聊天客户端,作为配置档应用插件在 dsh 进程内启动。

dsh-oc-tui 在终端里渲染 harness 的持久事件流 —— 流式回复、工具卡片、待办列表、思考块 —— 并把你输入的内容送回智能体。模型路由、工具执行、审批、命令、持久会话、凭据仍由 DSH 掌管;本包只负责终端输入与呈现。

已发布到 npm:dsh-oc-tui,并收录于 awesome-dsh-plugin 市场。

中文文档:docs/用户手册.md

目录

- 功能
- 环境要求
- 安装
- 快速开始
- 用法
- 快捷键
- 斜杠命令
- 交互式提示
- 思考强度
- 会话统计与上下文计量条
- 设置
- 应用内更新
- 工作原理
- @dsh-std 互操作
- 开发
- 故障排查
- 已知限制
- 布局
- 许可证

功能

| 持久会话 | 创建、恢复、列出、删除会话;对话记录从持久化事件日志重建,所以恢复后的会话和你离开时一模一样。 |
| 实时流式输出 | 助手文本和推理逐 token 流式输出;思考内容渲染在独立可折叠框里,流式输出期间保持折叠。 |
| 工具活动 | 工具卡片带一行摘要(read src/app.ts、run npm test),运行时显示流动的 spinner,结果用 markdown 渲染。 |
| 交互式提问 | 模型可以暂停并向你提问——选项列表、多选、自由文本,以及可滚动的计划审阅——全部内联在终端里。见交互式提示词。 |
| 内联审批 | approval/request 提示词直接用 y / n 回答,不用离开 UI;框里显示操作和原因,如果 @dsh-std 请求带了来源、风险和详情,也有地方展示。 |
| 会话统计 | 输入框上方一条统计栏——轮次/步数、LLM 与工具耗时、平均 TTFT、解码吞吐、缓存命中率,以及计费的输入/输出 token——从持久事件汇总而来。见会话统计与上下文计量条。 |
| 统计窗口 | 点击统计栏或上下文计量条,或输入 /stats,查看完整的会话统计与 token 用量明细。 |
| 上下文计量条 | 实时上下文占用(ctx ▓▓░░ 32K/128K 25%),同一窗口里还有 system/tools/messages 的构成。 |
| 思考强度 | Tab 循环切换当前模型真实的推理等级;Ctrl+E 打开滑块。等级按请求生效并持久保存。 |
| 共享设置 | 与 Web UI 使用同一套主机设置命名空间——通用、会话、按服务商的模型配置、凭据——持久化到 $DSH_HOME/settings.yaml。 |
| 应用内更新 | 在 TUI 内检测并切换 @deepseek-ai/dsh 和 dsh-oc-tui 的版本,Windows 上安全地延迟安装。 |
| 零依赖终端引擎 | 原始模式、备用屏幕、差分单元格缓冲区、truecolor ANSI、CJK 感知宽度、SGR + 旧版 X10 鼠标解码,以及 IME 光标锚定。 |

环境要求

| Node.js | >= 22 |
| dsh 命令行 | @deepseek-ai/dsh — 例如 npm install -g @deepseek-ai/dsh |
| pnpm | 在 PATH 上;dsh plugin 会转发给它 |
| 终端 | 交互式终端(Windows Terminal / ConPTY、iTerm2、GNOME Terminal……) |
| 模型路由 | $DSH_HOME/settings.yaml + $DSH_HOME/.credentials.yaml 里有可用路由(与 Web GUI 使用同一套配置) |

dsh --version
pnpm --version

兼容性。 已针对 dsh 0.2.0-rc.2 和 0.1.2-rc.1 验证(更早是 0.1.1-rc.2)。DSH 在 0.1.2 中重命名了部分会话 API——Session.events 变成了 snapshotEvents()——本插件会读取主机提供的任一访问器,所以一个构建同时服务两条版本线。
dsh(the harness)0.2.0 是破坏性更新——请先升级这个插件。 从 dsh 0.2.0-rc.1 起,agent-preset 系列包被拆开了:名单服务商 @deepseek-ai/dsh-agent-presets(自带预设)改名为 @deepseek-ai/dsh-agent-preset-registry,而新增的 @deepseek-ai/dsh-agent-preset 包只是一个声明式预设行,用来注入 registry 提供的服务。早于 0.1.5 的 TUI 仍写着旧服务商名字,dsh 每次启动都会报出来:

dsh: disabling profile plugin row "agent-presets": Plugin @deepseek-ai/dsh-agent-presets@0.1.1-rc.2 is
incompatible with dsh 0.2.0-rc.2: peerDependencies {…}. Running it may cause crashes or data loss.

TUI 还能启动,但 Settings → Default preset 会丢掉名单(没有服务商挂载这一行,它就被隐藏了)。把 dsh-oc-tui 升到 0.1.5 或更高版本,这条报告就没了:这个 bundle 根本不挂载名单行,因为没有任何一个包名能同时适配两条线——旧包在 dsh 0.2.0 上会告警,单数形式的新包永远不会激活,而 registry 除非宿主自己声明预设,否则激活后是空的。dsh 0.2.0 的配置档想要名单,就挂载 registry 再加上自己的预设声明,dsh-web-app 就是这么做的。两个方向上你的会话都不会有任何变化;cordis.patch.yml 在这些行旁边记录了同样的决定。

安装

从 npm 安装

这个包发布在 npm 上,名字是 dsh-oc-tui。把它装进 tui 配置档:

dsh plugin --profile tui add -w dsh-oc-tui

或者全局安装启动器——这样会把 dsh-oc-tui 命令放进 PATH,之后它就会启动 dsh --profile tui:

npm install -g dsh-oc-tui

版本通道

npm 上这个包有两条通道:latest 放稳定版,next 放预发布版(0.2.0-pre.x、-rc、-alpha、-beta)。所以不带 tag 的安装——上面那两条 dsh plugin … add -w dsh-oc-tui 和 npm install -g dsh-oc-tui——给你的仍然是最新的稳定版,绝不会是候选发布版。awesome-dsh-plugin 市场条目链接的是同一个包,从那里发起的安装同样落在稳定通道上。

要跑预发布版,就点名 tag:dsh plugin 会把 spec 原样转给 pnpm,由 pnpm 解析 dist-tag。

dsh plugin --profile tui add -w dsh-oc-tui@next # newest pre-release
npm install -g dsh-oc-tui@next

这个仓库里尚未发布的工作没有任何 dist-tag,所以要从打包好的 tarball 或 checkout 安装(见从 checkout 或 tarball 安装):

npm pack # -> dsh-oc-tui-<version>.tgz
dsh plugin --profile tui add -w ./dsh-oc-tui-<version>.tgz
这份 README 描述的是当前源码树,可能领先于已发布的版本——这里记录的功能,只有对应版本上了 npm,才能保证在稳定版里存在。

一条命令安装

仓库自带安装脚本,会检查 Node >= 22,确认 pnpm 存在,把插件装进 tui 配置档,还可以把 dsh-oc-tui 启动器加到全局。

Linux / macOS
curl -fsSL https://raw.githubusercontent.com/rayafriandion/dsh-oc-tui/main/install.sh | bash

Windows (PowerShell)
powershell -ExecutionPolicy Bypass -Command "iwr https://raw.githubusercontent.com/rayafriandion/dsh-oc-tui/main/install.ps1 -OutFile install.ps1; & .\install.ps1"

也可以从本地检出目录运行 ./install.sh / .\install.ps1,加上 --launcher / -Launcher 还会把 dsh-oc-tui 命令放进 PATH。其他参数:--local(-Local)安装当前检出目录,--source <spec>(-Source <spec>)使用自定义源,--profile <name>(-Profile <name>)指定另一个配置档。

从检出目录或 tarball 安装

npm pack # -> dsh-oc-tui-<version>.tgz
dsh plugin --profile tui add -w ./dsh-oc-tui-<version>.tgz

dsh plugin 转发给 pnpm 之前,会把相对路径锚定到你调用它的那个目录。

为什么要加 -w

配置档目录把自己声明为 pnpm workspace 根(pnpm-workspace.yaml → packages: [.]),所以 pnpm 会拒绝裸 add,报 ERR_PNPM_ADDING_TO_ROOT。-w 让依赖落进配置档自己的 manifest——它本来就是。随后 dsh plugin 会对照已安装的内容,校正 dsh.profile.bundles。

安装做了什么

1. 首次使用时,dsh plugin 初始化 $DSH_HOME/profiles/tui(@deepseek-ai/dsh-base 加一个空的用户补丁层)。
2. pnpm 把 dsh-oc-tui 装进配置档的 node_modules。
3. 因为包声明了 dsh.bundle.patch,dsh 会把 dsh-oc-tui 追加到 dsh.profile.bundles。
4. dsh --profile tui 会组合基础层、这个 bundle 的行、以及你自己的补丁——不用手动编辑。

不用启动就能验证:

dsh --profile tui --dump-config

dump 里会显示一个 # == dsh-oc-tui 层,包含 tui-startup、tui-app 和 tool-ask-user 行——三行,没有 agent-preset 那一行(见上面的兼容性说明)。

快速开始

dsh --profile tui # title screen; your first message creates a session
dsh --profile tui --resume <sessionId> # resume a persisted session
dsh --profile tui --model <modelId> # default model for new sessions
dsh --profile tui --provider <route> # default provider route
dsh --profile tui --no-sidebar # start without the session rail
dsh --profile tui --help # the TUI's own flags
原版启动器只把 web 和 plugin 硬编码为裸子命令,所以 --profile tui 才是预期写法。想要字面上的 dsh tui?加个 shell 别名:

function tui { dsh --profile tui @args } # PowerShell $PROFILE

doskey tui=dsh --profile tui $* :: CMD

便捷启动器

包里还附带一个 dsh-oc-tui 二进制,等价于 dsh --profile tui,但它会先检查该配置档是否真的装了插件,没装就打印一次性安装命令。

dsh-oc-tui # boot the tui profile
dsh-oc-tui --profile mytui # boot a different profile
dsh-oc-tui --help # launcher help
dsh-oc-tui --version # launcher version

它优先用 PATH 上的 dsh,找不到就回退到 npx --yes @deepseek-ai/dsh。用 npm install -g dsh-oc-tui 安装。

| 环境变量 | 作用 |
| DSH_TUI_PROFILE | 没有 --profile 时的默认配置档(默认 tui)。 |
| DSH_TUI_SKIP_CHECK | 设为 1 可跳过配置档预检(进阶安装)。 |

用法

快捷键

| 按键 | 操作 |
| Enter | 发送消息。 |
| Ctrl+Enter / Shift+Enter / Alt+Enter | 插入换行。 |
| Ctrl+C | 清空非空提示词、取消正在跑的回合,或空闲时按两次退出。 |
| Ctrl+P | 打开设置。 |
| Ctrl+E | 切换输入框下方的思考强度滑块。 |
| Tab | 会话页:循环切换思考强度。设置页:切换左侧菜单。 |
| Ctrl+N | 新建会话。 |
| Ctrl+D | 在设置 → 管理会话中:删除选中的会话(按两次确认)。 |
| Ctrl+L | 清空对话记录视图。 |
| Up / Down | 在多行提示词里移动光标;在第一行/最后一行时,翻阅输入历史。 |
| Left / Right | 在输入框内移动光标。 |
| PgUp / PgDn | 滚动对话记录。 |
| Esc | 关闭会话统计窗口、思考滑块或帮助;取消审批;取消正在跑的回合;清空你正在输入的提示词。 |
| Esc Esc | 空闲且提示词为空时:打开回退选择器。 |
| y / n | 回答内联审批提示。 |

鼠标。 滚轮滚动对话记录(设置窗口打开时则滚动设置窗口)。按住左键在对话记录上拖动可选中文本,然后按右键复制选中的内容。

斜杠命令

内置:/help /settings /new /resume <id> /model <id> /provider <route> /rewind /stats /clear /cancel /quit(/exit 也可以)。
回退。 Esc Esc(或 /rewind)会列出当前会话的提示词。恢复对话会从所选提示词之前的事件分叉出一个新会话——父会话在磁盘上原封不动,和 harness 自带的 session/fork 行为完全一致——选择器默认落在最近的提示词上,所以连按两次 Enter 就能回退最后一轮。/rewind <n|last> [conversation|code|both] 不打开选择器,直接执行。分叉出来的会话从空收件箱开始:在某一轮之前切开,也会一并切掉那一轮所做的收件箱认领,所以父会话里排队的任何东西——包括你刚刚回退掉的那条提示词——都不会再次投递;它留在父会话的日志里,如果有这类内容,结果行会显示 dropped N inherited pending input。恢复文件是尽力而为,且有边界:它需要 git worktree(在其他地方回退会报告 files not restored (not a git worktree),并且不做任何改动),它会从 HEAD 重写已跟踪文件,但不碰索引;只有当转录记录中对该路径的首次写入发生在回退点或之后时,它才会删除未跟踪文件。它覆盖或删除的所有内容都会先复制到 $DSH_HOME/rewind-backups/<sessionId>/<timestamp>/,结果行会给出该目录。由于日志不保存文件内容,已跟踪文件会回到它最后一次提交的状态,而不是回退点时的精确状态。

Harness 命令——/compact、/goal、/plan 等——会转发给 ctx.commands,执行时不消耗模型轮次。它们需要活跃会话:在标题界面上,TUI 会回答 /<name>: start a session first,而不是悄悄丢弃命令。

/stats 是 TUI 自己的命令:它切换会话统计窗口,和 harness 命令一样需要活跃会话。

交互式提示

审批。 当工具需要权限时,输入区会被审批框替换。第一行是操作(Approval · <tool>),按键提示在放得下时一并显示;请求提供的说明跟在后面,作为 Summary · 行。y 允许一次,n 拒绝,Esc 取消。来自 @dsh-std 组件的请求还可能带有 Origin、Risk 和 Details 行——有就画,没有就省略;标记为私密的详情只显示标签,不显示值;在窄终端上,中间部分会让位,显示 … N more 标记,而操作和提示始终保留。插件还会遵循生效的权限预设,所以自动批准的预设根本不会弹出提示。

审批如果超过了调用方设定的截止时间,会自行消失。这是过期,不是取消:调用方收到的是 expired 而不是 cancelled,所以超时永远不会被误读为人的决定。
提问。 模型可以通过 ask_user_question 工具直接向你提问。这个工具由本 bundle 的 tool-ask-user 行声明——dsh-base 挂载了 user-questions 服务,但没有挂载这个工具,而 TUI 会话是从 base 组合出来的,不是从智能体预设组合出来的——回答通过一个模态框完成:

| 按键 | 操作 |
| Up / Down | 在选项之间移动(循环)。 |
| Space | 切换高亮选项(多选)或选中它(单选)。 |
| Enter | 继续:单选会选中并前进;在自由文本行上开始编辑;在多选中确认已切换的选项集合。 |
| 任意可打印按键 | 跳到自由文本行并开始输入。 |
| PgUp / PgDn、滚轮 | 滚动较长的计划或详情面板。 |
| Esc | 推迟:拒绝在这里回答(编辑时按 Esc 返回选项)。 |
| Ctrl+C | 仍然取消正在运行的轮次;待回答的问题被撤回。 |

问题一次只暂存一个,和 Web UI 输入框的暂存方式完全一样,答案编码也相同:自由文本答案会替换单选题的选择,对多选题则与选择并存。

带有 plan-review 意图的问题——也就是 exit_plan_mode 发送的那种——会在 Approve / Keep planning 上方用可滚动面板渲染计划 markdown。回答 Approve 会退出计划模式,模型继续;其他任何回答都继续规划。

推迟是刻意为之,不是取消:没有其他回答者时,工具会报告 no user-questions answerer accepted the request,这不会被误认为是人类的选择。

思考强度

生效的级别显示在输入框右上边框上,只显示级别名,与 provider · model 标签斜对。

- 在会话页面按 Tab 循环当前模型的级别(从最强到最弱循环);Shift+Tab 反向切换。
- Ctrl+E 在输入框下方打开滑块:Tab 或 ←/→ 调整并持久化,Esc 或 Ctrl+E 关闭。
- 级别来自服务商适配器(ctx.llm.resolveModelInfo),所以布尔思考模型只显示它的两端,DeepSeek 的 Off/High/Max 显示这三个,全范围模型显示所有公布的级别——绝不会是一刀切的 none → max 刻度。

选择通过 agent/request 瀑布应用到会话的请求上,并存储在 agent-default-model.reasoningEffort 中。

会话统计与上下文计量

输入框上方那一行是会话统计条,相当于 TUI 版的网页聊天统计行:同样的数字、同样的顺序,用 │ 分隔:

▤ 1 turn · 2 steps│LLM 1.3s · tools 1.2s│TTFT avg 400ms · 20.0 tok/s│cache 55%│in 110 · out 30

- turns / steps 统计已关闭的步骤(step/end),所以失败、取消和达到 max-tokens 的步骤也算。LLM 是 step/start → 组装好的回复;tools 配对 tool/call → tool/result。
- TTFT avg 是每步的首 token 平均耗时;tok/s 是解码吞吐量(首 token → 组装好的回复,按报告的输出 token 数计算)。
- cache 是提示词侧的缓存命中占比(缓存读取量占全部计费输入的比例);in / out 是会话计费的输入和输出 token。
- 终端太窄时,末尾的几组数据会整组丢弃,并用 │… 标出省略,而不是把一个数字切成两半;窗口里始终保留完整的一组。
- 会话若没有已结束的步骤、也没有计费 token,整条信息条会完全隐藏,把这一行还给对话记录。

整条信息条都是点击目标。点击它——或者点击状态行右端的上下文计量条(ctx ▓▓░░ 32K/128K 25%),或者输入 /stats——会打开会话统计窗口;再点一次、点击别处,或按 Esc 即可关闭。窗口把同一行拆成带标签的几行(usage / duration / speed / tokens / cache),并加上上下文占用读数及其启发式构成——系统提示词、工具、消息——与 Web UI 的 ContextMeter 对话框一致。

数据来源与 Web UI 相同,优先用投影,投影缺失时回退到插件自己的折叠结果:tokenUsage、contextPressure、contextBreakdown 由 dsh-base 的 token-meter 行挂载,而 sessionStats 只由 web app bundle 挂载——所以 TUI 按同样的规则折叠持久化的 step / chunk / message / tool 事件。某个投影缺失时,只对那一项数字回退;谁都提供不了的数字则保持隐藏,而不是打印一个零。

设置

Ctrl+P 会在与 Web UI 相同的主机设置命名空间上打开设置菜单,通过 ctx.settings 持久化到 $DSH_HOME/settings.yaml。左侧菜单把它分成三个标签页(按 Tab 或点击切换):

- Main —— General(忙碌时按 Enter 的行为、权限预设)、Sessions(新建会话、管理会话)、System(服务商 API 提示、更新管理器快捷方式、设置文件路径)。
- Model —— 默认的服务商/模型/推理选择,然后每个服务商一组,包含其 URL、API key 和模型列表。在 Models 上按 Enter 会拉取该服务商公布的目录(ctx.llm.discoverModels)并打开复选框窗口;在列出的某个模型上按 Enter 会把它设为默认路由。
- Update —— 见应用内更新。
只列出你实际添加过的服务商(存在于你的用户设置层);从未添加过的服务商保持隐藏。Default preset 只在配置档挂载了智能体预设名册时才显示,因为可选列表就是那份名册——而这个插件刻意不挂载任何名册(见上文的兼容性说明),所以在原版 tui 配置档上,这一行是缺失而不是空的。如果确实挂载了名册,要注意 TUI 会话是按进程范围从基础配置合成的,因此存储的默认值只在会话由预设创建时生效。仅限 Web UI 的选项(ui-theme 外观、locale)不显示,因为它们在 TUI 里没有效果。

应用内更新

Ctrl+P → Update 会显示 @deepseek-ai/dsh 和 dsh-oc-tui 的已安装版本、npm 上最新的 dist-tag,以及一行状态,它只针对 stable 版本:

- Update available → x.y.z —— 有更新的 stable 版本。
- Up to date —— 无需操作。
- No stable release — pick from Versions —— 仓库里还没有 stable 版本;手动挑一个。
- Install damaged — reinstall below —— 全局 dsh 目录树处于新旧混杂状态;重新安装。

在某个包的 Versions 行按 Enter,会打开完整的仓库列表(最新的在前,[latest]/[next]/其他 tag 和 (installed) 用颜色区分),你可以挑任意版本——包括预发布版。选中的版本会先下载:tarball 流式写入 $DSH_HOME/updates,实时显示字节进度,中途按 Esc 可取消,文件保留前会校验 sha512 仓库哈希。应用它相当于重启:在 y/n 确认后,TUI 会拉起一个分离的 applier 进程,自己退出,由那个子进程从暂存的 tarball 安装、核对磁盘上的版本,然后在同一个终端里用相同的参数(包括 --profile tui --resume …)和工作目录重新启动 dsh。

如果某个 tarball 已经在磁盘上,对应的版本行会标记 [cached]——你之前下载过(或回滚自)的版本无需重新下载;等待应用的版本则标记 [staged]。暂存的安装还会出现在 Update 标签页上,显示为 Restart to apply x.y.z(按一次 Enter)和 Discard downloaded x.y.z,而且它的状态能扛住崩溃:只有暂存的 tarball 确实存在时,这一行才会出现。Check now 重新读取仓库;Startup check 开关启动时的静默 stable 版本检查。

为什么更新从不在 TUI 运行时安装

更新一个已被运行中的 dsh 进程加载的包,可能悄悄损坏它:在 Windows 上,进程持有内存映射的原生 DLL,npm 替换目录会失败,结果就是新旧混杂的目录树——而 npm 仍然以 0 退出。2026-09-04 那次崩溃正是这样发生的。把整个流程改成「先下载,再重启应用」,在所有平台上都消除了这个窗口,而不是把它缩小:

1. 只有这个进程退出,才算安装完成。 下载下来的只是一个文件;applier 会轮询这个 pid,只要还有别的 dsh 进程活着,TUI 就拒绝启动重启。
2. 成功与否由磁盘上的版本决定,而不是 npm 的退出码。版本不匹配会报成 Install damaged — reinstall below,并附上修复说明,绝不会谎报成功。
3. 暂存的 tarball 会保留,所以回滚到任何已下载的版本都是一次经过验证的缓存命中,走的是和更新完全相同的代码路径。

applier 把结果写入 $DSH_HOME/tui-dsh-install.json,Update 页面会读取它,并在下次访问时清空——所以即使安装被中断,结果依然会被上报。

工作原理

- 这个插件是一个 Cordis 函数插件,由 tui 配置档加载。lib/startup.js 解析应用的命令行参数,并提供 tuiStartup 服务;lib/index.js 负责 UI 循环。
- lib/term.js 是零依赖的终端引擎:raw 模式、备用屏幕、差分单元格缓冲区,以及按键解码器(truecolor ANSI,能识别 CJK 宽度)。它把隐藏的终端光标停在输入光标处,这样操作系统的输入法就会把候选窗口锚定在输入框内;它同时理解 SGR 和旧式 X10 鼠标编码,所以滚轮和点击的字节永远不会漏进输入文本。单元格缓冲区也是唯一的关卡,所有绘制出来的字符串都在这里做净化:C0、DEL 和 C1 字符会被替换成可见的 \uFFFD,这样工具名、路径、标题,或者 @dsh-std 请求的动作,都不可能把转义序列偷渡进输出流。这个占位符刻意保持宽度不变——直接丢掉字符会悄悄改变布局,而显示出来则让注入一目了然。
- lib/ui.js 是响应式视图模型和渲染器(DeepSeek 蓝白主题、会话栏、对话记录、多行输入框、命令建议、遥测页脚)。对话记录按块缓存,每帧只物化可见窗口,流式绘制会合并,实时块按短节流重新渲染——所以历史增长时渲染开销依然有界。思考内容会折叠起来,让对话记录保持可读;运行中的工具和思考块用流动的 spinner 做动画。
- lib/metrics.js 把持久化的 step/chunk/message 事件折算成 token、TTFT、吞吐量和缓存命中指标。
- lib/interrupt.js 负责 clear/cancel/double-exit 状态机,供 stdin 和 SIGINT 使用。
- lib/markdown.js 把模型输出(标题、列表、引用、代码、行内片段)渲染成带样式的行。
- lib/updates.js 把 Update 标签页里所有 npm/pnpm 交互都隔离开——查询 registry、无依赖的 semver 比较、检测 dsh 安装、执行安装——全部通过 child_process.spawn,绝不用 spawnSync。
- 智能体通过 ctx.agents 创建和恢复,transcript 从会话的持久日志重建,并由 session/event(包括 assistant/chunk)实时喂入,模型默认值来自 ctx.agentDefaultModel,审批则内联应答 approval/request waterfall。
- ask_user_question 通过 user-questions/request waterfall 应答:这是一个作用域受限的 Cordis waterfall,模态框要么返回答案,要么用 next() 委派出去。请求被中止时会 reject,于是服务报告自己的 ASK_ABORTED;发给其他智能体的请求则原样委派,不做改动。

@dsh-std 互操作

包里带了一份 dsh-plugin.json manifest,以及 lib/facet.js 里的一个 facet,所以 @dsh-std 宿主不用跑任何代码就能发现它。目前交付的是那份静态 manifest 和 preflight 接口面;运行时协议支持眼下被上游限制挡住了。 在这个生态里,插件定位是宿主与 Presentation 服务商,不是协议消费方——它的 facet 接好线,用来发布 presentation.dsh/v1alpha1 的 UserInteraction / Notification / CopyText,以及 commands.dsh/v1alpha1 的 CommandRuntime,供其他组件驱动。它唯一一条 requires.contracts 条目是 commands.dsh/v1alpha1 的 Command 资源,声明为 optional: true。这条目留给未来的消费方,今天不起作用——没有任何地方调用 protocols.client(...),它自己的命令行从 cordis 的 ctx.commands 服务解析,所以不管有没有 Command 服务商,TUI 的行为都一样。无法满足的非可选依赖会直接激活失败,而不是给个警告,optional 标志正是让这份声明在这一点上保持诚实。但 Community v0.15 manifest 无法声明协议 support,而 @dsh-std/lifecycle 的 implement——也就是 stageProtocol——拒绝 stage 一个 facet 没有声明过的 support,所以 facet 什么都不 stage。协议 shim 已经写好,也有测试覆盖;等上游补上这个缺口,它们原样就能上线。逐层证据见 docs/dsh-std-接入说明.md。

The facet does not own the TUI's lifecycle, and the double activation is avoided by design. @dsh-std/adapter-dsh's mountProfileComponents mounts the facet of every dsh-plugin.json it finds in a profile's dependencies, without checking whether the cordis bundle rows already loaded that component — and this plugin is a profile dependency that cordis.patch.yml also activates. Since the TUI seizes the terminal (raw mode, alternate screen, mouse tracking), a manifest alone would start it twice. The facet is therefore an interop shell: when it does stage protocol support, it forwards protocol calls through lib/bridge.js to whichever TUI instance is live, and it reports { state: 'degraded' } when none is — or when it staged nothing at all. A dsh-plugin.json whose facet never starts the TUI is working as intended, not broken.

中文说明:docs/dsh-std-接入说明.md — 本插件在 @dsh-std 生态里扮演 Host 与 Presentation 提供方;facet 只是协议外壳,不拥有 TUI 的生命周期,双激活已由设计规避。但当前上游下它一条协议都不发布:Community v0.15 清单无法声明 supports,而 lifecycle 要求先声明才能暂存,因此 Phase B(运行时互操作)休眠、Phase A(静态清单与预检)可交付——见该文档开头的「阻断性发现」。该文档还记录了清单字段、pin 的 rc 版本、lib/bridge.js 的契约、adapter 的 staging 契约、明确不实现的协议,以及评审确认的已知行为。

Development

npm run check # node --check over lib/, bin/
npm test # standalone smoke tests (no dsh needed)

The install is a build, so edit → build → install. The profile contains a tarball copy of the plugin, and the profile's HMR root is the profile directory, not the plugin directory — editing this checkout changes nothing until you repack and reinstall:

npm pack # -> dsh-oc-tui-<version>.tgz
dsh plugin --profile tui remove -w dsh-oc-tui # detach the old copy FIRST
Remove-Item .\.tgz # then drop the stale tarball
npm pack
dsh plugin --profile tui add -w .\dsh-oc-tui-<version>.tgz

Detach before deleting: pnpm resolves the profile's existing file: dependency while adding, so a dependency pointing at a deleted tarball aborts the whole install with ENOENT.

Verify the swap actually landed — the version string proves nothing:

foreach ($rel in @('lib\index.js','lib\ui.js','lib\util.js','lib\term.js','lib\metrics.js',
'lib\interrupt.js','lib\web-settings.js','lib\updates.js','lib\markdown.js',
'lib\startup.js','bin\dsh-oc-tui.js','cordis.patch.yml')) {
$a = (Get-FileHash ".\$rel").Hash
$b = (Get-FileHash "$env:USERPROFILE\.dsh\profiles\tui\node_modules\dsh-oc-tui\$rel").Hash
if ($a -ne $b) { "DIFFERS: $rel" }
}

Then boot it for real. Reaching the title screen is not enough — the session-open path is where host API breaks surface, so send a message. Test --resume separately, because it is an apply-time path that can lose a startup race the post-boot paths win.

For a zero-install bootstrap that skips packaging entirely, create the profile once and point its patch at this checkout:

dsh --profile tui --dump-config # initializes the base profile once

$DSH_HOME/profiles/tui/cordis.patch.yml
- insert:
- id: tui-startup
name: 'file:///path/to/dsh-oc-tui/lib/startup.js'
- id: tui-app
name: 'file:///path/to/dsh-oc-tui/lib/index.js'
config:
sidebar: true
showReasoning: true

The plugin's dsh imports resolve through the shared $DSH_HOME/profiles/node_modules fallback that dsh maintains, so nothing has to be installed into the plugin directory.

Troubleshooting

| Symptom | Cause and fix |
| pnpm failed in profile directory / ERR_PNPM_ADDING_TO_ROOT | The profile is a pnpm workspace root; add -w to the add/remove command. |
| ENOENT: … dsh-oc-tui-<v>.tgz during install | The profile still references a tarball you deleted. dsh plugin --profile tui remove -w dsh-oc-tui, then add again. |
| pnpm not found on PATH | Install pnpm (npm install -g pnpm) and retry. |
| --dump-config has no TUI layer | The install did not complete, or the package name is misspelled. Re-run the add and check dsh.profile.bundles. |
| Exits immediately / no UI | stdin and stdout must both be a TTY — do not pipe or redirect. Then verify the model route and credentials exist. |
| --resume or Manage sessions unavailable | Both need the shared sessionQuery service; keep @deepseek-ai/dsh-base first in dsh.profile.bundles. |
| no agent factory registered on --resume | A startup race with the agent-loop row; current builds retry it. On an older build, run /resume <id> after boot instead. |
| Loader errors after updating dsh (State, ./internal) | The global dsh install is a mixed old/new tree. Close every dsh process and reinstall: npm install -g @deepseek-ai/dsh@<version>. |
| Source edits have no effect | The installed copy is a tarball; repack and reinstall (see Development). |

More detail, in Chinese: docs/用户手册.md.

Known limitations

- IME composition is not exposed by the zero-dependency terminal engine yet. Pasted images are: a bracketed paste of raw image bytes, a data:image/...;base64,... URL, a local image path, or an image URL becomes a [Image N] attachment, and pasting text nothing recognizes asks the terminal for its clipboard (OSC 52).
- The plugin does not hot-reload: the profile's HMR root is the profile directory, so a running TUI keeps the copy it booted with.
- dsh tui as a bare subcommand needs a shell alias — the stock launcher hardcodes only web and plugin.
- Harness slash commands need a live session; on the title screen the TUI tells you to start one first.
- Deferring a question with Esc does not cancel the tool call — it delegates, and with no other answerer the tool call fails. Per-question skip (as the Web UI composer offers) is not implemented.
- --resume, Settings → Manage sessions, the context meter, and the stats strip depend on services mounted by @deepseek-ai/dsh-base (sessionQuery, sessionProjections); a hand-built profile must provide them. The sessionStats projection is a web-app-layer row, so the TUI folds those figures from the session log itself when no profile mounts it.
- An update restart only waits for the TUI that scheduled it: the "no other dsh process" check runs before* you confirm, so a dsh web window opened afterwards can still overlap the install. Close other dsh windows first.
- Two @dsh-std presentation surfaces have no caller today, because the runtime protocol support is gated off (see @dsh-std interop). A standard secret-input request would open a masked prompt with minLength/maxLength bounds, and a standard ApprovalRequest is the only thing that populates the approval box's Origin / Risk / Details rows. Both paths are written and integration-tested; neither can be reached from a normal dsh --profile tui session, which is why the harness-shaped approval box shows only the action and the summary.

Layout

lib/index.js plugin entry: agents, events, input, commands, approvals, user questions
lib/startup.js command-line provider (tuiStartup service)
lib/term.js terminal engine (raw mode, screen, key decoding)
lib/ui.js responsive view model + renderer (includes the question modal)
lib/metrics.js whole-session stats + token usage fold (web stats strip / tokenUsage port)
lib/rewind.js rewind picker: conversation fork from the event log plus fenced file restore
lib/interrupt.js Ctrl+C lifecycle state
lib/web-settings.js shared WebUI settings projection
lib/updates.js in-app update manager (npm registry, download + staging, install verification)
lib/markdown.js markdown -> styled lines
lib/util.js text/display helpers
lib/bridge.js live-TUI registry the @dsh-std facet forwards through
lib/facet.js @dsh-std facet entry (interop shell; never starts the TUI)
lib/std/ @dsh-std protocol adapters (adapt, presentation, commands, command-list)
bin/dsh-oc-tui.js convenience launcher for dsh --profile tui
bin/apply-update.cjs detached applier: waits for the TUI to exit, installs the staged tarball, relaunches dsh
install.sh one-command installer (Linux/macOS)
install.ps1 one-command installer (Windows)
dsh-plugin.json @dsh-std component manifest (discovery + preflight only)
cordis.patch.yml bundle patch layer (TUI rows, ask-user tool; no agent-preset row, with the reason in a comment)
docs/用户手册.md Chinese user manual docs/dsh-std-接入说明.md @dsh-std interop scope and constraints (Chinese) tests/smoke.test.mjs standalone smoke tests

License

LGPL-3.0-or-later.
预览分享卡片长什么样 ↓rayafriandion/dsh-oc-tui
分享 · DeepSeek Harness Hub
rayafriandion/dsh-oc-tui

由于自定义 TUI 引擎无法支持高级视觉效果,也无法完整集成 Markdown 和 Mermaid,未来版本会引入…

这是你的仓库?认领条目

回到顶部