Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef8d3a9050 | ||
|
|
9a40164d1a | ||
|
|
29b8615bff | ||
|
|
b7aab48deb | ||
|
|
7dbd9b75e2 | ||
|
|
cdda072973 | ||
|
|
646c91cf70 | ||
|
|
55ad7eec8d | ||
|
|
d38a411f6d | ||
|
|
940d0a8928 |
@@ -3,9 +3,15 @@
|
||||
<!-- Hero -->
|
||||
<div align="center">
|
||||
<b style="font-size: 1.15em;">一个服务化的侧边栏框架,一套开箱即用的完整工作台</b><br /><br />
|
||||
<a href="https://www.npmjs.com/package/dsh-better-sidebar"><img alt="npm version" src="https://img.shields.io/npm/v/dsh-better-sidebar" /></a>
|
||||
<a href="https://www.npmjs.com/package/dsh-better-sidebar"><img alt="npm downloads" src="https://img.shields.io/npm/dm/dsh-better-sidebar" /></a>
|
||||
<a href="https://github.com/omdsh-dev/DSH-better-sidebar/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/omdsh-dev/DSH-better-sidebar/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://github.com/omdsh-dev/DSH-better-sidebar/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/omdsh-dev/DSH-better-sidebar" /></a>
|
||||
<a href="https://opensource.org/licenses/MIT"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg" /></a>
|
||||
<a href="https://dshfind.com/zh/plugins/omdsh-dev/DSH-better-sidebar?ref=badge"><img alt="dshfind" src="https://dshfind.com/api/badge/omdsh-dev/DSH-better-sidebar?lang=zh" /></a><br /><br />
|
||||
<img alt="文件管理" src="https://img.shields.io/badge/-文件管理-4d6bfe" /> <img alt="编辑预览" src="https://img.shields.io/badge/-编辑预览-4d6bfe" /> <img alt="内嵌浏览器" src="https://img.shields.io/badge/-内嵌浏览器-4d6bfe" /> <img alt="真实终端" src="https://img.shields.io/badge/-真实终端-4d6bfe" /> <img alt="Git 面板" src="https://img.shields.io/badge/-Git%20面板-4d6bfe" /> <img alt="后台任务" src="https://img.shields.io/badge/-后台任务-4d6bfe" /> <img alt="插件接入" src="https://img.shields.io/badge/-插件接入-4d6bfe" /><br /><br />
|
||||
<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="支持的 DSH 版本:0.1.0-rc.8 · 0.1.1-rc.1 · 0.1.1-rc.2" src="https://img.shields.io/badge/DSH-0.1.0--rc.8_%C2%B7_0.1.1--rc.1_%C2%B7_0.1.1--rc.2-4d6bfe" /></a>
|
||||
<a href="https://github.com/topics/dsh-better-sidebar"><img alt="插件生态:GitHub topic dsh-better-sidebar" src="https://img.shields.io/badge/%E6%8F%92%E4%BB%B6%E7%94%9F%E6%80%81-topic%20dsh--better--sidebar-4d6bfe" /></a><br /><br />
|
||||
<img alt="文件管理" src="https://img.shields.io/badge/-文件管理-4d6bfe" /> <img alt="编辑预览" src="https://img.shields.io/badge/-编辑预览-4d6bfe" /> <img alt="内嵌浏览器" src="https://img.shields.io/badge/-内嵌浏览器-4d6bfe" /> <img alt="真实终端" src="https://img.shields.io/badge/-真实终端-4d6bfe" /> <img alt="Git 面板" src="https://img.shields.io/badge/-Git%20面板-4d6bfe" /> <img alt="后台任务" src="https://img.shields.io/badge/-后台任务-4d6bfe" /> <img alt="侧边对话" src="https://img.shields.io/badge/-侧边对话-4d6bfe" /> <img alt="插件接入" src="https://img.shields.io/badge/-插件接入-4d6bfe" /><br /><br />
|
||||
<b>右侧栏 + 底部面板双工作台</b>,并把 <code>ctx.betterSidebar</code> 服务开放给所有插件——<br />
|
||||
通过 <code>registerTab</code> / <code>registerFileViewer</code> 注册新的侧边栏页面与文件预览器。
|
||||
</div>
|
||||
@@ -19,9 +25,22 @@
|
||||
<video src="https://github.com/user-attachments/assets/23187822-047e-45cc-b480-fe997bd55b86" muted autoplay loop playsinline controls width="100%"></video>
|
||||
</div>
|
||||
|
||||
## 📑 目录
|
||||
|
||||
- [✨ 功能一览](#-功能一览)
|
||||
- [🚀 安装](#-安装)
|
||||
- [🖼️ 特性巡礼](#-特性巡礼)
|
||||
- [🌐 插件生态](#-插件生态)
|
||||
- [🆕 最近更新](#-最近更新)
|
||||
- [⌨️ 快捷键](#-快捷键)
|
||||
- [🔌 服务化扩展](#-服务化扩展)
|
||||
- [🛠️ 开发与构建](#-开发与构建)
|
||||
- [🔐 安全](#-安全) · [⚠️ 已知限制](#-已知限制) · [🖥️ 平台支持](#-平台支持)
|
||||
- [💬 社区](#-社区) · [🤝 参与贡献](#-参与贡献) · [⭐ Star History](#-star-history) · [🔗 友情链接](#-友情链接)
|
||||
|
||||
## ✨ 功能一览
|
||||
|
||||
- **🗂️ 文件工作台**:资源管理器(懒加载目录树;软链接按目标类型展示——目录软链接可展开、失效链接标红)+ CodeMirror 编辑器;图片 / Markdown(含 Mermaid 图表,strict 安全渲染 + 点击放大)/ HTML / PDF / Office 内联预览
|
||||
- **🗂️ 文件工作台**:资源管理器(懒加载目录树;软链接按目标类型展示——目录软链接可展开、失效链接标红)+ CodeMirror 编辑器;图片 / Markdown(含 Mermaid 图表,strict 安全渲染 + 点击放大)/ HTML / PDF
|
||||
- **🌐 内嵌浏览器**:多开网页 tab,后退 / 前进 / 刷新;内容运行在沙箱 iframe;外链默认按协议分流——HTTP 在侧边栏打开、HTTPS 走系统浏览器(设置页可分别调整)
|
||||
- **💻 真实终端**:xterm.js + node-pty 真实 shell,断线重连回放;可选为模型注入 `terminal_*` 工具
|
||||
- **🌿 Git 面板**:真 diff + VSCode 式 diff tab、历史、右键暂存 / 提交 / 还原
|
||||
@@ -33,103 +52,34 @@
|
||||
- **⚡ 按需加载**:启动只拉 ~325KB 核心,终端 / 编辑器 / Mermaid 图表等重依赖用到才按需拉取([设计文档](docs/plans/2026-08-12-lazy-chunks-design.md))
|
||||
- **🌏 多语言**:界面文案跟随 DSH 语言(zh / en)实时切换
|
||||
|
||||
> 🔌 **核心理念**:服务优先——内置的 7 tab + 6 viewer 与第三方插件通过同一套 `ctx.betterSidebar` API 注册,能力完全对等;官方不再内置、可由生态提供的功能,交由生态插件实现。接入文档见下方「🔌 服务化」与 [外部插件接入指南](./docs/external-plugin-guide.md)。
|
||||
|
||||
## 🆕 最近更新
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/user-attachments/assets/d2aea86b-a776-4f01-a6b8-b26b27314336"><img width="33%" alt="侧边栏" src="https://github.com/user-attachments/assets/d2aea86b-a776-4f01-a6b8-b26b27314336" /></a>
|
||||
<a href="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0"><img width="33%" alt="服务化基座截图" src="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0" /></a>
|
||||
<a href="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e"><img width="33%" alt="添加插件截图" src="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e" /></a>
|
||||
</div>
|
||||
|
||||
### v0.15.0
|
||||
|
||||
> ⚠️ **本版验证基线为 DSH 0.1.1-rc.2(@next)**:devDependencies / lockfile / CI 挂载冒烟基线升至 `0.1.1-rc.2`(lockfile 零 rc.8 残留);**peer 下限保持 `^0.1.0-rc.8`**——0.1.1-rc.x 为常规 rc 递进、与 rc.8 双向兼容,**rc.8 用户无需升级 DSH**。`cordis` 保持 `^4.0.0-rc.8`。自 v0.14.0 以来的全部更改:
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 💬 **侧边对话(beta) Tab**([#286](https://github.com/omdsh-dev/DSH-better-sidebar/pull/286)):Codex 风格的侧边线程,**每个对话一个独立 Tab**——子会话继承主会话完整上下文(已完成回合 + 未回答消息 + 进行中回合的 assistant 输出与工具调用,以「interrupted」冻结标记诚实继承);同组合创建(同 preset / provider / model)复用前缀输入缓存;线程对主会话列表不可见、零子代理目录噪音;线程内可持续追问(重启后自动冷恢复);一键「保存为新会话」提升为顶层会话([设计文档](docs/plans/2026-08-20-sidechat-tab-design.md))
|
||||
- 📤 **文件窗口上传**([#239](https://github.com/omdsh-dev/DSH-better-sidebar/pull/239)):头部「上传文件 / 上传文件夹」按钮 + 拖放上传(拖到树区 = 工作区根,目录行 = 进该目录,文件行 = 进其所在目录,对齐 VSCode);上传时全屏模糊进度弹层(文件级进度 + 取消 / Esc);上传中按钮禁用、成功后文件树自动刷新
|
||||
- 🧩 **桌面兼容四选项**([#284](https://github.com/omdsh-dev/DSH-better-sidebar/pull/284)):位置兼容模式改为**主行下拉**——**自动检测**(默认,保守:仅使用标准的 Window Controls Overlay 几何,32/36px 等各壳差异自动跟随、最大化/还原实时更新,网页环境零修改)/ **DSH官方Web**(显式零适配)/ **壳兼容方案**(内置预设,手动启用;只收录 issue/PR 中出现过且 100+ star 的壳,命中环境带「已检测」提示)/ **自定义方案**(自定义 CSS + 下移距离)。旧版本已有兼容配置的用户自动落到自定义方案;交互控件统一退出桌面拖拽区(`no-drag`);底栏推挤锚点复合选择器双保险(`[data-pane]` 与 `:has(> [data-slot])`)
|
||||
- 🎛️ **设置页 UI/UX 现代化**([#300](https://github.com/omdsh-dev/DSH-better-sidebar/pull/300)):侧边卡片二级设置入口改为卡片底部「功能设置」设置条(替代右下角隐形齿轮,可发现性提升);协调双色启用态(brand 激活强调 + success 绿勾选徽标);全部颜色仍为 `--dsw-alias-*` 令牌派生,皮肤体系自动跟随
|
||||
- ➕ **推荐插件目录新增**:`dsh-docs-panel` 全局文档面板([#230](https://github.com/omdsh-dev/DSH-better-sidebar/pull/230))、`dsh-flowglass`([#261](https://github.com/omdsh-dev/DSH-better-sidebar/pull/261))、`dsh-git-forge` 与 `dsh-ssh-tunnel`([#204](https://github.com/omdsh-dev/DSH-better-sidebar/pull/204))、`dsh-turn-review`([#102](https://github.com/omdsh-dev/DSH-better-sidebar/pull/102))
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- ⚡ **子代理页实时预览批量接口**([#298](https://github.com/omdsh-dev/DSH-better-sidebar/pull/298)):旧实现每个 running 子代理独立轮询 `subagents.history`,host 侧每次触发全量子代理枚举形成 O(N²) 放大、多子代理并发时页面卡顿——改为单个批量接口 `subagents.live`(一次枚举整棵子代理树)+ 客户端单轮询、单在途请求;展示逻辑与文案不变
|
||||
- 🖱️ **拖拽中断 / 快速释放不再回滚**([#249](https://github.com/omdsh-dev/DSH-better-sidebar/pull/249),关闭 [#247](https://github.com/omdsh-dev/DSH-better-sidebar/issues/247) [#248](https://github.com/omdsh-dev/DSH-better-sidebar/issues/248)):中断 / 快速释放提交最后已知位置;HMR 后中心列重定位兜底(修复热更新后底栏空白)
|
||||
- 📐 **推挤变量挂载期持续有效**([#259](https://github.com/omdsh-dev/DSH-better-sidebar/pull/259),修复 [#258](https://github.com/omdsh-dev/DSH-better-sidebar/issues/258)):拖拽松手后底边栏不再闪全宽
|
||||
- 🔧 **适配 DSH 0.1.1-rc.1 / rc.2(@next)**([#297](https://github.com/omdsh-dev/DSH-better-sidebar/pull/297) [#305](https://github.com/omdsh-dev/DSH-better-sidebar/pull/305)):devDependencies / lockfile / CI 验证基线平移;逐包比对所有既有契约(DOM 槽位、CSS 令牌、boot 协议、MarkdownText `codeLabels`、`webServer` 路由注册)零破坏,**无代码逻辑改动**;peer 下限保持 `^0.1.0-rc.8`
|
||||
- 🔒 **上传链路安全加固**([#239](https://github.com/omdsh-dev/DSH-better-sidebar/pull/239)):`relativePath` 空段 / 绝对路径显式拒绝;临时文件唯一命名(并发上传互不干扰、崩溃不阻塞);写流错误监听(磁盘失败不崩溃进程);客户端错误码与服务端统一、413 本地化
|
||||
|
||||
### v0.14.0
|
||||
|
||||
> ⚠️ **本版适配 DSH 0.1.0-rc.8**:全部 `@deepseek-ai/*` peer / devDependencies 升至 `^0.1.0-rc.8`(含传递依赖,lockfile 零 rc.7 残留),`cordis` 同步 `^4.0.0-rc.8`,CI 挂载冒烟钉版 `@deepseek-ai/dsh@0.1.0-rc.8`。**rc.7 及更早的 DSH 环境将无法解析本版依赖,请先升级 DSH。** 自 v0.13.1 以来的全部更改:
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 🖼️ **统一面板宿主注入重构**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):面板/开关簇迁入 `[data-dsh-panel-host]` 固定含块层(`fixed inset-0 z-40`),免疫桌面套壳中间层 transform 对 fixed 含块的劫持;挂载自检(页面级 transform → `data-dsh-panel-host-degraded` 降级同步,按未修正几何判定、祖先变换消失才退出);推挤锚点改 `#root [data-dsh-frame] > [data-pane="conversation"]` + `#root` calc 宽度防桌面壳加性溢出;chunk 激活重验证(HEAD+ETag 保留未变 chunk,5s 超时兜底 fail-open);`visualViewport` 键盘 inset + `env(safe-area-inset-*)` 移动端适配
|
||||
- 📂 **文件打开方式默认独立**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):`editorExplorer` 默认从「合并」改为「独立」——新会话树点击 / 打开文件按路径**新开**文件 tab,无路径窗口即纯资源管理器;合并模式保留为可选手动开启
|
||||
- 🖥️ **终端 shell / shellArgs 设置页可配**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):终端卡齿轮二级页面新增「Shell 路径」「Shell 参数」两行配置(此前只能通过 `cordis.patch.yml` 配置)——设置页写入后对**之后打开的** UI 终端与模型终端(`terminal_create`)即时生效;留空保持 yaml → `$SHELL` / 登录 shell / `powershell.exe` 的既有解析顺序
|
||||
- 🏷️ **设置页版本徽标**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):侧边卡片设置页顶部新增 `DSH-better-sidebar v0.14.0` 身份徽标(版本与服务实例同步,由测试守护)
|
||||
- 🔍 **添加插件目录搜索 / 分组 / 独立滚动**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):为插件生态增长做准备——目录列表顶部加实时搜索(按名称 / id / 描述过滤),条目支持可选 `category` 分组渲染,列表独立滚动(弹窗不再随条目数无限增长)
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 🔧 **适配 DSH 0.1.0-rc.8**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):13 个 `@deepseek-ai/*` peer / devDependencies 升至 `^0.1.0-rc.8`(含传递链,lockfile 零 rc.7 残留),`cordis` 同步 `^4.0.0-rc.8`;移除随 rc.8 消失的 `dsh-client-web-react` / `dsh-client-schema-form`(壳模块表不再提供、插件零引用);CI 挂载冒烟钉版 `@deepseek-ai/dsh@0.1.0-rc.8`;pnpm 11.8 supply-chain 校验适配
|
||||
- 🧩 **rc.8 模块系统迁移**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):rc.8 不再暴露 `window.__DSH_MODULES__` 页面全局(改由 `ctx.modules` 服务提供),懒加载 chunk 的外部依赖解析全面失效——client 注入 `modules` 服务 + 插件自有全局共享给 chunk 副本(终端 / 编辑器 / Mermaid 恢复正常按需加载)
|
||||
- 🧩 **chunk 重验证屏障健壮性**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):HEAD 重验证加 5s 超时兜底(路由挂起时 fail-open 重取,屏障不再可能无限期阻塞懒加载);`resetChunks` 清挂起的重验证屏障
|
||||
- 🖱️ **拖拽健壮性**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):快速释放(浏览器合并 / 丢失 pointermove 突发)时提交最后已知拖动位置而非回退;`pointercancel` / 捕获丢失中断同样保留拖动结果;提交后立即重测中心列(消除底栏宽度中间帧抖动);HMR 重激活后中心列重定位兜底(`<html>` 样式观察 + 底栏打开重测),修复热更新后底栏空白 / 输入框位移
|
||||
|
||||
### v0.13.1
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 📊 **Markdown 预览安全渲染 Mermaid 图表**([#164](https://github.com/omdsh-dev/DSH-better-sidebar/pull/164)):预览的 md 含 mermaid fence 时按需下发 `client-mermaid.js` chunk(~7MB,无 mermaid 文件零加载);纵深防御渲染——`securityLevel: 'strict'` + `htmlLabels: false`(节点文字走真实 SVG `<text>`)+ SVG 注入前二次清洗(删 `foreignObject`/`script`/外来 HTML 元素、剥 `@*`/`on*`/`href` 属性);点击图表在弹窗中放大(滚轮以鼠标为中心缩放、拖拽平移、工具栏与快捷键),深浅色跟随重渲、解析失败回退原码
|
||||
- 🖥️ **终端 shell 与 shellArgs 可配置**([#125](https://github.com/omdsh-dev/DSH-better-sidebar/pull/125)):`cordis.patch.yml` 的 `better-sidebar.config` 可指定 `shell` / `shellArgs`(`shellArgs` 非空时完全替换默认参数;未配置维持自动解析 `$SHELL` / 登录 shell / `powershell.exe` 原行为),UI 终端与 agent 终端(`terminal_create`)同时生效;终端 tab 标题改用 shell 名(bash / zsh / powershell),内部标识改 UUID,同 shell 可开多个终端
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 🔗 **聚合双挂载自动退让**([#200](https://github.com/omdsh-dev/DSH-better-sidebar/pull/200)):聚合包(如 dsh-web-ui-all)以独立条目 id 挂载同包时,`cordis.patch.yml` 的守卫表达式自动禁用自身 `better-sidebar` 行,不再重复注册 `/sidebar/api` 导致 `duplicate prefix route` 整个插件树启动失败(`dsh web` 崩溃);独立安装行为不变
|
||||
- 🔧 **peer 依赖对齐 DSH 0.1.0-rc.7**([#207](https://github.com/omdsh-dev/DSH-better-sidebar/pull/207),修复 [#206](https://github.com/omdsh-dev/DSH-better-sidebar/issues/206)):全部 `@deepseek-ai/*` peer / devDependencies 从 `^0.1.0-rc.6` 升至 `^0.1.0-rc.7`,CI 挂载冒烟同步钉版——消除主框架升至 rc.7 后 rc.6 / rc.7 混用依赖树导致的 `agent-presets: refusing to compose an unscoped context`(选模型 / 发消息报错)
|
||||
|
||||
### v0.13.0
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 📁 **文件窗口与资源管理器二合一**([#151](https://github.com/omdsh-dev/DSH-better-sidebar/pull/151)):新 `editorExplorer` 设置(编辑器卡齿轮)——文件 tab 增加路径输入框头部 + 可开关的右侧停靠文件树(每 tab 记忆展开/宽度,左缘拖拽调宽 160~480px,全局文件名搜索走 host `fs.search` 路由,预算封顶并跳过 `.git` / 符号链接目录);独立模式(默认)树点击 / 输入框 Enter **按路径新开**文件 tab,合并模式**原地切换**当前 tab;新会话默认 seed 空文件窗口(`Files`)替代 explorer tab,无路径窗口在独立模式为纯资源管理器、合并模式为带 chrome 的空文件窗口;树右键提供「在新 Tab 中打开」「在侧边打开」(split)
|
||||
- 🎛️ **声明式设置 select 行**([#151](https://github.com/omdsh-dev/DSH-better-sidebar/pull/151)):设置项新增 `type: 'select'`(`options` 支持 value/title/desc/icon,`multi` 多选存数组);带图标的选项渲染大图标选项卡、收起态同样显示图标;`editorExplorer` 改为图标化下拉(合并 / 独立);能力清单新增 `settingSelect`
|
||||
- 🔀 **与 dsh-web-ui 家族右侧面板互斥**([#181](https://github.com/omdsh-dev/DSH-better-sidebar/pull/181)):读取 `aionui-panel` 设置命名空间的提供方选择——当选择「使用 aionui-panel」时,整个 better-sidebar(右侧栏 / 底部面板 / 浮动入口 / 各类接管)不再挂载;选择 DSH-better-sidebar(或未安装 aionui)时正常。设置页保存后实时生效(settings-document 推送),无需刷新
|
||||
|
||||
### v0.12.3
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 🎨 **皮肤兼容(令牌驱动)**:全面消费 DSH 设计令牌,与 dsh-web-ui 皮肤中心 10 款皮肤兼容,换肤自动跟随;终端/编辑器表面在透明/半透明玻璃值下回退不透明底色,文字不叠在皮肤背景上([#110](https://github.com/omdsh-dev/DSH-better-sidebar/pull/110),修复 #106 #105 #90 #60,附带 #52 #57 #92)
|
||||
- 🗂️ **统一路径处理**:UNC 路径 / 软链接分类(目录软链接可展开、失效链接标红)、HTML 路由平台守卫([#134](https://github.com/omdsh-dev/DSH-better-sidebar/pull/134),#65 #67 #43 #79 #115)
|
||||
- 🖥️ **终端 shell 可配置**:设置项自定义 shell,Windows 自动探测 pwsh([#95](https://github.com/omdsh-dev/DSH-better-sidebar/pull/95))
|
||||
- 📝 **编辑器新增语言**:C# / Kotlin / Swift 语法高亮([#120](https://github.com/omdsh-dev/DSH-better-sidebar/pull/120))
|
||||
- 🧭 **设置页导航图标**:设置页导航图标与布局优化([#114](https://github.com/omdsh-dev/DSH-better-sidebar/pull/114))
|
||||
- ➕ **推荐插件目录新增**:`dsh-git-remotes`——Git 远程 Tab(分支/上游/ahead-behind、fetch 可 prune、ff-only pull、确认后才 push,不替换内置暂存/提交)([#91](https://github.com/omdsh-dev/DSH-better-sidebar/pull/91));`dsh-video-preview`——视频内联预览(.mp4/.webm/.mov/.mkv/.avi 等,自带 /video 宿主路由支持 HTTP Range 206 拖进度条,不受 20MB mediaLimit 限制)([#126](https://github.com/omdsh-dev/DSH-better-sidebar/pull/126))
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 🔧 **xterm 依赖迁移**:弃用的 xterm 迁移至 `@xterm/xterm`(Closes [#122](https://github.com/omdsh-dev/DSH-better-sidebar/issues/122),[#128](https://github.com/omdsh-dev/DSH-better-sidebar/pull/128))
|
||||
- 📝 **Markdown 编辑器**:选区转对话弹窗恢复可用([#24](https://github.com/omdsh-dev/DSH-better-sidebar/pull/24))
|
||||
- 🐛 **node-pty 加载失败不再拖垮 server**([#140](https://github.com/omdsh-dev/DSH-better-sidebar/issues/140)):宿主半改为懒加载 node-pty,缺失时插件照常挂载,终端以修复提示横幅(可复制命令 + 重试按钮)呈现,agent 终端工具自动跳过
|
||||
- 🧪 测试工程:单元测试拆分(#141)+ smoke 偶发失败修复
|
||||
> 🔌 **核心理念**:服务优先——内置的 7 tab + 6 viewer 与第三方插件通过同一套 `ctx.betterSidebar` API 注册,能力完全对等;官方不再内置、可由生态提供的功能,交由生态插件实现(已有 **28+ 生态插件**,见下方「🌐 插件生态」)。接入文档见「🔌 服务化扩展」与 [外部插件接入指南](./docs/external-plugin-guide.md)。
|
||||
|
||||
## 🚀 安装
|
||||
|
||||
**前置**:已装好 DSH(`dsh web` 能正常运行),Node.js ≥ 20、pnpm ≥ 10。
|
||||
|
||||
**支持的 DSH 版本**:
|
||||
<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="支持的 DSH 版本:0.1.0-rc.8 · 0.1.1-rc.1 · 0.1.1-rc.2" src="https://img.shields.io/badge/DSH-0.1.0--rc.8_%C2%B7_0.1.1--rc.1_%C2%B7_0.1.1--rc.2-4d6bfe" /></a>
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web add dsh-better-sidebar@latest
|
||||
dsh plugin --profile web add dsh-better-sidebar@latest # 首次会因 pnpm 11 拦截 node-pty 构建脚本而失败(依赖已写入)
|
||||
cd ~/.dsh/profiles/web && pnpm approve-builds --all # 放行构建脚本(自动重跑安装)
|
||||
dsh plugin --profile web add dsh-better-sidebar@latest # 重跑即成功
|
||||
```
|
||||
|
||||
装完**硬刷新浏览器**(Cmd/Ctrl+Shift+R)即可看到侧边栏(DSH 对 client 改动热加载,无需重启;仅 host 半更新时需要重启)。
|
||||
|
||||
**方式二:让 DSH 自己装**——把下面这段提示词发给任意一个 DSH 会话:
|
||||
|
||||
```text
|
||||
帮我安装 dsh-better-sidebar 插件(DSH 侧边栏工作台),步骤:
|
||||
1. 执行 dsh plugin --profile web add dsh-better-sidebar@latest(首次会被 pnpm 11 拦截 node-pty 构建脚本而失败,属正常)
|
||||
2. 在 ~/.dsh/profiles/web 下执行 pnpm approve-builds --all(放行构建脚本,会自动重跑安装)
|
||||
3. 再次执行 dsh plugin --profile web add dsh-better-sidebar@latest
|
||||
4. 完成后提醒我硬刷新浏览器(Cmd/Ctrl+Shift+R)
|
||||
遇到报错先查 https://github.com/omdsh-dev/DSH-better-sidebar README 的常见问题表。
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><b>更新</b></summary>
|
||||
|
||||
@@ -178,7 +128,7 @@ dsh plugin --profile web add dsh-better-sidebar@latest
|
||||
5. 硬刷新浏览器(Cmd/Ctrl+Shift+R)即可看到效果(client 改动无需重启 DSH;host 半改动才需重启)
|
||||
```
|
||||
|
||||
更新:`git pull && pnpm install && pnpm build` → 硬刷新浏览器即可(client 改动热加载生效,无需重启 DSH;host 半改动才需重启)。切回 npm 通道时,把依赖改回 `"dsh-better-sidebar": "^0.13.0"` 再 `pnpm install`。
|
||||
更新:`git pull && pnpm install && pnpm build` → 硬刷新浏览器即可(client 改动热加载生效,无需重启 DSH;host 半改动才需重启)。切回 npm 通道时,把依赖改回 `"dsh-better-sidebar": "^0.15.0"` 再 `pnpm install`。
|
||||
|
||||
</details>
|
||||
|
||||
@@ -199,6 +149,263 @@ dsh registry enable dsh-external/dsh-better-sidebar
|
||||
|
||||
</details>
|
||||
|
||||
## 🖼️ 特性巡礼
|
||||
|
||||
> 以下均为真实界面实拍。
|
||||
|
||||
### 🗂️ 文件工作台:资源管理器
|
||||
|
||||
支持两种格式的资源管理器:内嵌在文件预览中 / 独立显示文件树。懒加载目录树、软链接按目标类型展示(目录软链接可展开、失效链接标红)、全局文件名搜索、上传文件/文件夹与拖放上传、右键菜单(在新 Tab 打开 / 在侧边打开 / 复制路径)、悬浮 `@文件` 一键引用进输入框。
|
||||
|
||||
<div align="center"><img width="880" alt="文件资源管理器" src="https://github.com/user-attachments/assets/a410bfd2-a8ba-43e6-873e-22417756e94d" /></div>
|
||||
<div align="center"><img width="880" alt="CodeMirror 代码编辑器" src="https://github.com/user-attachments/assets/b44b488e-568c-4ee0-b96c-e9c906598a77" /></div>
|
||||
|
||||
### 📝 Markdown · 图片 · PDF 内联预览
|
||||
|
||||
Markdown 预览支持 **Mermaid 图表**(`securityLevel: 'strict'` 安全渲染 + 二次清洗;点击图表弹窗放大、滚轮缩放、拖拽平移);图片 / PDF 走媒体路由内联展示;Office 三件套由生态插件补齐。
|
||||
|
||||
<div align="center"><img width="880" alt="Markdown + Mermaid 预览" src="https://github.com/user-attachments/assets/fe0e5182-55bb-45cc-b98b-a2877c2bdd38" /></div>
|
||||
<div align="center"><img width="880" alt="图片内联预览" src="https://github.com/user-attachments/assets/f9a58c30-5b7a-48b5-9e22-37d7e071f593" /></div>
|
||||
|
||||
### 💻 真实终端
|
||||
|
||||
xterm.js + node-pty 真实 shell(不是模拟器):断线重连 transcript 回放、shell / shellArgs 可配置(设置页或 `cordis.patch.yml`)、可选为模型注入 `terminal_*` 工具(agent 可直接开终端跑命令)。
|
||||
|
||||
<div align="center"><img width="880" alt="真实终端" src="https://github.com/user-attachments/assets/0dad6ad3-ff3f-4b5a-86d2-f832ce65323e" /></div>
|
||||
|
||||
### 🌿 Git 面板
|
||||
|
||||
暂存 / 取消暂存 / 提交(`Ctrl+Enter`)/ 还原,历史列表;点击改动文件打开 **VSCode 式 diff tab**(红绿行级对比)。
|
||||
|
||||
<div align="center"><img width="880" alt="Git 面板" src="https://github.com/user-attachments/assets/e7fc1220-305f-4bca-8583-e77ab4f4fa78" /></div>
|
||||
|
||||
### 🌐 内嵌浏览器
|
||||
|
||||
多开网页 tab:后退 / 前进 / 刷新 / 地址栏;内容运行在**不透明源沙箱 iframe**(界面实时显示沙箱状态,可按页面临时解锁);聊天里的外链点击可被接管到侧边栏打开(按协议分流,可配)。
|
||||
|
||||
<div align="center"><img width="880" alt="内嵌浏览器" src="https://github.com/user-attachments/assets/9bc6b65a-64fc-4942-a685-76e391e55606" /></div>
|
||||
|
||||
### 🧩 任务页:子代理拓扑 + 后台任务
|
||||
|
||||
子代理树实时拓扑(运行状态、批量实时预览)+ 后台任务清单(退出码 / 实时输出 / 强制终止);新子代理 / 新任务可自动展开侧边栏(可关)。
|
||||
|
||||
<div align="center"><img width="880" alt="任务页:子代理拓扑" src="https://github.com/user-attachments/assets/dcd8ed2f-59fa-405b-937b-2d250f5034dd" /></div>
|
||||
|
||||
### 💬 侧边对话(beta)
|
||||
|
||||
Codex 风格侧边线程:**每个对话一个独立 Tab**;线程继承主会话完整上下文(含进行中回合,以 interrupted 诚实冻结)独立运行,不污染主会话;可持续追问、重启冷恢复;一键「保存为新会话」提升为顶层会话。
|
||||
|
||||
<div align="center"><img width="880" alt="侧边对话(beta)" src="https://github.com/user-attachments/assets/3a338c36-f5de-4000-95f3-4b1cd04f60fc" /></div>
|
||||
|
||||
### 🪟 双工作台:右侧栏 + 底部面板 + 分栏
|
||||
|
||||
右侧栏与底部面板可同时展开;拖 Tab 到分栏边缘**拆分**、拖到中间**合并**(可跨面板);面板宽高左缘/上缘拖拽调节;移动端自动合并为全宽抽屉。
|
||||
|
||||
<div align="center"><img width="880" alt="双工作台(右侧栏 + 底部面板)" src="https://github.com/user-attachments/assets/dfdb875e-a1a8-4d4b-8340-353736b1708f" /></div>
|
||||
|
||||
### ⚙️ 声明式设置
|
||||
|
||||
设置页「侧边卡片」分区:每个 tab / 预览器一张小卡片,独立开关(高亮启用态 + 品牌开关滑块);二级设置经卡片底部「功能设置」条弹窗(开关 / 文本 / 数字 / 下拉);插件自有设置持久化在 `pluginSettings`。
|
||||
|
||||
<div align="center"><img width="880" alt="声明式设置:侧边卡片" src="https://github.com/user-attachments/assets/0800ca64-621e-48da-b7df-aecfddc3ec29" /></div>
|
||||
|
||||
### 📱 移动端
|
||||
|
||||
窄屏(<768px)自动切换为全宽抽屉:底栏 tab 一次性并入右侧栏,触屏拖拽可调。
|
||||
|
||||
<div align="center"><img width="360" alt="移动端全宽抽屉" src="https://github.com/user-attachments/assets/a82ba78a-f4cf-4d85-80e8-050a05beb144" /></div>
|
||||
|
||||
## 🌐 插件生态
|
||||
|
||||
`ctx.betterSidebar` 服务向所有插件开放两个扩展点:**`registerTab`(注册侧边栏页面)** 与 **`registerFileViewer`(注册文件预览器)**。内置的 7 tab + 6 viewer 与第三方插件走同一套 API,能力完全对等。
|
||||
|
||||
```ts
|
||||
import type {} from 'dsh-better-sidebar' // 触发 ctx.betterSidebar 类型合并
|
||||
export const inject = ['betterSidebar']
|
||||
export function apply(ctx: Context) {
|
||||
ctx.effect(() => ctx.betterSidebar.registerTab({
|
||||
id: 'my-plugin:db', title: 'Database', component: ({ scope }) => <DbView sessionId={scope.sessionId} />,
|
||||
}))
|
||||
ctx.effect(() => ctx.betterSidebar.registerFileViewer({
|
||||
id: 'my-plugin:csv', exts: ['csv'], fetchStrategy: 'custom',
|
||||
load: async (path, scope) => parseCsv(await fetchText(scope, path)),
|
||||
component: ({ customData }) => <CsvGrid rows={customData} />,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
GitHub topic [`dsh-better-sidebar`](https://github.com/topics/dsh-better-sidebar) 下已有 **28+ 生态插件**(持续增长中):
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e"><img width="66%" alt="设置页「添加插件」弹窗:推荐插件目录 + 一键复制安装命令" src="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e" /></a><br />
|
||||
<i>设置页「侧边卡片」内置「添加插件」弹窗:推荐目录 + 一键复制安装命令 + 直达 GitHub topic</i>
|
||||
</div>
|
||||
|
||||
### 📑 Tab 插件(注册侧边栏页面)
|
||||
|
||||
<details>
|
||||
<summary><b>23 个插件(点击展开)</b></summary>
|
||||
|
||||
| 插件 | ⭐ | 简介 |
|
||||
|---|---|---|
|
||||
| [ChenRuoT/dsh-sidebar-qa](https://github.com/ChenRuoT/dsh-sidebar-qa) | <img alt="stars" src="https://img.shields.io/github/stars/ChenRuoT/dsh-sidebar-qa?style=flat&color=4d6bfe" /> | 划选追问侧边页:类 Codex 侧边提问 / Claude Code `/btw` |
|
||||
| [fuhefei/dsh-sentinel](https://github.com/fuhefei/dsh-sentinel) | <img alt="stars" src="https://img.shields.io/github/stars/fuhefei/dsh-sentinel?style=flat&color=4d6bfe" /> | 条件驱动唤醒系统:文件 / 命令 / HTTP / 进程 / Webhook 监视,到点唤醒 agent;dock + 侧栏分支 + 全局仪表盘 |
|
||||
| [jiuge2467/dsh-studio](https://github.com/jiuge2467/dsh-studio) | <img alt="stars" src="https://img.shields.io/github/stars/jiuge2467/dsh-studio?style=flat&color=4d6bfe" /> | 全栈增强工作台:多源 MCP 可视化调试中枢、视觉思考引擎 |
|
||||
| [Iwctwbh/dsh-flowglass](https://github.com/Iwctwbh/dsh-flowglass) | <img alt="stars" src="https://img.shields.io/github/stars/Iwctwbh/dsh-flowglass?style=flat&color=4d6bfe" /> | 流镜 Flowglass:会话流程图实时可视化(消息 / 工具组 / 子代理分支) |
|
||||
| [FeatherHunter/dsh-mattpocock-skills-deck](https://github.com/FeatherHunter/dsh-mattpocock-skills-deck) | <img alt="stars" src="https://img.shields.io/github/stars/FeatherHunter/dsh-mattpocock-skills-deck?style=flat&color=4d6bfe" /> | mattpocock/skills 游戏化任务系统:地图拨迷雾、任务栏推进 |
|
||||
| [GULI-lab/DSH-element-source](https://github.com/GULI-lab/DSH-element-source) | <img alt="stars" src="https://img.shields.io/github/stars/GULI-lab/DSH-element-source?style=flat&color=4d6bfe" /> | 点击页面任意 UI 元素直达 Vue / React / Svelte / Angular 源码并送入会话 |
|
||||
| [Lzh3070/dsh-file-review-tab](https://github.com/Lzh3070/dsh-file-review-tab) | <img alt="stars" src="https://img.shields.io/github/stars/Lzh3070/dsh-file-review-tab?style=flat&color=4d6bfe" /> | 文件改动审查页:行级红绿 diff + 撤销 + chat 行深链 |
|
||||
| [yq04/dsh-git-remotes](https://github.com/yq04/dsh-git-remotes) | <img alt="stars" src="https://img.shields.io/github/stars/yq04/dsh-git-remotes?style=flat&color=4d6bfe" /> | Git 远程页:分支 / 上游 / ahead-behind,fetch 可 prune、ff-only pull、确认后 push |
|
||||
| [ztyhehe/dsh-better-sidebar-svn](https://github.com/ztyhehe/dsh-better-sidebar-svn) | <img alt="stars" src="https://img.shields.io/github/stars/ztyhehe/dsh-better-sidebar-svn?style=flat&color=4d6bfe" /> | SVN 源码管理页:status / diff / log / commit / update / revert / 冲突解决,与内置 Git 面板对称 |
|
||||
| [Melody-max114/dsh-excel-panel](https://github.com/Melody-max114/dsh-excel-panel) | <img alt="stars" src="https://img.shields.io/github/stars/Melody-max114/dsh-excel-panel?style=flat&color=4d6bfe" /> | Excel 编辑页:xlsx 预览 / 编辑、公式实时计算、合并单元格、保存回原文件 |
|
||||
| [v587d/dsh-anysearch-refs](https://github.com/v587d/dsh-anysearch-refs) | <img alt="stars" src="https://img.shields.io/github/stars/v587d/dsh-anysearch-refs?style=flat&color=4d6bfe" /> | AnySearch 搜索结果引用卡片:搜索词、来源摘要、关键词高亮 |
|
||||
| [mlosun/dsh-docs-panel](https://github.com/mlosun/dsh-docs-panel) | <img alt="stars" src="https://img.shields.io/github/stars/mlosun/dsh-docs-panel?style=flat&color=4d6bfe" /> | 全局文档面板:随身 Markdown 笔记,任何工作区随时可读 |
|
||||
| [lnyuqian/dsh-skill-sidebar](https://github.com/lnyuqian/dsh-skill-sidebar) | <img alt="stars" src="https://img.shields.io/github/stars/lnyuqian/dsh-skill-sidebar?style=flat&color=4d6bfe" /> | 技能面板:扫描本机技能目录,4-6 字功能短语 + 一键复制调用 + 置顶 |
|
||||
| [g-yixuan/dsh-sidechat](https://github.com/g-yixuan/dsh-sidechat) | <img alt="stars" src="https://img.shields.io/github/stars/g-yixuan/dsh-sidechat?style=flat&color=4d6bfe" /> | Codex 风格侧边对话 + 划选引用注释(轻量消费插件) |
|
||||
| [thirsty5034/dsh-ssh-tunnel](https://github.com/thirsty5034/dsh-ssh-tunnel) | <img alt="stars" src="https://img.shields.io/github/stars/thirsty5034/dsh-ssh-tunnel?style=flat&color=4d6bfe" /> | 多主机 SSH 隧道 + SSH 管理器页 |
|
||||
| [thirsty5034/dsh-git-forge](https://github.com/thirsty5034/dsh-git-forge) | <img alt="stars" src="https://img.shields.io/github/stars/thirsty5034/dsh-git-forge?style=flat&color=4d6bfe" /> | GitHub / Gitea 账号、项目授权与推送策略 |
|
||||
| [YesSanSan/dsh-conversation-outline](https://github.com/YesSanSan/dsh-conversation-outline) | <img alt="stars" src="https://img.shields.io/github/stars/YesSanSan/dsh-conversation-outline?style=flat&color=4d6bfe" /> | 对话大纲页:按轮次结构化展示、一键跳转、LLM 一句话标题 |
|
||||
| [Wulabalabo/dsh-sidebar-Explorer-Plus](https://github.com/Wulabalabo/dsh-sidebar-Explorer-Plus) | <img alt="stars" src="https://img.shields.io/github/stars/Wulabalabo/dsh-sidebar-Explorer-Plus?style=flat&color=4d6bfe" /> | 文件管理页:上传 / 移动 / 删除 / 重命名 / 新建文件夹(补全写操作) |
|
||||
| [yq04/dsh-turn-review](https://github.com/yq04/dsh-turn-review) | <img alt="stars" src="https://img.shields.io/github/stars/yq04/dsh-turn-review?style=flat&color=4d6bfe" /> | 本轮审查:逐回合审查 agent 改动 |
|
||||
| [Ghz114514/dsh-refpics](https://github.com/Ghz114514/dsh-refpics) | <img alt="stars" src="https://img.shields.io/github/stars/Ghz114514/dsh-refpics?style=flat&color=4d6bfe" /> | Pinterest 风格参考图搜索:瀑布流、侧栏画板、下载与 Eagle 收藏 |
|
||||
| [yzlin499/dsh-yzlin499-easy-plugins](https://github.com/yzlin499/dsh-yzlin499-easy-plugins) | <img alt="stars" src="https://img.shields.io/github/stars/yzlin499/dsh-yzlin499-easy-plugins?style=flat&color=4d6bfe" /> | 实用小工具集(毛坯房 DSH 友好) |
|
||||
| [dong-victor/dsh-better-sidebar-starter](https://github.com/dong-victor/dsh-better-sidebar-starter) | <img alt="stars" src="https://img.shields.io/github/stars/dong-victor/dsh-better-sidebar-starter?style=flat&color=4d6bfe" /> | 运行配置页:IDEA 式 Run/Debug 配置(npm / springboot / python / custom)——一键启动、历史保存、WebSocket 实时日志(ANSI 彩色)、多实例并行、进程树跨平台杀死 |
|
||||
| [baosfeng/my-dsh-plugins](https://github.com/baosfeng/my-dsh-plugins) | <img alt="stars" src="https://img.shields.io/github/stars/baosfeng/my-dsh-plugins?style=flat&color=4d6bfe" /> | 个人多插件合集(`dsh-file-activity`):侧边栏文件活动页——记录文件读取 / 新增 / 修改历史与统计,按文件夹平铺,点击用原生预览打开 |
|
||||
|
||||
</details>
|
||||
|
||||
### 🖼️ 预览插件(注册文件预览器)
|
||||
|
||||
<details>
|
||||
<summary><b>3 个插件(点击展开)</b></summary>
|
||||
|
||||
| 插件 | ⭐ | 简介 |
|
||||
|---|---|---|
|
||||
| [HuanLinOTO/dsh-plugin-better-sidebar-plugin-office](https://github.com/HuanLinOTO/dsh-plugin-better-sidebar-plugin-office) | <img alt="stars" src="https://img.shields.io/github/stars/HuanLinOTO/dsh-plugin-better-sidebar-plugin-office?style=flat&color=4d6bfe" /> | Office 三件套预览(.docx / .xlsx / .pptx),独立 bundle 瘦身主体(官方推荐目录收录) |
|
||||
| [zemul/dsh-video-preview](https://github.com/zemul/dsh-video-preview) | <img alt="stars" src="https://img.shields.io/github/stars/zemul/dsh-video-preview?style=flat&color=4d6bfe" /> | 视频内联预览:.mp4 / .webm / .mov / .mkv / .avi,自带 /video 路由支持 HTTP Range 拖进度条 |
|
||||
| [dong-victor/dsh-better-sidebar-jupyter](https://github.com/dong-victor/dsh-better-sidebar-jupyter) | <img alt="stars" src="https://img.shields.io/github/stars/dong-victor/dsh-better-sidebar-jupyter?style=flat&color=4d6bfe" /> | `.ipynb` 可运行 Notebook 视图:懒启动 Python kernel、流式输出、保存回写 |
|
||||
|
||||
</details>
|
||||
|
||||
### 🧰 增强与工具
|
||||
|
||||
<details>
|
||||
<summary><b>2 个插件(点击展开)</b></summary>
|
||||
|
||||
| 插件 | ⭐ | 简介 |
|
||||
|---|---|---|
|
||||
| [dong-victor/dsh-better-sidebar-terminal-plus](https://github.com/dong-victor/dsh-better-sidebar-terminal-plus) | <img alt="stars" src="https://img.shields.io/github/stars/dong-victor/dsh-better-sidebar-terminal-plus?style=flat&color=4d6bfe" /> | 终端增强:内嵌 Nerd Font 图标字体、修复 xterm 图标渲染、稳定终端 cwd |
|
||||
| [Max-Null/dsh-sidebar-preview-select](https://github.com/Max-Null/dsh-sidebar-preview-select) | <img alt="stars" src="https://img.shields.io/github/stars/Max-Null/dsh-sidebar-preview-select?style=flat&color=4d6bfe" /> | 预览划选增强:侧边栏预览里划选文本 → 浮动「发送到会话」 |
|
||||
|
||||
</details>
|
||||
|
||||
> 📣 **上架你的插件**:给仓库打上 `dsh-better-sidebar` topic 即出现在 [topic 页](https://github.com/topics/dsh-better-sidebar);再向 [`src/client/plugins-tabs.ts`](./src/client/plugins-tabs.ts) / [`src/client/plugins-viewers.ts`](./src/client/plugins-viewers.ts) 提一条 `PluginEntry` PR,即可进入设置页内置推荐目录(数据完整性由 `tests/plugin-list.spec.ts` 守护)。
|
||||
|
||||
## 🆕 最近更新
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/user-attachments/assets/d2aea86b-a776-4f01-a6b8-b26b27314336"><img width="33%" alt="侧边栏" src="https://github.com/user-attachments/assets/d2aea86b-a776-4f01-a6b8-b26b27314336" /></a>
|
||||
<a href="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0"><img width="33%" alt="服务化基座截图" src="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0" /></a>
|
||||
</div>
|
||||
|
||||
**支持的 DSH 版本**:<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="支持的 DSH 版本:0.1.0-rc.8 · 0.1.1-rc.1 · 0.1.1-rc.2" src="https://img.shields.io/badge/DSH-0.1.0--rc.8_%C2%B7_0.1.1--rc.1_%C2%B7_0.1.1--rc.2-4d6bfe" /></a> · 完整发布历史见 [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases)
|
||||
|
||||
### v0.15.1
|
||||
|
||||
自 v0.15.0 以来的全部更改:
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 💬 **侧边对话 Codex 风格转录重构**([#314](https://github.com/omdsh-dev/DSH-better-sidebar/pull/314)):转录改为**折叠行**——工具调用 / 思考 / 上下文注入统一为安静的单行 chrome(chevron + 标签 + 单行参数摘要,展开为 hairline 缩进正文,无卡片无填充),流式标签与创建 shimmer(shimmer = 生成中)、失败工具 danger、`prefers-reduced-motion` 停帧;**首条问题不再被边界提示吞掉**——上下文注入与首问拆分交付(边界 + 快照经 `agent.inject` 排队、问题唤醒驱动),转录把注入映射为可折叠注入行、真实用户消息(**含首问**)渲染为用户气泡,旧线程的首问同样拆分为独立气泡
|
||||
- 📖 **README 重写**:功能导览(逐特性实机截图)、用户视角 DSH 兼容徽章、简化安装流程(`add` → `approve-builds` → `add`、node-pty 安全构建、粘贴到 DSH 安装提示)、插件生态 28+ 与分类折叠展示
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 🖥️ **终端跨会话切换保活**([#323](https://github.com/omdsh-dev/DSH-better-sidebar/pull/323)):切到其他会话不再被当作瞬时掉线——客户端卸载时发送 `park` 控制帧,主机跳过 30s 重连宽限倒计时;切回会话(`open()` 取消 parked)或显式关闭恢复正常生命周期;agent 终端保持无限期存活
|
||||
- 📂 **文件树上传遮罩不再拦截 Tab 拖拽**([#317](https://github.com/omdsh-dev/DSH-better-sidebar/pull/317)):拖拽 Tab(重排 / 跨 pane split)经过资源管理器时不再弹上传遮罩、不吞事件——统一按 `dataTransfer.types` 含 `Files` 门控(与面板宿主 shield 一致),Tab 正常落下;OS 文件拖拽行为不变
|
||||
- 💬 **子代理自动展开去抖**([#314](https://github.com/omdsh-dev/DSH-better-sidebar/pull/314)):Side Chat 线程创建不再误弹任务页——0→N 触发 500ms 重臂并对实时快照按原基线重评估,标题过滤器识别线程后才放行;真实子代理依然自动展开
|
||||
|
||||
### v0.15.0
|
||||
|
||||
自 v0.14.0 以来的全部更改:
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 💬 **侧边对话(beta) Tab**([#286](https://github.com/omdsh-dev/DSH-better-sidebar/pull/286)):Codex 风格的侧边线程,**每个对话一个独立 Tab**——子会话继承主会话完整上下文(已完成回合 + 未回答消息 + 进行中回合的 assistant 输出与工具调用,以「interrupted」冻结标记诚实继承);同组合创建(同 preset / provider / model)复用前缀输入缓存;线程对主会话列表不可见、零子代理目录噪音;线程内可持续追问(重启后自动冷恢复);一键「保存为新会话」提升为顶层会话([设计文档](docs/plans/2026-08-20-sidechat-tab-design.md))
|
||||
- 📤 **文件窗口上传**([#239](https://github.com/omdsh-dev/DSH-better-sidebar/pull/239)):头部「上传文件 / 上传文件夹」按钮 + 拖放上传(拖到树区 = 工作区根,目录行 = 进该目录,文件行 = 进其所在目录,对齐 VSCode);上传时全屏模糊进度弹层(文件级进度 + 取消 / Esc);上传中按钮禁用、成功后文件树自动刷新
|
||||
- 🧩 **桌面兼容四选项**([#284](https://github.com/omdsh-dev/DSH-better-sidebar/pull/284)):位置兼容模式改为**主行下拉**——**自动检测**(默认,保守:仅使用标准的 Window Controls Overlay 几何,32/36px 等各壳差异自动跟随、最大化/还原实时更新,网页环境零修改)/ **DSH官方Web**(显式零适配)/ **壳兼容方案**(内置预设,手动启用;只收录 issue/PR 中出现过且 100+ star 的壳,命中环境带「已检测」提示)/ **自定义方案**(自定义 CSS + 下移距离)。旧版本已有兼容配置的用户自动落到自定义方案;交互控件统一退出桌面拖拽区(`no-drag`);底栏推挤锚点复合选择器双保险(`[data-pane]` 与 `:has(> [data-slot])`)
|
||||
- 🎛️ **设置页 UI/UX 现代化**([#300](https://github.com/omdsh-dev/DSH-better-sidebar/pull/300)):侧边卡片二级设置入口改为卡片底部「功能设置」设置条(替代右下角隐形齿轮,可发现性提升);协调双色启用态(brand 激活强调 + success 绿勾选徽标);全部颜色仍为 `--dsw-alias-*` 令牌派生,皮肤体系自动跟随
|
||||
- ➕ **推荐插件目录新增**:`dsh-docs-panel` 全局文档面板([#230](https://github.com/omdsh-dev/DSH-better-sidebar/pull/230))、`dsh-flowglass`([#261](https://github.com/omdsh-dev/DSH-better-sidebar/pull/261))、`dsh-git-forge` 与 `dsh-ssh-tunnel`([#204](https://github.com/omdsh-dev/DSH-better-sidebar/pull/204))、`dsh-turn-review`([#102](https://github.com/omdsh-dev/DSH-better-sidebar/pull/102))
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- ⚡ **子代理页实时预览批量接口**([#298](https://github.com/omdsh-dev/DSH-better-sidebar/pull/298)):旧实现每个 running 子代理独立轮询 `subagents.history`,host 侧每次触发全量子代理枚举形成 O(N²) 放大、多子代理并发时页面卡顿——改为单个批量接口 `subagents.live`(一次枚举整棵子代理树)+ 客户端单轮询、单在途请求;展示逻辑与文案不变
|
||||
- 🖱️ **拖拽中断 / 快速释放不再回滚**([#249](https://github.com/omdsh-dev/DSH-better-sidebar/pull/249),关闭 [#247](https://github.com/omdsh-dev/DSH-better-sidebar/issues/247) [#248](https://github.com/omdsh-dev/DSH-better-sidebar/issues/248)):中断 / 快速释放提交最后已知位置;HMR 后中心列重定位兜底(修复热更新后底栏空白)
|
||||
- 📐 **推挤变量挂载期持续有效**([#259](https://github.com/omdsh-dev/DSH-better-sidebar/pull/259),修复 [#258](https://github.com/omdsh-dev/DSH-better-sidebar/issues/258)):拖拽松手后底边栏不再闪全宽
|
||||
- 🔧 **适配 DSH 0.1.1-rc.1 / rc.2(@next)**([#297](https://github.com/omdsh-dev/DSH-better-sidebar/pull/297) [#305](https://github.com/omdsh-dev/DSH-better-sidebar/pull/305)):无代码逻辑改动
|
||||
- 🔒 **上传链路安全加固**([#239](https://github.com/omdsh-dev/DSH-better-sidebar/pull/239)):`relativePath` 空段 / 绝对路径显式拒绝;临时文件唯一命名(并发上传互不干扰、崩溃不阻塞);写流错误监听(磁盘失败不崩溃进程);客户端错误码与服务端统一、413 本地化
|
||||
|
||||
<details>
|
||||
<summary><b>历史版本(v0.12.0 – v0.14.0)</b></summary>
|
||||
|
||||
### v0.14.0
|
||||
|
||||
> ⚠️ 本版起需要 DSH ≥ 0.1.0-rc.8。自 v0.13.1 以来的全部更改:
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 🖼️ **统一面板宿主注入重构**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):面板/开关簇迁入 `[data-dsh-panel-host]` 固定含块层(`fixed inset-0 z-40`),免疫桌面套壳中间层 transform 对 fixed 含块的劫持;挂载自检(页面级 transform → `data-dsh-panel-host-degraded` 降级同步,按未修正几何判定、祖先变换消失才退出);推挤锚点改 `#root [data-dsh-frame] > [data-pane="conversation"]` + `#root` calc 宽度防桌面壳加性溢出;chunk 激活重验证(HEAD+ETag 保留未变 chunk,5s 超时兜底 fail-open);`visualViewport` 键盘 inset + `env(safe-area-inset-*)` 移动端适配
|
||||
- 📂 **文件打开方式默认独立**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):`editorExplorer` 默认从「合并」改为「独立」——新会话树点击 / 打开文件按路径**新开**文件 tab,无路径窗口即纯资源管理器;合并模式保留为可选手动开启
|
||||
- 🖥️ **终端 shell / shellArgs 设置页可配**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):终端卡齿轮二级页面新增「Shell 路径」「Shell 参数」两行配置(此前只能通过 `cordis.patch.yml` 配置)——设置页写入后对**之后打开的** UI 终端与模型终端(`terminal_create`)即时生效;留空保持 yaml → `$SHELL` / 登录 shell / `powershell.exe` 的既有解析顺序
|
||||
- 🏷️ **设置页版本徽标**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):侧边卡片设置页顶部新增 `DSH-better-sidebar v0.14.0` 身份徽标(版本与服务实例同步,由测试守护)
|
||||
- 🔍 **添加插件目录搜索 / 分组 / 独立滚动**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):为插件生态增长做准备——目录列表顶部加实时搜索(按名称 / id / 描述过滤),条目支持可选 `category` 分组渲染,列表独立滚动(弹窗不再随条目数无限增长)
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 🧩 **rc.8 模块系统迁移**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):rc.8 不再暴露 `window.__DSH_MODULES__` 页面全局(改由 `ctx.modules` 服务提供),懒加载 chunk 的外部依赖解析全面失效——client 注入 `modules` 服务 + 插件自有全局共享给 chunk 副本(终端 / 编辑器 / Mermaid 恢复正常按需加载)
|
||||
- 🧩 **chunk 重验证屏障健壮性**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):HEAD 重验证加 5s 超时兜底(路由挂起时 fail-open 重取,屏障不再可能无限期阻塞懒加载);`resetChunks` 清挂起的重验证屏障
|
||||
- 🖱️ **拖拽健壮性**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):快速释放(浏览器合并 / 丢失 pointermove 突发)时提交最后已知拖动位置而非回退;`pointercancel` / 捕获丢失中断同样保留拖动结果;提交后立即重测中心列(消除底栏宽度中间帧抖动);HMR 重激活后中心列重定位兜底(`<html>` 样式观察 + 底栏打开重测),修复热更新后底栏空白 / 输入框位移
|
||||
|
||||
### v0.13.1
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 📊 **Markdown 预览安全渲染 Mermaid 图表**([#164](https://github.com/omdsh-dev/DSH-better-sidebar/pull/164)):预览的 md 含 mermaid fence 时按需下发 `client-mermaid.js` chunk(~7MB,无 mermaid 文件零加载);纵深防御渲染——`securityLevel: 'strict'` + `htmlLabels: false`(节点文字走真实 SVG `<text>`)+ SVG 注入前二次清洗(删 `foreignObject`/`script`/外来 HTML 元素、剥 `@*`/`on*`/`href` 属性);点击图表在弹窗中放大(滚轮以鼠标为中心缩放、拖拽平移、工具栏与快捷键),深浅色跟随重渲、解析失败回退原码
|
||||
- 🖥️ **终端 shell 与 shellArgs 可配置**([#125](https://github.com/omdsh-dev/DSH-better-sidebar/pull/125)):`cordis.patch.yml` 的 `better-sidebar.config` 可指定 `shell` / `shellArgs`(`shellArgs` 非空时完全替换默认参数;未配置维持自动解析 `$SHELL` / 登录 shell / `powershell.exe` 原行为),UI 终端与 agent 终端(`terminal_create`)同时生效;终端 tab 标题改用 shell 名(bash / zsh / powershell),内部标识改 UUID,同 shell 可开多个终端
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 🔗 **聚合双挂载自动退让**([#200](https://github.com/omdsh-dev/DSH-better-sidebar/pull/200)):聚合包(如 dsh-web-ui-all)以独立条目 id 挂载同包时,`cordis.patch.yml` 的守卫表达式自动禁用自身 `better-sidebar` 行,不再重复注册 `/sidebar/api` 导致 `duplicate prefix route` 整个插件树启动失败(`dsh web` 崩溃);独立安装行为不变
|
||||
- 🔧 **适配 DSH 0.1.0-rc.7**([#207](https://github.com/omdsh-dev/DSH-better-sidebar/pull/207),修复 [#206](https://github.com/omdsh-dev/DSH-better-sidebar/issues/206)):修复 DSH 主框架升至 rc.7 后选模型 / 发消息报 `agent-presets: refusing to compose an unscoped context` 的问题
|
||||
|
||||
### v0.13.0
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 📁 **文件窗口与资源管理器二合一**([#151](https://github.com/omdsh-dev/DSH-better-sidebar/pull/151)):新 `editorExplorer` 设置(编辑器卡齿轮)——文件 tab 增加路径输入框头部 + 可开关的右侧停靠文件树(每 tab 记忆展开/宽度,左缘拖拽调宽 160~480px,全局文件名搜索走 host `fs.search` 路由,预算封顶并跳过 `.git` / 符号链接目录);独立模式(默认)树点击 / 输入框 Enter **按路径新开**文件 tab,合并模式**原地切换**当前 tab;新会话默认 seed 空文件窗口(`Files`)替代 explorer tab,无路径窗口在独立模式为纯资源管理器、合并模式为带 chrome 的空文件窗口;树右键提供「在新 Tab 中打开」「在侧边打开」(split)
|
||||
- 🎛️ **声明式设置 select 行**([#151](https://github.com/omdsh-dev/DSH-better-sidebar/pull/151)):设置项新增 `type: 'select'`(`options` 支持 value/title/desc/icon,`multi` 多选存数组);带图标的选项渲染大图标选项卡、收起态同样显示图标;`editorExplorer` 改为图标化下拉(合并 / 独立);能力清单新增 `settingSelect`
|
||||
- 🔀 **与 dsh-web-ui 家族右侧面板互斥**([#181](https://github.com/omdsh-dev/DSH-better-sidebar/pull/181)):读取 `aionui-panel` 设置命名空间的提供方选择——当选择「使用 aionui-panel」时,整个 better-sidebar(右侧栏 / 底部面板 / 浮动入口 / 各类接管)不再挂载;选择 DSH-better-sidebar(或未安装 aionui)时正常。设置页保存后实时生效(settings-document 推送),无需刷新
|
||||
|
||||
### v0.12.3
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 🎨 **皮肤兼容(令牌驱动)**:全面消费 DSH 设计令牌,与 dsh-web-ui 皮肤中心 10 款皮肤兼容,换肤自动跟随;终端/编辑器表面在透明/半透明玻璃值下回退不透明底色,文字不叠在皮肤背景上([#110](https://github.com/omdsh-dev/DSH-better-sidebar/pull/110),修复 #106 #105 #90 #60,附带 #52 #57 #92)
|
||||
- 🗂️ **统一路径处理**:UNC 路径 / 软链接分类(目录软链接可展开、失效链接标红)、HTML 路由平台守卫([#134](https://github.com/omdsh-dev/DSH-better-sidebar/pull/134),#65 #67 #43 #79 #115)
|
||||
- 🖥️ **终端 shell 可配置**:设置项自定义 shell,Windows 自动探测 pwsh([#95](https://github.com/omdsh-dev/DSH-better-sidebar/pull/95))
|
||||
- 📝 **编辑器新增语言**:C# / Kotlin / Swift 语法高亮([#120](https://github.com/omdsh-dev/DSH-better-sidebar/pull/120))
|
||||
- 🧭 **设置页导航图标**:设置页导航图标与布局优化([#114](https://github.com/omdsh-dev/DSH-better-sidebar/pull/114))
|
||||
- ➕ **推荐插件目录新增**:`dsh-git-remotes`——Git 远程 Tab(分支/上游/ahead-behind、fetch 可 prune、ff-only pull、确认后才 push,不替换内置暂存/提交)([#91](https://github.com/omdsh-dev/DSH-better-sidebar/pull/91));`dsh-video-preview`——视频内联预览(.mp4/.webm/.mov/.mkv/.avi 等,自带 /video 宿主路由支持 HTTP Range 206 拖进度条,不受 20MB mediaLimit 限制)([#126](https://github.com/omdsh-dev/DSH-better-sidebar/pull/126))
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 🔧 **xterm 依赖迁移**:弃用的 xterm 迁移至 `@xterm/xterm`(Closes [#122](https://github.com/omdsh-dev/DSH-better-sidebar/issues/122),[#128](https://github.com/omdsh-dev/DSH-better-sidebar/pull/128))
|
||||
- 📝 **Markdown 编辑器**:选区转对话弹窗恢复可用([#24](https://github.com/omdsh-dev/DSH-better-sidebar/pull/24))
|
||||
- 🐛 **node-pty 加载失败不再拖垮 server**([#140](https://github.com/omdsh-dev/DSH-better-sidebar/issues/140)):宿主半改为懒加载 node-pty,缺失时插件照常挂载,终端以修复提示横幅(可复制命令 + 重试按钮)呈现,agent 终端工具自动跳过
|
||||
- 🧪 测试工程:单元测试拆分(#141)+ smoke 偶发失败修复
|
||||
|
||||
</details>
|
||||
|
||||
## ⌨️ 快捷键
|
||||
|
||||
| 操作 | 按键 |
|
||||
@@ -210,21 +417,9 @@ dsh registry enable dsh-external/dsh-better-sidebar
|
||||
| 引用文件到输入框 | 悬浮行尾 `@文件` 按钮 |
|
||||
| 复制文件路径 | 右键行 → 复制相对/绝对地址 |
|
||||
|
||||
## 🔌 服务化:注册 tab 与文件预览器
|
||||
## 🔌 服务化扩展
|
||||
|
||||
从 v0.4.0 起暴露 `ctx.betterSidebar` 服务,其他插件可注册侧边栏页面与文件预览器(内置 7 tab + 6 viewer 亦通过同一服务注册):
|
||||
|
||||
```ts
|
||||
import type {} from 'dsh-better-sidebar' // 触发 ctx.betterSidebar 类型合并
|
||||
export const inject = ['betterSidebar']
|
||||
export function apply(ctx: Context) {
|
||||
ctx.effect(() => ctx.betterSidebar.registerTab({
|
||||
id: 'my-plugin:db', title: 'Database', component: ({ scope }) => <DbView sessionId={scope.sessionId} />,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
v0.12.1 补齐基座能力(完整类型导出、能力探测、状态订阅、tab 角标、生命周期回调、定向打开、插件自有设置等),详见下方接入文档。
|
||||
从 v0.4.0 起暴露 `ctx.betterSidebar` 服务,其他插件可注册侧边栏页面与文件预览器(内置 7 tab + 6 viewer 亦通过同一服务注册)。v0.12.1 补齐基座能力(完整类型导出、能力探测、状态订阅、tab 角标、生命周期回调、定向打开、插件自有设置等)。
|
||||
|
||||
完整接入文档:
|
||||
- **[`AGENTS.md`](./AGENTS.md)**——仓库内维护的接入文档(全字段、匹配算法、HMR 陷阱、声明式设置、版本探测);
|
||||
@@ -267,9 +462,52 @@ pnpm watch # tsdown --watch
|
||||
|
||||
Windows / Linux / macOS 三平台适配(macOS 日常验证;其余经单元测试覆盖);`node-pty` 优先预编译二进制,失败需编译工具链(Windows VS Build Tools / Linux make+g+++python3 / macOS Xcode CLT)。
|
||||
|
||||
## 💬 社区
|
||||
|
||||
微信 / QQ 交流群二维码将在此处展示。上传二维码图片(拖入任意 issue / comment 即可获得 `user-attachments` 链接)后,替换下方 `src` 并取消注释:
|
||||
|
||||
<div align="center">
|
||||
<!-- 微信群二维码
|
||||
<img width="220" alt="微信群二维码" src="https://github.com/user-attachments/assets/REPLACE_ME" />
|
||||
-->
|
||||
<!-- QQ 群二维码
|
||||
<img width="220" alt="QQ群二维码" src="https://github.com/user-attachments/assets/REPLACE_ME" />
|
||||
-->
|
||||
</div>
|
||||
|
||||
## 🤝 参与贡献
|
||||
|
||||
- **代码改动走 PR**:`feat/*` / `fix/*` 分支开发 → `gh pr create`;纯文档改动可直接推 main
|
||||
- **收录生态插件**:给仓库打 `dsh-better-sidebar` topic + 向 [`src/client/plugins-tabs.ts`](./src/client/plugins-tabs.ts) / [`plugins-viewers.ts`](./src/client/plugins-viewers.ts) 提 PR
|
||||
- **提交前自检**:`pnpm typecheck && pnpm build && pnpm test`(CI 另有 npm 打包 → 真实挂载 → 无头渲染门禁 `pnpm test:mount`)
|
||||
- 仓库工作规范见 [`AGENTS.md`](./AGENTS.md)(含仓库硬约束与 CI 说明)
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
<a href="https://star-history.com/#omdsh-dev/DSH-better-sidebar&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=omdsh-dev/DSH-better-sidebar&type=Date&theme=dark" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=omdsh-dev/DSH-better-sidebar&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 👥 贡献者
|
||||
|
||||
感谢每一位贡献者:
|
||||
|
||||
<a href="https://github.com/omdsh-dev/DSH-better-sidebar/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=omdsh-dev/DSH-better-sidebar" alt="贡献者" />
|
||||
</a>
|
||||
|
||||
## 🔗 友情链接
|
||||
|
||||
- [dsh-tianshu-tui](https://github.com/huiliyi37/dsh-tianshu-tui):DeepSeek Harness 交互式终端 UI 插件(渲染核心由自研 harness agent Tianshu-Tui 演进而来),在官方基础上增加 TDD 与证据门等工作流
|
||||
- [dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI):Claude Code 风格全屏交互终端插件——像素鲸鱼顶栏、实时工作状态行、思考流式展开、双击 Esc 回滚、上下文进度条 + TPS 仪表,npm 一键安装
|
||||
- [dshfind 插件超市](https://dshfind.com/zh/plugins):三方插件市场——GitHub topic `dsh-plugin` 下的公开仓库清单,每日同步 star、贡献者与增长数据
|
||||
- [DeepSeek Harness Desktop](https://github.com/anywhere-labs/deepseek-harness-desktop):为 DeepSeek Harness 生态打造的现代化桌面端——无需配置 Node.js 或执行命令即可启动和管理本地 Harness 服务;[官网](https://www.dshdesktop.cn)
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<sub>MIT License · Built for the <a href="https://github.com/deepseek-ai/deepseek-harness">DeepSeek Harness</a> ecosystem · 在 <a href="https://github.com/topics/dsh-better-sidebar">topic dsh-better-sidebar</a> 发现更多生态插件</sub>
|
||||
</div>
|
||||
|
||||
+343
-136
@@ -3,9 +3,15 @@
|
||||
<!-- Hero -->
|
||||
<div align="center">
|
||||
<b style="font-size: 1.15em;">A service-oriented sidebar framework, and a complete workbench out of the box</b><br /><br />
|
||||
<a href="https://www.npmjs.com/package/dsh-better-sidebar"><img alt="npm version" src="https://img.shields.io/npm/v/dsh-better-sidebar" /></a>
|
||||
<a href="https://www.npmjs.com/package/dsh-better-sidebar"><img alt="npm downloads" src="https://img.shields.io/npm/dm/dsh-better-sidebar" /></a>
|
||||
<a href="https://github.com/omdsh-dev/DSH-better-sidebar/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/omdsh-dev/DSH-better-sidebar/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://github.com/omdsh-dev/DSH-better-sidebar/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/omdsh-dev/DSH-better-sidebar" /></a>
|
||||
<a href="https://opensource.org/licenses/MIT"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg" /></a>
|
||||
<a href="https://dshfind.com/en/plugins/omdsh-dev/DSH-better-sidebar?ref=badge"><img alt="dshfind" src="https://dshfind.com/api/badge/omdsh-dev/DSH-better-sidebar?lang=en" /></a><br /><br />
|
||||
<img alt="File management" src="https://img.shields.io/badge/-File%20management-4d6bfe" /> <img alt="Edit & preview" src="https://img.shields.io/badge/-Edit%20%26%20preview-4d6bfe" /> <img alt="Embedded browser" src="https://img.shields.io/badge/-Embedded%20browser-4d6bfe" /> <img alt="Real terminal" src="https://img.shields.io/badge/-Real%20terminal-4d6bfe" /> <img alt="Git panel" src="https://img.shields.io/badge/-Git%20panel-4d6bfe" /> <img alt="Background tasks" src="https://img.shields.io/badge/-Background%20tasks-4d6bfe" /> <img alt="Plugin integration" src="https://img.shields.io/badge/-Plugin%20integration-4d6bfe" /><br /><br />
|
||||
<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="Supported DSH versions: 0.1.0-rc.8 · 0.1.1-rc.1 · 0.1.1-rc.2" src="https://img.shields.io/badge/DSH-0.1.0--rc.8_%C2%B7_0.1.1--rc.1_%C2%B7_0.1.1--rc.2-4d6bfe" /></a>
|
||||
<a href="https://github.com/topics/dsh-better-sidebar"><img alt="Plugin ecosystem: GitHub topic dsh-better-sidebar" src="https://img.shields.io/badge/plugin%20ecosystem-topic%20dsh--better--sidebar-4d6bfe" /></a><br /><br />
|
||||
<img alt="File management" src="https://img.shields.io/badge/-File%20management-4d6bfe" /> <img alt="Edit & preview" src="https://img.shields.io/badge/-Edit%20%26%20preview-4d6bfe" /> <img alt="Embedded browser" src="https://img.shields.io/badge/-Embedded%20browser-4d6bfe" /> <img alt="Real terminal" src="https://img.shields.io/badge/-Real%20terminal-4d6bfe" /> <img alt="Git panel" src="https://img.shields.io/badge/-Git%20panel-4d6bfe" /> <img alt="Background tasks" src="https://img.shields.io/badge/-Background%20tasks-4d6bfe" /> <img alt="Side Chat" src="https://img.shields.io/badge/-Side%20Chat-4d6bfe" /> <img alt="Plugin integration" src="https://img.shields.io/badge/-Plugin%20integration-4d6bfe" /><br /><br />
|
||||
<b>A dual workbench (right sidebar + bottom panel)</b> that opens its <code>ctx.betterSidebar</code> service to every plugin —<br />
|
||||
register new sidebar pages and file viewers via <code>registerTab</code> / <code>registerFileViewer</code>.
|
||||
</div>
|
||||
@@ -19,9 +25,22 @@
|
||||
<img alt="dsh-better-sidebar workbench" src="https://github.com/user-attachments/assets/dfdb875e-a1a8-4d4b-8340-353736b1708f" />
|
||||
</div>
|
||||
|
||||
## 📑 Contents
|
||||
|
||||
- [✨ Features](#-features)
|
||||
- [🚀 Installation](#-installation)
|
||||
- [🖼️ Feature Tour](#-feature-tour)
|
||||
- [🌐 Plugin Ecosystem](#-plugin-ecosystem)
|
||||
- [🆕 Recent Updates](#-recent-updates)
|
||||
- [⌨️ Keyboard Shortcuts](#-keyboard-shortcuts)
|
||||
- [🔌 Service API](#-service-api)
|
||||
- [🛠️ Development & Build](#-development--build)
|
||||
- [🔐 Security](#-security) · [⚠️ Known Limitations](#-known-limitations) · [🖥️ Platform Support](#-platform-support)
|
||||
- [💬 Community](#-community) · [🤝 Contributing](#-contributing) · [⭐ Star History](#-star-history) · [🔗 Friends](#-friends)
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **🗂️ File Workbench**: file explorer (lazy-loading tree; symlinks show their target kind — directory links expand, dangling links flagged) + CodeMirror editor; inline preview for images / Markdown (incl. Mermaid diagrams, strict-mode safe rendering + click-to-zoom) / HTML / PDF / Office
|
||||
- **🗂️ File Workbench**: file explorer (lazy-loading tree; symlinks show their target kind — directory links expand, dangling links flagged) + CodeMirror editor; inline preview for images / Markdown (incl. Mermaid diagrams, strict-mode safe rendering + click-to-zoom) / HTML / PDF
|
||||
- **🌐 Embedded Browser**: multiple web tabs with back / forward / refresh; content runs in a sandboxed iframe; external links are routed by protocol by default — HTTP opens in the sidebar, HTTPS goes to the system browser (both adjustable in settings)
|
||||
- **💻 Real Terminal**: xterm.js + node-pty real shell, reconnect with transcript replay; optionally injects `terminal_*` tools for the model
|
||||
- **🌿 Git Panel**: real diff + VSCode-style diff tabs, history, right-click to stage / commit / revert
|
||||
@@ -33,134 +52,34 @@
|
||||
- **⚡ On-demand Loading**: only ~325KB core at startup; heavy deps (terminal / editor / mermaid diagrams) load on demand ([design](docs/plans/2026-08-12-lazy-chunks-design.md))
|
||||
- **🌏 i18n**: UI text follows DSH's language (zh / en) with live switching
|
||||
|
||||
> 🔌 **Core principle**: service-first — the 7 built-in tabs + 6 viewers register through the same `ctx.betterSidebar` API as third-party plugins, with fully equal capabilities; anything the ecosystem can provide better is delegated to ecosystem plugins. See the "🔌 Service" section below and the [external plugin guide](./docs/external-plugin-guide.md).
|
||||
|
||||
## 🆕 Recent Updates
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0"><img width="45%" alt="Service API base screenshot" src="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0" /></a>
|
||||
<a href="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e"><img width="45%" alt="Add Plugins screenshot" src="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e" /></a>
|
||||
</div>
|
||||
|
||||
### v0.15.0
|
||||
|
||||
> ⚠️ **This release is verified against DSH 0.1.1-rc.2 (@next)**: devDependencies / lockfile / CI mount-smoke baseline raised to `0.1.1-rc.2` (zero rc.8 leftovers in the lockfile); **the peer floor stays at `^0.1.0-rc.8`** — 0.1.1-rc.x is a routine rc progression, mutually compatible with rc.8, so **rc.8 users don't need to upgrade DSH**. `cordis` stays at `^4.0.0-rc.8`. All changes since v0.14.0:
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 💬 **Side Chat (beta) tab** ([#286](https://github.com/omdsh-dev/DSH-better-sidebar/pull/286)): Codex-style side threads, **one independent tab per conversation** — the child inherits the parent's full context (completed turns + pending messages + the in-progress turn's assistant output and tool activity, honestly frozen with an "interrupted" marker); created with an identical composition (same preset / provider / model) so the first request reuses the parent's input prefix cache; threads stay invisible in the main session list with zero subagent-catalog noise; follow-ups survive DSH restarts (auto cold-resume); one-click "Save as new session" promotes the thread to a top-level session ([design](docs/plans/2026-08-20-sidechat-tab-design.md))
|
||||
- 📤 **Upload into the files window** ([#239](https://github.com/omdsh-dev/DSH-better-sidebar/pull/239)): header "upload file / upload folder" buttons plus drag-drop (drop on the tree body = workspace root, on a directory row = that directory, on a file row = its parent directory, VSCode semantics); full-window blurred progress overlay while uploading (per-file progress + cancel / Esc); buttons disabled while busy, tree refreshes after the upload settles
|
||||
- 🧩 **Desktop compatibility in four options** ([#284](https://github.com/omdsh-dev/DSH-better-sidebar/pull/284)): "Position compatibility mode" is now a main-row dropdown — **Auto-detect** (default, conservative: only the standard Window Controls Overlay geometry contributes; real 32/36px caption-overlay heights per shell, live on maximize/restore; zero modification on plain web) / **DSH official web** (explicitly no adaptation) / **Shell preset** (built-in, opt-in; only shells that appeared in this repo's issues/PRs with 100+ stars, "detected" badge when the environment matches) / **Custom** (free-form CSS + shift distance). Documents that already carried compatibility values migrate to the custom scheme; interactive chrome opts out of desktop drag regions (`no-drag`); the bottom-push anchor is a composite selector (`[data-pane]` and `:has(> [data-slot])`)
|
||||
- 🎛️ **Settings page UI/UX modernization** ([#300](https://github.com/omdsh-dev/DSH-better-sidebar/pull/300)): the side-card secondary-settings entry is now a full-width "Feature settings" strip at the card bottom (replacing the invisible corner gear — much easier to discover); coordinated two-tone enabled state (brand activation accent + success-green check badge); every color is still `--dsw-alias-*` token-derived so skins follow automatically
|
||||
- ➕ **New entries in the recommended-plugin catalog**: `dsh-docs-panel` (global docs, [#230](https://github.com/omdsh-dev/DSH-better-sidebar/pull/230)), `dsh-flowglass` ([#261](https://github.com/omdsh-dev/DSH-better-sidebar/pull/261)), `dsh-git-forge` and `dsh-ssh-tunnel` ([#204](https://github.com/omdsh-dev/DSH-better-sidebar/pull/204)), `dsh-turn-review` ([#102](https://github.com/omdsh-dev/DSH-better-sidebar/pull/102))
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- ⚡ **Batched live preview for the subagent page** ([#298](https://github.com/omdsh-dev/DSH-better-sidebar/pull/298)): the old implementation polled `subagents.history` per running subagent, each poll triggering a full host-side enumeration — an O(N²) amplification that stalled the page with many concurrent subagents; now a single batch route `subagents.live` (one enumeration of the whole tree) plus one client poller with a single in-flight request; display logic and copy unchanged
|
||||
- 🖱️ **Interrupted / fast-release drags no longer roll back** ([#249](https://github.com/omdsh-dev/DSH-better-sidebar/pull/249), closes [#247](https://github.com/omdsh-dev/DSH-better-sidebar/issues/247) [#248](https://github.com/omdsh-dev/DSH-better-sidebar/issues/248)): interrupted or fast-released drags commit the last known position; HMR re-activation re-locates the center column (fixes the blank bottom panel after a hot reload)
|
||||
- 📐 **Push variables stay effective while mounted** ([#259](https://github.com/omdsh-dev/DSH-better-sidebar/pull/259), fixes [#258](https://github.com/omdsh-dev/DSH-better-sidebar/issues/258)): the bottom panel no longer flashes full-width after a drag is released
|
||||
- 🔧 **Adapted to DSH 0.1.1-rc.1 / rc.2 (@next)** ([#297](https://github.com/omdsh-dev/DSH-better-sidebar/pull/297) [#305](https://github.com/omdsh-dev/DSH-better-sidebar/pull/305)): devDependencies / lockfile / CI verification baseline shifted; every consumed contract (DOM slot, CSS tokens, boot protocol, MarkdownText `codeLabels`, `webServer` route registration) compared package by package with zero breakage — **no code changes needed**; peer floor stays at `^0.1.0-rc.8`
|
||||
- 🔒 **Upload-chain hardening** ([#239](https://github.com/omdsh-dev/DSH-better-sidebar/pull/239)): empty and absolute `relativePath` segments are refused outright; uniquely named temp files (concurrent uploads stay independent, crashed processes never block later uploads); write-stream error listeners (a failing disk can no longer crash the host); client error codes unified with the wire (`too-large`), 413s localized
|
||||
|
||||
### v0.14.0
|
||||
|
||||
> ⚠️ **This release adapts to DSH 0.1.0-rc.8**: all `@deepseek-ai/*` peer/devDependencies raised to `^0.1.0-rc.8` (transitives included; zero rc.7 leftovers in the lockfile), `cordis` to `^4.0.0-rc.8`, and the CI mount lane pins `@deepseek-ai/dsh@0.1.0-rc.8`. **DSH rc.7 and earlier cannot resolve this release's dependencies — upgrade DSH first.** All changes since v0.13.1:
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 🖼️ **Unified panel-host injection refactor** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): panels/toggle clusters moved into a `[data-dsh-panel-host]` fixed containing block (`fixed inset-0 z-40`), immune to desktop-shell intermediate transforms hijacking `fixed`; mount self-check (page-level transform → `data-dsh-panel-host-degraded` degraded sync, judged on uncorrected geometry, exits only when the ancestor transform is gone); push anchor switched to `#root [data-dsh-frame] > [data-pane="conversation"]` + `#root` calc width against desktop-shell additive overflow; chunk revalidation on activation (HEAD+ETag keeps unchanged chunks, 5s timeout fails open); `visualViewport` keyboard inset + `env(safe-area-inset-*)` mobile adaptation
|
||||
- 📂 **Separate file windows by default** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): `editorExplorer` now defaults to **separate** — tree clicks / file opens create a new tab per path and the path-less window is a pure file manager; merged mode stays available as an opt-in
|
||||
- 🖥️ **Terminal shell / shellArgs configurable from the settings page** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): the terminal card's gear popup gains "Shell path" and "Shell arguments" rows (previously yaml-only via `cordis.patch.yml`) — saved values take effect immediately for terminals opened afterwards (UI terminals and model `terminal_create` alike); empty keeps the existing yaml → `$SHELL` / login shell / `powershell.exe` resolution order
|
||||
- 🏷️ **Version badge on the settings page** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): the side-card settings section now opens with a `DSH-better-sidebar v0.14.0` identity badge (version synced with the service instance, test-guarded)
|
||||
- 🔍 **Add-plugin catalog: search / grouping / independent scroll** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): built for a growing plugin ecosystem — a live search box (filters by name / id / description), optional `category` grouping for entries, and an independently scrolling list (the modal no longer grows unbounded with catalog size)
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- 🔧 **Adapted to DSH 0.1.0-rc.8** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): all 13 `@deepseek-ai/*` peer/devDependencies raised to `^0.1.0-rc.8` (transitives included; zero rc.7 leftovers in the lockfile), `cordis` to `^4.0.0-rc.8`; `dsh-client-web-react` / `dsh-client-schema-form` removed (no rc.8 publish, no longer in the shell module table, zero plugin references); CI mount lane pins `@deepseek-ai/dsh@0.1.0-rc.8`; pnpm 11.8 supply-chain verification adapted
|
||||
- 🧩 **rc.8 module-system migration** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): rc.8 no longer exposes the `window.__DSH_MODULES__` page global (it moved to the `ctx.modules` service), which broke every lazy chunk's externals resolution — the client now injects the `modules` service and shares it with chunk-bundle copies through a plugin-owned global (terminal / editor / Mermaid on-demand loading restored)
|
||||
- 🧩 **Chunk revalidation barrier hardening** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): HEAD revalidation gains a 5s timeout (fails open on a stuck route so the barrier can never wedge lazy loads); `resetChunks` clears a pending revalidation barrier
|
||||
- 🖱️ **Drag robustness** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): fast releases (browsers merge/lose pointermove bursts) commit the last known dragged position instead of rolling back; `pointercancel` / lost-capture interruptions keep the drag result too; the center column is re-measured right after commit (no mid-frame bottom-panel width jump); HMR re-activation re-locates the center column via an `<html>` style observer plus a retry when the bottom panel opens (fixes the blank bottom panel / shifted input bar after a hot reload)
|
||||
|
||||
### v0.13.1
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 📊 **Safe Mermaid rendering in the Markdown preview** ([#164](https://github.com/omdsh-dev/DSH-better-sidebar/pull/164)): when a previewed md file contains mermaid fences, a `client-mermaid.js` chunk (~7MB) is served on demand (zero load without mermaid); defense-in-depth rendering — `securityLevel: 'strict'` + `htmlLabels: false` (node labels use real SVG `<text>`) + a second sanitize pass before SVG injection (foreignObject/script/foreign HTML elements removed, `@*`/`on*`/`href` attributes stripped); click a diagram to zoom in a modal overlay (wheel zoom centered on the cursor, drag pan, toolbar & shortcuts), re-renders with light/dark theme, falls back to the raw code block on parse failure
|
||||
- 🖥️ **Configurable terminal shell & shellArgs** ([#125](https://github.com/omdsh-dev/DSH-better-sidebar/pull/125)): `cordis.patch.yml` `better-sidebar.config` can set `shell` / `shellArgs` (a non-empty `shellArgs` fully replaces the defaults; unset keeps the previous auto-resolution of `$SHELL` / login shell / `powershell.exe`), applied to both UI terminals and agent terminals (`terminal_create`); terminal tab titles now show the shell name (bash/zsh/powershell) and internal tab ids use UUIDs so the same shell can open multiple terminals
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- 🔗 **Aggregate double-mount auto-yield** ([#200](https://github.com/omdsh-dev/DSH-better-sidebar/pull/200)): when an aggregate package (e.g. dsh-web-ui-all) mounts the same package under its own entry id, the guard expression in `cordis.patch.yml` disables the plugin's own `better-sidebar` row so `/sidebar/api` is no longer registered twice (`duplicate prefix route` crashing the whole plugin tree / `dsh web`); standalone installs behave as before
|
||||
- 🔧 **Peer dependencies aligned to DSH 0.1.0-rc.7** ([#207](https://github.com/omdsh-dev/DSH-better-sidebar/pull/207), fixes [#206](https://github.com/omdsh-dev/DSH-better-sidebar/issues/206)): all `@deepseek-ai/*` peer/devDependencies raised from `^0.1.0-rc.6` to `^0.1.0-rc.7`, and the CI mount lane pins the same version — eliminating the mixed rc.6/rc.7 dependency tree (after the main framework moved to rc.7) that surfaced as `agent-presets: refusing to compose an unscoped context` when picking a model / sending a message
|
||||
|
||||
### v0.13.0
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 📁 **Files window merged with the explorer** ([#151](https://github.com/omdsh-dev/DSH-better-sidebar/pull/151)): new `editorExplorer` setting (editor card gear) — file tabs gain a path-input header plus a toggleable right-docked file tree (per-tab open/width memory, drag-resize 160–480px from the left edge, global filename search via the host `fs.search` route with a hard budget, skipping `.git` and symlink dirs); in separate mode (default) tree clicks / Enter in the path input open each file **in its own new tab**, merged mode switches the current tab **in place**; fresh sessions seed an empty Files window instead of the explorer tab, and a path-less window is a bare file manager in separate mode / a chrome'd empty file window in merged mode; the tree context menu offers "Open in new tab" and "Open to the side" (split)
|
||||
- 🎛️ **Select rows for declarative settings** ([#151](https://github.com/omdsh-dev/DSH-better-sidebar/pull/151)): settings rows gain `type: 'select'` (`options` with value/title/desc/icon, `multi` stores the picked values as an array); options with icons render big-icon option cards and keep the icon in the closed anchor; `editorExplorer` became an iconed select (merged vs separate); the capability list gained `settingSelect`
|
||||
- 🔀 **Mutual exclusion with the dsh-web-ui family right panel** ([#181](https://github.com/omdsh-dev/DSH-better-sidebar/pull/181)): reads the `aionui-panel` settings namespace's provider choice — when "Use aionui-panel" is selected, the whole better-sidebar (right sidebar / bottom panel / floating entry / all takeovers) does not mount; with DSH-better-sidebar (or no aionui installed) it behaves as before. Takes effect live after a settings save (settings-document push), no reload needed
|
||||
|
||||
**📝 Other**
|
||||
|
||||
- Install / update commands unified to `dsh-better-sidebar@latest` (bilingual README sync)
|
||||
|
||||
### v0.12.3
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 🎨 **Skin compatibility (token-driven)**: fully consumes DSH design tokens and follows the dsh-web-ui skin center's 10 skins automatically; terminal/editor surfaces fall back to opaque backgrounds under transparent/translucent-glass token values so text never scrolls over the skin art ([#110](https://github.com/omdsh-dev/DSH-better-sidebar/pull/110), fixes #106 #105 #90 #60, also #52 #57 #92)
|
||||
- 🗂️ **Unified path handling**: UNC / symlink classification (directory symlinks expandable, broken links highlighted) + HTML-route platform guards ([#134](https://github.com/omdsh-dev/DSH-better-sidebar/pull/134), #65 #67 #43 #79 #115)
|
||||
- 🖥️ **Configurable terminal shell**: custom shell setting with Windows pwsh auto-probe ([#95](https://github.com/omdsh-dev/DSH-better-sidebar/pull/95))
|
||||
- 📝 **Editor languages**: C# / Kotlin / Swift syntax highlighting ([#120](https://github.com/omdsh-dev/DSH-better-sidebar/pull/120))
|
||||
- 🧭 **Settings nav icon**: settings-page navigation icon and layout polish ([#114](https://github.com/omdsh-dev/DSH-better-sidebar/pull/114))
|
||||
- ➕ **Recommended-plugin catalog**: added `dsh-git-remotes` — Git Remotes tab (branches/upstream/ahead-behind, fetch with prune, ff-only pull, confirm-before-push; does not replace the built-in stage/commit tab) ([#91](https://github.com/omdsh-dev/DSH-better-sidebar/pull/91)); and `dsh-video-preview` — inline video preview (.mp4/.webm/.mov/.mkv/.avi etc.) backed by a /video host route with HTTP Range (206) scrubbing, not capped by the 20MB mediaLimit ([#126](https://github.com/omdsh-dev/DSH-better-sidebar/pull/126))
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- 🔧 **xterm migration**: deprecated xterm dependency migrated to `@xterm/xterm` (Closes [#122](https://github.com/omdsh-dev/DSH-better-sidebar/issues/122), [#128](https://github.com/omdsh-dev/DSH-better-sidebar/pull/128))
|
||||
- 📝 **Markdown editor**: selection-to-conversation popup restored ([#24](https://github.com/omdsh-dev/DSH-better-sidebar/pull/24))
|
||||
- 🐛 **node-pty load failure no longer crashes the server** ([#140](https://github.com/omdsh-dev/DSH-better-sidebar/issues/140)): the host half now lazy-loads node-pty — when it is missing the plugin still mounts, the terminal shows a repair banner (copyable command + Retry button), and agent terminal tools are skipped
|
||||
- 🧪 Test engineering: unit spec split (#141) + flaky smoke cleanup fix
|
||||
|
||||
**🚀 Engineering**
|
||||
|
||||
- npm publishing wired to GitHub Releases (Trusted Publishing, provenance-attached tarballs); tagging a release publishes automatically ([#148](https://github.com/omdsh-dev/DSH-better-sidebar/pull/148))
|
||||
|
||||
### v0.12.2
|
||||
|
||||
- 📐 **Position compat mode**: new "Position compatibility mode" setting: reserves top space for the native Windows title bar (top-right) so the sidebar buttons and content sit below it (off by default); the shift distance is customizable in the gear popup (0–120px)
|
||||
- 🔌 **Service API base**: complete type exports + `version`/`features` capability detection, state subscription (`getSnapshot`/`subscribeState`), tab `badge`, `onOpen`/`onActivate`/`onClose` lifecycle callbacks, `updateTab`/`activateTab`/`openFile`, targeted open, `meta` persisted across reloads, plugin-owned settings (`pluginToggles`/`render`), external-link claim (`urlTarget`)
|
||||
- ➕ **Add Plugins**: recommended plugin catalog in settings + one-click copy install command; built-in Office preview moved to the recommended plugin
|
||||
- 🖱️ **Tab-bar scroll**: mouse-wheel horizontal scrolling on the tab bar
|
||||
- 🐛 **Fixes**: remote access 403 (trust fence now uses `trustedHosts`), sidebar crash [#31](https://github.com/omdsh-dev/DSH-better-sidebar/issues/31), Windows HTML-preview drive-path
|
||||
|
||||
### v0.12.1
|
||||
|
||||
- 🔌 **Service API base**: complete type exports + `version`/`features` capability detection, state subscription (`getSnapshot`/`subscribeState`), tab `badge`, `onOpen`/`onActivate`/`onClose` lifecycle callbacks, `updateTab`/`activateTab`/`openFile`, targeted open, `meta` persisted across reloads, plugin-owned settings (`pluginToggles`/`render`)
|
||||
- ➕ **Add Plugins**: recommended plugin catalog in settings + one-click copy install command; built-in Office preview moved to the recommended plugin
|
||||
- 🖱️ **Tab-bar scroll**: mouse-wheel horizontal scrolling on the tab bar
|
||||
- 🐛 **Fixes**: remote access 403 (trust fence now uses `trustedHosts`), sidebar crash [#31](https://github.com/omdsh-dev/DSH-better-sidebar/issues/31), Windows HTML-preview drive-path
|
||||
|
||||
> 📝 Note: the 0.12.0 final could not be reused (npm reported the version as already published), so the public release became 0.12.1 — both carry identical content.
|
||||
|
||||
### v0.12.0
|
||||
|
||||
- 🔌 **Service API base**: complete type exports + `version`/`features` capability detection, state subscription, tab badges, lifecycle callbacks, targeted open, `meta` persisted across reloads, plugin-owned settings
|
||||
- ➕ **Add Plugins**: recommended plugin catalog in settings + one-click copy install command; built-in Office preview moved to the recommended plugin
|
||||
- 🖱️ **Tab-bar scroll**: mouse-wheel horizontal scrolling on the tab bar
|
||||
- 🐛 **Fixes**: remote access 403 (trust fence now uses `trustedHosts`), sidebar crash [#31](https://github.com/omdsh-dev/DSH-better-sidebar/issues/31), Windows HTML-preview drive-path
|
||||
> 🔌 **Core principle**: service-first — the 7 built-in tabs + 6 viewers register through the same `ctx.betterSidebar` API as third-party plugins, with fully equal capabilities; anything the ecosystem can provide better is delegated to ecosystem plugins (**28+ ecosystem plugins** already — see "🌐 Plugin Ecosystem" below). See "🔌 Service API" and the [external plugin guide](./docs/external-plugin-guide.md).
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
**Prerequisites**: DSH installed (`dsh web` boots), Node.js ≥ 20, pnpm ≥ 10.
|
||||
|
||||
**Supported DSH versions**:
|
||||
<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="Supported DSH versions: 0.1.0-rc.8 · 0.1.1-rc.1 · 0.1.1-rc.2" src="https://img.shields.io/badge/DSH-0.1.0--rc.8_%C2%B7_0.1.1--rc.1_%C2%B7_0.1.1--rc.2-4d6bfe" /></a>
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web add dsh-better-sidebar@latest
|
||||
dsh plugin --profile web add dsh-better-sidebar@latest # first run fails: pnpm 11 blocks node-pty build scripts (the dependency is still written)
|
||||
cd ~/.dsh/profiles/web && pnpm approve-builds --all # allow the build scripts (re-runs the install automatically)
|
||||
dsh plugin --profile web add dsh-better-sidebar@latest # re-run succeeds
|
||||
```
|
||||
|
||||
Then **hard-refresh the browser** (Cmd/Ctrl+Shift+R) to see the sidebar (DSH hot-reloads client changes; only host-half updates need a restart).
|
||||
|
||||
**Or let DSH install it for you** — paste this prompt into any DSH session:
|
||||
|
||||
```text
|
||||
Install the dsh-better-sidebar plugin (a sidebar workbench for DSH):
|
||||
1. Run: dsh plugin --profile web add dsh-better-sidebar@latest (the first run fails because pnpm 11 blocks node-pty build scripts — that's expected)
|
||||
2. In ~/.dsh/profiles/web run: pnpm approve-builds --all (allows the build scripts and re-runs the install)
|
||||
3. Run the add command again: dsh plugin --profile web add dsh-better-sidebar@latest
|
||||
4. When done, remind me to hard-refresh the browser (Cmd/Ctrl+Shift+R)
|
||||
If anything fails, check the troubleshooting table in the README at https://github.com/omdsh-dev/DSH-better-sidebar
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><b>Updating</b></summary>
|
||||
|
||||
@@ -168,7 +87,7 @@ Then **hard-refresh the browser** (Cmd/Ctrl+Shift+R) to see the sidebar (DSH hot
|
||||
dsh plugin --profile web add dsh-better-sidebar@latest
|
||||
```
|
||||
|
||||
or bump the version in `~/.dsh/profiles/web/package.json` (e.g. `"^0.13.0"`) and run `pnpm install`. Then hard-refresh the browser (Cmd/Ctrl+Shift+R) — client changes do not need a DSH restart.
|
||||
or bump the version in `~/.dsh/profiles/web/package.json` (e.g. `"^0.15.0"`) and run `pnpm install`. Then hard-refresh the browser (Cmd/Ctrl+Shift+R) — client changes do not need a DSH restart.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -209,7 +128,7 @@ To debug local changes or track the dev branch, point the dependency at a local
|
||||
5. Restart DSH and hard-refresh
|
||||
```
|
||||
|
||||
Update: `git pull && pnpm install && pnpm build` → just hard-refresh the browser (client changes hot-reload; only host-half changes need a DSH restart). To switch back to the npm channel, restore `"dsh-better-sidebar": "^0.13.0"` and re-run `pnpm install`.
|
||||
Update: `git pull && pnpm install && pnpm build` → just hard-refresh the browser (client changes hot-reload; only host-half changes need a DSH restart). To switch back to the npm channel, restore `"dsh-better-sidebar": "^0.15.0"` and re-run `pnpm install`.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -230,6 +149,263 @@ Update: `git pull && pnpm install && pnpm build` → `node scripts/package-regis
|
||||
|
||||
</details>
|
||||
|
||||
## 🖼️ Feature Tour
|
||||
|
||||
> Below are real UI screenshots.
|
||||
|
||||
### 🗂️ File Workbench: Explorer
|
||||
|
||||
Two explorer modes: embedded in the file preview / standalone file tree. Lazy-loading directory tree, symlinks classified by target kind (directory links expand, dangling links flagged), global filename search, file/folder upload buttons plus drag-drop upload, context menu (open in new tab / open to the side / copy paths), and a hover `@file` button that references a file straight into the composer.
|
||||
|
||||
<div align="center"><img width="880" alt="File explorer" src="https://github.com/user-attachments/assets/a410bfd2-a8ba-43e6-873e-22417756e94d" /></div>
|
||||
<div align="center"><img width="880" alt="CodeMirror editor" src="https://github.com/user-attachments/assets/b44b488e-568c-4ee0-b96c-e9c906598a77" /></div>
|
||||
|
||||
### 📝 Inline Preview: Markdown · Images · PDF
|
||||
|
||||
The Markdown preview renders **Mermaid diagrams** (strict-mode safe rendering + a second sanitize pass; click a diagram for a zoom modal with wheel-zoom and drag-pan); images / PDFs display inline via the media route; the Office suite is covered by an ecosystem plugin.
|
||||
|
||||
<div align="center"><img width="880" alt="Markdown + Mermaid preview" src="https://github.com/user-attachments/assets/fe0e5182-55bb-45cc-b98b-a2877c2bdd38" /></div>
|
||||
<div align="center"><img width="880" alt="Inline image preview" src="https://github.com/user-attachments/assets/f9a58c30-5b7a-48b5-9e22-37d7e071f593" /></div>
|
||||
|
||||
### 💻 Real Terminal
|
||||
|
||||
xterm.js + node-pty real shell (not an emulator): transcript replay on reconnect, configurable shell / shellArgs (settings page or `cordis.patch.yml`), and optional `terminal_*` model tools so the agent can open terminals and run commands itself.
|
||||
|
||||
<div align="center"><img width="880" alt="Real terminal" src="https://github.com/user-attachments/assets/0dad6ad3-ff3f-4b5a-86d2-f832ce65323e" /></div>
|
||||
|
||||
### 🌿 Git Panel
|
||||
|
||||
Stage / unstage / commit (`Ctrl+Enter`) / revert, plus a history list; clicking a changed file opens a **VSCode-style diff tab** (line-level red/green).
|
||||
|
||||
<div align="center"><img width="880" alt="Git panel" src="https://github.com/user-attachments/assets/e7fc1220-305f-4bca-8583-e77ab4f4fa78" /></div>
|
||||
|
||||
### 🌐 Embedded Browser
|
||||
|
||||
Multiple web tabs with back / forward / reload / address bar; content runs in an **opaque-origin sandboxed iframe** (live sandbox status in the UI, per-page temporary unlock available); external-link clicks in the chat can be taken over into the sidebar (protocol-based routing, configurable).
|
||||
|
||||
<div align="center"><img width="880" alt="Embedded browser" src="https://github.com/user-attachments/assets/9bc6b65a-64fc-4942-a685-76e391e55606" /></div>
|
||||
|
||||
### 🧩 Tasks: Agent Topology + Background Jobs
|
||||
|
||||
Live subagent-tree topology (run states, batched live previews) plus the background-jobs list (exit codes / live output / force-kill); new subagents / jobs can auto-expand the sidebar (configurable).
|
||||
|
||||
<div align="center"><img width="880" alt="Tasks: subagent topology" src="https://github.com/user-attachments/assets/dcd8ed2f-59fa-405b-937b-2d250f5034dd" /></div>
|
||||
|
||||
### 💬 Side Chat (beta)
|
||||
|
||||
Codex-style side threads: **one independent tab per conversation**; the thread inherits the parent's full context (including the in-progress turn, honestly frozen as "interrupted") and runs independently without polluting the main session; follow-ups survive restarts; one click promotes the thread to a top-level session.
|
||||
|
||||
<div align="center"><img width="880" alt="Side Chat (beta)" src="https://github.com/user-attachments/assets/3a338c36-f5de-4000-95f3-4b1cd04f60fc" /></div>
|
||||
|
||||
### 🪟 Dual Workbench: Sidebar + Bottom Panel + Split Panes
|
||||
|
||||
The right sidebar and the bottom panel can stay open together; drag a tab to a pane edge to **split**, to the middle to **merge** (works across panels); panel width/height drag from the left/top edge; on mobile everything merges into a full-width drawer.
|
||||
|
||||
<div align="center"><img width="880" alt="Dual workbench (right sidebar + bottom panel)" src="https://github.com/user-attachments/assets/dfdb875e-a1a8-4d4b-8340-353736b1708f" /></div>
|
||||
|
||||
### ⚙️ Declarative Settings
|
||||
|
||||
The "Side card" section in DSH settings: one small card per tab / viewer with an independent toggle (highlighted enabled state + brand switch); secondary settings open from the "Feature settings" strip at the card bottom (switch / text / number / select rows); plugin-owned settings persist under `pluginSettings`.
|
||||
|
||||
<div align="center"><img width="880" alt="Declarative settings: side cards" src="https://github.com/user-attachments/assets/0800ca64-621e-48da-b7df-aecfddc3ec29" /></div>
|
||||
|
||||
### 📱 Mobile
|
||||
|
||||
On narrow screens (<768px) the panels become a full-width drawer: bottom-panel tabs merge into the sidebar once, with touch-friendly dragging.
|
||||
|
||||
<div align="center"><img width="360" alt="Mobile full-width drawer" src="https://github.com/user-attachments/assets/a82ba78a-f4cf-4d85-80e8-050a05beb144" /></div>
|
||||
|
||||
## 🌐 Plugin Ecosystem
|
||||
|
||||
The `ctx.betterSidebar` service opens two extension points to every plugin: **`registerTab` (sidebar pages)** and **`registerFileViewer` (file previewers)**. The 7 built-in tabs + 6 viewers register through the exact same API — fully equal capabilities.
|
||||
|
||||
```ts
|
||||
import type {} from 'dsh-better-sidebar' // triggers the ctx.betterSidebar type merge
|
||||
export const inject = ['betterSidebar']
|
||||
export function apply(ctx: Context) {
|
||||
ctx.effect(() => ctx.betterSidebar.registerTab({
|
||||
id: 'my-plugin:db', title: 'Database', component: ({ scope }) => <DbView sessionId={scope.sessionId} />,
|
||||
}))
|
||||
ctx.effect(() => ctx.betterSidebar.registerFileViewer({
|
||||
id: 'my-plugin:csv', exts: ['csv'], fetchStrategy: 'custom',
|
||||
load: async (path, scope) => parseCsv(await fetchText(scope, path)),
|
||||
component: ({ customData }) => <CsvGrid rows={customData} />,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
The GitHub topic [`dsh-better-sidebar`](https://github.com/topics/dsh-better-sidebar) already hosts **28+ ecosystem plugins** (and growing):
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e"><img width="66%" alt="The built-in Add Plugins modal: recommended catalog + one-click install command" src="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e" /></a><br />
|
||||
<i>The built-in "Add plugins" modal in settings: recommended catalog + one-click install command + a direct link to the GitHub topic</i>
|
||||
</div>
|
||||
|
||||
### 📑 Tab Plugins (sidebar pages)
|
||||
|
||||
<details>
|
||||
<summary><b>23 plugins (click to expand)</b></summary>
|
||||
|
||||
| Plugin | ⭐ | Description |
|
||||
|---|---|---|
|
||||
| [ChenRuoT/dsh-sidebar-qa](https://github.com/ChenRuoT/dsh-sidebar-qa) | <img alt="stars" src="https://img.shields.io/github/stars/ChenRuoT/dsh-sidebar-qa?style=flat&color=4d6bfe" /> | Selection-based side Q&A — Codex-style side questions / Claude Code `/btw` |
|
||||
| [fuhefei/dsh-sentinel](https://github.com/fuhefei/dsh-sentinel) | <img alt="stars" src="https://img.shields.io/github/stars/fuhefei/dsh-sentinel?style=flat&color=4d6bfe" /> | Condition-driven wakeup: file / command / HTTP / process / webhook watches that wake the agent; dock + sidebar branch + global dashboard |
|
||||
| [jiuge2467/dsh-studio](https://github.com/jiuge2467/dsh-studio) | <img alt="stars" src="https://img.shields.io/github/stars/jiuge2467/dsh-studio?style=flat&color=4d6bfe" /> | Full-stack enhancement workbench: multi-source MCP visual debugging hub, visual thinking engine |
|
||||
| [Iwctwbh/dsh-flowglass](https://github.com/Iwctwbh/dsh-flowglass) | <img alt="stars" src="https://img.shields.io/github/stars/Iwctwbh/dsh-flowglass?style=flat&color=4d6bfe" /> | Flowglass: live session flowgraph (messages / tool groups / subagent branches) |
|
||||
| [FeatherHunter/dsh-mattpocock-skills-deck](https://github.com/FeatherHunter/dsh-mattpocock-skills-deck) | <img alt="stars" src="https://img.shields.io/github/stars/FeatherHunter/dsh-mattpocock-skills-deck?style=flat&color=4d6bfe" /> | Game-like mission system for mattpocock/skills: fog-of-war map + task bar |
|
||||
| [GULI-lab/DSH-element-source](https://github.com/GULI-lab/DSH-element-source) | <img alt="stars" src="https://img.shields.io/github/stars/GULI-lab/DSH-element-source?style=flat&color=4d6bfe" /> | Click any UI element on your dev page to jump to its Vue / React / Svelte / Angular source, straight into the chat |
|
||||
| [Lzh3070/dsh-file-review-tab](https://github.com/Lzh3070/dsh-file-review-tab) | <img alt="stars" src="https://img.shields.io/github/stars/Lzh3070/dsh-file-review-tab?style=flat&color=4d6bfe" /> | File-change review tab: line-level red/green diffs + undo + chat-line deep links |
|
||||
| [yq04/dsh-git-remotes](https://github.com/yq04/dsh-git-remotes) | <img alt="stars" src="https://img.shields.io/github/stars/yq04/dsh-git-remotes?style=flat&color=4d6bfe" /> | Git remotes tab: branches / upstream / ahead-behind, fetch with prune, ff-only pull, confirm-before-push |
|
||||
| [ztyhehe/dsh-better-sidebar-svn](https://github.com/ztyhehe/dsh-better-sidebar-svn) | <img alt="stars" src="https://img.shields.io/github/stars/ztyhehe/dsh-better-sidebar-svn?style=flat&color=4d6bfe" /> | SVN source-control tab: status / diff / log / commit / update / revert / conflict resolution — symmetric to the built-in Git panel |
|
||||
| [Melody-max114/dsh-excel-panel](https://github.com/Melody-max114/dsh-excel-panel) | <img alt="stars" src="https://img.shields.io/github/stars/Melody-max114/dsh-excel-panel?style=flat&color=4d6bfe" /> | Excel editing: xlsx preview/edit, live formula evaluation, merged cells, save back to the original file |
|
||||
| [v587d/dsh-anysearch-refs](https://github.com/v587d/dsh-anysearch-refs) | <img alt="stars" src="https://img.shields.io/github/stars/v587d/dsh-anysearch-refs?style=flat&color=4d6bfe" /> | AnySearch results as sidebar cards: query, source snippets, highlighted keywords |
|
||||
| [mlosun/dsh-docs-panel](https://github.com/mlosun/dsh-docs-panel) | <img alt="stars" src="https://img.shields.io/github/stars/mlosun/dsh-docs-panel?style=flat&color=4d6bfe" /> | Global docs panel: portable Markdown notes, readable from any workspace |
|
||||
| [lnyuqian/dsh-skill-sidebar](https://github.com/lnyuqian/dsh-skill-sidebar) | <img alt="stars" src="https://img.shields.io/github/stars/lnyuqian/dsh-skill-sidebar?style=flat&color=4d6bfe" /> | Skills panel: scans local skill directories, one-click invocation copy, pinning |
|
||||
| [g-yixuan/dsh-sidechat](https://github.com/g-yixuan/dsh-sidechat) | <img alt="stars" src="https://img.shields.io/github/stars/g-yixuan/dsh-sidechat?style=flat&color=4d6bfe" /> | Codex-style side chat + selection annotations (a thin consumer plugin) |
|
||||
| [thirsty5034/dsh-ssh-tunnel](https://github.com/thirsty5034/dsh-ssh-tunnel) | <img alt="stars" src="https://img.shields.io/github/stars/thirsty5034/dsh-ssh-tunnel?style=flat&color=4d6bfe" /> | Multi-host SSH tunnels + SSH manager tab |
|
||||
| [thirsty5034/dsh-git-forge](https://github.com/thirsty5034/dsh-git-forge) | <img alt="stars" src="https://img.shields.io/github/stars/thirsty5034/dsh-git-forge?style=flat&color=4d6bfe" /> | GitHub / Gitea accounts, project grants and push policy |
|
||||
| [YesSanSan/dsh-conversation-outline](https://github.com/YesSanSan/dsh-conversation-outline) | <img alt="stars" src="https://img.shields.io/github/stars/YesSanSan/dsh-conversation-outline?style=flat&color=4d6bfe" /> | Conversation outline tab: per-turn structure, quick jump, one-line LLM titles |
|
||||
| [Wulabalabo/dsh-sidebar-Explorer-Plus](https://github.com/Wulabalabo/dsh-sidebar-Explorer-Plus) | <img alt="stars" src="https://img.shields.io/github/stars/Wulabalabo/dsh-sidebar-Explorer-Plus?style=flat&color=4d6bfe" /> | File-manager tab: upload / move / delete / rename / new folder (write operations) |
|
||||
| [yq04/dsh-turn-review](https://github.com/yq04/dsh-turn-review) | <img alt="stars" src="https://img.shields.io/github/stars/yq04/dsh-turn-review?style=flat&color=4d6bfe" /> | Turn review: review agent changes turn by turn |
|
||||
| [Ghz114514/dsh-refpics](https://github.com/Ghz114514/dsh-refpics) | <img alt="stars" src="https://img.shields.io/github/stars/Ghz114514/dsh-refpics?style=flat&color=4d6bfe" /> | Pinterest-style reference-image search: masonry wall, sidebar board, downloads, save-to-Eagle |
|
||||
| [yzlin499/dsh-yzlin499-easy-plugins](https://github.com/yzlin499/dsh-yzlin499-easy-plugins) | <img alt="stars" src="https://img.shields.io/github/stars/yzlin499/dsh-yzlin499-easy-plugins?style=flat&color=4d6bfe" /> | A handy utility bundle for a bare-bones DSH |
|
||||
| [dong-victor/dsh-better-sidebar-starter](https://github.com/dong-victor/dsh-better-sidebar-starter) | <img alt="stars" src="https://img.shields.io/github/stars/dong-victor/dsh-better-sidebar-starter?style=flat&color=4d6bfe" /> | Run-configurations tab: IDEA-style Run/Debug configs (npm / springboot / python / custom) — one-click launch, history, WebSocket live logs (ANSI colors), parallel instances, cross-platform process-tree kill |
|
||||
| [baosfeng/my-dsh-plugins](https://github.com/baosfeng/my-dsh-plugins) | <img alt="stars" src="https://img.shields.io/github/stars/baosfeng/my-dsh-plugins?style=flat&color=4d6bfe" /> | Personal multi-plugin collection (`dsh-file-activity`): a sidebar file-activity tab recording read / added / modified history and stats, flat-browsed by folder, opened with the native preview |
|
||||
|
||||
</details>
|
||||
|
||||
### 🖼️ Viewer Plugins (file previewers)
|
||||
|
||||
<details>
|
||||
<summary><b>3 plugins (click to expand)</b></summary>
|
||||
|
||||
| Plugin | ⭐ | Description |
|
||||
|---|---|---|
|
||||
| [HuanLinOTO/dsh-plugin-better-sidebar-plugin-office](https://github.com/HuanLinOTO/dsh-plugin-better-sidebar-plugin-office) | <img alt="stars" src="https://img.shields.io/github/stars/HuanLinOTO/dsh-plugin-better-sidebar-plugin-office?style=flat&color=4d6bfe" /> | Office-suite preview (.docx / .xlsx / .pptx) as a separate bundle to slim the core (in the official recommended catalog) |
|
||||
| [zemul/dsh-video-preview](https://github.com/zemul/dsh-video-preview) | <img alt="stars" src="https://img.shields.io/github/stars/zemul/dsh-video-preview?style=flat&color=4d6bfe" /> | Inline video preview: .mp4 / .webm / .mov / .mkv / .avi with a /video host route supporting HTTP Range scrubbing |
|
||||
| [dong-victor/dsh-better-sidebar-jupyter](https://github.com/dong-victor/dsh-better-sidebar-jupyter) | <img alt="stars" src="https://img.shields.io/github/stars/dong-victor/dsh-better-sidebar-jupyter?style=flat&color=4d6bfe" /> | Runnable `.ipynb` notebook view: lazy-start Python kernel, streaming outputs, save-back |
|
||||
|
||||
</details>
|
||||
|
||||
### 🧰 Enhancements & Tools
|
||||
|
||||
<details>
|
||||
<summary><b>2 plugins (click to expand)</b></summary>
|
||||
|
||||
| Plugin | ⭐ | Description |
|
||||
|---|---|---|
|
||||
| [dong-victor/dsh-better-sidebar-terminal-plus](https://github.com/dong-victor/dsh-better-sidebar-terminal-plus) | <img alt="stars" src="https://img.shields.io/github/stars/dong-victor/dsh-better-sidebar-terminal-plus?style=flat&color=4d6bfe" /> | Terminal enhancement: bundled Nerd Font icons, xterm glyph fixes, stable terminal cwd |
|
||||
| [Max-Null/dsh-sidebar-preview-select](https://github.com/Max-Null/dsh-sidebar-preview-select) | <img alt="stars" src="https://img.shields.io/github/stars/Max-Null/dsh-sidebar-preview-select?style=flat&color=4d6bfe" /> | Preview selection boost: select text in any sidebar preview → floating "send to session" |
|
||||
|
||||
</details>
|
||||
|
||||
> 📣 **List your plugin**: tag your repo with the `dsh-better-sidebar` topic to appear on the [topic page](https://github.com/topics/dsh-better-sidebar); then PR one `PluginEntry` into [`src/client/plugins-tabs.ts`](./src/client/plugins-tabs.ts) / [`src/client/plugins-viewers.ts`](./src/client/plugins-viewers.ts) to join the built-in recommended catalog (data integrity is guarded by `tests/plugin-list.spec.ts`).
|
||||
|
||||
## 🆕 Recent Updates
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/user-attachments/assets/d2aea86b-a776-4f01-a6b8-b26b27314336"><img width="33%" alt="Sidebar" src="https://github.com/user-attachments/assets/d2aea86b-a776-4f01-a6b8-b26b27314336" /></a>
|
||||
<a href="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0"><img width="33%" alt="Service API base screenshot" src="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0" /></a>
|
||||
</div>
|
||||
|
||||
**Supported DSH versions**: <a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="Supported DSH versions: 0.1.0-rc.8 · 0.1.1-rc.1 · 0.1.1-rc.2" src="https://img.shields.io/badge/DSH-0.1.0--rc.8_%C2%B7_0.1.1--rc.1_%C2%B7_0.1.1--rc.2-4d6bfe" /></a> · full release history on the [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases) page
|
||||
|
||||
### v0.15.1
|
||||
|
||||
All changes since v0.15.0:
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 💬 **Codex-style transcript rework for Side Chat** ([#314](https://github.com/omdsh-dev/DSH-better-sidebar/pull/314)): transcript rows became **collapsible** — tool calls, thinking and context injections share one quiet single-line chrome (chevron + label + one-line argument summary) that expands into an indented body on a hairline thread, no cards or fills; streaming labels and a creating hero shimmer (shimmer = generating), failed tools go danger, `prefers-reduced-motion` stills every loop; **the first question is no longer swallowed by the boundary prompt** — context injection and first contact are delivered as separate events (boundary + parked snapshot ride `agent.inject`, the question wakes the driver), so the transcript maps injections onto a collapsible injection row while genuine user messages — the first one included — render as user bubbles; legacy threads' first message is split out into its own bubble too
|
||||
- 📖 **README rewrite**: feature tour (real UI screenshots per feature), user-facing DSH compatibility badges, simplified install (`add` → `approve-builds` → `add`, node-pty-safe build, paste-to-DSH install prompt), 28+ plugin ecosystem with per-category collapsed listings
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- 🖥️ **Terminal pty stays alive across conversation switches** ([#323](https://github.com/omdsh-dev/DSH-better-sidebar/pull/323)): switching sessions is no longer treated as a transient drop — the client sends a `park` control frame on unmount and the host skips the 30s reconnect-grace countdown; switching back (`open()` cancels parked) or explicitly closing the tab resumes the normal lifecycle; agent terminals keep their indefinite lifetime
|
||||
- 📂 **File-tree upload overlay no longer intercepts Tab drags** ([#317](https://github.com/omdsh-dev/DSH-better-sidebar/pull/317)): dragging tabs (reorder / cross-pane split) across the explorer no longer shows the upload overlay or swallows the event — gated on `dataTransfer.types` containing `Files` (consistent with the panel-host shield), so tabs land normally; OS file drags behave as before
|
||||
- 💬 **Subagent auto-open debounced** ([#314](https://github.com/omdsh-dev/DSH-better-sidebar/pull/314)): Side Chat thread creation no longer pops the task page — the 0→N trigger rearms for 500ms and re-evaluates the original baseline against the live snapshot, by which time the title filter recognizes the thread; genuine subagents still auto-open
|
||||
|
||||
### v0.15.0
|
||||
|
||||
All changes since v0.14.0:
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 💬 **Side Chat (beta) tab** ([#286](https://github.com/omdsh-dev/DSH-better-sidebar/pull/286)): Codex-style side threads, **one independent tab per conversation** — the child inherits the parent's full context (completed turns + pending messages + the in-progress turn's assistant output and tool activity, honestly frozen with an "interrupted" marker); created with an identical composition (same preset / provider / model) so the first request reuses the parent's input prefix cache; threads stay invisible in the main session list with zero subagent-catalog noise; follow-ups survive DSH restarts (auto cold-resume); one-click "Save as new session" promotes the thread to a top-level session ([design](docs/plans/2026-08-20-sidechat-tab-design.md))
|
||||
- 📤 **Upload into the files window** ([#239](https://github.com/omdsh-dev/DSH-better-sidebar/pull/239)): header "upload file / upload folder" buttons plus drag-drop (drop on the tree body = workspace root, on a directory row = that directory, on a file row = its parent directory, VSCode semantics); full-window blurred progress overlay while uploading (per-file progress + cancel / Esc); buttons disabled while busy, tree refreshes after the upload settles
|
||||
- 🧩 **Desktop compatibility in four options** ([#284](https://github.com/omdsh-dev/DSH-better-sidebar/pull/284)): "Position compatibility mode" is now a main-row dropdown — **Auto-detect** (default, conservative: only the standard Window Controls Overlay geometry contributes; real 32/36px caption-overlay heights per shell, live on maximize/restore; zero modification on plain web) / **DSH official web** (explicitly no adaptation) / **Shell preset** (built-in, opt-in; only shells that appeared in this repo's issues/PRs with 100+ stars, "detected" badge when the environment matches) / **Custom** (free-form CSS + shift distance). Documents that already carried compatibility values migrate to the custom scheme; interactive chrome opts out of desktop drag regions (`no-drag`); the bottom-push anchor is a composite selector (`[data-pane]` and `:has(> [data-slot])`)
|
||||
- 🎛️ **Settings page UI/UX modernization** ([#300](https://github.com/omdsh-dev/DSH-better-sidebar/pull/300)): the side-card secondary-settings entry is now a full-width "Feature settings" strip at the card bottom (replacing the invisible corner gear — much easier to discover); coordinated two-tone enabled state (brand activation accent + success-green check badge); every color is still `--dsw-alias-*` token-derived so skins follow automatically
|
||||
- ➕ **New entries in the recommended-plugin catalog**: `dsh-docs-panel` (global docs, [#230](https://github.com/omdsh-dev/DSH-better-sidebar/pull/230)), `dsh-flowglass` ([#261](https://github.com/omdsh-dev/DSH-better-sidebar/pull/261)), `dsh-git-forge` and `dsh-ssh-tunnel` ([#204](https://github.com/omdsh-dev/DSH-better-sidebar/pull/204)), `dsh-turn-review` ([#102](https://github.com/omdsh-dev/DSH-better-sidebar/pull/102))
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- ⚡ **Batched live preview for the subagent page** ([#298](https://github.com/omdsh-dev/DSH-better-sidebar/pull/298)): the old implementation polled `subagents.history` per running subagent, each poll triggering a full host-side enumeration — an O(N²) amplification that stalled the page with many concurrent subagents; now a single batch route `subagents.live` (one enumeration of the whole tree) plus one client poller with a single in-flight request; display logic and copy unchanged
|
||||
- 🖱️ **Interrupted / fast-release drags no longer roll back** ([#249](https://github.com/omdsh-dev/DSH-better-sidebar/pull/249), closes [#247](https://github.com/omdsh-dev/DSH-better-sidebar/issues/247) [#248](https://github.com/omdsh-dev/DSH-better-sidebar/issues/248)): interrupted or fast-released drags commit the last known position; HMR re-activation re-locates the center column (fixes the blank bottom panel after a hot reload)
|
||||
- 📐 **Push variables stay effective while mounted** ([#259](https://github.com/omdsh-dev/DSH-better-sidebar/pull/259), fixes [#258](https://github.com/omdsh-dev/DSH-better-sidebar/issues/258)): the bottom panel no longer flashes full-width after a drag is released
|
||||
- 🔧 **Adapted to DSH 0.1.1-rc.1 / rc.2 (@next)** ([#297](https://github.com/omdsh-dev/DSH-better-sidebar/pull/297) [#305](https://github.com/omdsh-dev/DSH-better-sidebar/pull/305)): no code changes needed
|
||||
- 🔒 **Upload-chain hardening** ([#239](https://github.com/omdsh-dev/DSH-better-sidebar/pull/239)): empty and absolute `relativePath` segments are refused outright; uniquely named temp files (concurrent uploads stay independent, crashed processes never block later uploads); write-stream error listeners (a failing disk can no longer crash the host); client error codes unified with the wire (`too-large`), 413s localized
|
||||
|
||||
<details>
|
||||
<summary><b>Older releases (v0.12.0 – v0.14.0)</b></summary>
|
||||
|
||||
### v0.14.0
|
||||
|
||||
> ⚠️ This release requires DSH ≥ 0.1.0-rc.8. All changes since v0.13.1:
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 🖼️ **Unified panel-host injection refactor** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): panels/toggle clusters moved into a `[data-dsh-panel-host]` fixed containing block (`fixed inset-0 z-40`), immune to desktop-shell intermediate transforms hijacking `fixed`; mount self-check (page-level transform → `data-dsh-panel-host-degraded` degraded sync, judged on uncorrected geometry, exits only when the ancestor transform is gone); push anchor switched to `#root [data-dsh-frame] > [data-pane="conversation"]` + `#root` calc width against desktop-shell additive overflow; chunk revalidation on activation (HEAD+ETag keeps unchanged chunks, 5s timeout fails open); `visualViewport` keyboard inset + `env(safe-area-inset-*)` mobile adaptation
|
||||
- 📂 **Separate file windows by default** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): `editorExplorer` now defaults to **separate** — tree clicks / file opens create a new tab per path and the path-less window is a pure file manager; merged mode stays available as an opt-in
|
||||
- 🖥️ **Terminal shell / shellArgs configurable from the settings page** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): the terminal card's gear popup gains "Shell path" and "Shell arguments" rows (previously yaml-only via `cordis.patch.yml`) — saved values take effect immediately for terminals opened afterwards (UI terminals and model `terminal_create` alike); empty keeps the existing yaml → `$SHELL` / login shell / `powershell.exe` resolution order
|
||||
- 🏷️ **Version badge on the settings page** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): the side-card settings section now opens with a `DSH-better-sidebar v0.14.0` identity badge (version synced with the service instance, test-guarded)
|
||||
- 🔍 **Add-plugin catalog: search / grouping / independent scroll** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): built for a growing plugin ecosystem — a live search box (filters by name / id / description), optional `category` grouping for entries, and an independently scrolling list (the modal no longer grows unbounded with catalog size)
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- 🧩 **rc.8 module-system migration** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): rc.8 no longer exposes the `window.__DSH_MODULES__` page global (it moved to the `ctx.modules` service), which broke every lazy chunk's externals resolution — the client now injects the `modules` service and shares it with chunk-bundle copies through a plugin-owned global (terminal / editor / Mermaid on-demand loading restored)
|
||||
- 🧩 **Chunk revalidation barrier hardening** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): HEAD revalidation gains a 5s timeout (fails open on a stuck route so the barrier can never wedge lazy loads); `resetChunks` clears a pending revalidation barrier
|
||||
- 🖱️ **Drag robustness** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): fast releases (browsers merge/lose pointermove bursts) commit the last known dragged position instead of rolling back; `pointercancel` / lost-capture interruptions keep the drag result too; the center column is re-measured right after commit (no mid-frame bottom-panel width jump); HMR re-activation re-locates the center column via an `<html>` style observer plus a retry when the bottom panel opens (fixes the blank bottom panel / shifted input bar after a hot reload)
|
||||
|
||||
### v0.13.1
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 📊 **Safe Mermaid rendering in the Markdown preview** ([#164](https://github.com/omdsh-dev/DSH-better-sidebar/pull/164)): when a previewed md file contains mermaid fences, a `client-mermaid.js` chunk (~7MB) is served on demand (zero load without mermaid); defense-in-depth rendering — `securityLevel: 'strict'` + `htmlLabels: false` (node labels use real SVG `<text>`) + a second sanitize pass before SVG injection (foreignObject/script/foreign HTML elements removed, `@*`/`on*`/`href` attributes stripped); click a diagram to zoom in a modal overlay (wheel zoom centered on the cursor, drag pan, toolbar & shortcuts), re-renders with light/dark theme, falls back to the raw code block on parse failure
|
||||
- 🖥️ **Configurable terminal shell & shellArgs** ([#125](https://github.com/omdsh-dev/DSH-better-sidebar/pull/125)): `cordis.patch.yml` `better-sidebar.config` can set `shell` / `shellArgs` (a non-empty `shellArgs` fully replaces the defaults; unset keeps the previous auto-resolution of `$SHELL` / login shell / `powershell.exe`), applied to both UI terminals and agent terminals (`terminal_create`); terminal tab titles now show the shell name (bash/zsh/powershell) and internal tab ids use UUIDs so the same shell can open multiple terminals
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- 🔗 **Aggregate double-mount auto-yield** ([#200](https://github.com/omdsh-dev/DSH-better-sidebar/pull/200)): when an aggregate package (e.g. dsh-web-ui-all) mounts the same package under its own entry id, the guard expression in `cordis.patch.yml` disables the plugin's own `better-sidebar` row so `/sidebar/api` is no longer registered twice (`duplicate prefix route` crashing the whole plugin tree / `dsh web`); standalone installs behave as before
|
||||
- 🔧 **Adapted to DSH 0.1.0-rc.7** ([#207](https://github.com/omdsh-dev/DSH-better-sidebar/pull/207), fixes [#206](https://github.com/omdsh-dev/DSH-better-sidebar/issues/206)): fixes the `agent-presets: refusing to compose an unscoped context` error when picking a model / sending a message after DSH moved to rc.7
|
||||
|
||||
### v0.13.0
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 📁 **Files window merged with the explorer** ([#151](https://github.com/omdsh-dev/DSH-better-sidebar/pull/151)): new `editorExplorer` setting (editor card gear) — file tabs gain a path-input header plus a toggleable right-docked file tree (per-tab open/width memory, drag-resize 160–480px from the left edge, global filename search via the host `fs.search` route with a hard budget, skipping `.git` and symlink dirs); in separate mode (default) tree clicks / Enter in the path input open each file **in its own new tab**, merged mode switches the current tab **in place**; fresh sessions seed an empty Files window instead of the explorer tab, and a path-less window is a bare file manager in separate mode / a chrome'd empty file window in merged mode; the tree context menu offers "Open in new tab" and "Open to the side" (split)
|
||||
- 🎛️ **Select rows for declarative settings** ([#151](https://github.com/omdsh-dev/DSH-better-sidebar/pull/151)): settings rows gain `type: 'select'` (`options` with value/title/desc/icon, `multi` stores the picked values as an array); options with icons render big-icon option cards and keep the icon in the closed anchor; `editorExplorer` became an iconed select (merged vs separate); the capability list gained `settingSelect`
|
||||
- 🔀 **Mutual exclusion with the dsh-web-ui family right panel** ([#181](https://github.com/omdsh-dev/DSH-better-sidebar/pull/181)): reads the `aionui-panel` settings namespace's provider choice — when "Use aionui-panel" is selected, the whole better-sidebar (right sidebar / bottom panel / floating entry / all takeovers) does not mount; with DSH-better-sidebar (or no aionui installed) it behaves as before. Takes effect live after a settings save (settings-document push), no reload needed
|
||||
|
||||
### v0.12.3
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 🎨 **Skin compatibility (token-driven)**: fully consumes DSH design tokens and follows the dsh-web-ui skin center's 10 skins automatically; terminal/editor surfaces fall back to opaque backgrounds under transparent/translucent-glass token values so text never scrolls over the skin art ([#110](https://github.com/omdsh-dev/DSH-better-sidebar/pull/110), fixes #106 #105 #90 #60, also #52 #57 #92)
|
||||
- 🗂️ **Unified path handling**: UNC / symlink classification (directory symlinks expandable, broken links highlighted) + HTML-route platform guards ([#134](https://github.com/omdsh-dev/DSH-better-sidebar/pull/134), #65 #67 #43 #79 #115)
|
||||
- 🖥️ **Configurable terminal shell**: custom shell setting with Windows pwsh auto-probe ([#95](https://github.com/omdsh-dev/DSH-better-sidebar/pull/95))
|
||||
- 📝 **Editor languages**: C# / Kotlin / Swift syntax highlighting ([#120](https://github.com/omdsh-dev/DSH-better-sidebar/pull/120))
|
||||
- 🧭 **Settings nav icon**: settings-page navigation icon and layout polish ([#114](https://github.com/omdsh-dev/DSH-better-sidebar/pull/114))
|
||||
- ➕ **Recommended-plugin catalog**: added `dsh-git-remotes` — Git Remotes tab (branches/upstream/ahead-behind, fetch with prune, ff-only pull, confirm-before-push; does not replace the built-in stage/commit tab) ([#91](https://github.com/omdsh-dev/DSH-better-sidebar/pull/91)); and `dsh-video-preview` — inline video preview (.mp4/.webm/.mov/.mkv/.avi etc.) backed by a /video host route with HTTP Range (206) scrubbing, not capped by the 20MB mediaLimit ([#126](https://github.com/omdsh-dev/DSH-better-sidebar/pull/126))
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- 🔧 **xterm migration**: deprecated xterm dependency migrated to `@xterm/xterm` (Closes [#122](https://github.com/omdsh-dev/DSH-better-sidebar/issues/122), [#128](https://github.com/omdsh-dev/DSH-better-sidebar/pull/128))
|
||||
- 📝 **Markdown editor**: selection-to-conversation popup restored ([#24](https://github.com/omdsh-dev/DSH-better-sidebar/pull/24))
|
||||
- 🐛 **node-pty load failure no longer crashes the server** ([#140](https://github.com/omdsh-dev/DSH-better-sidebar/issues/140)): the host half now lazy-loads node-pty — when it is missing the plugin still mounts, the terminal shows a repair banner (copyable command + Retry button), and agent terminal tools are skipped
|
||||
- 🧪 Test engineering: unit spec split (#141) + flaky smoke cleanup fix
|
||||
|
||||
</details>
|
||||
|
||||
## ⌨️ Keyboard Shortcuts
|
||||
|
||||
| Action | Keys |
|
||||
@@ -241,21 +417,9 @@ Update: `git pull && pnpm install && pnpm build` → `node scripts/package-regis
|
||||
| Reference file to input | Hover the `@file` button at end of line |
|
||||
| Copy file path | Right-click row → copy relative/absolute path |
|
||||
|
||||
## 🔌 Service: register tabs & file viewers
|
||||
## 🔌 Service API
|
||||
|
||||
Since v0.4.0 the plugin exposes the `ctx.betterSidebar` service — other plugins can register sidebar pages and file viewers (the 7 built-in tabs + 6 viewers register through the same service):
|
||||
|
||||
```ts
|
||||
import type {} from 'dsh-better-sidebar' // triggers the ctx.betterSidebar type merge
|
||||
export const inject = ['betterSidebar']
|
||||
export function apply(ctx: Context) {
|
||||
ctx.effect(() => ctx.betterSidebar.registerTab({
|
||||
id: 'my-plugin:db', title: 'Database', component: ({ scope }) => <DbView sessionId={scope.sessionId} />,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
v0.12.1+ base capabilities (complete type exports, capability detection, state subscription, tab badge, lifecycle callbacks, targeted open, plugin-owned settings, etc.) — see the integration docs below.
|
||||
Since v0.4.0 the plugin exposes the `ctx.betterSidebar` service — other plugins can register sidebar pages and file viewers (the 7 built-in tabs + 6 viewers register through the same service). v0.12.1 completed the base capabilities (complete type exports, capability detection, state subscription, tab badges, lifecycle callbacks, targeted open, plugin-owned settings, etc.).
|
||||
|
||||
Full integration docs:
|
||||
- **[`AGENTS.md`](./AGENTS.md)** — the in-repo integration doc (full fields, matching algorithm, HMR pitfalls, declarative settings, version detection);
|
||||
@@ -298,9 +462,52 @@ pnpm watch # tsdown --watch
|
||||
|
||||
Windows / Linux / macOS (macOS validated daily; the rest covered by unit tests); `node-pty` prefers prebuilt binaries, otherwise a build toolchain is required (Windows VS Build Tools / Linux make+g+++python3 / macOS Xcode CLT).
|
||||
|
||||
## 💬 Community
|
||||
|
||||
WeChat / QQ group QR codes will live here. After uploading the QR images (drag them into any issue/comment to get a `user-attachments` link), replace `src` below and uncomment:
|
||||
|
||||
<div align="center">
|
||||
<!-- WeChat group QR code
|
||||
<img width="220" alt="WeChat group QR code" src="https://github.com/user-attachments/assets/REPLACE_ME" />
|
||||
-->
|
||||
<!-- QQ group QR code
|
||||
<img width="220" alt="QQ group QR code" src="https://github.com/user-attachments/assets/REPLACE_ME" />
|
||||
-->
|
||||
</div>
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
- **Code changes go through PRs**: develop on a `feat/*` / `fix/*` branch, then `gh pr create`; docs-only changes may be pushed to main directly
|
||||
- **Curate an ecosystem plugin**: tag your repo with `dsh-better-sidebar` + PR a `PluginEntry` into [`src/client/plugins-tabs.ts`](./src/client/plugins-tabs.ts) / [`plugins-viewers.ts`](./src/client/plugins-viewers.ts)
|
||||
- **Before submitting**: `pnpm typecheck && pnpm build && pnpm test` (CI additionally gates on npm-pack → real-mount → headless-render via `pnpm test:mount`)
|
||||
- See [`AGENTS.md`](./AGENTS.md) for the repository rules (hard constraints, CI lanes, release flow)
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
<a href="https://star-history.com/#omdsh-dev/DSH-better-sidebar&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=omdsh-dev/DSH-better-sidebar&type=Date&theme=dark" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=omdsh-dev/DSH-better-sidebar&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
Thanks to everyone who contributed:
|
||||
|
||||
<a href="https://github.com/omdsh-dev/DSH-better-sidebar/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=omdsh-dev/DSH-better-sidebar" alt="Contributors" />
|
||||
</a>
|
||||
|
||||
## 🔗 Friends
|
||||
|
||||
- [dsh-tianshu-tui](https://github.com/huiliyi37/dsh-tianshu-tui): an interactive terminal UI plugin for DeepSeek Harness (its rendering core evolved from the self-developed harness agent Tianshu-Tui), adding TDD and evidence-gate workflows on top of the official harness
|
||||
- [dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI): a Claude Code-style fullscreen interactive TUI plugin — pixel-whale top bar, live working-status row, streaming thought expansion, double-Esc rollback, context progress bar + TPS meter; one-command npm install
|
||||
- [dshfind Plugin Market](https://dshfind.com/zh/plugins): a third-party plugin marketplace — a listing of public repos under the GitHub topic `dsh-plugin`, with stars, contributors and growth data synced daily
|
||||
- [DeepSeek Harness Desktop](https://github.com/anywhere-labs/deepseek-harness-desktop): a modern desktop client for the DeepSeek Harness ecosystem — start and manage a local Harness service without configuring Node.js or running commands; [official site](https://www.dshdesktop.cn)
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<sub>MIT License · Built for the <a href="https://github.com/deepseek-ai/deepseek-harness">DeepSeek Harness</a> ecosystem · discover more on the <a href="https://github.com/topics/dsh-better-sidebar">dsh-better-sidebar topic</a></sub>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "dsh-external/dsh-better-sidebar",
|
||||
"version": "0.15.0",
|
||||
"version": "0.15.1",
|
||||
"main": "./lib/index.js",
|
||||
"description": "VSCode 风格右侧侧边栏:文件资源管理器 / 编辑器 / 终端 / Git / 浏览器,按会话隔离。暴露服务供其他插件注册侧边栏页面和文件预览器",
|
||||
"engines": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dsh-better-sidebar",
|
||||
"version": "0.15.0",
|
||||
"version": "0.15.1",
|
||||
"description": "DSH web plugin: a VSCode-like right sidebar (explorer / editor / terminal / git / browser), isolated per conversation session. Exposes a service for other plugins to register sidebar tabs and file viewers.",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
|
||||
@@ -52,6 +52,13 @@ function parentOf(path: string): string {
|
||||
return at <= 0 ? path : path.slice(0, at)
|
||||
}
|
||||
|
||||
/** Only OS file drags belong to the upload surface; in-app drags (tab reorder,
|
||||
* split zones) must pass through untouched to the pane's tab-drop handling
|
||||
* (mirror of Sidebar.tsx's panel-host shield gate). */
|
||||
function isFileDrag(event: DragEvent): boolean {
|
||||
return event.dataTransfer?.types.includes('Files') ?? false
|
||||
}
|
||||
|
||||
/** How long the row's "copied" label stays after a successful write. */
|
||||
const COPIED_MS = 1200
|
||||
|
||||
@@ -160,12 +167,14 @@ export function FileTree(props: {
|
||||
})
|
||||
}
|
||||
const handleBodyDrop = (event: DragEvent): void => {
|
||||
if (!isFileDrag(event)) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
resetDrop()
|
||||
if (cwd !== undefined) reportDrop(cwd, event.dataTransfer)
|
||||
}
|
||||
const handleDirDrop = (event: DragEvent, dir: string): void => {
|
||||
if (!isFileDrag(event)) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
resetDrop()
|
||||
@@ -176,6 +185,7 @@ export function FileTree(props: {
|
||||
handleDirDrop(event, parentOf(path))
|
||||
}
|
||||
const handleBodyDragEnter = (event: DragEvent): void => {
|
||||
if (!isFileDrag(event)) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
dropDepth.current += 1
|
||||
@@ -195,6 +205,7 @@ export function FileTree(props: {
|
||||
setDropRect(null)
|
||||
}
|
||||
const handleBodyDragOver = (event: DragEvent): void => {
|
||||
if (!isFileDrag(event)) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.dataTransfer.dropEffect = busy ? 'none' : 'copy'
|
||||
@@ -205,6 +216,7 @@ export function FileTree(props: {
|
||||
setDropTarget(null)
|
||||
}
|
||||
const handleRowDragOver = (event: DragEvent, dir: string): void => {
|
||||
if (!isFileDrag(event)) return
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.dataTransfer.dropEffect = busy ? 'none' : 'copy'
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
* language mirrors the main conversation pane: right-aligned user bubbles
|
||||
* on --dsw-specific-bubble, full-width assistant markdown, a capsule
|
||||
* composer card on --dsw-specific-input-major with a circular accent
|
||||
* send/stop button, and shimmer reserved for the running status line and
|
||||
* executing tools. Motion is crossfade-only (≤200ms entrances, no
|
||||
* springs); reduced-motion disables every loop. All values consume DSW
|
||||
* tokens (fallbacks point at older alias tokens for rc-era hosts). */
|
||||
* send/stop button. Tool calls, thinking and context injections share one
|
||||
* collapsible-line chrome (no cards); shimmer is reserved for generating
|
||||
* states (running status line, streaming rows, the creating hero). Motion
|
||||
* is crossfade-only (≤200ms entrances, no springs); reduced-motion disables
|
||||
* every loop. All values consume DSW tokens (fallbacks point at older alias
|
||||
* tokens for rc-era hosts). */
|
||||
.sidechat {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -172,82 +174,99 @@
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.sidechatReasoning {
|
||||
/* ── collapsible context rows (tool / thinking / injection, Codex-style) ──
|
||||
* One quiet line — chevron + label + one-line summary — that expands into
|
||||
* an indented body hung on a hairline thread. No cards, no fills: hierarchy
|
||||
* rides ink weight alone. Streaming rows shimmer the label (shimmer =
|
||||
* generating); failed rows go danger; reduced-motion stills every loop. */
|
||||
.sidechatRow {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.sidechatReasoningSummary {
|
||||
display: inline-block;
|
||||
.sidechatRowLine {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 1px 0;
|
||||
font: var(--dsw-font-xxs-12);
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sidechatReasoningSummary::-webkit-details-marker {
|
||||
.sidechatRowSummary {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sidechatRowSummary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidechatReasoningBody {
|
||||
margin-top: 2px;
|
||||
padding-left: 2px;
|
||||
.sidechatRowSummary:hover {
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.sidechatRowStatic {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.sidechatRowChevron {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
transition: transform 100ms ease-out;
|
||||
}
|
||||
|
||||
.sidechatRow[open] .sidechatRowChevron {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.sidechatRowLabel {
|
||||
flex: none;
|
||||
max-width: 60%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
.sidechatRowMono {
|
||||
font-family: var(--dsw-font-mono);
|
||||
}
|
||||
|
||||
.sidechatRowMeta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--dsw-font-mono);
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
|
||||
.sidechatRowFailed .sidechatRowLabel,
|
||||
.sidechatRowFailed .sidechatRowMeta {
|
||||
color: var(--dsw-alias-danger);
|
||||
}
|
||||
|
||||
.sidechatRowBody {
|
||||
margin: 2px 0 4px 7px;
|
||||
padding: 2px 0 2px 10px;
|
||||
border-left: 1px solid var(--dsw-alias-hairline);
|
||||
}
|
||||
|
||||
.sidechatRowProse {
|
||||
font: var(--dsw-font-xxs-12);
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidechatTool {
|
||||
align-self: stretch;
|
||||
border: 1px solid var(--dsw-alias-hairline);
|
||||
border-radius: 10px;
|
||||
background: var(--dsw-alias-bg-base);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidechatToolSummary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
font: var(--dsw-font-xxs-12);
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.sidechatToolSummary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidechatToolName {
|
||||
font-family: var(--dsw-font-mono);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Running tool: a light sweep across the summary row (shimmer = generating). */
|
||||
.sidechatToolExecuting {
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
var(--dsw-alias-interactive-bg-hover) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: sidechatSweep 2.6s linear infinite;
|
||||
}
|
||||
|
||||
.sidechatToolFailed {
|
||||
color: var(--dsw-alias-danger);
|
||||
}
|
||||
|
||||
.sidechatToolArgs,
|
||||
.sidechatToolResult {
|
||||
.sidechatRowCode {
|
||||
margin: 0;
|
||||
padding: 6px 10px;
|
||||
border-top: 1px solid var(--dsw-alias-hairline);
|
||||
padding: 4px 0;
|
||||
font: var(--dsw-font-xxs-12);
|
||||
font-family: var(--dsw-font-mono);
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
@@ -257,6 +276,25 @@
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidechatRowCode + .sidechatRowCode {
|
||||
border-top: 1px solid var(--dsw-alias-hairline);
|
||||
}
|
||||
|
||||
/* Shimmer sweep text: generating (streaming row labels, creating hero). */
|
||||
.sidechatShimmerText {
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
var(--dsw-alias-label-tertiary) 0%,
|
||||
var(--dsw-alias-label-primary) 50%,
|
||||
var(--dsw-alias-label-tertiary) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
animation: sidechatSweep 2.6s linear infinite;
|
||||
}
|
||||
|
||||
/* ── running status line (above the composer, main-pane style) ──────── */
|
||||
.sidechatStatus {
|
||||
flex: none;
|
||||
@@ -388,9 +426,12 @@
|
||||
animation: none;
|
||||
}
|
||||
.sidechatStatusText,
|
||||
.sidechatToolExecuting {
|
||||
.sidechatShimmerText {
|
||||
animation: none;
|
||||
background-image: none;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
}
|
||||
.sidechatRowChevron {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
+103
-27
@@ -24,6 +24,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
IconChevronRightOutline14,
|
||||
IconNewChatOutline16,
|
||||
IconPlusOutline16,
|
||||
IconSendOutline16,
|
||||
@@ -43,7 +44,7 @@ import {
|
||||
threadTrailingPending,
|
||||
type SidechatThreadInfo,
|
||||
} from '../sidechat-core.ts'
|
||||
import { collectOwnEvents, transcriptRows, type SidechatTranscriptRow } from './sidechat-transcript.ts'
|
||||
import { collectOwnEvents, toolArgsSummary, transcriptRows, type SidechatTranscriptRow } from './sidechat-transcript.ts'
|
||||
import { api } from './api.ts'
|
||||
import { t } from './locales.ts'
|
||||
import type { SessionScope } from './api.ts'
|
||||
@@ -102,6 +103,7 @@ interface RowLabels {
|
||||
copyLabel: string
|
||||
copiedLabel: string
|
||||
thinkLabel: string
|
||||
injectionLabel: string
|
||||
}
|
||||
|
||||
/** Merge history entries by event seq (newest wins), log order preserved. */
|
||||
@@ -122,48 +124,112 @@ function threadDisplayTitle(title: string): string {
|
||||
return title.startsWith(SIDE_LABEL_PREFIX) ? title.slice(SIDE_LABEL_PREFIX.length) : title
|
||||
}
|
||||
|
||||
/**
|
||||
* One collapsible context row — the shared Codex-style chrome of tool
|
||||
* calls, thinking and context injections: a single quiet line (chevron +
|
||||
* label + one-line summary) that expands into an indented body hung on a
|
||||
* hairline thread. Rows with nothing to reveal render as a static line.
|
||||
*/
|
||||
function CollapsibleRow(props: {
|
||||
label: string
|
||||
meta?: string
|
||||
mono?: boolean
|
||||
streaming?: boolean
|
||||
failed?: boolean
|
||||
children?: React.ReactNode
|
||||
}): React.ReactNode {
|
||||
const label = (
|
||||
<span
|
||||
className={clsx(
|
||||
css.sidechatRowLabel,
|
||||
props.mono === true && css.sidechatRowMono,
|
||||
props.streaming === true && css.sidechatShimmerText,
|
||||
)}
|
||||
>
|
||||
{props.label}
|
||||
</span>
|
||||
)
|
||||
const meta = props.meta !== undefined && props.meta !== ''
|
||||
? <span className={css.sidechatRowMeta}>{props.meta}</span>
|
||||
: null
|
||||
if (props.children === undefined) {
|
||||
return (
|
||||
<div className={clsx(css.sidechatRowLine, css.sidechatRowStatic, props.failed === true && css.sidechatRowFailed)}>
|
||||
{label}
|
||||
{meta}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<details className={css.sidechatRow}>
|
||||
<summary
|
||||
className={clsx(
|
||||
css.sidechatRowLine,
|
||||
css.sidechatRowSummary,
|
||||
props.failed === true && css.sidechatRowFailed,
|
||||
)}
|
||||
>
|
||||
<span className={css.sidechatRowChevron}>
|
||||
<IconChevronRightOutline14 size={12} />
|
||||
</span>
|
||||
{label}
|
||||
{meta}
|
||||
</summary>
|
||||
<div className={css.sidechatRowBody}>{props.children}</div>
|
||||
</details>
|
||||
)
|
||||
}
|
||||
|
||||
/** One row renderer (React keys ride the source event seq). */
|
||||
function renderRow(row: SidechatTranscriptRow, labels: RowLabels): React.ReactNode {
|
||||
switch (row.kind) {
|
||||
case 'user':
|
||||
return (
|
||||
<div key={row.seq} className={css.sidechatUser}>
|
||||
<div key={`${row.kind}:${row.seq}`} className={css.sidechatUser}>
|
||||
<MarkdownText text={row.text} codeLabels={labels} />
|
||||
</div>
|
||||
)
|
||||
case 'assistant':
|
||||
return (
|
||||
<div key={row.seq} className={css.sidechatAssistant}>
|
||||
<div key={`${row.kind}:${row.seq}`} className={css.sidechatAssistant}>
|
||||
<MarkdownText text={row.text} codeLabels={labels} />
|
||||
</div>
|
||||
)
|
||||
case 'reasoning':
|
||||
return (
|
||||
<details key={row.seq} className={css.sidechatReasoning}>
|
||||
<summary className={css.sidechatReasoningSummary}>{labels.thinkLabel}</summary>
|
||||
<div className={css.sidechatReasoningBody}>{row.text}</div>
|
||||
</details>
|
||||
<CollapsibleRow
|
||||
key={`${row.kind}:${row.seq}`}
|
||||
label={labels.thinkLabel}
|
||||
streaming={!row.settled}
|
||||
>
|
||||
<div className={css.sidechatRowProse}>{row.text}</div>
|
||||
</CollapsibleRow>
|
||||
)
|
||||
case 'tool':
|
||||
case 'injection':
|
||||
return (
|
||||
<details key={row.seq} className={css.sidechatTool}>
|
||||
<summary
|
||||
className={clsx(
|
||||
css.sidechatToolSummary,
|
||||
row.executing === true && css.sidechatToolExecuting,
|
||||
row.failed && css.sidechatToolFailed,
|
||||
)}
|
||||
>
|
||||
<StateDot
|
||||
state={row.executing === true ? 'ongoing' : row.failed ? 'error' : 'done'}
|
||||
size={8}
|
||||
/>
|
||||
<span className={css.sidechatToolName}>{row.name}</span>
|
||||
</summary>
|
||||
{row.args !== undefined && <pre className={css.sidechatToolArgs}>{row.args}</pre>}
|
||||
{row.resultText !== undefined && <pre className={css.sidechatToolResult}>{row.resultText}</pre>}
|
||||
</details>
|
||||
<CollapsibleRow key={`${row.kind}:${row.seq}`} label={labels.injectionLabel}>
|
||||
<div className={css.sidechatRowProse}>{row.text}</div>
|
||||
</CollapsibleRow>
|
||||
)
|
||||
case 'tool': {
|
||||
const body = (
|
||||
<>
|
||||
{row.args !== undefined && <pre className={css.sidechatRowCode}>{row.args}</pre>}
|
||||
{row.resultText !== undefined && <pre className={css.sidechatRowCode}>{row.resultText}</pre>}
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<CollapsibleRow
|
||||
key={`${row.kind}:${row.seq}`}
|
||||
label={row.name}
|
||||
meta={toolArgsSummary(row.args)}
|
||||
mono
|
||||
streaming={row.executing === true}
|
||||
failed={row.failed}
|
||||
{...(row.args === undefined && row.resultText === undefined ? {} : { children: body })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +242,12 @@ export function SideChatView(props: {
|
||||
}): React.ReactNode {
|
||||
const { ctx, scope, tab, visible } = props
|
||||
const rowLabels = useMemo<RowLabels>(
|
||||
() => ({ copyLabel: t('copy'), copiedLabel: t('copied'), thinkLabel: t('sideChatThink') }),
|
||||
() => ({
|
||||
copyLabel: t('copy'),
|
||||
copiedLabel: t('copied'),
|
||||
thinkLabel: t('sideChatThink'),
|
||||
injectionLabel: t('sideChatInjection'),
|
||||
}),
|
||||
[],
|
||||
)
|
||||
|
||||
@@ -448,7 +519,12 @@ export function SideChatView(props: {
|
||||
<div className={css.sidechat}>
|
||||
<div className={css.sidechatHero}>
|
||||
<IconNewChatOutline16 />
|
||||
<div className={css.sidechatHeroTitle}>
|
||||
<div
|
||||
className={clsx(
|
||||
css.sidechatHeroTitle,
|
||||
busy === 'starting' && css.sidechatShimmerText,
|
||||
)}
|
||||
>
|
||||
{busy === 'starting' ? t('sideChatCreating') : t('sideChatEmpty')}
|
||||
</div>
|
||||
<div className={css.sidechatHeroDesc}>{t('sideChatEmptyDesc')}</div>
|
||||
|
||||
+40
-8
@@ -62,6 +62,16 @@ import css from './sidebar.module.css'
|
||||
* (mirror of the terminal view's own cap; the loop restarts on session switch). */
|
||||
const FAILURE_LIMIT = 3
|
||||
|
||||
/**
|
||||
* Subagent auto-open debounce (ms). The host delivers a new child's origin
|
||||
* and its title in SEPARATE frames: a Side Chat thread's first visible
|
||||
* frame still shows a fallback title (no 'Side: ' prefix), so an immediate
|
||||
* 0→N decision mistakes it for a genuine subagent and pops the task page.
|
||||
* The trigger therefore re-evaluates against the live snapshot once the
|
||||
* title frame has had time to land.
|
||||
*/
|
||||
const AUTO_OPEN_DEBOUNCE_MS = 500
|
||||
|
||||
/**
|
||||
* OS file drags over the sidebar belong to the sidebar, not to the chat:
|
||||
* DSH's composer (InputBar) listens for file drags on the DOCUMENT and
|
||||
@@ -395,23 +405,45 @@ export function Sidebar(props: { ctx: Context; store: SidebarStore }) {
|
||||
* Switching to a session that already has subagents never triggers — its
|
||||
* baseline starts at the current count — so a deliberate layout is never
|
||||
* fought.
|
||||
*
|
||||
* The decision is DEBOUNCED (AUTO_OPEN_DEBOUNCE_MS): a Side Chat thread
|
||||
* is also a subagent-origin child, and its 'Side: ' title lands one frame
|
||||
* after its origin — an immediate check would misread that first frame as
|
||||
* a new subagent and pop this page on every thread creation. The timer
|
||||
* re-evaluates the ORIGINAL baseline against the live snapshot; by then
|
||||
* the title filter (isSideThreadSummary) sees the settled label.
|
||||
*/
|
||||
const listBaselineRef = useRef<SidebarSessionList | undefined>(undefined)
|
||||
const autoOpenPendingRef = useRef<{ baseline: SidebarSessionList; timer: number } | null>(null)
|
||||
useEffect(() => {
|
||||
const prev = listBaselineRef.current
|
||||
listBaselineRef.current = sessionList
|
||||
if (sessionId === undefined || prev === undefined) return
|
||||
if (autoOpenPendingRef.current !== null) return
|
||||
if (!detectNewDirectSubagent(prev, sessionList, sessionId)) return
|
||||
if (!store.getPrefs().autoOpenSubagent) return
|
||||
if (ctx.betterSidebar?.isTabEnabled('subagent') === false) return
|
||||
store.reduce(s => s.panelOpen ? s : togglePanel(s))
|
||||
// Pin the landing to the right panel: the auto-opened Subagent page must
|
||||
// appear where the panel just expanded, not in a bottom-panel pane the
|
||||
// user last touched.
|
||||
store.reduce(s => ({ ...s, activePane: firstLeaf(s.splits).id }))
|
||||
ctx.betterSidebar?.openTab({ type: 'subagent', title: t('subagent') })
|
||||
const baseline = prev
|
||||
const timer = window.setTimeout(() => {
|
||||
autoOpenPendingRef.current = null
|
||||
if (!detectNewDirectSubagent(baseline, ctx.sessions.list.getSnapshot(), sessionId)) return
|
||||
if (!store.getPrefs().autoOpenSubagent) return
|
||||
if (ctx.betterSidebar?.isTabEnabled('subagent') === false) return
|
||||
store.reduce(s => s.panelOpen ? s : togglePanel(s))
|
||||
// Pin the landing to the right panel: the auto-opened Subagent page must
|
||||
// appear where the panel just expanded, not in a bottom-panel pane the
|
||||
// user last touched.
|
||||
store.reduce(s => ({ ...s, activePane: firstLeaf(s.splits).id }))
|
||||
ctx.betterSidebar?.openTab({ type: 'subagent', title: t('subagent') })
|
||||
}, AUTO_OPEN_DEBOUNCE_MS)
|
||||
autoOpenPendingRef.current = { baseline, timer }
|
||||
}, [sessionList, sessionId, store, ctx])
|
||||
|
||||
// A session switch (or unmount) voids any armed auto-open recheck.
|
||||
useEffect(() => () => {
|
||||
const pending = autoOpenPendingRef.current
|
||||
if (pending !== null) window.clearTimeout(pending.timer)
|
||||
autoOpenPendingRef.current = null
|
||||
}, [sessionId])
|
||||
|
||||
/**
|
||||
* Job auto-activation: the moment a NEW background job appears for the
|
||||
* current conversation (a job id the previous snapshot lacked), the
|
||||
|
||||
+38
-13
@@ -8,16 +8,28 @@
|
||||
* retry, and repeated unreasoned failures surface the close code after three
|
||||
* attempts, so the banner never spins forever.
|
||||
*
|
||||
* Three control frames shape the pty lifecycle on unmount:
|
||||
* - `{type:'close'}` — the user closed the tab. The host kills the pty
|
||||
* immediately (quota released).
|
||||
* - `{type:'park'}` — the user switched to another conversation. The tab is
|
||||
* still open in its session's persisted state but its view unmounted; the
|
||||
* host keeps the pty alive indefinitely (no grace countdown), so switching
|
||||
* back reattaches the same shell instead of respawning one.
|
||||
* - bare socket drop (no frame) — page refresh, crash, plugin teardown, or a
|
||||
* same-session re-render. The host's reconnect grace keeps the shell alive
|
||||
* for a quick reconnect.
|
||||
*
|
||||
* Two attach modes share one upgrade endpoint:
|
||||
* - `tabId` starting with `agent:` is an agent-owned terminal (created by
|
||||
* the `terminal_create` tool). The uuid is the suffix after `agent:`; the
|
||||
* view connects with `?uuid=...`. A close frame kills the pty (the agent's
|
||||
* terminal closes when the user closes the tab); a bare socket drop
|
||||
* leaves the pty alive (the agent owns the lifetime).
|
||||
* leaves the pty alive (the agent owns the lifetime) — agent terminals
|
||||
* never send park (their lifetime is already indefinite on bare drop).
|
||||
* - Any other `tabId` is a UI-tab terminal (the user created it from the +
|
||||
* menu). The view connects with `?tab=...&sessionId=...&cwd=...`. A close
|
||||
* frame schedules a 0-ms close; a bare socket drop gets the host's
|
||||
* reconnect grace.
|
||||
* frame schedules a 0-ms close; a park frame marks the pty as parked; a
|
||||
* bare socket drop gets the host's reconnect grace.
|
||||
*/
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Terminal, type ITheme } from '@xterm/xterm'
|
||||
@@ -274,18 +286,31 @@ export function TerminalView(props: { scope: SessionScope; tabId: string; store:
|
||||
fontSub()
|
||||
schemeSub()
|
||||
inputSub.dispose()
|
||||
// The close frame tells the host the owning tab is GONE (immediate
|
||||
// pty release). A bare unmount — conversation switch, re-render,
|
||||
// page unload — leaves the tab open, so the socket drop alone hands
|
||||
// the process to the host's reconnect grace: switching back or
|
||||
// refreshing reattaches the SAME shell instead of respawning one.
|
||||
// (The host respawns on its own when the authoritative cwd changed.)
|
||||
// Agent terminals follow the same rule: a close frame kills the pty
|
||||
// (the user closed the sidebar tab); a bare socket drop leaves it
|
||||
// alive (the agent owns the lifetime).
|
||||
if (!store.tabOpen(scope.sessionId, tabId)
|
||||
// Three unmount cases, distinguished by the store's tab/open state and
|
||||
// the active session id:
|
||||
// 1. The tab was closed by the user (NOT in its session's state): send
|
||||
// `{type:'close'}` — the host releases the pty immediately.
|
||||
// 2. The user switched to another conversation (the tab IS still open
|
||||
// in scope.sessionId's state, but the active session is now a
|
||||
// different one): send `{type:'park'}` — the host keeps the pty
|
||||
// alive indefinitely (no grace countdown), so switching back
|
||||
// reattaches the SAME shell. Without this, the bare socket drop
|
||||
// would start the 30s reconnect-grace countdown and kill the shell
|
||||
// while the user is still actively working in the other session.
|
||||
// 3. A same-session unmount (page refresh, crash, plugin teardown, a
|
||||
// re-render that re-mounts the view): bare socket drop — the host's
|
||||
// reconnect grace keeps the shell alive for a quick reconnect.
|
||||
// Agent terminals follow the close-frame rule; their lifetime is owned
|
||||
// by the agent, so a bare drop (case 3) already leaves them alive
|
||||
// indefinitely — no park frame needed.
|
||||
const tabStillOpen = store.tabOpen(scope.sessionId, tabId)
|
||||
const sessionSwitched = store.getSnapshot().sessionId !== scope.sessionId
|
||||
if (!tabStillOpen
|
||||
&& socket !== null && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify({ type: 'close' }))
|
||||
} else if (tabStillOpen && sessionSwitched && !isAgentTabId(tabId)
|
||||
&& socket !== null && socket.readyState === WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify({ type: 'park' }))
|
||||
}
|
||||
socket?.close()
|
||||
term.dispose()
|
||||
|
||||
@@ -264,6 +264,7 @@ export const zh = {
|
||||
sideChatComposerPlaceholder: '追问…',
|
||||
sideChatThinking: '正在深入…',
|
||||
sideChatThink: '思考过程',
|
||||
sideChatInjection: '已注入上下文',
|
||||
sideChatSend: '发送',
|
||||
sideChatCancel: '停止',
|
||||
sideChatCancelTitle: '中止当前回合(保留队列)',
|
||||
@@ -570,6 +571,7 @@ export const en: Record<keyof typeof zh, string> = {
|
||||
sideChatComposerPlaceholder: 'Ask a follow-up…',
|
||||
sideChatThinking: 'Deep diving…',
|
||||
sideChatThink: 'Thinking',
|
||||
sideChatInjection: 'Context injected',
|
||||
sideChatSend: 'Send',
|
||||
sideChatCancel: 'Stop',
|
||||
sideChatCancelTitle: 'Abort the running turn (queued work is kept)',
|
||||
|
||||
@@ -470,7 +470,7 @@ export function matchUrlTarget(tabs: readonly TabDescriptor[], url: URL): TabDes
|
||||
* The plugin version this service instance reports. Keep in lockstep with
|
||||
* `package.json`'s version — `tests/service.spec.ts` asserts the pair.
|
||||
*/
|
||||
export const SIDEBAR_SERVICE_VERSION = '0.15.0'
|
||||
export const SIDEBAR_SERVICE_VERSION = '0.15.1'
|
||||
|
||||
/**
|
||||
* Monotonic capability list consumers use to gate new API usage (features
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
*
|
||||
* A thread child's log starts with the ENTIRE inherited parent log as its
|
||||
* fork seed. The mapping therefore cuts everything up to the LAST
|
||||
* `session/end-seed` marker and drops the "Side conversation boundary"
|
||||
* prompt row, so the view shows only the thread's own conversation.
|
||||
* `session/end-seed` marker and maps context injections (the "Side
|
||||
* conversation boundary" prompt, plugin-sourced context) onto a collapsible
|
||||
* injection row, so the view shows only the thread's own conversation.
|
||||
*
|
||||
* Live streaming: `assistant/message` events only land when a step
|
||||
* completes, but `assistant/chunk` events stream token-level text and
|
||||
@@ -14,7 +15,7 @@
|
||||
* them with the assembled message once it lands (settled rows).
|
||||
*/
|
||||
import type { SidebarHistoryEntry } from '../context-types.ts'
|
||||
import { SIDE_BOUNDARY_PREFIX } from '../sidechat-core.ts'
|
||||
import { isContextInjectionMessage, SIDE_BOUNDARY_PROMPT } from '../sidechat-core.ts'
|
||||
|
||||
/** One compact transcript row rendered in the thread view. `seq` is the
|
||||
* source event's log sequence — stable row identity for React keys across
|
||||
@@ -22,6 +23,10 @@ import { SIDE_BOUNDARY_PREFIX } from '../sidechat-core.ts'
|
||||
* rows). */
|
||||
export type SidechatTranscriptRow =
|
||||
| { kind: 'user'; seq: number; text: string }
|
||||
/** A context injection (the side boundary prompt + the parked in-progress
|
||||
* snapshot, or any plugin-sourced context): rendered as one collapsible
|
||||
* row, never as a user bubble. */
|
||||
| { kind: 'injection'; seq: number; text: string }
|
||||
/** `settled` distinguishes an assembled message from a still-streaming
|
||||
* chunk accumulation (streaming rows are superseded by the settle). */
|
||||
| { kind: 'assistant'; seq: number; text: string; settled: boolean }
|
||||
@@ -54,6 +59,39 @@ export function blockText(content: readonly unknown[]): string {
|
||||
return text === '' ? '…' : text
|
||||
}
|
||||
|
||||
/** Cap for a tool row's one-line argument summary (display only). */
|
||||
const ARGS_SUMMARY_MAX = 80
|
||||
|
||||
/** The most identifying argument keys, in priority order (bash's command,
|
||||
* fs tools' paths, search's pattern, …). */
|
||||
const ARGS_SUMMARY_KEYS = ['command', 'file_path', 'path', 'pattern', 'query', 'url', 'prompt'] as const
|
||||
|
||||
function flatTruncate(text: string): string {
|
||||
const flat = text.replace(/\s+/g, ' ').trim()
|
||||
return flat.length > ARGS_SUMMARY_MAX ? `${flat.slice(0, ARGS_SUMMARY_MAX - 1)}…` : flat
|
||||
}
|
||||
|
||||
/**
|
||||
* One-line summary of a tool call's raw arguments JSON for the collapsed
|
||||
* row: the first identifying string field when the JSON parses, else the
|
||||
* flattened raw text; empty when there is nothing worth showing.
|
||||
*/
|
||||
export function toolArgsSummary(args: string | undefined): string {
|
||||
if (args === undefined) return ''
|
||||
try {
|
||||
const parsed = JSON.parse(args) as Record<string, unknown> | null
|
||||
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
for (const key of ARGS_SUMMARY_KEYS) {
|
||||
const value = parsed[key]
|
||||
if (typeof value === 'string' && value.trim() !== '') return flatTruncate(value)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Raw text fallthrough.
|
||||
}
|
||||
return flatTruncate(args)
|
||||
}
|
||||
|
||||
/** The plain text of a tool/result message (text blocks inside its
|
||||
* `tool-result` content block). */
|
||||
function resultTextOf(data: Record<string, unknown>): string {
|
||||
@@ -141,12 +179,12 @@ export async function collectOwnEvents(
|
||||
|
||||
/**
|
||||
* Map a thread child's history rows onto compact transcript rows: the
|
||||
* inherited fork seed is cut at the last `session/end-seed`, the boundary
|
||||
* prompt row is dropped, `assistant/chunk` deltas accumulate into streaming
|
||||
* rows per (turn, step, block) and are superseded by the assembled
|
||||
* `assistant/message`, and tool invocations render one expandable line each
|
||||
* (arguments, paired result text, failure marker; a still-executing call is
|
||||
* marked until its result lands).
|
||||
* inherited fork seed is cut at the last `session/end-seed`, context
|
||||
* injections map onto a collapsible injection row, `assistant/chunk`
|
||||
* deltas accumulate into streaming rows per (turn, step, block) and are
|
||||
* superseded by the assembled `assistant/message`, and tool invocations
|
||||
* render one expandable line each (arguments, paired result text, failure
|
||||
* marker; a still-executing call is marked until its result lands).
|
||||
* @param entries - history rows (event + host-computed view) in seq order.
|
||||
* @returns display rows in log order.
|
||||
*/
|
||||
@@ -166,7 +204,26 @@ export function transcriptRows(entries: readonly SidebarHistoryEntry[]): Sidecha
|
||||
switch (event.type) {
|
||||
case 'user/message': {
|
||||
const text = blockText(Array.isArray(data.content) ? data.content : [])
|
||||
if (text.startsWith(SIDE_BOUNDARY_PREFIX)) break
|
||||
// Context injections (the boundary prompt + snapshot, plugin-sourced
|
||||
// context) collapse into an injection row; genuine user messages —
|
||||
// including the FIRST one, which the host now delivers as its own
|
||||
// event — render as user rows.
|
||||
if (isContextInjectionMessage(data)) {
|
||||
const source = data.source as { kind?: unknown } | undefined
|
||||
// Threads logged BEFORE the host split carry boundary(+snapshot)+
|
||||
// question in ONE 'user' message. The boundary prompt is a known
|
||||
// constant, so the message splits THERE: the injection row keeps
|
||||
// the prompt, the remainder (snapshot + question if any — pure
|
||||
// question in the common case) renders as the user's real message.
|
||||
if (source?.kind === 'user' && text.startsWith(`${SIDE_BOUNDARY_PROMPT}\n\n`)) {
|
||||
rows.push({ kind: 'injection', seq: event.seq, text: SIDE_BOUNDARY_PROMPT })
|
||||
const body = text.slice(SIDE_BOUNDARY_PROMPT.length + 2)
|
||||
if (body !== '') rows.push({ kind: 'user', seq: event.seq, text: body })
|
||||
break
|
||||
}
|
||||
rows.push({ kind: 'injection', seq: event.seq, text })
|
||||
break
|
||||
}
|
||||
rows.push({ kind: 'user', seq: event.seq, text })
|
||||
break
|
||||
}
|
||||
|
||||
+22
-4
@@ -895,6 +895,10 @@ async function attachAgentList(
|
||||
* - `?tab=...&sessionId=...` attaches to a UI-tab terminal (the user
|
||||
* created it from the + menu). The close frame schedules a 0-ms close
|
||||
* (the host's reconnect grace keeps the shell alive across a refresh).
|
||||
* The park frame (sent when the user switches to another conversation)
|
||||
* marks the pty as parked so the upcoming bare socket drop does NOT start
|
||||
* the grace countdown — the tab is still open in its session's state, so
|
||||
* the shell must survive until the user switches back or closes the tab.
|
||||
*/
|
||||
async function attachTerminal(
|
||||
ctx: Context,
|
||||
@@ -971,6 +975,16 @@ async function attachTerminal(
|
||||
ptyManager.scheduleClose(handle.key, 0)
|
||||
return
|
||||
}
|
||||
if (control !== null && control.type === 'park') {
|
||||
// The user switched to another conversation: the tab is still open in
|
||||
// its session's persisted state, but its view unmounted. Park the pty
|
||||
// so the upcoming bare socket drop does NOT start the reconnect-grace
|
||||
// countdown — the pty stays alive until the user switches back (a
|
||||
// reconnecting view clears the parked state) or explicitly closes the
|
||||
// tab (a close frame's scheduleClose clears it).
|
||||
ptyManager.park(handle.key)
|
||||
return
|
||||
}
|
||||
if (handle.exited) return
|
||||
if (
|
||||
control !== null
|
||||
@@ -986,10 +1000,14 @@ async function attachTerminal(
|
||||
ws.on('close', () => {
|
||||
dataSub.dispose()
|
||||
exitSub.dispose()
|
||||
// A bare socket drop (refresh, tab switch) leaves the process alive
|
||||
// for a grace period so a quick reconnect keeps it; the reconnect's
|
||||
// open() cancels the pending close.
|
||||
ptyManager.scheduleClose(handle.key, resolved.reconnectGraceMs)
|
||||
// A parked pty (the user switched conversations and sent `{type:'park'}`)
|
||||
// stays alive indefinitely — do NOT start the grace countdown. A bare
|
||||
// socket drop without a prior park (refresh, crash) starts the grace
|
||||
// period so a quick reconnect keeps the process; the reconnect's open()
|
||||
// cancels the pending close.
|
||||
if (!ptyManager.isParked(handle.key)) {
|
||||
ptyManager.scheduleClose(handle.key, resolved.reconnectGraceMs)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
ws.close(1011, error instanceof Error ? error.message : String(error))
|
||||
|
||||
+49
-2
@@ -64,10 +64,32 @@ export interface SidebarPty {
|
||||
/**
|
||||
* The terminal registry. `maxPerSession` bounds concurrent processes per
|
||||
* conversation (the client caps tabs at the same number).
|
||||
*
|
||||
* Lifecycle of a UI-tab pty when its WebSocket drops:
|
||||
* - **Close frame** (`{type:'close'}`): the user closed the tab → schedule a
|
||||
* 0-ms close (quota released immediately).
|
||||
* - **Park frame** (`{type:'park'}`): the user switched to another
|
||||
* conversation; the tab is still open in its session's persisted state but
|
||||
* its view unmounted → mark the pty as parked (no auto-close countdown).
|
||||
* The pty stays alive until the user switches back (a reconnecting view
|
||||
* calls `open()` which clears the parked state) or the tab is later closed
|
||||
* (a `{type:'close'}` frame from a fresh connection). Without `park`, a
|
||||
* bare socket drop would start the reconnect-grace countdown and kill the
|
||||
* shell after `reconnectGraceMs` — wrong for a session switch, where the
|
||||
* user is still actively using the app, just in another conversation.
|
||||
* - **Bare socket drop** (no frame): page refresh, crash, plugin teardown →
|
||||
* schedule a close after `reconnectGraceMs` so a quick reconnect reattaches
|
||||
* the same shell.
|
||||
*/
|
||||
export class PtyManager {
|
||||
private readonly sessions = new Map<string, SidebarPty>()
|
||||
private readonly pendingCloses = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
/** Tabs whose view unmounted because the user switched conversations — the
|
||||
* tab is still open in its session's state, so the pty must NOT enter the
|
||||
* reconnect-grace countdown. Cleared by `cancelClose` (a reconnecting
|
||||
* view's `open()` cancels it) or by `scheduleClose` (an explicit close
|
||||
* frame still kills a parked pty). */
|
||||
private readonly parked = new Set<string>()
|
||||
|
||||
constructor(
|
||||
private readonly shell: string,
|
||||
@@ -159,7 +181,9 @@ export class PtyManager {
|
||||
* Schedule the terminal's destruction after `delayMs`. A tab close sends
|
||||
* delay 0 (release the quota immediately); a bare socket drop (refresh,
|
||||
* crash) uses the grace period so a quick reconnect keeps the process.
|
||||
* `open()` cancels any pending close.
|
||||
* `open()` cancels any pending close. Clears the parked state — an explicit
|
||||
* close frame on a parked pty (the user switched back and closed the tab)
|
||||
* still kills it.
|
||||
*/
|
||||
scheduleClose(key: string, delayMs: number): void {
|
||||
const handle = this.sessions.get(key)
|
||||
@@ -169,13 +193,36 @@ export class PtyManager {
|
||||
this.pendingCloses.set(key, timer)
|
||||
}
|
||||
|
||||
/** Cancel a pending scheduled close (the terminal is being reopened). */
|
||||
/**
|
||||
* Park a terminal: the owning tab's view unmounted because the user
|
||||
* switched to another conversation, but the tab is still open in its
|
||||
* session's persisted state. Cancels any pending grace close and marks
|
||||
* the pty so the host's `ws.on('close')` handler does NOT start the
|
||||
* reconnect-grace countdown — the pty stays alive until the user switches
|
||||
* back (a reconnecting view's `open()` clears this) or explicitly closes
|
||||
* the tab (a `{type:'close'}` frame's `scheduleClose` clears this).
|
||||
*/
|
||||
park(key: string): void {
|
||||
if (this.sessions.get(key) === undefined) return
|
||||
this.cancelClose(key)
|
||||
this.parked.add(key)
|
||||
}
|
||||
|
||||
/** Whether this pty was parked (its view unmounted for a session switch). */
|
||||
isParked(key: string): boolean {
|
||||
return this.parked.has(key)
|
||||
}
|
||||
|
||||
/** Cancel a pending scheduled close (the terminal is being reopened).
|
||||
* Also clears the parked state — a reconnecting view reattaches a parked
|
||||
* pty and resumes normal lifecycle. */
|
||||
cancelClose(key: string): void {
|
||||
const timer = this.pendingCloses.get(key)
|
||||
if (timer !== undefined) {
|
||||
clearTimeout(timer)
|
||||
this.pendingCloses.delete(key)
|
||||
}
|
||||
this.parked.delete(key)
|
||||
}
|
||||
|
||||
/** Resolve a live handle by key, or undefined. */
|
||||
|
||||
+32
-8
@@ -35,6 +35,11 @@ export const LABEL_MAX_CHARS = 48
|
||||
* the two plugins' threads render consistently in either UI). */
|
||||
export const SIDE_BOUNDARY_PREFIX = 'Side conversation boundary'
|
||||
|
||||
/** The plugin identity stamped on the source of context-injection messages
|
||||
* (boundary prompt + parked snapshot), so the transcript recognizes them
|
||||
* structurally — not by text prefix. */
|
||||
export const SIDE_INJECTION_PLUGIN = 'dsh-better-sidebar'
|
||||
|
||||
/**
|
||||
* The boundary prompt delivered as the thread's first user message: the
|
||||
* inherited seed is reference context only, never active instruction.
|
||||
@@ -365,18 +370,37 @@ export function sideLabel(question: string): string {
|
||||
export function boundaryDelivered(events: readonly SidechatLogEvent[]): boolean {
|
||||
for (const event of events) {
|
||||
if (event.type !== 'user/message') continue
|
||||
const content = dataOf(event).content
|
||||
const first = Array.isArray(content) ? content[0] : content
|
||||
const text = typeof first === 'string'
|
||||
? first
|
||||
: (typeof first === 'object' && first !== null && 'text' in first
|
||||
? String((first as { text: unknown }).text)
|
||||
: '')
|
||||
if (text.startsWith(SIDE_BOUNDARY_PREFIX)) return true
|
||||
if (messageLeadText(dataOf(event)).startsWith(SIDE_BOUNDARY_PREFIX)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** The leading text of a user/message's content (block array or bare string). */
|
||||
function messageLeadText(data: Record<string, unknown>): string {
|
||||
const content = data.content
|
||||
const first = Array.isArray(content) ? content[0] : content
|
||||
return typeof first === 'string'
|
||||
? first
|
||||
: (typeof first === 'object' && first !== null && 'text' in first
|
||||
? String((first as { text: unknown }).text)
|
||||
: '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a logged user/message is a CONTEXT INJECTION (the boundary prompt
|
||||
* plus the parked in-progress snapshot) rather than a real user message.
|
||||
* New threads deliver the injection via `agent.inject` stamped with a
|
||||
* non-'user' source kind; threads created before that split carry
|
||||
* boundary+question in ONE 'user' message, recognized by the boundary
|
||||
* prefix. Both render as one collapsible injection row — never as a user
|
||||
* bubble.
|
||||
*/
|
||||
export function isContextInjectionMessage(data: Record<string, unknown>): boolean {
|
||||
const source = data.source as { kind?: unknown } | null | undefined
|
||||
if (source?.kind !== undefined && source.kind !== 'user') return true
|
||||
return messageLeadText(data).startsWith(SIDE_BOUNDARY_PREFIX)
|
||||
}
|
||||
|
||||
/** The info the thread header shows (live runtime state + agent identity). */
|
||||
export interface SidechatThreadInfo {
|
||||
/** A live agent drives the thread right now (false = cold/persisted). */
|
||||
|
||||
+23
-4
@@ -37,6 +37,7 @@ import {
|
||||
buildSidechatInheritance,
|
||||
resolvePresetId,
|
||||
SIDE_BOUNDARY_PROMPT,
|
||||
SIDE_INJECTION_PLUGIN,
|
||||
SIDE_NEW_THREAD_TITLE,
|
||||
sideLabel,
|
||||
type SeedEvent,
|
||||
@@ -124,6 +125,26 @@ function admitFollowup(agent: Agent, blocks: ContentBlock[]): void {
|
||||
agent.followup(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deliver the thread's FIRST contact as TWO log-separated messages: the
|
||||
* boundary prompt (+ the parked in-progress snapshot) rides `agent.inject`
|
||||
* — queued model-facing context that does NOT wake the driver and is
|
||||
* claimed FIRST at the opening step (Inbox.claim drains next-step before
|
||||
* next-turn) — and the user's question is the follow-up that wakes it. The
|
||||
* log therefore records two user/message events (injection, then question)
|
||||
* instead of one wrapped blob: the transcript shows the question as a user
|
||||
* bubble and collapses the injection as a context row. The injection source
|
||||
* is stamped `kind: 'plugin'` so recognition is structural; its text still
|
||||
* opens with SIDE_BOUNDARY_PREFIX, keeping boundaryDelivered intact.
|
||||
*/
|
||||
function admitFirstContact(agent: Agent, injectionText: string, question: string): void {
|
||||
agent.inject(createUserMessage({
|
||||
content: textPrompt(injectionText),
|
||||
source: { kind: 'plugin', plugin: SIDE_INJECTION_PLUGIN },
|
||||
}))
|
||||
admitFollowup(agent, textPrompt(question))
|
||||
}
|
||||
|
||||
/** The live thread agent, or undefined (cold — the caller resumes). */
|
||||
function liveThreadAgent(ctx: Context, childId: string): Agent | undefined {
|
||||
const agents = ctx.get('agents') as { get(id: string): Agent | undefined } | undefined
|
||||
@@ -217,8 +238,7 @@ export function buildSidechatApi(ctx: Context): SidechatRoutes {
|
||||
} else {
|
||||
const promptParts = [SIDE_BOUNDARY_PROMPT]
|
||||
if (inheritance.snapshot !== null) promptParts.push(inheritance.snapshot)
|
||||
promptParts.push(question)
|
||||
admitFollowup(handle.agent, textPrompt(promptParts.join('\n\n')))
|
||||
admitFirstContact(handle.agent, promptParts.join('\n\n'), question)
|
||||
pinTitle(sideLabel(question))
|
||||
}
|
||||
return { childId }
|
||||
@@ -257,8 +277,7 @@ export function buildSidechatApi(ctx: Context): SidechatRoutes {
|
||||
const snapshot = pendingSnapshots.get(childId)
|
||||
pendingSnapshots.delete(childId)
|
||||
if (snapshot !== undefined) parts.push(snapshot)
|
||||
parts.push(text)
|
||||
admitFollowup(agent, textPrompt(parts.join('\n\n')))
|
||||
admitFirstContact(agent, parts.join('\n\n'), text)
|
||||
const titles = ctx.get('sessionTitle') as SidebarSessionTitleService | undefined
|
||||
if (titles !== undefined) {
|
||||
try {
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createElement } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import { FileTree } from '../src/client/FileTree.tsx'
|
||||
import { TAB_DRAG_TYPE } from '../src/client/TabBar.tsx'
|
||||
import type { UploadItem } from '../src/client/upload.ts'
|
||||
|
||||
// The act() environment flag (React 18.2 reads it before flushing effects).
|
||||
@@ -72,12 +73,12 @@ async function mountTree(busy = false): Promise<Harness> {
|
||||
}
|
||||
|
||||
/** A bubbling drag event carrying a stub dataTransfer (jsdom has no DragEvent). */
|
||||
function dragEvent(type: string): Event {
|
||||
function dragEvent(type: string, dataTypes: string[] = ['Files']): Event {
|
||||
const event = new Event(type, { bubbles: true, cancelable: true })
|
||||
Object.defineProperty(event, 'dataTransfer', {
|
||||
// No entry API on the items, so drops fall back to the flat file list.
|
||||
value: {
|
||||
types: ['Files'],
|
||||
types: dataTypes,
|
||||
items: [],
|
||||
files: [new File(['x'], 'dropped.txt')],
|
||||
dropEffect: '',
|
||||
@@ -87,8 +88,8 @@ function dragEvent(type: string): Event {
|
||||
}
|
||||
|
||||
/** Dispatch inside act() so React flushes the state update. */
|
||||
function fire(target: Element, type: string): Event {
|
||||
const event = dragEvent(type)
|
||||
function fire(target: Element, type: string, dataTypes?: string[]): Event {
|
||||
const event = dragEvent(type, dataTypes)
|
||||
act(() => { target.dispatchEvent(event) })
|
||||
return event
|
||||
}
|
||||
@@ -179,4 +180,26 @@ describe('FileTree drag-drop surface', () => {
|
||||
expect(busyHarness.uploads).toHaveLength(0)
|
||||
busyHarness.unmount()
|
||||
})
|
||||
|
||||
it('ignores an in-app tab drag: no upload drop zone and events pass through', () => {
|
||||
const enter = fire(harness.body, 'dragenter', [TAB_DRAG_TYPE])
|
||||
expect(enter.defaultPrevented).toBe(false)
|
||||
expect(dropZone()).toBeNull()
|
||||
const over = fire(harness.body, 'dragover', [TAB_DRAG_TYPE])
|
||||
expect(over.defaultPrevented).toBe(false)
|
||||
expect(dropZone()).toBeNull()
|
||||
// A row hover must not retarget the drop directory either.
|
||||
const dir = rowByName(harness.container, 'src')
|
||||
const rowOver = fire(dir, 'dragover', [TAB_DRAG_TYPE])
|
||||
expect(rowOver.defaultPrevented).toBe(false)
|
||||
expect(dir.className).not.toContain('explorerRowDropTarget')
|
||||
})
|
||||
|
||||
it('passes an in-app tab drop through without reporting an upload', async () => {
|
||||
const drop = fire(harness.body, 'drop', [TAB_DRAG_TYPE])
|
||||
await act(async () => {})
|
||||
expect(drop.defaultPrevented).toBe(false)
|
||||
expect(harness.uploads).toHaveLength(0)
|
||||
expect(dropZone()).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -12,12 +12,14 @@ import {
|
||||
buildOpenTurnSnapshot,
|
||||
buildSidechatInheritance,
|
||||
hasDanglingToolCall,
|
||||
isContextInjectionMessage,
|
||||
resolvePresetId,
|
||||
sideLabel,
|
||||
sideThreadRows,
|
||||
threadHasCompletedTurn,
|
||||
threadTrailingPending,
|
||||
SIDE_BOUNDARY_PROMPT,
|
||||
SIDE_INJECTION_PLUGIN,
|
||||
SIDE_LABEL_PREFIX,
|
||||
type SeedEvent,
|
||||
type SidechatLogEvent,
|
||||
@@ -333,3 +335,31 @@ describe('boundaryDelivered', () => {
|
||||
})])).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isContextInjectionMessage', () => {
|
||||
it('recognizes plugin-stamped sources structurally', () => {
|
||||
expect(isContextInjectionMessage({
|
||||
content: [{ type: 'text', text: 'runtime context' }],
|
||||
source: { kind: 'plugin', plugin: SIDE_INJECTION_PLUGIN },
|
||||
})).toBe(true)
|
||||
expect(isContextInjectionMessage({
|
||||
content: [{ type: 'text', text: 'q' }],
|
||||
source: { kind: 'user' },
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('falls back to the boundary prefix for pre-split logs and sourceless rows', () => {
|
||||
expect(isContextInjectionMessage({
|
||||
content: [{ type: 'text', text: `${SIDE_BOUNDARY_PROMPT}\n\nlegacy question` }],
|
||||
source: { kind: 'user' },
|
||||
})).toBe(true)
|
||||
expect(isContextInjectionMessage({
|
||||
content: `${SIDE_BOUNDARY_PROMPT}\n\nlegacy`,
|
||||
})).toBe(true)
|
||||
expect(isContextInjectionMessage({
|
||||
content: [{ type: 'text', text: 'ordinary question' }],
|
||||
source: { kind: 'user' },
|
||||
})).toBe(false)
|
||||
expect(isContextInjectionMessage({ content: [{ type: 'text', text: 'no source row' }] })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { buildSidechatApi } from '../src/sidechat-routes.ts'
|
||||
import { SidebarError } from '../src/wire.ts'
|
||||
import { SIDE_BOUNDARY_PROMPT, SIDE_NEW_THREAD_TITLE, sideLabel } from '../src/sidechat-core.ts'
|
||||
import { SIDE_BOUNDARY_PROMPT, SIDE_INJECTION_PLUGIN, SIDE_NEW_THREAD_TITLE, sideLabel } from '../src/sidechat-core.ts'
|
||||
import type { Context } from '../src/context-types.ts'
|
||||
|
||||
/** A fake live agent (followup/cancel spied). */
|
||||
/** A fake live agent (inject/followup/cancel spied). */
|
||||
function agent(id: string, over: { events?: unknown[]; header?: Record<string, unknown>; provider?: string; model?: string } = {}) {
|
||||
return {
|
||||
id,
|
||||
@@ -22,6 +22,7 @@ function agent(id: string, over: { events?: unknown[]; header?: Record<string, u
|
||||
header: { cwd: '/p', delegationDepth: 0, agentPreset: 'preset-a', ...over.header },
|
||||
events: over.events ?? [],
|
||||
},
|
||||
inject: vi.fn(),
|
||||
followup: vi.fn(),
|
||||
cancel: vi.fn(),
|
||||
}
|
||||
@@ -138,12 +139,17 @@ describe('sidechat.start', () => {
|
||||
agentProvider: 'test',
|
||||
agentModel: 'model-x',
|
||||
})
|
||||
// First contact is SPLIT: the boundary rides agent.inject (plugin-stamped
|
||||
// context, no wake), the question is the follow-up that wakes the driver.
|
||||
expect(child.inject).toHaveBeenCalledTimes(1)
|
||||
const injection = child.inject.mock.calls[0]![0] as { content: Array<{ type: string; text: string }>; source: { kind: string; plugin: string } }
|
||||
expect(injection.source).toEqual({ kind: 'plugin', plugin: SIDE_INJECTION_PLUGIN })
|
||||
expect(injection.content[0]!.text.startsWith(SIDE_BOUNDARY_PROMPT)).toBe(true)
|
||||
expect(injection.content[0]!.text).not.toContain('explain the event flow')
|
||||
expect(child.followup).toHaveBeenCalledTimes(1)
|
||||
const message = child.followup.mock.calls[0]![0] as { content: Array<{ type: string; text: string }>; source: { kind: string } }
|
||||
expect(message.source).toEqual({ kind: 'user' })
|
||||
const promptText = message.content[0]!.text
|
||||
expect(promptText.startsWith(SIDE_BOUNDARY_PROMPT)).toBe(true)
|
||||
expect(promptText).toContain('explain the event flow')
|
||||
expect(message.content[0]!.text).toBe('explain the event flow')
|
||||
expect(services.rename).toHaveBeenCalledWith(child.session, sideLabel('explain the event flow'))
|
||||
})
|
||||
|
||||
@@ -188,8 +194,11 @@ describe('sidechat.start', () => {
|
||||
const options = services.create.mock.calls[0]![0] as { seed: Array<{ type: string }> }
|
||||
// Cut BEFORE the open turn: only the pending user message is inherited.
|
||||
expect(options.seed.map(event => event.type)).toEqual(['user/message', 'subagent/descriptor'])
|
||||
// The snapshot rides the injection, not the follow-up question.
|
||||
const injection = child.inject.mock.calls[0]![0] as { content: Array<{ text: string }> }
|
||||
expect(injection.content[0]!.text).toContain('`bash` (executing)')
|
||||
const message = child.followup.mock.calls[0]![0] as { content: Array<{ text: string }> }
|
||||
expect(message.content[0]!.text).toContain('`bash` (executing)')
|
||||
expect(message.content[0]!.text).toBe('what now?')
|
||||
})
|
||||
|
||||
it('creates an EMPTY thread on a blank question (Codex-style immediate create)', async () => {
|
||||
@@ -209,18 +218,22 @@ describe('sidechat.start', () => {
|
||||
|
||||
// No prompt yet: the composer owns the first message; the placeholder
|
||||
// label is pinned and the in-progress snapshot is parked for it.
|
||||
expect(child.inject).not.toHaveBeenCalled()
|
||||
expect(child.followup).not.toHaveBeenCalled()
|
||||
expect(services.rename).toHaveBeenCalledWith(child.session, SIDE_NEW_THREAD_TITLE)
|
||||
|
||||
// The first prompt carries the boundary + the parked snapshot and earns
|
||||
// the thread its real label.
|
||||
// The first prompt injects the boundary + the parked snapshot (split
|
||||
// from the question) and earns the thread its real label.
|
||||
services.agents.get = vi.fn((id: unknown) => (id === childId ? child : parent))
|
||||
await api['sidechat.prompt']({ childId, text: 'explain the event flow' })
|
||||
expect(child.inject).toHaveBeenCalledTimes(1)
|
||||
const injection = child.inject.mock.calls[0]![0] as { content: Array<{ text: string }>; source: { kind: string } }
|
||||
expect(injection.source.kind).toBe('plugin')
|
||||
expect(injection.content[0]!.text.startsWith(SIDE_BOUNDARY_PROMPT)).toBe(true)
|
||||
expect(injection.content[0]!.text).toContain('`bash` (executing)')
|
||||
expect(child.followup).toHaveBeenCalledTimes(1)
|
||||
const message = child.followup.mock.calls[0]![0] as { content: Array<{ text: string }> }
|
||||
expect(message.content[0]!.text.startsWith(SIDE_BOUNDARY_PROMPT)).toBe(true)
|
||||
expect(message.content[0]!.text).toContain('`bash` (executing)')
|
||||
expect(message.content[0]!.text).toContain('explain the event flow')
|
||||
expect(message.content[0]!.text).toBe('explain the event flow')
|
||||
expect(services.rename).toHaveBeenCalledWith(child.session, sideLabel('explain the event flow'))
|
||||
})
|
||||
|
||||
@@ -256,6 +269,7 @@ describe('sidechat.prompt', () => {
|
||||
const api = buildSidechatApi(ctxWith(services))
|
||||
const result = await api['sidechat.prompt']({ childId: 'child', text: 'tell me more' })
|
||||
expect(result).toEqual({ accepted: true })
|
||||
expect(child.inject).not.toHaveBeenCalled()
|
||||
expect(child.followup).toHaveBeenCalledTimes(1)
|
||||
const message = child.followup.mock.calls[0]![0] as { content: Array<{ text: string }> }
|
||||
expect(message.content[0]!.text).toBe('tell me more')
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
/**
|
||||
* Unit tests for the Side Chat transcript mapping (src/client/sidechat-
|
||||
* transcript.ts): the seed cut at session/end-seed, the boundary-row drop,
|
||||
* chunk streaming accumulation superseded by assembled messages, tool
|
||||
* call/result pairing, and orphan failed results.
|
||||
* transcript.ts): the seed cut at session/end-seed, context-injection rows
|
||||
* (plugin-stamped sources and the legacy boundary-prefix blob), chunk
|
||||
* streaming accumulation superseded by assembled messages, tool call/result
|
||||
* pairing, and orphan failed results.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { SidebarHistoryEntry, SidebarSessionEvent } from '../src/context-types.ts'
|
||||
import { SIDE_BOUNDARY_PREFIX } from '../src/sidechat-core.ts'
|
||||
import { collectOwnEvents, transcriptRows, type SidechatTranscriptRow } from '../src/client/sidechat-transcript.ts'
|
||||
import { SIDE_BOUNDARY_PREFIX, SIDE_BOUNDARY_PROMPT, SIDE_INJECTION_PLUGIN } from '../src/sidechat-core.ts'
|
||||
import { collectOwnEvents, toolArgsSummary, transcriptRows, type SidechatTranscriptRow } from '../src/client/sidechat-transcript.ts'
|
||||
|
||||
/** One history entry (event + optional view). */
|
||||
function entry(event: SidebarSessionEvent): SidebarHistoryEntry {
|
||||
@@ -29,15 +30,48 @@ function textBlocks(...texts: string[]): unknown[] {
|
||||
}
|
||||
|
||||
describe('transcriptRows', () => {
|
||||
it('cuts the inherited seed at the last end-seed and drops the boundary row', () => {
|
||||
it('cuts the inherited seed at the last end-seed and renders the boundary as an injection row', () => {
|
||||
const entries = [
|
||||
entry(ev('user/message', 0, { content: textBlocks('inherited'), source: { kind: 'user' } })),
|
||||
entry(ev('session/end-seed', 1)),
|
||||
entry(ev('user/message', 2, { content: textBlocks(`${SIDE_BOUNDARY_PREFIX}\n\nmode`), source: { kind: 'user' } })),
|
||||
entry(ev('user/message', 2, { content: textBlocks(`${SIDE_BOUNDARY_PREFIX}\n\nmode`), source: { kind: 'plugin', plugin: SIDE_INJECTION_PLUGIN } })),
|
||||
entry(ev('user/message', 3, { content: textBlocks('the side question'), source: { kind: 'user' } })),
|
||||
]
|
||||
const rows = transcriptRows(entries)
|
||||
expect(rows).toEqual([{ kind: 'user', seq: 3, text: 'the side question' }])
|
||||
expect(rows).toEqual([
|
||||
{ kind: 'injection', seq: 2, text: `${SIDE_BOUNDARY_PREFIX}\n\nmode` },
|
||||
{ kind: 'user', seq: 3, text: 'the side question' },
|
||||
])
|
||||
})
|
||||
|
||||
it('splits the LEGACY wrapped first message (boundary + question in one user row) at the boundary prompt', () => {
|
||||
const entries = [
|
||||
entry(ev('session/end-seed', 0)),
|
||||
entry(ev('user/message', 1, {
|
||||
content: textBlocks(`${SIDE_BOUNDARY_PROMPT}\n\nthe first question`),
|
||||
source: { kind: 'user' },
|
||||
})),
|
||||
entry(ev('user/message', 2, { content: textBlocks('follow-up'), source: { kind: 'user' } })),
|
||||
]
|
||||
const rows = transcriptRows(entries)
|
||||
expect(rows).toEqual([
|
||||
{ kind: 'injection', seq: 1, text: SIDE_BOUNDARY_PROMPT },
|
||||
{ kind: 'user', seq: 1, text: 'the first question' },
|
||||
{ kind: 'user', seq: 2, text: 'follow-up' },
|
||||
])
|
||||
})
|
||||
|
||||
it('renders any plugin-sourced context message as an injection row, boundary prefix or not', () => {
|
||||
const entries = [
|
||||
entry(ev('session/end-seed', 0)),
|
||||
entry(ev('user/message', 1, { content: textBlocks('runtime context'), source: { kind: 'plugin', plugin: 'other-plugin' } })),
|
||||
entry(ev('user/message', 2, { content: textBlocks('q'), source: { kind: 'user' } })),
|
||||
]
|
||||
const rows = transcriptRows(entries)
|
||||
expect(rows).toEqual([
|
||||
{ kind: 'injection', seq: 1, text: 'runtime context' },
|
||||
{ kind: 'user', seq: 2, text: 'q' },
|
||||
])
|
||||
})
|
||||
|
||||
it('accumulates chunk deltas per block and supersedes them on settle', () => {
|
||||
@@ -192,3 +226,23 @@ describe('collectOwnEvents', () => {
|
||||
expect(result.entries.map(e => e.event.seq)).toEqual([20, 21, 22, 23, 24, 25, 26, 27, 28, 29])
|
||||
})
|
||||
})
|
||||
|
||||
describe('toolArgsSummary', () => {
|
||||
it('picks the most identifying string field', () => {
|
||||
expect(toolArgsSummary('{"command":"ls -la","timeout":1000}')).toBe('ls -la')
|
||||
expect(toolArgsSummary('{"file_path":"/a/b.ts","old_string":"x"}')).toBe('/a/b.ts')
|
||||
expect(toolArgsSummary('{"pattern":"foo","path":"/repo"}')).toBe('/repo')
|
||||
})
|
||||
|
||||
it('flattens and truncates raw text when no known key parses', () => {
|
||||
expect(toolArgsSummary('{"custom":"v"}')).toBe('{"custom":"v"}')
|
||||
expect(toolArgsSummary('not json at all')).toBe('not json at all')
|
||||
expect(toolArgsSummary(`{"command":"${'x'.repeat(200)}"`)).toHaveLength(80)
|
||||
expect(toolArgsSummary(`{"command":"${'x'.repeat(200)}"`)).toMatch(/…$/)
|
||||
})
|
||||
|
||||
it('reads empty for missing or blank input', () => {
|
||||
expect(toolArgsSummary(undefined)).toBe('')
|
||||
expect(toolArgsSummary('{"command":" "}')).toBe('{"command":" "}')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -198,6 +198,60 @@ describe('host plugin smoke', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('pty manager: a parked pty survives past the reconnect grace (session switch)', async () => {
|
||||
const manager = new PtyManager(defaultShell(), 3)
|
||||
try {
|
||||
const handle = manager.open('s2', 't1', process.cwd(), 80, 24)
|
||||
manager.park(handle.key)
|
||||
expect(manager.isParked(handle.key)).toBe(true)
|
||||
// A parked pty does NOT enter the grace countdown — it stays alive
|
||||
// well past any realistic reconnectGraceMs.
|
||||
await new Promise(resolve => setTimeout(resolve, 300))
|
||||
expect(manager.get(handle.key)).toBeDefined()
|
||||
expect(manager.isParked(handle.key)).toBe(true)
|
||||
} finally {
|
||||
manager.disposeAll()
|
||||
}
|
||||
})
|
||||
|
||||
it('pty manager: a reconnecting view clears the parked state (switch back)', () => {
|
||||
const manager = new PtyManager(defaultShell(), 3)
|
||||
try {
|
||||
const handle = manager.open('s2', 't1', process.cwd(), 80, 24)
|
||||
manager.park(handle.key)
|
||||
expect(manager.isParked(handle.key)).toBe(true)
|
||||
// open() calls cancelClose(), which clears the parked state — the
|
||||
// user switched back to the session and the view reattached.
|
||||
manager.open('s2', 't1', process.cwd(), 80, 24)
|
||||
expect(manager.isParked(handle.key)).toBe(false)
|
||||
expect(manager.get(handle.key)).toBeDefined()
|
||||
} finally {
|
||||
manager.disposeAll()
|
||||
}
|
||||
})
|
||||
|
||||
it('pty manager: an explicit close frame on a parked pty still kills it', async () => {
|
||||
const manager = new PtyManager(defaultShell(), 3)
|
||||
try {
|
||||
const handle = manager.open('s2', 't1', process.cwd(), 80, 24)
|
||||
manager.park(handle.key)
|
||||
// The user switched back and closed the tab — scheduleClose (the
|
||||
// close-frame handler) clears the parked state and kills the pty.
|
||||
manager.scheduleClose(handle.key, 0)
|
||||
expect(manager.isParked(handle.key)).toBe(false)
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
expect(manager.get(handle.key)).toBeUndefined()
|
||||
} finally {
|
||||
manager.disposeAll()
|
||||
}
|
||||
})
|
||||
|
||||
it('pty manager: park on an unknown key is a no-op', () => {
|
||||
const manager = new PtyManager(defaultShell(), 3)
|
||||
expect(() => manager.park('s2:nonexistent')).not.toThrow()
|
||||
expect(manager.isParked('s2:nonexistent')).toBe(false)
|
||||
})
|
||||
|
||||
it('pty manager: reopening with a different cwd respawns in the new directory', async () => {
|
||||
const manager = new PtyManager(defaultShell(), 3)
|
||||
// A real second directory: os.tmpdir() exists on every platform ('/tmp'
|
||||
|
||||
@@ -98,6 +98,33 @@ describe('subagent detection over the sessions list feed', () => {
|
||||
expect(detectNewDirectSubagent(before, after, 'p2')).toBe(false)
|
||||
})
|
||||
|
||||
it('documents the title-frame race the Sidebar auto-open debounce absorbs', () => {
|
||||
// The host delivers a new child's origin and title in SEPARATE frames:
|
||||
// a Side Chat thread's FIRST visible frame still shows a fallback title
|
||||
// (the cwd basename — no 'Side: ' prefix), so an immediate 0→N check
|
||||
// misreads it as a genuine subagent. Sidebar.tsx therefore debounces
|
||||
// AUTO_OPEN_DEBOUNCE_MS and re-evaluates the ORIGINAL baseline against
|
||||
// the live snapshot; once the title frame has landed the same baseline
|
||||
// yields no trigger. These two assertions pin exactly that dependency.
|
||||
const baseline: SidebarSessionList = { current: 'p2', byId: { p2: { id: 'p2', displayTitle: 'P2' } } }
|
||||
const firstFrame: SidebarSessionList = {
|
||||
current: 'p2',
|
||||
byId: {
|
||||
p2: { id: 'p2', displayTitle: 'P2' },
|
||||
s2: { id: 's2', displayTitle: 'DSH-better-sidebar', origin: 'subagent', parentId: 'p2' },
|
||||
},
|
||||
}
|
||||
expect(detectNewDirectSubagent(baseline, firstFrame, 'p2')).toBe(true) // the race
|
||||
const settled: SidebarSessionList = {
|
||||
current: 'p2',
|
||||
byId: {
|
||||
p2: { id: 'p2', displayTitle: 'P2' },
|
||||
s2: { id: 's2', displayTitle: 'Side: New thread', origin: 'subagent', parentId: 'p2' },
|
||||
},
|
||||
}
|
||||
expect(detectNewDirectSubagent(baseline, settled, 'p2')).toBe(false) // after the debounce
|
||||
})
|
||||
|
||||
it('resolves the main-agent root of the current session tree', () => {
|
||||
const byId: SidebarSessionList['byId'] = {
|
||||
main: { id: 'main', displayTitle: 'Main' },
|
||||
|
||||
Reference in New Issue
Block a user