dsh-small-talk
Manifest valid★ 1一个用于deepseek harness网页版的侧边助手:当你想知道任务进行的怎么样但是又无法暂停时,侧边助手可以读取主对话的思维链并且告诉你现在正在发生什么。 A side assistant for the DeepSeek Harness web version: when you want to know how a task is progressing but can't pause it, the side assistant can read the main conversation's chain of thought and tell you what is currently happening.
dsh-small-talk
给当前会话一键开一个常驻侧边助手:它知道这个会话此前聊了什么(以一段压缩摘要的形式,不进它的聊天记录), 可以反复追问,不会打断主任务; 面板顶部是一条实时进展条,默认只占一行——主 Agent 在跑什么、想到了什么,最新一句摘要直接看得见,展开能回看最近几步的思维链。
主 Agent 之后每跑完一步,侧助手都会收到一块进展(这一步的想法、调了什么工具、得出了什么)——所以它不再只知道创建那一刻的历史: 你问「它现在在干什么」,侧助手答的是最新一块。块走的是系统提示,不进聊天记录(见「进展条」一节)。
按钮长在会话标题栏右侧。按一下,右侧栏里就出现一个独立的对话面板: 它知道你这个会话之前聊过什么、干过什么,之后你可以一直问它,主 Agent 继续跑自己的。 已经有侧助手时,按一下会先弹出选择菜单:助手 1 / 助手 2 / … / 新建助手—— 可以换一个重开,也可以再开一个(各自独立,各自拿到同一份历史摘要)。
侧助手不是子智能体:它不出现在子智能体目录、不参与子智能体计数、不占 continuable 名额, 也从会话列表里缺席。它对「子智能体系统」唯一可见的痕迹是会话头上的一行血统信息(见「工作原理」)。
它做什么
| 需求 | 落点 |
|---|---|
| 侧边对话面板 + 输入框 + 消息流 | 由本插件自己的标签页承载,聊天本体直接选 DSH 的 conversation.content 工厂(embedded 变体) |
| 看主 Agent 的实时进展(工具调用 + 思维链) | 面板顶部自绘进展条,session.follow 直连主会话的事件流与 assistant stream |
| 侧助手也知道之后发生了什么 | 主 Agent 每完成一个 step,宿主把这一步折成一块写进侧助手的系统提示片段(side-assistant:progress),不进聊天记录 |
| 知道主会话已完成的历史 | 建会话时把主会话已完成回合压成一段摘要,作为系统提示片段 side-assistant:history(order 120)——它自己的日志从开场白开始,侧栏里看不到主对话 |
| 可反复追问(持久化侧助手) | 会话自带 subagent/descriptor,每次提问按需冷恢复,记录一直留着 |
| 一个都不占子智能体名额 | 用 ctx.agents.create() 建游离会话,不追加 subagent/catalog 事实 |
| 关掉再开不丢 | 侧栏按会话把打开的标签页存进 localStorage |
| 一个会话多个侧助手、换一个重开 | 按一下弹菜单:已有的按创建顺序编号,末尾另有「新建助手」 |
| 回答下面两颗快捷卡片 | conversation.input.dock 里挂两颗圆角矩形卡片:这个项目是在干什么? / agent 正在进行什么工作?。点一下等于自己打出来发出去(conversation.send,和输入框同一条路) |
它不做什么(有意为之)
- 不重新实现聊天界面。 消息流、Markdown、工具调用、流式输出、Stop、向上翻页都来自
@deepseek-ai/dsh-client-ui-conversation的conversation.content工厂(embedded变体); 本插件只加自己的标签页类型、资源提供方和那条进展条。自己画一套只会更差。 相位固定传active:游离会话在客户端没有可判的元数据,交给工厂自己判会停在空壳上。 - 不把自己登记成子智能体。 不走
ctx.subagents.startContinuable(),不往父会话追加subagent/catalog;parentSession只是血统字段(工作区分组 + 侧栏寻址),不代表运行时父子关系 (创建时不传parentAgent)。子智能体目录、计数、会话树、list_agents全部只读那份目录事实。 - 不轮询主会话。 进展条用的是框架自己的推送:
session.follow(持久事件 +assistantStream增量), 没有自建/status、没有定时器。 - 不往侧助手的聊天记录里塞“进展”。 块走系统提示片段,不走
user/assistant消息: 聊天记录里只有你和它的对话,翻历史时不会被一堆系统播报淹掉。 - 不往会话流里塞插件私有事件。 唯二写进日志的是框架自己的两个协议事件:
subagent/descriptor(这个会话是什么)与创建窗口内的沙箱/审批策略覆盖(与旧版委派路径一致)。
安装
profile 是 web 的话,直接从仓库安装:
dsh plugin --profile web add https://github.com/TPSCE/dsh-small-talk.git
不想让 pnpm 走 git clone(网络慢、或者本机没装 git)就用 tarball,一次 HTTPS 下载:
dsh plugin --profile web add https://github.com/TPSCE/dsh-small-talk/archive/refs/heads/main.tar.gz
装完刷新页面;如果按钮没出现,重启 dsh web(Host 半区的新路由需要重新加载进程)。
卸载:
dsh plugin --profile web remove dsh-side-assistant
使用
- 打开任意会话。
- 点标题栏右侧的 侧边助手。
- 一个侧助手都还没有:直接创建(读取历史摘要、发一句开场白),随即在右侧栏打开。
- 已经有侧助手:先弹出菜单,列出 助手 1 / 助手 2 / …(按创建顺序)和 新建助手。 选其中一个 = 把那个重开到右侧栏(各自保留自己的对话记录);选「新建助手」= 再开一个。
- 收起菜单:菜单外点一下、按
Esc、或者再点一次按钮。收起菜单不会创建任何东西。
- 面板顶部是一条进展条,默认折叠成一行(一个小圆点 + 「主 Agent 进展」+ 最新一句思考): 展开后是主 Agent 的工具调用、工具结果,以及每个 step 各自的思维链——正在写的标亮,跑完的变灰留着, 所以一轮跑完再点开,最近几步想过什么还在。中途打开面板也会从当前 step 的开头补齐。 折叠只是显示——它照样把主会话的流订阅着,圆点该闪还是闪;展开/折叠的选择在本次页面里一直记着。
- 在下方输入框里正常提问:它起手一行高,写满后像主对话一样逐行长高。可以一直问,也可以按 Stop 打断正在生成的回答。
- 不要了:再点一次按钮,在菜单里那个助手右边点 删除,再点一次 确认。它从菜单里消失,标签页随即关闭 (删掉的是「显示」——会话日志留在盘上,可从归档列表恢复,见「已知边界」第 6 条)。
- 关闭面板不销毁会话,再点按钮就回来了。
界面:侧栏只有一个输入框
复用自带的对话视图意味着连主对话的输入区一起继承——所有插件往输入区塞的东西都会出现在侧栏里。
client.js 里的 SIDEBAR_CSS 把侧栏面板裁回一个朴素的输入框,只在这个面板内生效:
| 裁掉 | 谁带来的 |
|---|---|
conversation.composer.dock(N 轮 · M 步 · tok/s · 缓存命中) | dsh-client-ui-chat 的 stats 统计条、dsh-cost-meter |
conversation.input.dock(未提交的更改:N 个文件、额度条) | dsh-client-ui-git-graph、dsh-cost-meter、@openmaic/dsh-openmaic |
conversation.input.attachments(附件栏 / 拖放区) | dsh-client-ui-attachment |
conversation.input.left / .right(余额、插件杂项按钮) | deepseek-harness-wallet、@linxin666/dsh-web-all 等 |
conversation.input.activity(横跨工具行的活动指示) | 对话外壳 |
输入区左侧整行(+ 上传/命令菜单 + 计划 + 权限选择) | 对话外壳 |
留下的是:消息流、输入框、模型选择、发送/Stop,以及每条回复下的复制 / 用量 / 时间。
想调整就改 client.js 顶部的 TRIMMED_SLOTS 数组(每个槽一行,删掉哪行哪样东西回来);
只想藏 + 而保留「计划 / 权限」两个座位,就删掉 SIDEBAR_CSS 里最后那条
div:has(> input[type="file"]) 规则。
裁剪规则选的是
[data-sidebar-chat]。这个属性以前由自带的 subagent 标签页打上, 现在由本插件自己的面板根节点打上(chatTab的根div),所以规则依然只在侧栏面板内生效。
消息动作行
助手回复下面那排按钮不在槽里,是聊天视图自己画的固定条:
复制 → conversation.chat.assistant-actions 座位 → 分支 → 用量 + 时间。只留复制、用量、时间:
| 裁掉 | 怎么认得它 |
|---|---|
点赞 / 踩(dsh-client-ui-message-feedback)、在上下文浏览器中查看(dsh-context) | 两者都注册进 conversation.chat.assistant-actions 座位,藏掉座位一次全消 |
| 分支按钮 | 助手尾部这一行的最后一个直接子元素按钮 |
分支那条规则带了 [data-actions-reveal] 限定:这个属性只有助手轮次尾部会写,
而用户气泡那一排的最后一个按钮正是复制——不限定就会顺手把用户消息的复制也藏掉。
用量按钮在 span.endInfo 里,不是这一行的直接子元素,所以 > button:last-of-type 够不到它。
输入框:起手一行,之后照抄主对话
侧栏输入框起手只有一行高(24px,正好是自带输入区的行高),主对话那份是 36px——没写满第一行时一眼分得开。 写满之后它和主对话完全一样:每多一行长 24px,长到自带的上限(336px,约 14 行)才开始在框内滚动。
只动了两处,都是「下限」而不是「上限」:
| 规则 | 作用 |
|---|---|
[data-composer-input] 的 min-height: 24px + padding-top: 0 | 一行的行高就是 24px,所以这就是「一行」的起手高度;之后每换一行自然 +24 |
[data-composer-placeholder] 的 inset 上边归零 | 提示语是绝对定位的,跟着编辑区的新内边距走,否则会飘到文字下面 |
没有设 max-height、没有给编辑区挂 overflow、也没有重声明 --dsh-composer-text-max-height——
这三样任何一个都会把「第二行」变成滚动而不是长高:光标在第二行,滚动跟着光标走,第一行就被挤出那个没变高的盒子,
看起来像「第一行消失了,第二行顶到第一行的位置」。这个坑 0.9.1 踩过一次,测试里现在反过来钉死这三条不许出现。
卡片的上内边距与间距顺手收窄(8→6、12→6);下面那排按钮(模型选择、发送 / Stop)不动——它们不是本插件的东西。
想完全照主对话(36px 起手),删掉 min-height 与 inset 这两条即可。
运行中只有一个按钮
主对话在跑的时候,那颗主按钮会变成 Stop。侧栏用的是子会话,自带输入区不做这个替换
(primaryStops 要求 subagent === null),而是在 Send 旁边额外长出一颗 Stop
(interruptible = running && continuable)。消息一提交草稿就清空,Send 便停在禁用态——
于是侧栏会出现「Stop + 一颗灰色的 Send」,看着像坏了一颗。
SIDEBAR_CSS 只在这一种状态下藏掉后面那颗:行尾容器里出现两颗相邻按钮、且最后一颗是 [disabled] 时才隐藏;
一旦你开始打字、Send 重新可用,规则立刻失效,按钮自己回来。判据只用属性选择器与行结构,不依赖构建期哈希类名;
自带 DOM 若改成嵌套结构,这条只是不再匹配,不会误藏别的按钮。
输入框提示语
提示语是真实渲染出来的文本节点([data-composer-placeholder],与可编辑区同级),CSS 换不掉,
所以做法是:把原文字号压成 0 藏起来,再用 ::before 画上自己的。
ask me anything you wanna know~
只有输入框可用时才替换。不可用 / 离线 / 被拦截时,原提示语是状态而不是装饰,
那几条规则会把它放回来——判据是同一父节点里可编辑区的 aria-disabled="true"。
无障碍名称仍来自原本的 aria-label,读屏听到的还是官方那句(含 /、@ 的提示)。
为什么用 CSS 而不是接管 conversation.composer 链。 那条链是官方的「替换本会话输入区」扩展点,
但它的 select 只拿到 Session:一旦接管,子会话在主视图里被打开时输入区也会跟着变;
而且接管会把常驻输入区(它持有待处理的审批交互和 Stop 按钮)藏起来。CSS 只动观感,不动功能。
为什么不在 locale 里改提示语。 那会连主对话的输入框一起改,而主对话正需要
/ 指令和 @ 文件的提示;侧栏要的是「这是聊天」的口气,不是全局换文案。
开场消息:行为不走对话框
新建侧助手时 Host 会投递一条 hello。它不是提示词,只是点火:DSH 用首条消息打开会话的第一轮。
侧助手的行为走的是会话的 agent 组合,不经过对话框:
| 行为载体 | 落在哪 |
|---|---|
persona | 这个会话的 scoped deployment:persona-prefix 段,只覆盖它自己 |
denyTools | 这个会话的 scoped tools.restrict():工具既从提示里消失,也不可调用 |
reasoningEffort | 创建时的 agentOptions,并随 subagent/descriptor 持久化 |
这样面板读起来就是「一个助手,不是用户塞了一段提示词」。代价是那句 hello 仍是一条真实的
user 消息,所以它会在面板里显示成一个用户气泡——见「已知边界」第 11 条。
不想要它就配 opener: "":会话会开在一个空对话上,等你先说话。
面板不等这句开场白。 /side-assistant/ensure 在会话建出来、问候投递出去之后就应答,
问候那一轮在后台跑完才落盘、释放 agent。所以「按下按钮 → 面板出现」之间没有模型往返:
面板先开,回复随后流进来,这期间你已经可以输入(消息排在问候之后)。
点开之前就已经准备好了
侧边助手不是「点一下才开始做」——面板里那个按钮出现在会话标题栏之后,页面会先安静半秒,
然后向宿主请求把这个会话的侧助手提前建好(POST /side-assistant/preload)。
点开时只剩一次查表:会话已经在列表里,面板直接进对话。
它挑的是便宜的做法:
- 已经有侧助手就复用,不重建——所以反复切换、反复重开都不会越建越多。
- 只在你真的在的会话里做。 请求里只有一个
sessionId,宿主先看这个会话有没有驻留 agent; 没有就跳过(skipped: "parent-not-live")——那种会话本来就建不了侧助手,也不该建。 - 半秒防抖。 只是路过一个会话不该留下一个助手。
- 开场白照样在后台跑,不等它;面板开的时候你可能已经能打字了。
代价是实打实的:每个你停留过的会话,提前建好一个侧助手(一份会话日志 + 一轮问候的模型调用)。
不想要这笔开销就配 preload: false,回到「点了才建」。
进展条:主 Agent 在想什么
面板顶部那条是只读的观察窗,数据全部来自框架自己的会话流:
| 看到什么 | 从哪来 |
|---|---|
工具名 + 参数摘要、工具结果(错误标红) | 主会话的 tool/call / tool/result 持久事件 |
| 每个 step 的思维链(流着的是 live,跑完的标灰保留) | assistantStream 的 reasoning-delta,以及 assistant/message 里的 stream |
| 本回合结束 | turn/start / turn/end |
小圆点状态:待命 / 进行中 / 读不到 | 流是否活着 |
思维链是留下来的,不是闪一下就没。 主 Agent 每个 step 的思考单独成块:正在写的那块标 live(左边框高亮),
这个 step 结束就转成 done(变灰保留),下一个 step 不会把它冲掉——所以跑完一轮再点开,最近几步想过什么还在。
两块补看是自动的:
- 面板是中途打开的:
session.follow的快照帧带assistantStream.activeAttempt,也就是"这个 attempt 到现在 已经流出了什么"的压缩前缀。打开时把它展开、按nextIndex截到已发布的部分,从 step 开头补齐,之后接增量—— 不是你打开那一刻才开始记。 - 面板打开时某步已经结束:该步的
assistant/message持久事件里带着整段stream,从里面还原 reasoning。 快照里的历史事件也走这条路,所以刚打开面板就能看到之前几步的思考。
上限:思维链最多留 6 块(每块 4000 字,超了从前面截断,保留结论),工具行 30 行;块与行各有各的预算, 工具多的一轮不会把思考挤出去。展开时自动跟到底部,但你手动往上滚之后就停止自动跟随,不会跟你抢滚动条。
默认折叠。 展开态的子任务行 + 思维链有几十行高,而它们和你在主界面看到的是同一份东西:
用户在主视图里看着主 Agent 跑,侧栏再重复一遍只是把聊天的位置挤掉。所以面板打开时它只有一行——
一个状态圆点、主 Agent 进展、最新一句思考的摘要(折叠也能看见它在想什么)、一个折叠箭头,
右端还有一个折叠箭头——需要时点开,选择在本次页面生命周期内记住(client.js 的 progressOpen:换会话、重新挂载都不丢,刷新页面回到折叠)。
折叠的是显示,不是订阅:session.follow 照样连着,圆点该闪还是闪,读不到时那一行也会显示
「进展读不到,看看控制台」,所以折叠状态下也不会漏掉异常。
实现是 ctx.remote.session.follow({ "address": { kind: "session", sessionId: 主会话 }, assistantStream: true }):
一次订阅拿到快照(已经发生过的事件 + 正在跑的 attempt 的压缩前缀)+ 之后的每条增量,面板关掉即断开。
压缩流记录的展开(reasoning-chunks → 逐条 delta)在插件里本地实现了一份,因为框架的那个 helper
不在浏览器半区可 require 的面里。
这些块也会送到侧助手那里,但走系统提示,不走聊天记录。 主 Agent 每完成一个 step,宿主就把这一步折成一块
(结论 + 工具调用 + 想法),挂进侧助手会话的系统提示片段 side-assistant:progress(order 125,紧跟委派声明)。
每块第一行是一句话结论:取这一步自己收尾文字的第一句;只动了工具、没有文字时,结论就是「Conclusion: this step only used tools (read, bash) …」
这样的动作概述。后面才是工具动作,想法原文放在最后——先给结论,再给细节,所以问「现在在干什么」时,
它答的是一句话,而不是一段思维链。手上同时有几块时,按由早到晚的顺序逐块讲(每块一句话),最后落在最新一块,
让人看到的是「它先做了什么、接着做了什么、现在到哪了」,而不是一团并行的碎块。块不进日志,面板里仍然只有你和它之间的对话,
历史摘要也仍然停在创建那一刻(见「已知边界」第 1 条)。
上限与代价:最多 4 块(每块想法 200 字、结论 120 字、工具 3 条 × 60 字,超了从最早的一块滚掉)。 一份完整注入通常只有几百字——刻意的:注入太长会挤掉你真正想聊的内容,也让「一眼看清」变成读报告。 块一变,侧助手的系统提示就变一次,提示词缓存会跟着失效——这是刻意的取舍:实时进展比缓存命中重要。 片段文本是 getter,每次拼提示词时重算,所以用户提问之后、模型开口之前刚落下的那一步也在里面。
删除助手:在菜单里,先归档再关标签页
删除按钮跟着它要删的东西走:再点一次头部按钮打开菜单,每个「助手 N」右边就是一颗 删除——两个字,
吃的是菜单行的宽度,不占进展条,也不用记「右端那颗按钮删的是谁」。文案跟随系统语言(zh 是 删除,
en 是 Delete)。
两段式:按下变成 确认,再按一下才动手;四秒内不按第二下就自己放弃(一次误点不会在半小时后被「补完」)。
删除走的是框架的工作区归档(workspaces.archiveSession,带 stopActivity):
- 从菜单里消失。 宿主列菜单时把归档集合一起减掉,所以删掉的不会被
ensure({})重新打开, 下次点按钮是新建一个;剩下的行重新编号(助手 1、助手 2……)。 - 标签页关闭。 菜单只知道子会话 id,不知道它开在哪个标签页上——标签页 id 是停靠区发的。
所以面板自己在挂载时把这对关系记进
client.js的openTabs,卸载时忘掉;删除时查这张表, 当前打开的正是它就把标签页关掉(否则留下的只会是个空壳)。 - 日志还在。 归档不是擦除:会话记录留在盘上,可以从工作区的归档列表恢复(恢复之后菜单里又会出现它, 因为它重新变回「游离血统 + 当前组合 + 未编目」)。
- 失败会说话。 归档失败时那一行变成
删除失败,行和标签页都不动,再点一次即可重试(细节在控制台)。
刻意没做的:没有批量删除、没有二次弹窗(确认写在按钮自己身上,省一次点击)、也没有用它替换
sidebar.right.tab.menu.item 槽——那个槽只在标签页自己的下拉菜单里出现,而这个面板的标签页头在折叠栏里,
藏得太深;菜单里那一行是用户本来就要打开的地方。
回答下面的快捷卡片
最后一条回答下面、输入框上面,有两颗圆角矩形卡片:
- 这个项目是在干什么?
- agent 正在进行什么工作?
点一下就把这句话发出去——走的是 conversation.send(),和用户在输入框里按发送是同一条路径,
所以会出现一条正常的用户气泡,接着是一轮正常的回答(正在跑的时候进队列,等这一轮结束再发)。
它们挂在会话自己的 conversation.input.dock 槽位上(队列条、todo 条也在这里),所以位置天然是「回答下面、输入框上面」。
槽位本身是全会话共享的:同样的入口会出现在每一个对话视图里,所以两条 CSS 规则把默认态设成 display: none,
只有祖先带 [data-sidebar-chat](也就是本插件的面板)时才 display: flex —— 主对话那边一点痕迹都没有。
这个槽位原来整块被输入区精简规则藏掉了(它同一条里还带着 git 分支行、额度条、队列条)。0.11.2 起改成
只藏别人的子节点:[data-sidebar-chat] [data-slot="conversation.input.dock"] > *:not(:has(.dsh-sa-cards)),
卡片自己是这个锚点的直接子节点,所以照常显示,其余条目(git 行、额度、队列、todo)仍不出现在面板里。
配置
cordis.patch.yml 里的 config 会原样传给 apply(ctx, config),每个键都可省略:
| 键 | 默认 | 说明 |
|---|---|---|
label | 侧边助手 | 菜单里的标签,同时写进会话的描述符(不再是识别键,见「工作原理」) |
opener | hello | 打开新侧助手第一轮的那条消息。只用来开场,别放指令——DSH 把它当普通用户消息投递,指令写在这里就等于「用户自己把提示词敲进了输入框」。给空字符串 = 不打招呼 |
persona | 侧助手行为规则(英文,规则里写明用用户的语言回答) | 侧助手的行为在这里:作为 scoped persona 前缀注入会话;不要写 {{…}},会被当模板插值 |
denyTools | [send_message] | 从会话组合里删掉的工具。删掉的工具既不在它的提示里也不可调用。给空数组 = 不限制 |
reasoningEffort | off | 侧助手的思考档位。默认关掉(侧问是你在等的,不开思考模式);填 low / high / max 打开,填 inherit = 完全不传,继承主会话的路由 |
preload | true | 你在某个会话里停留半秒后就提前把它的侧助手建好,把「复制历史」那一步挪到点开之前(见「点开之前就已经准备好了」)。设 false 关掉 |
例:
- insert:
- id: side-assistant
name: dsh-side-assistant
config:
label: 我的助手
reasoningEffort: low # 想让它带思考,就这样打开
denyTools: [send_message, shell]
工作原理
两半区,一个入口。
Host index.js 浏览器 client.js
POST /side-assistant/list 标题栏按钮(order -15)
├─ sessionQuery.listSessions() 按一下 → POST /list
│ 筛 parentSession 是本会话 + origin=subagent ├─ 空列表 → POST /ensure {sessionId}
└─ 减去 subagents.listChildren()(真正的委派) └─ 非空 → 弹菜单
⇒ 剩下的才是游离侧助手 助手 i → /ensure {sessionId, childId}
按创建时间从旧到新 新建助手 → /ensure {sessionId, create:true}
POST /side-assistant/ensure ↓
├─ { sessionId } 当前那个 └─ create 时: sidebarRight.openResource(
├─ { childId } 指定重开 agents.create({ dsh-resource://sideassistant/chat/
├─ { create:true } 一定新建 没有 parentAgent, <child>?parent=<parent>)
└─ 其余:复用最新那个 meta.parentSession, ↓
→ { childId, created } history: 历史摘要 自己的标签页 = 进展条
setup: 描述符 + 组合 + 自带对话视图
})
- 创建必须走 Host。 Client 的
subagents远程命名空间只暴露prompt与interruptByParent,没有创建。 agents.create()不传parentAgent⇒ 运行时它是个 root agent。 这不是偷懒,是需求: 子智能体的身份来自父会话日志里的subagent/catalog事实(listChildren/ 计数 / 会话树 /list_agents全读它)。不追加那条事实,就没有任何地方会把它算成子智能体。- 为什么还留着
origin: 'subagent'+parentSession。 侧栏聊天要能寻址和驱动这个会话: 地址校验要求origin === 'subagent'且parentSession与地址里的一致, 并要求这个会话自己的日志里有subagent/descriptor;送消息时的冷恢复也只读那份描述符 (coldResume():mode必须是continuable,其余从描述符重建)。 也就是说:这两个字段是给侧栏用的接口,不是给子智能体系统用的登记。 setup是创建窗口。agents.create()的setup(childCtx, child)在会话与 agent 被插入、发布之前跑, 框架自己也是在这里 append 描述符的。这里做四件事:写subagent/descriptor(v3:version / mode / provider / label / agentProvider / agentModel / agentReasoningEffort / persona / toolFilter,未知键会抛)、 继承沙箱与审批策略(和旧版委派路径一样把审批钉成never)、agentPresets.composeFrom()加入父会话的组合、scoped persona 段 +tools.restrict()。- 收尾:先落盘,再
dispose()。AgentHandle.dispose()只把会话从运行时注销并停掉循环, 不动它的日志;所以创建完sessions.flush()再 dispose,之后每次提问都走冷恢复。 (卸载插件时不能 dispose:那会连会话一起删掉。) - 发现靠「相减」。
sessionQuery.listSessions()里凡是parentSession指向本会话且origin === 'subagent'的,减去subagents.listChildren()里有的——剩下的就是游离侧助手; 再减去工作区归档集合(workspaceRegistry.archivedSessionIds)里有的,删掉的才不会回来。 所以label不再是识别键(0.6 之前靠它,改名等于换一个;现在改名只改显示)。 另有一道时间闸COMPOSITION_SINCE(index.js里的常量),比它早建的一律不算—— 包括 0.7 之前那版真子智能体侧助手,它们大多也在目录里,两道闸都拦得住。 - 名额。 不再占 continuable 名额(那是激活表的事,激活表只由委派路径填充)。
唯一还会碰到上限的是提问那一轮:冷恢复要临时占一个激活位(框架的
maxActiveSubagents,默认 8), 占满时报错由框架给出(subagent limit reached …),面板里会看到那条错误。 - 地址是契约,不是字符串拼接。 本插件有自己的协议与标签页类型
(
sideassistant/dsh-resource://sideassistant/chat/<child>?parent=<parent>), 与自带subagentchat语法互斥:两个语法解析对方地址都必须失败,谁也不会抢走谁的标签页。test/address.contract.mjs会把这条契约连同 glob、编码、拒收样例一起校验。 - 进展块怎么到模型手里。 宿主订阅主会话的
session/event(tool/call/tool/result/assistant/message), 每个完成的 step 折成一块,写进侧助手会话的系统提示片段side-assistant:progress。注册挂在agent/created上: 侧助手每次提问都是一次冷恢复(新的 agent 上下文),所以不能只在创建时注册一次。 块只按父会话 id 归属(parentSession血统),别的会话的事件一律不进来。 谁会收块:新建的、菜单里点开的、以及 Host 启动时扫出来的那批(adoptExisting(),判据与菜单相同: 游离血统 + 当前组合 + 不在父会话的委派目录里)——所以重启之后直接往恢复出来的标签页里打字, 照样能拿到重启之后攒的块。 - 历史摘要也挂在这个钩子上。 冷恢复时框架只按描述符重建
persona与toolFilter,创建窗口里注册的 提示配置会随 agent 上下文一起丢掉,所以摘要(side-assistant:history,order 120,在进展块之前)不是创建时注册的, 而是和块一样在agent/created时挂上去——否则用户问第一句话时就「失忆」了。它是静态文本(创建时的快照, 和块的动态 getter 不同),随记录存在宿主内存里的progress表上。 - 思考模式默认关(
reasoningEffort: "off")。 侧问是用户在那儿等着的,它也不必规划任务; 关掉能省掉整段思考的生成时间,第一句话(和每次问候)都更快。只覆盖 effort、不传 provider/model (框架的resolveChildAgentOptions()有一条:换了路由却没点名 effort 会丢掉继承来的 effort)。 这个值同时写进描述符(agentReasoningEffort),所以冷恢复也不会掉回去;行配置里写low/high/max就打开,写inherit则完全不动主会话的路由。
已知边界
- 历史是摘要,进展是流。 侧助手不再复制主会话的聊天记录:建会话时把主会话到最后一个
turn/end为止的内容 压成一段摘要(用户问句 + 主 Agent 的回答,每条截断、整体有字数上限,最近的优先),作为系统提示片段side-assistant:history交给它。所以侧栏打开是一段干净的对话,而它仍知道你们聊过什么——但摘要是压缩过的, 它不该假装记得逐字原文。那之后的进展按块推进系统提示(见「进展条」)。 - 主会话里正在跑的那一轮不进摘要。 未闭合的回合没有结论可摘,那一段由进展块负责。
- 没有任何已完成回合的会话也能建侧助手,只是它从零摘要开始(不报错)。
- 一个主会话可以有多个侧助手,它们只靠「血统 + 未编目」识别,不靠
label。 菜单序号是创建顺序;删掉中间某个之后,后面的一起前移一位(删掉的那个不再进列表)。 真正的委派子智能体不会被误认——它们在目录里,第一道闸就筛掉了。 persona/denyTools/reasoningEffort是创建时定死的。 它们进的是会话的 agent 组合, 事后改不了(描述符虽然落在日志里、理论上可读,但插件不打算依赖它的 schema 稳定)。 改完配置要让旧侧助手失效,把index.js里的COMPOSITION_SINCE推到新的时间点。- 删除是归档,不是擦除。 菜单里每行右侧的
删除调的是工作区的archiveSession:会话从菜单与工作区列表里消失、 标签页随即关闭,但日志留在盘上,可以从归档列表恢复。它不进会话树,所以没有「父会话里清掉子智能体」那条路—— 恢复之后它重新算作「游离血统 + 当前组合 + 未编目」,菜单里又会出现。想彻底销毁日志就得手动删 DSH 会话存储里的记录。 - 依赖自带的对话视图与右侧栏。 聊天本体由
dsh-client-ui-conversation的conversation.content工厂提供(dsh-client-ui-chat画消息流),标签页由ui-sidebar-right提供; 禁用它们之后面板要么打不开,要么只剩下那条进展条。缺失时插件不会崩: 面板服务是用ctx.inject单独申请的,聊天区加载失败会显示一行「侧边对话加载失败」而不是空白。 - 不进会话列表。
origin === 'subagent'的会话本来就被工作区列表过滤掉,这正是我们要的零污染。 - 输入区精简(以及快捷卡片的可见性)靠两个 DOM 契约:面板根节点上的
data-sidebar-chat(现在是本插件自己打的) 和槽位锚点div[data-slot="<槽名>"]。前者失效只影响裁剪规则的命中范围, 后者是别人给的外部契约(渲染器设置,display:contents,DSH 自己的样式表也按它选元素), DSH 大版本升级时值得回看一眼;失效的表现是「某些控件又出现了」,不会报错、不会坏功能。 消息动作行与提示语同理,另外还依赖data-actions-reveal(助手轮次尾部独有)、data-composer-placeholder与可编辑区的aria-disabled——这几个都是dsh-client-ui-chat/dsh-client-ui-conversation的渲染细节,比槽名更容易变。失效的表现分别是「按钮又回来了」和 「提示语又变回官方那句」,两者都不影响功能。 - 提示语只在不可用 / 离线 / 被拦截时才让位。 计划模式、引导排队这两种状态也会被换成
「ask me anything you wanna know~」——它们不是错误态,但也不是这个面板要表达的重点;想让它们也保留原文,
把
SIDEBAR_CSS里那两条:has(> [data-placeholder][aria-disabled="true"])规则的条件放宽即可。 - 开场那句
hello会在面板里显示成一个用户气泡。 它是真实的 user 消息(DSH 固定这么投递, 插件改不了角色),而面板渲染的就是这个会话的记录。现在它的日志从这句开场白开始(历史是摘要、不是种子), 「第一个用户气泡」就是它,按位置藏也是安全的;插件仍然默认不藏,不想要开场白就配opener: ""。 - 思考档位的取值是适配器自有的。 DeepSeek 适配器是
off | low | high | max,插件不校验: 填了该路由不认的值,表现是建会话直接失败并带UNSUPPORTED_REASONING_EFFORT, 这时改成inherit即可。想中途改已建好的侧助手,就用侧栏里的模型选择器(它带 effort 档位)。 - 菜单是「每次点都重新问一遍」,不缓存。 所以在别的标签页里新建/删掉侧助手之后,
下一次点按钮就能看到,不用刷新页面。代价是每次点击都发一次
POST /side-assistant/list; 它读的是会话索引 + 这一层目录,不读会话日志。 - 菜单从「已有 1 个」就开始弹,不是「2 个以上」。 你的原话是「已存在助手有一个以上时候生效」,
但按字面实现(>1)时只剩一个助手就再也点不出「新建助手」,等于少了一个入口;
所以这里把「新建助手」放在菜单最后、并在一个都没有时走直接创建。
想改成严格的 >1,把
client.js里if (rows.length === 0)改成if (rows.length < 2)即可, 代价是恰好剩一个时无法再新建。 - 菜单与进展条都是自绘的,只用主题 token。 没有引用
@deepseek-ai/dsh-client-ui-primitives的下拉组件(普通 JS 插件不许 require Harness Client 包),所以样式跟随--dsw-*变量, 不跟随组件库的行为细节。 - 审批策略在创建窗口里被钉成
never(与旧版委派路径一致的语义):侧助手不该替你做需要审批的事。 它确实会尝试时,得到的是一条拒绝,而不是一个悬在侧栏里没人点的审批提示。 想要它更放手,得改index.js里capturePolicyOverrides()/appendPolicyOverrides()。 - 进展条只在面板开着时订阅,主会话被关掉或归档时它显示「读不到」而不会报错; 它也不订阅侧助手自己的会话(面板下方的聊天已经能看到那些)。 折叠只改显示:订阅不断,圆点与「读不到」那一行照样更新。 留存有上限:思维链 6 块 × 4000 字、工具行 30 行,更早的会滚掉;面板重建后靠"快照里的最近几步"补, 再往前的思考本来就不在侧栏里(主界面自己的记录里还在)。
- 展开/折叠的偏好不落盘。 记在
client.js的模块变量里,页面刷新就回到折叠状态; 做成localStorage会让一个纯 UI 细节多出一份需要清理的持久状态,暂时不值。 - 进展块只活在内存里,而且只有一个方向。 块表在
index.js的Map里,Host 重启就清空 (侧助手还在,只是「之后的新进展」从重启那一刻重新累积);每块都有上限(想法 200 字、结论 120 字、工具 3 条 × 60 字), 最多 4 块,更早的滚掉;结论是这一步文字的第一句,不是模型再写一遍的摘要。块里没有主会话里用户说的话,也没有回合边界,只有侧助手被创建(或被菜单收养)之后主 Agent 走过的 step。 冷恢复那条路径也一样:每次提问都会重新挂上片段,所以重启之后的第一问看到的是重启之后攒的块。 - 输入框起手一行,长到 336px 后改为框内滚动。 起手比主对话矮一行,之后增长、上限、滚动都与主对话一致;
想完全一样就删掉
SIDEBAR_CSS里min-height与inset那两条。
测试
没有测试框架,直接跑:
node test/host.smoke.mjs # Host 半区:路由、幂等、并发合并、创建窗口、进展块、错误分支
node test/client.smoke.mjs # Client 半区:工厂契约、槽/资源/标签页注册、地址、菜单、进展条
node test/address.contract.mjs # 地址契约:自家语法、独立 URL 读法、与自带 subagentchat 互斥
三个都用假 Context、假 DOM、假 fetch,不需要启动 DSH;client.smoke.mjs 里有一个几十行的
hook 运行时(尊重依赖数组,能安装/卸载 effect),能把按钮与面板真的渲染出来、按下去、
再喂几帧 session.follow 的假数据,所以菜单与进展条的每个分支都是行为验证,而不是只断言字符串。
address.contract.mjs 会自动找到本机 DSH 安装并交叉校验两条语法的互斥性,
找不到就只跑本地语法(用 DSH_SUBAGENT_CLIENT=<…>/dsh-client-ui-subagent/lib/client.js 可强制指定)。
许可
Copyright (C) 2026 the dsh-side-assistant authors.
本程序是自由软件:你可以依照 GNU Affero 通用公共许可证第 3 版(AGPL-3.0,由自由软件基金会发布) 重新发布和/或修改它——第 3 版,或者(按你的选择)任何更新版本。
本程序是希望它有用而发布的,但不带任何担保,甚至不带适销性或特定用途适用性的默示担保。 详见 GNU Affero 通用公共许可证。许可证全文见 LICENSE,没拿到就看 https://www.gnu.org/licenses/。
因为是 AGPL-3.0:把这个插件改过之后作为网络服务提供给别人用,有义务向使用者提供对应的源码—— 它本来就是作为 DSH 网页界面的一部分运行的,所以这条义务覆盖的是你运行的那份插件代码,不只是分发出去的副本。
Comments
Loading…
From the same category
by anywhere-labs
为 DeepSeek Harness (DSH) 插件生态打造的现代化桌面端解决方案。万物皆「插件」,桌面本身也是「插件」。
★ 31.3k
↓ 115/wk
MIT
TypeScript
Oct 17, 2026
dsh plugin --profile web add dsh-plugin-desktopby zhu1090093659
DeepSeek Harness (DSH) Web Plugin Aggregation Ecosystem · Everything is a plugin, distributed via the Creative Workshop
★ 8.9k
↓ 145/wk
Apache-2.0
TypeScript
Oct 17, 2026
dsh plugin --profile web add dsh-webA collection of independent Web UI plugins and skins, including task boards, Git graphs, mobile access, and live token stats.
★ 8.4k
↓ 145/wk
Apache-2.0
TypeScript
dsh plugin --profile web add dsh-webby omdsh-dev
开放的侧边栏底座,支持三方拓展注册新侧边栏页面。内置文件渲染编辑/终端/侧边对话/Git/子代理页面 | Open sidebar foundation, supports third-party extensions to register new sidebar pages. Built-in file rendering/editing, terminal, side chat, Git,
★ 4.3k
↓ 57.6k/wk
MIT
TypeScript
Oct 17, 2026
dsh plugin --profile web add dsh-better-sidebarby bowenliang123
The best DeepSeek Harness plugin for context insight and management, with context dashboard / browser / sidebar and context command, for context statistics, composition, breakdown, evolution details,
★ 2.3k
↓ 52.1k/wk
Apache-2.0
TypeScript
Oct 18, 2026
dsh plugin --profile web add dsh-contextby shaobeichen
把 DeepSeek Harness 装进你的口袋:电脑上跑 dsh web,手机扫码即同步访问(局域网 + 公网,实时同屏)Put DeepSeek Harness in your pocket: run dsh web on your computer and access it synchronously by scanning a QR code on your phone (LAN +
★ 1.8k
↓ 3.4k/wk
GPL-2.0
JavaScript
Oct 17, 2026
dsh plugin --profile web add dsh-pocket