Pular para o conteúdo principal
V

dsh-multi-session

vegetable86/dsh-multi-session

Multi-session split view panel for DeepSeek Harness

Instalar

dsh plugin --profile web add github:vegetable86/dsh-multi-session

README

多会话面板(dsh-multi-session)

在 DeepSeek Harness 里把多个会话并排看:一个独立的「多会话」主面板,格子数量不设上限,每格绑定一个已有会话,每格都是完整的原生对话(消息流 + 输入框)。所有格子不重叠地平铺,拖动分隔条就能像 tmux 那样调整相邻格子的长宽。

npm 包:@vegataa/dsh-multi-session

用法

  1. 左侧边栏点「多会话」图标进入面板。 1.1 面板最上方是类似浏览器的标签栏:每个标签都是一整套独立的多会话布局(自己的格子、分隔条权重、最大化 / 最小化状态)。点标签切换,+ 新建一个标签(默认填入最近两个会话),× 关闭标签(至少保留一个),双击标签可以改名,按住标签拖动可以调整它们的前后顺序;标签标题默认跟着它的第一个会话走,会话正在跑时标签上有个小绿点。
  2. 首次打开自动填入最近两个会话;每格标题栏点会话名可以换一个会话(支持搜索)。
  3. 工具条「+ 添加会话格」想加多少加多少。格子不重叠地平铺:两格并排,三格是上面一整格、下面两格,四格是 2×2,更多格子按两列往下排。拖行与行之间的横分隔条,上下两行一起变高变矮;拖同一行里两格之间的竖分隔条,一边变宽另一边就变窄,和 tmux 的分屏手感一样。任何时候格子都会铺满面板、不会互相遮挡。单格可最大化 / 还原,也可关闭(至少保留一格)。工具条的「均分」把所有分隔条恢复成平均分配。 3.1 每格标题栏的「最小化」把它收成面板上方的一条标题(带运行状态点);收起的格子不占网格空间,剩下的格子自动铺满,点标题即可展开回原来的位置。所有格子都可以收起。 3.2 每格标题栏的「在主面板打开」(箭头图标)把该格的会话交给普通对话页显示,多会话面板随之退到侧边栏;适合临时想仔细看某一段对话时用。标题栏还可以按住拖到另一个格子上:松手后两格交换各自显示的会话,格子的位置和大小不变。
  4. 空格的「新建会话」按钮、以及每格选择器底部的「新建会话」,会先弹出一个工作区选择框:选好工作区后再在这一格里创建会话,整个过程都留在多会话页面,不会跳到新会话。没有工作区时才退回 Host 的默认目录。
  5. 工具条右上角的「沉浸全屏」(四个角的图标)把整个面板铺满窗口,盖住左右两栏,能多出侧栏那一整条宽度;再点一次或按 Esc 退出,布局原样保留。
  6. 工具条上的密度按钮在「紧凑 / 舒适」之间切换:紧凑是默认值,同样的窗口能多显示几行对话;舒适会把间距、按钮、标签和分隔条整体放大一档。选择随布局一起记住。
  7. 第一次打开面板时,工具条下方有一条操作提示(拖分隔条调大小、拖标题栏交换会话、标题栏箭头在主面板打开),点「知道了」收起;工具条上的 ? 随时能再叫出来。图标按钮悬停会立即显示名称,标题栏左侧的六点手柄表示这一格可以拖动。
  8. 布局、每格绑定的会话、最大化 / 最小化 / 全屏 / 密度状态都会自动记住,刷新或重启后恢复;会话不存在时那一格显示「不可用」,可以重新选择。
  9. 选择器默认不列子代理会话,也不列空白会话(当前格自己绑定的空白会话除外)。

实现要点

  • 只注册一个 main 面板(key multisession)和一个 sidebar.panellist 图标,普通对话页完全不受影响。
  • 每格通过 <SessionProvider session={reference}> 显式绑定会话,引用由 sessions.retain(...) 获取、卸载时 release()。
  • 格子内部用官方 conversation.content 工厂槽(variant: 'embedded')渲染,得到的是原生对话组件,不是自绘的聊天界面。
  • 每格包两层错误边界:格子内的渲染异常会在 0.6s / 1.2s 后自动重建两次,仍失败才显示错误卡片(可手动「重试」);整个面板崩了还有一层兜底,可以一键清掉布局重置。
  • conversation.content 是 Harness 的内部工厂槽,签名变了会抛错:每格对这次调用做了捕获,失败时只显示「此视图暂不可用」+ 重试,不会把整个面板打空。
  • 「新建会话」自己问工作区,然后直接 sessions.create({ workspaceId });不走 uiWorkspace.openWorkspace——后者在 hero 工作区选择器里会 layout.selectPanel(null),把主面板切回单个会话,这正是新建后跳走的原因。
  • 格子的外壳用 React.memo 包起来:拖动分隔条时只有父级容器重算样式,各格的对话组件不会跟着重渲染。
  • 最大化改为 CSS 覆盖层(其余格子 visibility: hidden 但保持挂载),切换最大化不会重挂载对话,滚动位置和正在流式的回复都保留。
  • 沉浸全屏不是新窗口,也不是新面板:根节点切到 position: fixed; inset: 0,一层 z-index 盖住三栏框架,面板里的会话组件完全不用重挂载;Esc 先退最大化、再退全屏。顶部留出 Windows 标题栏的高度,避免盖住窗口拖动区。
  • 「在主面板打开」调用平台的 uiWorkspace.openSession(sessionId);它会把该会话交给当前主面板并切换到单个对话,属于宿主既有的导航入口,插件不自己造一套。
  • 交换会话走 HTML5 拖放,MIME 用自定义的 text/x-multisession-pane,只在面板内部认;拖拽过程中不改 DOM 结构,松手时只交换两个格子的 sessionId,所以对话视图和滚动位置都还在原来的格子实例里。
  • 格子按「行 → 列」两级权重存储:每行的 rowWeights 决定高度,每行内两格的 colWeights 决定宽度;拖动分隔条只改这两级权重,所以相邻格子永远互补、整块面板始终铺满。最小化是每个格子的 minimized 标志:收起后该格不再参与网格计算(行里只剩一格就独占整行),但它在 rows 里的位置保留着,展开即回原位。
  • 标签栏是「多套布局」而不是「多套面板」:v7 的存储是一份 { tabs, activeTabId, immersive, density },每个 tab 里放的正是原来那套 panes / rows / rowWeights / colWeights / maximizedId。切标签只是换 activeTabId,面板本身不重建;旧版本(v1–v6)的布局会原样升级,不会丢。
  • 最近访问的 3 个标签会保持挂载(MAX_MOUNTED_TABS):切回来时滚动位置、输入框草稿和正在流式输出的内容都还在;超出的标签整块卸载,但会话本身在后台继续跑。
  • 密度不是两套样式表:METRICS 里放紧凑 / 舒适两档的尺寸,组件通过 DensityContext 取当前这档,拖动计算(beginSplitDrag 的可用像素、最小格宽高)也跟着同一份数字走,避免尺寸和命中区各算各的。
  • 分隔条只在视觉上是 6px / 10px 的细线:外层是命中区,内层 span 才是那条线,悬停只改内层颜色和透明度,不改布局,所以拖起来不会抖。
  • 标签拖动排序走 HTML5 拖放(自定义类型 text/x-multisession-tab),松手时只重排 board.tabs 数组,activeTabId 不变、布局不重建;拖到标签条空白处则移到最末。落点只按左右半区判断插到目标前还是后,不额外算几何。

开发

纯手写客户端 bundle,无需构建步骤。client.js 直接就是 window.__ModuleLoader__.load(...) 包。

npm test   # 注册形态冒烟 + jsdom 渲染冒烟

目录

index.js        # Host 侧空实现(本插件纯客户端)
client.js       # 客户端 bundle:面板、格子、选择器、持久化、i18n
cordis.patch.yml
locale/*.json   # 面板名称与描述
icon.svg
scripts/        # 冒烟测试、发布打包、市场索引生成
market/         # 插件市场索引与目录提交模板
docs/superpowers/{specs,plans}/
LICENSE

许可证

MIT,见 LICENSE。

Plugins relacionados