Bundle
dsh-foreground-stream
前台命令执行实时输出流式展示——对话历史命令卡片实时输出 + 底部流条(Host 镜像旁路 subprocess collect 游标 + 只读轮询路由 + WebUI 设置栏目)
- Source
- XiaoYuOvO
- License
- MIT
- Updated
- Updated 6 days ago
Readme
# dsh-foreground-stream
DeepSeek Harness Web bundle 插件:**前台命令执行(bash / pwsh 工具、非后台模式)期间的实时输出流式展示**——对话页输入区上方出现一条"命令输出流条",随命令执行**实时滚动**显示 stdout/stderr,而不是等命令结束后才一次性看到。
- 不修改任何官方工具包:`tool/result` 内容与执行路径逐字节不变,模型语义零改动。
- 只旁路展示:命令完成后官方工具卡片照常呈现最终结果;本条只负责"执行期间"的增量画面。
- 纯内存缓冲,进程重启即清,不做任何跨会话持久化。
## 安装
### 从 GitHub 一键添加(推上仓库后)
包已按可分发结构打包(`dependencies` 声明了 host 依赖、`files`/`.gitignore` 排除了开发期 junction):
```powershell
# 在任意位置执行(git 需走本地代理 127.0.0.1:7800):
node "G:\Deepseek Harness Desktop\dependencies\dsh\node_modules\@deepseek-ai\dsh\lib\bin.js" plugin --profile web add github:<owner>/<repo>
# 或带版本:add github:<owner>/<repo>#<tag|commit>
```
`dsh plugin` 会自动把声明了 `dsh.bundle.patch` 的包追加进 `dsh.profile.bundles`(自动 reconcile)。
**安装后必做一步(settings 白名单补丁,幂等脚本)**:
```powershell
node "G:\Deepseek Harness Desktop\data\dsh\plugins\dsh-foreground-stream\scripts\patch-allowlist.mjs"
# 或:npm run patch:allowlist
```
> 原因:Web 客户端读写 settings 命名空间受 harness `dsh-host-apiproxy` 的
> `WEB_SETTINGS_NAMESPACES` 硬编码白名单过滤,插件侧无注册逃生通道;本脚本把
> `dsh-foreground-stream` 幂等加入该白名单。**harness 升级/重装会覆盖补丁,需重跑。**
然后重启 DSH web 宿主(Host half 改动不热更新;Client 刷新页面即可)。
### 本地目录安装(开发中)
```powershell
# 在插件目录(含 package.json 的目录)下执行:
node "G:\Deepseek Harness Desktop\dependencies\dsh\node_modules\@deepseek-ai\dsh\lib\bin.js" plugin --profile web add .
```
> 等价于 pnpm 在 profile 目录里 `add file:<插件绝对路径>`。Windows 路径含空格时
> CLI 可能拆参,可改为:在 profile 目录 `pnpm add "file:<插件绝对路径>"`,再手动把包名
> 追加进 `dsh.profile.bundles`(`profiles/web/package.json`)。
验证安装:
```powershell
node "G:\Deepseek Harness Desktop\dependencies\dsh\node_modules\@deepseek-ai\dsh\lib\bin.js" --profile web --dump-config | Select-String -Pattern 'dsh-foreground-stream'
```
应出现:
```yaml
- id: dsh-foreground-stream
name: dsh-foreground-stream
```
## 重启后如何确认效果
1. 重启 DSH web(新组合在启动时加载)。
2. 让 agent 跑一条**长时间前台命令**,例如:
- pwsh(Windows):`1..1000 | ForEach-Object { Write-Output "line $_"; Start-Sleep -Milliseconds 200 }`
- bash(POSIX):`for i in $(seq 1 1000); do echo "line $i"; sleep 0.2; done`
- 或 `ping -t 8.8.8.8`(需注意默认 timeout)。
3. 观察对话页输入框上方:出现"pwsh · <命令>"条,命令输出随执行逐行滚动;
命令结束显示"命令完成 · 退出码 N"约 4 秒后自动收起,官方工具卡片照常呈现最终结果。
## 设计说明
### 为什么是"镜像旁路"而不是改官方包
官方链路(已读源码确认):
```
dsh-tool-bash/pwsh.execute → ctx.shell.run(spec) → 执行器 runArgv
→ ctx.subprocess.spawn(spec) (stdio 恒为 collect 模式,无增量回调)
→ await handle.done → readFrom(0) 一次性汇总
```
`dsh-subprocess` 服务契约明确:collect-mode reader 是 **offset-based 非消耗式**的
(`readFrom(fromByte)` 返回 `{ text, nextOffset, lossy, spillPath }`,独立读者互不消耗)。
因此插件用**自己的游标**增量读取 `handle.collected.stdout/stderr`,官方最终 `readFrom(0)`
完全不受影响——这就是 task-status "镜像补丁保证零竞争" 哲学的同一个缝。
### callId 关联(把输出挂到具体工具调用)
- agent-loop 在 execute **之前**同步 append `tool/call`(`{turn, step, callId, name, arguments}`),
经 `session/event` 派发 → 插件记录"当前前台命令"并建立 per-callId 缓冲。
- 真实事件形态(源码 + 实测确认,勿想当然):
- `tool/call` 的 `arguments` 是模型侧**原始 JSON 字符串**(agent-loop 直接落
`block.arguments`,只有 `exec.arguments` 经过 `parseArguments`)——插件
先 `JSON.parse` 再读 `command` / `run_in_background`。
- `tool/result` 的 `message.content` 是**嵌套**形态
`[{ type:'tool-result', content:[{type:'text', text}] }]`
(`createToolResultMessage` 包装)——退出码提取需穿透包装层。
- 前台 shell 调用在 agent-loop 中严格串行(`startCall` await dispatch 直到进程结束),
因此"当前前台命令"是单槽;`subprocess.spawn` 包装器在 spawn 时校验
**argv 包含该命令字符串**(bash:`["bash","-c",cmd]`;pwsh:`["pwsh",...,"-Command",<preamble>cmd]`;
win32 sandbox 的 runner argv 同样内嵌原 argv)才挂 tap——MCP / skill / worker 等
其他工具的 spawn 因 argv 不匹配而绝不会被误挂。
- `tool/result`(`message.source.callId`)收尾:running=false、尽力恢复退出码/信号、
清 pending。
### 数据通道与节流
- Host 注册只读路由 `GET /plugins/dsh-foreground-stream/active?sessionId=...`
(流条用,返回当前/最近命令 + 全文 + `dock` 配置位)与
`GET /plugins/dsh-foreground-stream/output?callId=...`(历史卡片实时轮询用);
客户端**整段替换**渲染(tail 效果)。
- 增量 tap 统一 120ms 心跳 drain(不随输出量放大);缓冲 64KB 保尾截断;
完成后保留 120s 供展示,之后清理;缓冲条目上限 100。
- 客户端运行中 400ms / 空闲 1s 轮询;输出区 max-height 180px 滚动 + 自动跟随底部
(用户上翻暂停跟随,回底恢复);ANSI 转义防御性清洗。
- **长命令行展开**:命令行超过 64 字符(或含换行)时,流条标题变为可点击
(带 ▾/▴ 指示与 `aria-expanded`),点击展开完整命令(等宽字体块、
max-height 120px 可滚动),再点收起;调用切换时自动复位。短命令保持单行省略。
- **历史卡片实时输出**:运行中的前台命令,其对话历史卡片**自动展开**终端面板,
面板输出区按 callId 轮询 `/plugins/dsh-foreground-stream/output` 实时更新
(400ms/次),无需底部流条即可在记录里直接看到命令输出;命令结束后停止
轮询,改由官方最终结果呈现。
- **自动展开/收起(`config.autoExpand`,默认开)**:运行中的新命令面板自动展开、
运行完成后自动收起;用户手动展开/收起后不再自动干涉(手动状态优先)。
- **复制命令按钮**:历史卡片终端面板内命令行右侧有复制按钮,**悬停卡片时显示**,
点击一键复制完整命令行(短暂显示 ✓)。
- **面板展开/收起动画(`config.animMs`,默认 150ms)**:终端面板展开播
`dshfs-panel-in`、收起先播 `dshfs-panel-out` 再卸载(opacity + 轻微位移);
时长由设置项"面板动画速度"控制(关闭 0 / 快 80 / 标准 150 / 慢 300),
`animMs: 0` 时无动画、即时展开/收起。
- **WebUI 设置栏目(命令输出)**:`通用设置 → 外观 → 命令输出`,两个开关——
"显示底部命令流条"(showDock)、"命令面板自动展开/收起"(autoExpand)
与动画速度下拉(animMs)。
值存 Host settings 命名空间 `dsh-foreground-stream`(settings.yaml 持久化),
即时生效(流条/卡片随下一次轮询应用),无需改组合行配置。
注册方式:Host 在 apply 时经 `ctx.get('settings')` 直连注册,服务未就绪时
500ms 轮询重试(最多 60s)——不依赖嵌套 `ctx.inject`(真实宿主上该路径
对部分插件不触发)。诊断:`GET /plugins/dsh-foreground-stream/settings-state`
返回 `{ state, source, registered }`。
- **⚠️ harness 依赖**:Web 客户端能否读写某命名空间由 `dsh-host-apiproxy`
的 `WEB_SETTINGS_NAMESPACES` 硬编码白名单决定(代码注释明说:未列入即
`settings-not-exposed`,即使命名空间已注册)。本插件已把
`dsh-foreground-stream` 加入该白名单(`dependencies/dsh/node_modules/
@deepseek-ai/dsh-host-apiproxy/lib/index.js`)。**升级/重装 harness 会覆盖
该修改,需重新打补丁**,否则设置栏目会退回"设置服务暂不可用"。
- **组合行配置作为默认层**:`cordis.patch.yml` 的 `config.showDock` /
`config.autoExpand` 仍是 base 默认值;UI 里手动开关后以 settings 用户值为准。
```yaml
- id: dsh-foreground-stream
config:
showDock: false # base 默认层(UI 可再覆盖)
autoExpand: false
```
### 槽位选择
| 候选槽 | 结论 |
| --- | --- |
| `conversation.input.dock`(list)✅ 流条 | 官方既有 additive 座位(task-status 同款),对 bash/pwsh/任何 shell 工具统一生效;负责"执行期间"的实时滚动展示 |
| `tool.call.toolview`(keyed)✅ 历史卡片 | 接管 bash + pwsh 两个 key(`priority: -1` 最低者胜出,替换官方 bash-toolview-sample 与 pwsh 的通用回退行),**原版面板形态**:标题行 = 命令执行图标(悬停/已展开变箭头)+ 工具名标题(Bash/PowerShell)· 描述摘要 + 状态;点击行展开终端面板,面板内命令行单行省略、**点击命令行本身原位展开为完整多行**(无第二面板),下方为输出(等宽、滚动)。数据取自 block 的 `callView`/`resultView`(`card:'terminal'`),缺失回退 `parseArgs(argsRaw)`/`resultText` |
| `conversation.details.tool`(single) | 接管 = 渲染所有工具的输出详情面板,太重,未采用 |
| `settings.general.item`(list)✅ 设置栏目 | 通用设置页的"命令输出"行(id `command-output`,order 15,紧随"外观"行):标题 + 两个开关(showDock / autoExpand),经 `settingsScope` 绑定 Host 命名空间读写 |
> 两处都只读展示:官方 execute 行为与模型可见的 `tool/result` 内容零改动。
### 局限
- 只覆盖经 `ctx.shell` 的前台 bash/pwsh 工具调用;`run_code` 内部子调度、终端
(PTY)路径、`tool-bash-persistent` 不经此链路,无流式旁路(不影响其正常执行)。
- 输出缓冲是"插件视角的镜像",与官方最终 tool/result 在超长输出截断边界上可能有
细微差异(各用各的 64KB 上限),但展示用途完全够用。
## 开发与自检
```powershell
node --check lib/index.mjs # Host half 语法
node --check lib/client.js # Client half(浏览器 bundle)语法
node scripts/smoke.mjs # Host 逻辑冒烟:补丁/关联/增量/跳过/隔离/dispose
```
## 文件
```
package.json # dsh.bundle.patch + dsh.client(web platform + inject)
cordis.patch.yml # 挂载行:- id: dsh-foreground-stream
lib/index.mjs # Host half:镜像 spawn 补丁 + session/event 关联 + /active 路由
lib/client.js # Client half:dock 流条 + 历史命令卡片(手写 ModuleLoader bundle)
scripts/smoke.mjs # Host 逻辑冒烟:补丁/关联/增量/跳过/隔离/dispose
scripts/smoke-client.mjs # Client 冒烟:bundle 结构/槽注册/展开交互/卡片渲染
README.md
```
> Client half 是手写 plain-JS `window.__ModuleLoader__.load({ id, factory })` bundle
> (与 dsh-message-edit 同款格式),无需 tsdown 构建步骤,提交的产物即源码;
> 安装时无 build 脚本,pnpm 不会触发 allowBuilds 拦截。
Install
dsh plugin --profile web add github:XiaoYuOvO/dsh-foreground-stream#cf51338cf8a75d838f9431f9305bf93ef81e6ab2
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-foreground-stream from the hub