Compare commits

...
10 Commits
Author SHA1 Message Date
Atlas Zeng ef8d3a9050 chore(release): 0.15.1 (#324)
* chore(release): 0.15.1

- bump version: package.json / dsh.plugin.json / SIDEBAR_SERVICE_VERSION
- README / README_EN: add v0.15.1 changelog (sidechat Codex-style transcript
  rework, terminal park-frame pty keep-alive, file-tree drag gating, README
  rewrite); plugin ecosystem 28+ with two new entries
  (dsh-better-sidebar-starter, my-dsh-plugins) and per-category <details>
  collapsible listings

* docs(readme): add community section placeholder for WeChat/QQ group QR codes
2026-08-22 17:46:26 +08:00
Atlas Zeng 9a40164d1a fix(explorer): 非文件拖拽不再触发文件树上传遮罩 (#317)
拖动 Tab(重排/跨 pane split)经过资源管理器文件树时,FileTree 的
dragenter/dragover/drop 此前对任何拖拽都响应:显示上传遮罩并吞掉
事件,导致 pane 根的 Tab-drop 处理收不到、Tab 无法落下。

统一按 dataTransfer.types 含 'Files' 门控(与 Sidebar.tsx 面板宿主
shield 一致):非文件拖拽原样冒泡给 pane 根,OS 文件拖拽行为不变。

回归测试:应用内 Tab 拖拽不出现 uploadDropZone、不改 dropTarget、
不 preventDefault、不触发 onUploadRequest。
2026-08-22 17:22:10 +08:00
多玩幻灵qwq 29b8615bff fix(terminal): keep pty alive across conversation switches via park frame (#323)
Switching from session A to B unmounted A's TerminalView, whose socket
close the host treated as a transient drop and started the 30s reconnect-
grace countdown — leaving the user in B for >30s killed A's shell, so
switching back respawned a fresh one instead of reattaching.

Add a third control frame {type:'park'} distinct from refresh/crash
drops: the client sends it on unmount when the tab is still open in its
session's state but the active session has moved on; the host marks the
pty as parked and skips the grace countdown. Switching back (open()
cancels the parked state) or explicitly closing the tab (close frame's
scheduleClose clears it) resume the normal lifecycle. Agent terminals
skip park — their lifetime is already indefinite on a bare drop.
2026-08-22 17:21:27 +08:00
Atlas Zeng b7aab48deb feat(sidechat): Codex 风格转录重构 — 首问显示 / 注入拆分 / 折叠行 / 弹窗去抖 (#314)
* fix(sidechat): split context injection from the first user message

The first composer message rode the same user/message event as the side
boundary prompt (+ parked snapshot), and the transcript dropped that whole
row by its boundary prefix — swallowing the user's first question.

Deliver first contact as TWO messages: the boundary+snapshot rides
agent.inject (queued next-step context, claimed first at the opening step,
no wake) stamped with source kind 'plugin', and the question is the
follow-up that wakes the driver. The log records injection and question as
separate events; the transcript maps injections (plugin-sourced, or the
legacy boundary-prefix blob of pre-split threads) onto a collapsible
injection row and renders genuine user messages — the first one included —
as user rows. boundaryDelivered still recognizes the injection by its
unchanged boundary-prefix opening.

* feat(sidechat): codex-style collapsible rows and creating shimmer

Tool calls, thinking and context injections now share one quiet
single-line chrome (chevron + label + a one-line argument summary) that
expands into an indented body on a hairline thread — no cards or fills.
Streaming labels and the creating hero shimmer (shimmer = generating);
failed tools go danger; reduced-motion stills every loop.

* fix(sidebar): debounce subagent auto-open past the title frame

The host delivers a new child's origin and title in separate frames, so a
Side Chat thread's first visible frame lacks its 'Side: ' label and
tripped the 0→N auto-open — every thread creation popped the task page.
The trigger now 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, half a second later.

* fix(sidechat): render legacy threads' first message as its own bubble

Threads created before the host split still log the boundary (+snapshot)
and the question as ONE 'user' message; the transcript mapped that row
wholly onto the injection row, so the user's first question only appeared
inside the expansion. The boundary prompt is a known constant, so the
legacy row now splits THERE: the injection row keeps the prompt, the
remainder renders as the user's real message (a pre-split snapshot, when
present, rides along — the common case is a pure question). Row React keys
become kind:seq so the split's two rows never collide.
2026-08-22 15:34:05 +08:00
Menghuan1918 7dbd9b75e2 docs(readme): simplify install to add -> approve-builds -> add
The node one-liner prewrite was overkill for the README. Plain double
'add' does NOT work (both runs exit 1 on ERR_PNPM_IGNORED_BUILDS,
verified in scratch); the working minimal flow is add (fails, dep
written) -> pnpm approve-builds --all (re-runs install scripts) ->
add (succeeds, bundle registered). Verified end-to-end in a scratch
profile.
2026-08-22 12:10:24 +08:00
Menghuan1918 cdda072973 docs(readme): make first-run install succeed (pre-write allowBuilds before dsh plugin add)
pnpm 11's strict-dep-builds blocks node-pty/protobufjs build scripts and
makes the FIRST 'dsh plugin add' exit non-zero (dsh aborts on
ERR_PNPM_IGNORED_BUILDS), forcing a second run after approve-builds.

The install block now pre-writes allowBuilds (node-pty/protobufjs) and
minimumReleaseAgeExclude (dsh-better-sidebar) into the profile's
pnpm-workspace.yaml with the same idempotent merge as scripts/install.sh
ensure_workspace_settings, so the first add succeeds. Verified in a
scratch DSH_HOME: preflight -> add exit 0 (node-pty postinstall ran),
rerun unchanged (no duplicate keys), second add exit 0; merge branch
tested when allowBuilds already exists. Paste-to-DSH prompt updated to
match.
2026-08-22 12:06:32 +08:00
Menghuan1918 646c91cf70 docs(readme): sync EN with uploaded screenshots, normalize image sizes, add Side Chat badge
- Mirror the zh feature tour (user-uploaded CDN images) into
  README_EN.md: real screenshots for explorer/editor, markdown,
  image, terminal, git, browser, tasks, side chat, settings, mobile;
  drop manual-shot placeholders (git-diff, bottom-panel) and the
  CodeMirror subsection (folded into the explorer section)
- Normalize image display size to width=880 (mobile 360) across both
  languages; remove the stale 'placeholders to come' note
- Add the missing Side Chat badge to the hero feature-chip row
  (zh + en)
- Mirror user's trim: 'Office' removed from the features bullet
  (Office preview is an ecosystem plugin), Git panel ecosystem
  sentence dropped
2026-08-22 02:03:25 +08:00
Atlas Zeng 55ad7eec8d Update README.md 2026-08-22 01:29:06 +08:00
Menghuan1918 d38a411f6d docs(readme): user-facing DSH compat badge, node-pty-safe install script, paste-to-DSH install prompt
- Drop peer-floor / CI-baseline jargon everywhere; supported DSH
  versions are stated only via the shields badge
- Install block: add 'pnpm approve-builds --all && pnpm rebuild
  node-pty' unconditionally so the terminal's prebuilt binary is
  always in place (pnpm 11 blocks build scripts by default)
- Add a copy-paste prompt block that lets a DSH session install the
  plugin itself (mirrors the manual script + node-pty step)
- Trim internal process detail from historical changelog bullets
  (v0.14.0 / v0.13.1), keep the user-facing symptoms and gates
2026-08-22 01:04:16 +08:00
Menghuan1918 940d0a8928 docs(readme): feature tour with screenshots, plugin ecosystem section, DSH compat badges
- Hero badge wall: npm version/downloads, CI, stars, DSH compat
  (0.1.0-rc.8 / 0.1.1-rc.1 / 0.1.1-rc.2), ecosystem topic badge
- New 🖼️ feature tour: one subsection per feature with real UI
  screenshots (10 captured from a scratch DSH instance; 4 slots left
  as commented placeholders for manual upload via GitHub web)
- New 🌐 plugin ecosystem section: 26 topic plugins in three grouped
  tables (tab / viewer / enhancement) with live star badges, the two
  extension points, and curation instructions
- Recent updates: keep v0.15.0 visible, collapse v0.12-v0.14 into
  <details>; add Releases link and per-version DSH support line
- Add TOC, contributing section, star history chart, contributors wall
- Bilingual README.md / README_EN.md kept in sync
2026-08-22 00:49:08 +08:00
22 changed files with 1406 additions and 406 deletions
+342 -104
View File
@@ -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
View File
@@ -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 &amp; 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 &amp; 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
View File
@@ -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
View File
@@ -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": {
+12
View File
@@ -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'
+105 -64
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+2
View File
@@ -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)',
+1 -1
View File
@@ -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
+67 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+27 -4
View File
@@ -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()
})
})
+30
View File
@@ -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)
})
})
+25 -11
View File
@@ -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')
+62 -8
View File
@@ -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":" "}')
})
})
+54
View File
@@ -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'
+27
View File
@@ -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' },