# dsh-run-button **给 DSH 回答里的每一个命令行代码框加一个「运行」按钮。** 点一下命令就在宿主机上执行,stdout/stderr 实时流进**右下角的浮动面板**(默认行为)。**不需要任何可选插件**:另有一个 `Panel` 模式可以把每个运行送进底部工作台标签页,而它才是唯一用到 [`dsh-better-sidebar`](https://github.com/omdsh-dev/DSH-better-sidebar) 的模式 —— 见[三种输出位置](#三种输出位置)。 > 状态:`0.1.0` —— 可工作的插件包,纯手写(无打包器、无 TypeScript 构建)。 > > **已在真实 DSH 上验证**(`0.1.5-rc.2`):运行按钮能在会话自己的工作目录里执行命令、实时输出、并停止正在运行的进程。两种输出形态都端到端跑过 —— `Dock` 模式在右下角堆叠卡片;装了 `dsh-better-sidebar` 时 `Panel` 模式每个运行一个底部工作台标签页。
English · 中文
--- ## 为什么做它 DSH 把回答渲染成 Markdown,而代码框只提供一个操作:**复制**。每次回答里出现想试的命令(一段 `git`、一个 `pnpm` 脚本、一条诊断命令),你都得复制 → 切终端 → 粘贴 → 回车。 这个插件补上缺的那个动词。语言是命令行的代码框会在「复制」旁边多一个 **▶ 运行**。点它之后: - 命令在**宿主机**上执行,用它所在会话自己的工作目录与沙箱; - 输出流进**右下角的浮动面板(dock)** —— 每个运行一张卡片,最新的在最上面,始终不覆盖对话区; - 按钮反映状态:`▶ 运行` → `■ 停止` → `✓ 运行`(退出码 0)/ `✕ 运行`(非 0 或被终止); - 输入框上方的运行条列出进行中与最近的运行:点 chip 重新展开该运行的卡片,点它的 `×` 丢弃该运行; - 长时间运行的命令可以从按钮、浮动卡片或底部标签页里停止。 ### 三种输出位置 三种模式,可在运行条上切换(`Dock` / `Panel` / `Off`),选择会被记住: | 模式 | 输出去哪 | 需要额外装东西吗 | | --- | --- | --- | | **Dock**(默认) | 右下角固定堆叠,每个运行一张浮动卡片 | 不需要 | | **Panel** | 每个运行一个底部工作台标签页 —— 就是终端所在的那个面板 | [`dsh-better-sidebar`](https://github.com/omdsh-dev/DSH-better-sidebar) | | **Off** | 不渲染输出;按钮仍反映状态 | 不需要 | `Panel` 是通过 `dsh-better-sidebar` 公开的 `ctx.betterSidebar` 服务提供的(`registerTab` + `openTab({ target: 'bottom' })`),与它内置的终端 / git / 任务标签页用的是同一个扩展点。该插件是**可选**依赖:没装时 `Panel` 会被置灰,`Dock` 仍是默认,所以这个插件不依赖任何额外东西就能完整使用。 卡片**刻意不锚定**在发起它的代码框上。锚定看起来更整齐,直到该代码框滚出虚拟化列表、锚点查不到,同一个运行就会**同时出现在两处**。固定的右下角面板没有这种失效模式。 ## 识别哪些代码框 当代码框的 info string 是命令行语言时才会出现按钮: | 语言标记 | 按钮文字 | | --- | --- | | `bash`、`sh`、`zsh`、`shell`、`shellscript` | `▶ Run` | | `console` | `▶ console` | | `powershell`、`pwsh`、`ps1` | `▶ PowerShell` | | `cmd`、`bat`、`batch`、`dos` | `▶ cmd` | 其他代码框(`js`、`python`、`json` 等)保持原样:执行它们需要自行拼装解释器调用,那是另一个功能。 ## 安全姿态 这是一个**由用户主动触发**的执行入口,并且刻意不比 Agent 本身更强: - 一次运行继承调用会话的**沙箱策略**(`ctx.sandboxPolicy.resolve`)与**工作目录**;点运行能到的范围不会超过该会话里 Agent 能到的范围。 - 命令**按代码框里写的样子**执行,不做任何插值,也不从模型输出拼接 shell 字符串。 - RPC 通道仅限本机回环,并且位于 Connection 自带的信任栅栏与浏览器鉴权之后;没有凭据的请求直接 `401`。 - 执行是异步的,界面不会阻塞;插件停止或更新时会杀掉仍在运行的进程,重载后不留孤儿进程。 - 每条流上限 256 KB;被沙箱拦截的命令会把执行器自己的拒绝信息显示在面板里。 ## 安装 ### 装进本地 profile(开发) ```bash # 把包 junction 进 profile 的 node_modules 并挂载 dsh dev inject` 文本,把运行按钮与 cwd 小标签**追加为 banner 动作行的尾部子节点** —— React 的 reconciler 从不枚举 DOM 子节点,因此尾部额外节点能在重渲染中存活。由于记录超过 100 条时列表会虚拟化,扫描在 mutation 与滚动时各跑一次。
2. 运行存储通过 `connection.rpc.call` 每 180 ms 轮询 `output` 并通知订阅者;React 视图都从这一份存储渲染,所以按钮、运行条与输出面板三者始终一致。
3. 右下角 dock 是一个固定容器,每个运行一张卡片;`Panel` 模式下改为通过 `ctx.betterSidebar.registerTab(...)` 注册 per-run 标签页,并用 `openTab({ target: 'bottom' })` 打开。标签页本体就是一个普通 React 组件,接收标准的 `TabComponentProps`。
运行条注册在会话 Slot `conversation.input.dock`。
### 没有构建步骤
两个半边都是手写纯 JavaScript:
- `lib/index.js` 就是普通 ESM —— Cordis loader 直接 import(不需要 `tsconfig`/`tsdown`)。
- `lib/client.js` 是**经典脚本**(不是 ES module),采用浏览器内核唯一接受的注册格式:
```js
window.__ModuleLoader__.load({
id: "dsh-run-button", // 必须等于 package.json 的 name
factory: (require) => { var module = {exports:{}}; var exports = module.exports; /* … */ return module.exports }
})
```
`require` 只能命中 shell 冻结的基线表(`react`、`react/jsx-runtime`、`react-dom`、`@deepseek-ai/cordis` 等),所以这个插件除 `react` 外不依赖任何东西。
发布前跑这两条闸门:
```bash
node scripts/check-client.mjs # 解析 + 物化 factory;并审计未声明的环境全局
node scripts/simulate-client.mjs # 在真实 DOM 上挂载,走完整链路
```
`simulate-client.mjs` 是关键那条。它像 shell 一样求值 bundle(`window.__ModuleLoader__`,再给一个类 Cordis 的 `ctx`),在 [happy-dom](https://github.com/capricorn86/happy-dom) 文档上挂载,渲染真实的代码框标记,然后断言:
- 运行按钮确实被注入进 banner,并被点击;
- `run/start` 与 `output` 确实在 RPC 通道上被调用;
- 切换输出模式后 dock 卡片出现又消失,且**只有一个 dock 容器、每个运行恰好一张卡**;
- 标签类型已注册、**不去重**,两次运行产生**两个不同标签 id 且各自携带自己的 `runId`** —— per-run 配对是最容易悄悄退化的地方;
- 卸载后没有留下任何残留。
它之所以存在:早先的 `check-client.mjs` 只证明 bundle **能解析**,不跑 `apply()`,于是未声明的全局变量(`styles`)顺利通过,把整页变成 "Failed to load plugins"。那两个上线的 bug **都是这个 harness 在编写过程中自己抓出来的** —— 这就是保留它的理由。
## 宿主契约(写 client 半边前必读)
`lib/client.js` 是手写纯 JS,没有类型检查兜底:臆造 API、漏填必填字段都不会在构建期报错,只会变成整页白屏或崩掉的侧栏标签页。以下契约摘自宿主的实际定义。
### 没有环境全局
**动态** Cordis 插件沙箱会给出 `styles` 与 `harness` 这两个 builtin。而一个真正发布的包 bundle **两个都没有** —— 引用其中一个就会在 `apply()` 里抛错,而抛错的 `apply()` 会把整个 composition 拖垮("Failed to load plugins"),不只是这个插件。因此每个浏览器全局都通过 `window.` 访问,并且 `check-client.mjs` 会在裸引用再次出现时让构建失败。
`apply()` 同时包在 `try/catch` 里:失败时会报告、回滚已挂载部分并返回,所以这个插件永远不可能是页面停止渲染的原因。
### 注入 CSS:内核没有 `styles` 服务
客户端内核**不提供** `styles` 服务,`ctx.get("styles")` 也拿不到东西。全局样式自己插 `