# Changelog 本项目的版本遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/),日志格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 ## [1.2.2] - 2026-10-29 ### Fixed - **issue #23:配置接缝优先识别条目表单能力**(host 半,需重启 `dsh web` / 桌面应用):`configure` + `describe/update` 存在时按本插件 profile 行 id 读写,即使宿主仍保留 legacy `register`;不再让命名空间分支覆盖 volatile 配置读源。缺少条目 id 时明确拒绝,不回退写入另一份存储。旧命名空间宿主继续沿用原路径。 - **Flash 模型目录兼容**(host 半):回答配置、摘要与标题调用在使用时查 provider 目录;仅当所配名称缺失、另一名称确实存在时,兼容 `deepseek-v4-flash` / `deepseek-flash`。保留已有条目、自定义模型及保存值;目录查询失败或无匹配时保留原值。 - 补充混合 settings API、配置读取/保存/冲突与新旧模型目录回归;31 文件 / 455 用例通过。真实 settings 产物验证与桌面验收边界见 [issue #23 验证记录](docs/issue-23-verification.md)。 ## [1.2.1] - 2026-10-19 **修 DSH 0.2.1 平台上的两个 P0:`/sidebarqa/api` 信任围栏退化(LAN / `--trusted-host` 恒 403),以及追问面板不再流式、轮次失败没有任何提示。同时把类型桩升到 `0.2.1-rc.2`、装载闸测试改成「0.2.0 peer / 0.2.1+ engines」两代语义。** ### Fixed - **信任围栏迁移到 `webStartup`**(**host 半改动,需重启 `dsh web` / 桌面应用**):DSH `0.2.1-alpha.1` 删除了 `webRuntime` 服务;`src/index.ts` 仍只读它,回退读到 loader row 里未求值的 `!!js` 表达式,`Array.isArray` 判否后围栏静默退化为 loopback-only——`dsh web --host 10.0.0.7` 或 `--trusted-host app.example` 的浏览器访问 `/sidebarqa/api` **全部 403**(`config` / `context` / `title` / `catalog` 一起失效),且没有任何报错。 - `trustedHostsOf` 现在按 `webStartup.trustedHosts`(0.2.1+)→ `webRuntime.trustedHosts`(旧宿主)→ loader 的 `id: connection` 行字面量顺序读;**服务存在即以其值为准**(空列表表示 loopback-only,不再回退到别的来源)。 - 新增 `bindHostOf` / `bindProtocolOf`(读 `webServer.host` / `.protocol`),围栏按上游 `packages/client/connection/src/api-request-trust.ts` 接受**监听 bind IP**、按 http(s) 规范化默认端口、拒绝 cross-site 与不匹配的 Origin。 - `webServer` 镜像补 `host` / `protocol`,`webStartup` 镜像补上出处;`webRuntime` 保留为旧宿主回退。 - 回归:新增 `tests/trust-fence.spec.ts`(17 例)与 `tests/host-route.spec.ts`(9 例,用 fake host 驱动真实 `apply` 路由:webStartup 优先、bind IP、旧 face 回退、malformed 值不抛、403/200、Origin 与 cross-site)。 - **追问恢复流式;失败轮次不再永久停在「生成中…」**(**client 半改动,浏览器硬刷新即可**):DSH 0.2.x 的 agent loop 不再写持久化的 `assistant/chunk`——成功提交 `assistant/message`,失败/中断提交 `assistant/attempt`(含 compact `stream`),实时增量改为 opt-in 的 `follow({ assistantStream: true })`。插件原实现两者都没接,回答只在结束时整段出现,模型报错时面板没有任何提示。 - `followSession` 显式传 `assistantStream: true`,转发 `start` / `chunk` / `end` 帧与重连 snapshot 的 `assistantStream.activeAttempt`。 - `answer.ts` 新增 `foldAssistantStreamRecords` / `assistantAttemptOf` / `liveStreamOf` / `applyAssistantStreamFrame`,把文本增量、失败原因(`finish.reason.failure`)与旧格式 `assistant/chunk` 统一折叠;`assistant/attempt` 成为带 `failure` 的 transcript 行(保留已生成的部分文本)。 - `AskPanel` 维护 live 行:首 token 起逐段渲染,durable 事件落地后替换;失败轮次显示「本轮回答失败:」。`MarkdownText` 的 `labels` 仍按 locale memo。 - 新增文案键 `askTurnFailed`(zh/en);回归:`tests/answer.spec.ts`(新增 9 例)、`tests/session-wire.spec.ts`(新增 follow 帧转发用例)。 ### Changed - **装载闸叙事与测试对齐 0.2.1 的新语义**:DSH `0.2.1-alpha.3+` 的 `evaluatePluginCompatibility` 只要 manifest 有 `engines.dsh` 就**只按它判**、完全忽略 peer(0.2.0 仍只看 peer)。`tests/dsh-compat.spec.ts` 重写为两代闸门(14 例):两代区间都必须是纯下限、反向检测器覆盖「peer 天花板」与「engines 天花板」、`engines.dsh` 存在时忽略 peer、并新增 `package.json` ↔ `dsh.plugin.json` 的 version / `engines.dsh` 同步守卫。 - README / README_EN / AGENTS / CLAUDE 修正:配置面板是 **8 个常用键**(schema 共 12 字段);测试规模更新为 30 文件 / 443 用例;`engines.dsh` 不再是「DSH 不读」的纯声明;安装/升级补充 0.2.1-alpha.2+ 的 **remove → add** 规则;平台种子词与 `webStartup` 的出处同步。 - `pnpm-workspace.yaml` 的 `peerDependencyRules.allowedVersions` 随桩的 cordis peer 从 `4.0.4` 升到 `4.0.5`(与该 checkout 的 vendored cordis 一致)。 ### Dependencies - 类型桩精确钉到 `0.2.1-rc.2`:`@deepseek-ai/dsh-client-locale`、`@deepseek-ai/dsh-client-ui-primitives`。 - 构建期打包的 schema 库同步到宿主版本:`@deepseek-ai/schemastery@3.18.5`(精确钉,`noExternal` 打进 `lib/index.js`)。 - **升级需重新 `pnpm install`。** > 部署提醒:host 半(信任围栏)需重启宿主;client 半(流式/失败行)只需硬刷新浏览器。桌面版请完全退出应用后重启 launcher。 ## [1.2.0] - 2026-10-15 **修 [issue #21](https://github.com/ChenRuoT/dsh-sidebar-qa/issues/21)「设置保存失败」:配置改为声明在插件自己的 `Config` schema 上、并按 profile 条目 id 寻址——DSH ≥ 0.1.7(含 0.2.x)上「追问」设置页从「只读默认值」变成可保存。** ### Fixed - **设置面板在 DSH ≥ 0.1.7 上可以保存了**(**host 半改动,需重启 `dsh web`**):`dsh-settings@0.1.7-alpha.1` 起没有 `register`,可编辑字段改为从插件自报的 `Config` schema 派生(`volatileForm` 只认标了 `volatile()` 的字段),`describe` / `update` 接受的是 **profile 条目 id**(`cordis.patch.yml` 里那一行的 `id`,本插件是 `sidebar-qa`),而不是插件声明的命名空间。1.1.0 的接缝只会注册命名空间,于是在这类宿主上 `configFace` 恒为 `undefined`——**每一次保存都回 503「this host exposes no sidebarqa settings namespace」**(正是 #21 截图里那行红字),面板只能显示默认值。 - host 半现在导出 `export const Config = SidebarqaPrefsSchema`,12 个字段**全部 `volatile()`**;`apply(ctx, config)` 拿到的是每字段一个**活引用**,新增的 `src/live-config.ts` 负责把它读成普通配置(字段缺失或读抛错只回落该字段的默认值,绝不让一次读失败打掉整个路由)。 - `src/settings-face.ts` 的接缝现在有两形、互斥:宿主有 `register`(≤ 0.1.6,低于本包声明下限)走原来的命名空间;没有就按**条目 id**(取 `ctx.fiber.entry.options.id`)打开**同一个** revision 守卫的 `describe` / `update` 面。id 取不到、或服务连 `describe`/`update` 都没有时给出**带原因的拒绝**(`no-entry` / `no-form`),503 文案由 `settingsRefusalMessage` 按原因生成,不再是一句「可能缺服务、也可能缺注册」的含糊话。 - 保存的值经 loader 的 volatile 提交(`updateVolatile` / `loader/volatile-update`)**直接写进运行中的引用**,插件不重挂;摘要与标题路由下一次调用读到的就是新值。 - 新增 `claimOwnSettingsCard`:在同代宿主上用 `settings.configure({ auto: false }, ctx.fiber)` 抑制宿主按 schema 自动生成的表单——本插件自己已有一整页设置卡(`settings.section`),同一份配置不该出现两个入口(结构探测;宿主没有该方法时静默跳过,且 disposer 挂在 effect 上随插件卸载)。 - **配置的落点变了**(≥ 0.1.7):写在插件自己的 profile 行(`cordis.patch.yml` 里 `id: sidebar-qa` 的 `config:`),不再写 `settings.yaml` 的 `sidebarqa` 段(那一代宿主本来也没有该命名空间)。老宿主行为与 1.1.0 一字不变。 - **回归**:新增 `tests/live-config.spec.ts`(12 例:活引用识别全有全无、读永远给出完整普通配置、坏引用只降级单个字段);`tests/config.spec.ts` 增加「每个字段必须 `volatile()`、根不可 `volatile`」(漏一个字段 = 宿主表单少一项且写入被 `Config field "…" is not volatile` 拒绝)与「解析结果就是运行期能就地更新的活引用」;`tests/settings-face.spec.ts` 重写(21 例)——两形接缝、**条目 id 寻址**、描述符值解引用(活引用直接上线会 `JSON.stringify` 成 `{}`,面板全空)、畸形宿主不抛、拒绝文案逐原因唯一,以及接线守卫(`profileEntryIdOf(ctx)`;`src/index.ts` 不得再出现 `no-register`)。 - **实测(本机 checkout DSH `0.2.1-alpha.3`,只读)**:用**真实的** `SettingsForms` + `volatileForm` / `isVolatilePath` + loader 的 volatile 提交原语,驱动本插件**构建产物**的 `apply` 与 `/sidebarqa/api` 路由处理器(config editor 换成内存假件,不写用户 profile):`config.get` → 200 / revision 0 / 12 个字段;`config.update` → 200(#21 里这是 503)/ revision 1 / 值落进行 config;活引用**立刻**读到新值;stale 写入 → 409 `settings-conflict`;未声明键 → 400;非 loopback Host → 403。同一路径上「按命名空间寻址」仍然失败(`No configurable plugin entry "sidebarqa"`),反向钉住了这个修法的必要性。 ### Dependencies - **`schemastery` → `@deepseek-ai/schemastery@3.18.4`(devDependency,精确钉)**:`volatile()` 只存在于 DSH 的分叉(npm `schemastery` 停在 `3.18.0`),而宿主只读 schema 的 `meta` / `dict` / `toJSON()`,所以这份 schemastery 由**构建期打包进 `lib/index.js`**(`tsdown.config.ts` 的 `noExternal`)——运行期不依赖宿主那一份,也就不会与它漂移。`dependencies` 因此变空(npm `schemastery` 不再被引用)。**升级需要重新 `pnpm install`。** ## [1.1.0] - 2026-09-29 **修复 DSH `0.2.0` 上插件整个不加载:peer 区间写成了闸门会拒的「天花板形」,而 DSH 0.2.0 新增了一道启动期 peer 兼容闸。** ### Fixed - **peer 区间从「^锚点」改成「纯下限」,插件重新能在 0.2.x 上加载**(**host 半改动,需重启 `dsh web`**):DSH `0.2.0` 的 boot 在装载每个 profile bundle 前先跑 `packages/boot/app-boot/src/plugin-compatibility.ts` 的 `evaluatePluginCompatibility`——对 manifest 里**每一个** `@deepseek-ai/dsh` / `@deepseek-ai/dsh-*` peer 区间做 `semver.satisfies(runtimeVersion, range, { includePrerelease: true })`。只要有一个不满足,`loadProfileDirectory` 就把这个 bundle 的**整个 patch 层**丢掉(`skippedBundles`),只在 stderr 打一行 `dsh: skipping profile bundle "…"`——**host 半不加载、client bundle 不组装,插件像没装一样**。不是降级,是消失。 - 本插件的 peer 写的是 `@deepseek-ai/dsh-client-locale: ^0.1.0-rc.8`。**带预发布的 caret 不是下限而是「下限 + 天花板」**:`validRange('^0.1.0-rc.8')` = `>=0.1.0-rc.8 <0.2.0-0`。运行时的 `0.2.0-rc.1` 已经越过 `<0.2.0-0`,于是**每个 0.2.x 宿主上插件全层被跳过**——这正是「升级 0.2.0 后侧边栏追问没了」的样子。 - 现改为纯下限:`@deepseek-ai/dsh-client-locale: >=0.1.0-rc.8`、`@deepseek-ai/dsh-client-ui-primitives: >=0.1.7-alpha.1`。这条修法本身就是**新版本生成上线时插件再次消失**的通解:不带天花板,任何将来的 `0.x.y` / `0.x.y-pre` 运行时都满足;带天花板的区间(含 `^` 落在 0.1.0 预发布上、以及任何 `>=a =0.1.2-alpha.1` 抬到 `>=0.1.7-alpha.1`(如实反映 1.0.1 起就依赖的 `SettingsForms` 世代与 1.0.2 的图标改名世代;DSH 不校验 `engines`,它只是声明)。 - `dsh.plugin.json` 的 `engines.dsh` 同步更新、`version` 对齐 `package.json`(此前它一直停留在 `1.0.0`;随本次 `pnpm version minor` 一起到 `1.1.0`)。 ### Changed - **类型桩钉到 `0.2.0-rc.1` 世代**(dev-only):`@deepseek-ai/dsh-client-locale` 与 `@deepseek-ai/dsh-client-ui-primitives` 两个 devDependency 均**精确钉 `0.2.0-rc.1`**,`pnpm-workspace.yaml` 的 `peerDependencyRules.allowedVersions` 随之更新(两个桩 peer 的是 `@deepseek-ai/cordis@~4.0.4`,本包从不链接)。**client 半源码零改动**(随包发布的 `lib/client.js` / `lib/client-registry.js` 重新构建过一次,内容与 1.0.2 等价)。升级后 `tsc --noEmit` **零报错通过**——client 半对 0.2.0 API 世代的全部调用(`MarkdownText.labels` / `Tooltip` / `Menu` / `iconOf` 的 7 个 glyph / `conversation.input.for(actx).setDraft` / `locale.register(ns, locale, dict)` / `sidebarRightTabs` 三阶段注册)都在新桩上重新校验过一遍。 - **`pnpm-workspace.yaml` 的 `minimumReleaseAgeExclude` 改为作用域通配符**(dev-only,装机 prerequisite):pnpm ≥ 11 默认开启 24h `minimumReleaseAge` 供应链接入闸,而 `@deepseek-ai/*` 的 rc 发布总是「当天发、当天被本类插件钉上」,`name@version` 形式的旧条目在 pnpm 11 上**已不再生效**,`pnpm install` 会以 `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION` 直接失败。改为 `'@deepseek-ai/*'` 一条(范围仅限 devDependency 的类型桩:不随包发布、运行期不链接、且全部精确钉),旧的 rc.6 条目保留作溯源注释。 - **0.2.0 兼容性逐点核对结论(无源码改动)**:右侧栏类型注册表(`SidebarRightGuideEntry` 的 `id` + `order` + 可选 `icon`、`title: (address) => string`)、`sidebar.right.pane.tab` / `.title` 两个 keyed 席位、`session-controller` 的远端动词(`rename` / `fork` / `prompt` / `page` / `follow` / `create` / `selectModel` / `modelCatalog`)、`session-query` 的 `SessionSurfaceSnapshot`、`llm` 的 `GenerateOptions`、`webserver` 的 `WebRoute`、`WebRuntimeValues.trustedHosts`(信任围栏读的那个服务面)、`PLATFORM_MODULES` 九个平台种子词、`SettingsForms`(无 `register` → 设置接缝安静降级,与 1.0.1 的行为一致)——`v0.1.7-rc.2 → v0.2.0-rc.1` 两档之间这些文件的源码 diff 为空,仅版本号变化。**因此本次没有源码层适配工作,唯一的根因就是 peer 区间。** - **新回归 `tests/dsh-compat.spec.ts`(7 例)**:在 node 里逐行复刻 0.2.0 的闸门——(1)插件声明的每一档 DSH 运行时(`0.1.7-alpha.1` / `0.1.7-rc.2` / `0.2.0-rc.1` / `0.2.0` / `0.3.0-rc.1`)都必须通过;(2)「旧区间 `^0.1.0-rc.8` 在 `0.2.0-rc.1` 上必须被判不兼容」这条**检测器**反向钉住本套测试能抓到这个故障;(3)每个 gated peer 区间**不得含 `<` 天花板**(`semver.validRange` 归一化后不含 `<`);(4)装了 DSH checkout 时直接读它 `app-boot` 的版本再跑一遍闸门;(5)每个 `@deepseek-ai/*` devDependency 必须是**精确钉**(`semver.valid`)且与 `node_modules` 里装好的副本逐字一致。 - `tests/primitives.spec.ts` 的桩世代判定改为**从装好的桩自身读**(`Regular`/`Medium` 后缀 = 改名后世代,否则为改名前),不再假定「桩一定在旧世代」;两个世代的解析链仍各自全量覆盖。 - 新回归在 node 里复刻 DSH 闸门需要同一个 semver 判定器,故 devDependency 增加 `semver` + `@types/semver`(**仅 tests 使用**,不进 client bundle、纯度门不受影响)。 - `tests/markdown-labels.spec.ts` 的说明改为指向新的钉子(精确钉 + 纯下限 peer),语义不变。 ## [1.0.2] - 2026-09-24 **修「打开侧面板就报 `面板渲染出错:Minified React error #130 … but got: undefined`」:宿主在 `0.1.7-alpha.1` 重命名了整套图标,而本插件按名 import 的那些 glyph 在运行期就是 `undefined`。** ### Fixed - **宿主图标改为按名解析,不再按名 import**(**仅 client 半改动,浏览器硬刷新即可**):`feat(web): unify the client visual language`(首个发布 `0.1.7-alpha.1`)把宿主整套图标从「尺寸后缀」改成「字重后缀」——`IconQuestionOutline14` / `IconCheckOutline16` → `IconQuestionOutlineRegular` / `IconQuestionOutlineMedium`——**且不留任何别名**(`packages/client/ui-primitives/src/icons/index.tsx` 里旧名字已彻底删除)。 - **症状**:面板输入区(`ModelSelect` / `StrategySelect` 的 chevron、勾选、警告 glyph)在**本插件的 containment 边界内**抛错,于是整块面板被替换成那条可重试的崩溃说明条;tab chip 的 glyph 在边界**之外**(活标题席位)抛错,由宿主的 per-entry 边界接管——代价是整个标题注册被 retire(页面上 tab 标题丢失)。两处都是同一个根因。(那条说明条本身按设计工作:以前这种情况是**整块变白且无法恢复**。) - **为什么 `tsc` 拦不住**:本包的类型桩精确钉在 `0.1.6-alpha.2`(改名前的世代),旧名字在桩里仍然声明;而运行期交给插件的是**宿主自己的模块命名空间**,于是这些绑定就是 `undefined`——React 对 `undefined` 的答复是 error #130(`Element type is invalid … but got: undefined`,URL 里 `args[]=undefined&args[]=` 正是它)。 - 新增 `src/client/primitives.ts`(**纯规则**,node 可测):`pickExport` / `iconIn` / `Glyph`——解析链 `Regular → Medium → `;「名字在但不是组件」的值会被跳过;三种名字都没有时返回 `undefined`,由 `Glyph` 渲染为空(**素材缺失是降级,不是故障**)。 - 新增 `src/client/host-primitives.ts`:**唯一** value-import `@deepseek-ai/dsh-client-ui-primitives` 的模块,把宿主命名空间绑给上面那条规则(`iconOf`)。平台模块表在任何 bundle factory 运行前就已冻结,所以在模块作用域读取是安全的——这与 cordis 服务「必须在调用点现场探测」的规则不同。 - 调用点全部改走 `iconOf` + `Glyph`:`index.tsx`(两个 tab 的 glyph)、`ModelSelect.tsx`(4 个)、`StrategySelect.tsx`、`HistoryPanel.tsx`。`SidebarTab.icon` 变成**可选**;guide 条目在拿不到 glyph 时**不写 `icon` 键**(把宿主的方块占位留给宿主自己画);`titleFor` 经 `Glyph` 渲染,绝不 `createElement(undefined, …)`。 - `MarkdownText` / `Tooltip` / `Menu` 仍是按名 import:它们是**有类型的 API 面**,宿主跨版本保留;会被随意改名的只有图标素材。 - 回归:`tests/primitives.spec.ts`(解析链 / 非组件值 / 两种世代 / 接线守卫「只有 `host-primitives.ts` 能 value-import 宿主命名空间」「任何 client 文件都不得再按名 import `Icon*`」/ 装了 DSH 源码 checkout 时直接核对真机导出表);`tests/panel-icons.spec.ts`(用**只有 0.1.7 名字**的宿主 mock 渲染 `StrategySelect`——旧代码在这条用例上会抛 #130);`tests/sidebar-native.spec.ts` 新增「宿主没有这个 glyph 时」的注册 + 标题渲染用例。 ## [1.0.1] - 2026-09-24 **修 [issue #19](https://github.com/ChenRuoT/dsh-sidebar-qa/issues/19):在 `@deepseek-ai/dsh@0.1.7-alpha.1+` 上启动不再打 `settings registration failed: TypeError: settingsService.register is not a function`。** ### Fixed - **settings 命名空间改为按结构探测,不再假定 `register` 存在**(**host 半,需重启 `dsh web`**):`@deepseek-ai/dsh-settings` 在 0.1.7 把服务从 `SettingsProvider`(有公开 `register(ns, schema, options)`)换成 `SettingsForms`——**服务同名、没有 `register`**(`0.1.7-alpha.1` / `0.1.7-alpha.2` 的 npm tarball 逐字节核对过,无兼容入口),可编辑字段改由**插件自报的 `Config` schema** 派生、`describe` / `update` 按 **profile 条目 id** 寻址。此前 `src/index.ts` 直接调用 `settingsService.register(...)`,在没有该方法的宿主上**每次启动**抛 `TypeError`;外层 `catch` 让它只是降级(回落 `SIDEBARQA_DEFAULTS`、追问链路照常)而不是崩溃,但日志里永远多一行失败。 - 新增 `src/settings-face.ts`:`openSettingsSeam(service, ns, schema)` 用 `typeof …register === 'function'` 判定(**结构校验,不是版本比较**——与 client 半对 `sidebarRightTabs` / `slots` 的规则一致),返回 `{ ok: true, scope, face }` 或带原因的拒绝 `{ ok: false, reason: 'service-missing' | 'no-register' | 'register-failed', error? }`;**探测本身绝不抛**(它跑在 cordis `inject` 回调里,抛出即启动期故障)。`describe` / `update` 也各自探测,并补 `Array.isArray` 校验。 - 宿主没有 `register` 时只记**一条 `console.info`**(说明该宿主把可编辑字段交给插件 `Config` schema、本插件因此走默认值),不再是 warn + 堆栈;只有 `register` 真的被拒(命名空间重复等)才 warn。 - `config.update` 的 503 文案改为如实描述:原来的「the settings service is not mounted in this deployment」在 0.1.7 上是误导——服务在,缺的是 `register`。 - `src/context-types.ts` 的 `SidebarqaSettingsService` 镜像补上出处与「0.1.7+ 没有 `register`」的说明,避免下一个人再按版本假设这个成员存在。 - **0.1.7+ 上设置页仍不可写**(回落默认值):保留设置卡需要按新模型重做(导出带 volatile 的 `Config` 并按条目 id 寻址),属于单独一条,不在本次范围。 - 回归用例 `tests/settings-face.spec.ts`(12 例):0.1.7 形状必须被判成 `no-register` 且**不触碰** `describe` / `update`;provider 形状必须转发 ns/schema、读回 value + revision、把 `expectedRevision` 透传下去;`describe` 返回非数组 / 无该命名空间 / 缺 `update` 都必须降级而不是抛;另有一条接线守卫——`src/index.ts` 必须走 `openSettingsSeam`,且**不得**再出现 `settingsService.register`。 - `engines.dsh` 已是 `>=0.1.2-alpha.1`([issue #14](https://github.com/ChenRuoT/dsh-sidebar-qa/issues/14) 的建议早已落地),#19 里「建议顺带补 `engines.dsh`」那句属过时信息。 ## [1.0.0] - 2026-09-20 **首个 1.x:只注册进 DSH 自带右侧栏,零额外依赖。** 走 major 的原因只有一条——**移除了 `dsh-better-sidebar` 后端**(`### Removed` 一节):升级后不再需要任何 peer 侧边栏基座,装 `dsh-sidebar-qa` 一个包即可;此前依赖 better-sidebar 的部署请一并卸掉它。 ### Added - **只注册进 DSH 自带右侧栏(`@deepseek-ai/dsh-client-ui-sidebar-right`,DSH ≥ `0.1.5-alpha.1`)**:本版本**移除 `dsh-better-sidebar` 兼容**,插件只有一个侧边栏后端,且**不需要任何额外依赖**。 - **三阶段注册,共用一个实现 id**:① 类型进 `ctx.sidebarRightTabs.register({ id, kind, priority: 'extension', title, guide })`(页面类型:无 `patterns`、无 `multiple`);② body 进 keyed 席位 `sidebar.right.pane.tab`;③ **活的标题**进 keyed 席位 `sidebar.right.pane.tab.title`。后两者都是 `ctx.slots.inject(name, () => ctx.slots.register({ name, key: id }, Component))`——**席位按实现 id 分发,不是按 kind**;打开走 `ctx.sidebarRight.openTab(kind, { params })`。 - **标题席位是新增能力**:DSH 的 tab 标题只在**打开时刻**求值一次并写进布局记录、之后永不改写(上游也明确「没有标题注册时使用打开时捕获的文本」),所以以前已打开的 tab 会**冻结在打开时的语言**。注册一个**组件**到标题席位后,它订阅语言 revision 并在原处重渲染——**已打开 tab 的标题现在跟随语言切换**。宿主不声明该席位时回退为那个冻结字符串(陈旧但无害)。 - **`+`(guide)条目仍是可发现性的唯一来源**:原生侧边栏的 `+` 控件只打开 guide 页,注册的类型靠 `guide` 条目暴露成可点胶囊;本插件为两个 tab 各注册一个条目(标题/说明按语言实时读取),新增 `askGuideDesc` / `histGuideDesc` 文案。 - **配置面板有了归宿:迁入 DSH 官方设置页**。它注册成一个 `settings.section`(注册逻辑在 `src/client/settings-slot.ts`,纯模块、可测;页面组件 `src/client/settings-section.tsx` 渲染 `ConfigPanel`),因此设置页左侧导航新增**一整页**「追问 / Follow-up」(文案键 `cfgNavLabel` / `cfgSectionTitle` / `cfgSectionDesc`),`order: 30` —— 排在 DSH 自带各页(general 0 / models 10 / plugins 15 / agent-presets 20 / archived-sessions 25)之后。**不再有齿轮弹窗**。 - 注册形态:`ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'sidebar-qa', order: 30, label: () => t('cfgNavLabel') }, ConfigSection))`;插槽注册表仍是 `ctx.get()` 探测(`src/client/slots.ts`,与侧边栏模块共用同一个探针)。 - **为什么不是 `plugins.item`**:该席位的契约明文写着它留给 `ui-settings-plugins` 那些 host-plane 配置页,并要求 bundle 的配置走 `plugins.bundle.config`;而那个插件页在没有受管 profile 的部署上整页不可用,会把配置入口一起带走。`settings.section` 永远可达。 - 新增 client 纯模块 `src/client/ask-mode.ts`(`resolveAskMode`:面板视图模式;自 `meta-quote.ts` 搬出,后者随 better-sidebar 一起删除)与 `src/client/show-session.ts`(`showSessionFirst`:用 `ctx.uiWorkspace.openSession` 把目标会话切到屏幕上)。 - 回归用例:`tests/sidebar-native.spec.ts`(重写)、`tests/ask-mode.spec.ts`(新增)、`tests/settings-section.spec.ts`(新增)。 ### Changed - **`inject` 收窄为 `['sessions', 'remote', 'remote.session', 'workspaces']`**,`betterSidebar` 不再出现(它此前是硬依赖:任何没装 `dsh-better-sidebar` 的部署都会让**整个插件**不激活)。侧边栏服务 `sidebarRightTabs` / `sidebarRight` / `slots` **一律 `ctx.get()` 探测 + 监听 `internal/service` 重探**,**绝不写进 cordis `inject`**——cordis 没有可选依赖,注入一个宿主不提供的服务会让 fiber 停在 PENDING,而 `boot-client.ts` 会让**整个 web boot 抛错**(白屏,不是降级)。三件套齐备才落定:只有注册表没有席位,tab 会出现但 body 渲染不出来。全都缺失时插件仍激活(浮层 + 「添加到对话」),只记一条 `console.warn`。 - **面板不再感知侧边栏**:`AskPanel` / `HistoryPanel` 删掉了「推 tab 标题」与「面板自愈展开」两个 effect(前者由标题席位承担,后者原生 `openTab` 自己会展开栏位),也删掉了 `onTabActivated` 订阅(原生路径上本来就是死代码)。面板现在只消费一个中性的「出现实例」`SidebarqaTabOccurrence`:`{ tabId, revision, takeQuote(), openHistory(scope) }`(`src/context-types.ts`)。`revision` 就是 `navigation.revision`——每次被导航到都递增,是「再次打开」唯一的信号(一次外部打开 = 一个新的出现实例,所以引文能重新递送)。 - **跨会话手势统一走 `src/client/show-session.ts`**:原生 `openTab` 只作用于**已挂载**的会话 surface(无 surface 时抛 `no session surface is mounted`),而导航后会话绑定要到下一次 React commit 才由 seat 的 `useEffect` 重新发布——同 tick 的 `openTab` 会**静默**打到刚离开的会话。现在先 `uiWorkspace.openSession` 切屏,随后用 `openTabIn(sessionId, …)`(DSH 自己 Tab 域的路径,静默但绝不会落错会话)跨帧重发并以 `active()` 确认;宿主没有 `openTabIn` 时退回跨帧 `openTab`,且检查期间没有更晚的导航接管。 - **`id` 与 `kind` 是同一个 `SidebarTab` 对象上的两个字段**,打开时从**被注册的那个对象**读回 kind。「注册 `ask` 却打开 `dsh-sidebar-qa:ask`」这类分叉(`placeTab` 会抛 `no tab type is registered as "…"`)因此不可表达。 - `engines.dsh` 仍是 `>=0.1.2-alpha.1`(浏览器侧 RPC 走 `ctx.remote.session` 的下限),但 **DSH 完全不校验 `engines`**,所以它只是声明、不是闸门;真正的前置是**侧边栏 tab 需要 DSH ≥ `0.1.5-alpha.1`**。更早的 DSH 上插件**仍然激活**,只是不注册侧边栏 tab。 - `dsh.client.inject`(包名依赖边)保留:它是**纯信息性**的声明,不决定加载顺序,缺失会被静默跳过,**不是版本硬约束**。 - `src/context-types.ts` 随之收敛:删掉端口/桥/两后端相关类型,`SidebarqaTabComponentProps` 只保留 `tab.id` + 可选的 `sidebar` 出现实例,新增原生侧边栏、`slots`、`uiWorkspace`、`webRuntime` 的服务面镜像并挂 Context augmentation;与上游的漂移仍然只收敛在这一个文件。 ### Removed - **peer 依赖 `dsh-better-sidebar`** 及其 `peerDependenciesMeta` 条目(`package.json`)。 - `pnpm-workspace.yaml`:两条 `dsh-better-sidebar` 的 `minimumReleaseAgeExclude`,以及整个 `allowBuilds` 块(`node-pty` / `protobufjs` 只因 better-sidebar 才在依赖图里);`pnpm-lock.yaml` 重生成后二者已完全消失。 - 源文件:`src/client/sidebar-port.ts`(端口契约 + 后端探测)、`sidebar-install.ts`(后端选择与安装)、`sidebar-better-sidebar.ts`(适配器)、`ensure-panel.ts`(面板收起自愈——原生 `openTab` 自己展开栏位)、`tab-activation.ts`(`onActivate` 桥——`navigation.revision` 取代)、`meta-quote.ts`(`resolveAskMode` 搬到 `ask-mode.ts`,引文形状校验改在 `sidebar-native.ts` 的 `quoteOfNavParams` 里)。 - 测试:`tests/sidebar-port.spec.ts`、`sidebar-seam.spec.ts`、`sidebar-better-sidebar.spec.ts`、`ensure-panel.spec.ts`、`tab-activation.spec.ts`、`meta-quote.spec.ts`。 - 文档:`docs/better-sidebar-expand-feature-request.md`(该功能请求随 better-sidebar 一起作废)。 - **随旧后端一起作废的还有 [issue #16](https://github.com/ChenRuoT/dsh-sidebar-qa/issues/16)**([PR #17](https://github.com/ChenRuoT/dsh-sidebar-qa/pull/17) 曾按「`panelOpen` 缺席 = 原生时代」修过这条路径):那个 bug 出在 `ensure-panel.ts` 的宽屏判定上——better-sidebar ≥ 0.19 把右栏交给 DSH 原生 Sidebar 后,插件只剩底部工作台一棵树,而它的 `activePane` **恒为底部叶子**,于是每次「提问」都会在原生侧栏之外**再强行打开底部面板**。1.0.0 已不存在任何旧面板自愈(`ensure-panel.ts` / `tab-activation.ts` 一并删除),该问题在结构上不可能再出现;这段记录是说明**为什么不把 #17 的补丁带进来**,而不是漏合。 ### Fixed - **侧边栏的 tab 图标没了:`+` 页(guide)的胶囊画的是宿主的方块占位,tab 条上只有裸文字**。原生改造(1.0.0 的重构)把图标整个丢了——旧的 better-sidebar 注册里写着 `icon: (size) => `(追问)与 `IconQueueOutline14`(追问记录),而新的 `SidebarTab` 描述里没有这个字段。宿主的两处放置都会**在缺省时回退到别的东西**(guide 胶囊 → 自己的方块占位 `GuideBody.tsx:48,59`;tab chip → 裸标题),所以缺图标不是「朴素」,而是别人家的占位符。现修: - `SidebarTab` 新增必填 `icon: ComponentType`,`index.tsx` 两个 tab 分别用回 `IconQuestionOutline14` / `IconQueueOutline14`(宿主自带图标集,走平台模块表,不引入其它特性插件的值)。 - **guide 胶囊**:`guide[].icon` 直接传该 glyph。 - **tab chip**:布局记录里的标题只是**字符串**,图标不可能随标题捕获,所以 glyph 由**活标题席位**渲染——`titleFor` 现在返回 `Fragment(glyph, title)`(与宿主自己的 chip 标题 `GuideTitle.tsx` 同构),glyph 的样式在新的 `src/client/tab-title.module.css`(`flex: none` + `color: var(--dsw-alias-label-tertiary)`,与宿主一致)。 - 顺带修掉一处**镜像漏声明**(同一类:镜像自己就是唯一声明,漏了编译器不会报,运行时静默丢):`context-types.ts` 的 `SidebarqaSidebarRightGuideEntry` 没有 `icon`(上游 `tab-registry.ts:79`),所以即便传了也无处安放。现已补上并注明出处;`IconProps` 直接从宿主包里 `import type`,不再二次镜像。 - `useLocaleRevision` 的 `useSyncExternalStore` 补上第三个参数(server snapshot):缺它时任何服务端渲染都会抛 `Missing getServerSnapshot`,也让标题席位在 node 环境无法验收。活动语言 id 是稳定原语,正是服务端渲染该读的值。 - 回归用例:`tests/sidebar-native.spec.ts`(guide 条目必须带 tab 的 glyph;活标题席位必须渲染「glyph 在前、标题在后」)。 - **「全量继承」追问会让面板全白且不可恢复**(本轮实测发现,**根因是一条上游 prop 改名**)。`MarkdownText` 的本地化文案 prop 从 `codeLabels` 改名成了 `labels`(`MarkdownLabels = { code: { copyLabel, copiedLabel }, footnotes }`,改名发生在 DSH `0.1.2-alpha.2` 之前),而**宿主渲染器读 `labels.code.copyLabel` 时没有任何守卫**(`ui-primitives/src/markdown/render.tsx:400`)。本插件一直在传已退役的 `codeLabels`,于是 `labels` 为 `undefined` —— **任何一条含围栏代码块的消息只要被渲染就抛 `TypeError`**。这解释了为什么它看起来是「全量继承特有的」:继承会把主导对话的整段历史(写代码的对话,满是代码块)画进面板,而普通追问的回答常常是纯文字。异常逃出席位后被 **abdicate**(`ui-slots/src/index.ts:1541`),本插件的 tab body 因此在**整页所有会话**上被永久摘除——只能刷新。 - 现在传 `labels`(`AskPanel` 的 `markdownLabels`,按 locale memo:`MarkdownText` 以它的引用身份缓存流式渲染),新增文案键 `mdFootnotes`(脚注小节标题),退役的 `codeLabels` 从代码里彻底删除。 - **为什么此前 `tsc` 报不出来**:类型来自 devDependency 桩 `@deepseek-ai/dsh-client-ui-primitives`,其范围写的是 `^0.1.0-rc.8`,而**带预发布的 semver 区间永远匹配不到另一个 patch 的预发布版本**(`0.1.6-alpha.2` 不满足 `^0.1.0-rc.8`,也不满足 `>=0.1.2-alpha.2`)——桩因此被永久冻结在改名之前的 API 上,编译器一直拿旧契约校验新代码。现把 devDependency **精确钉到 `0.1.6-alpha.2`**(与运行中的 DSH 一致),peer 只声明下限 `>=0.1.2-alpha.2`;`pnpm-workspace.yaml` 新增 `peerDependencyRules.allowedVersions`(该桩 peer 了一个本包从不链接的 `@deepseek-ai/cordis` 副本,cordis 由平台模块表提供)。lock 随之去掉旧桩那条重依赖图(katex / micromark / mdast 一族,约 860 行),新桩自身零依赖。 - 回归用例:`tests/markdown-labels.spec.ts` 直接钉住**装好的桩**(必须声明 `labels:`、必须不再有 `codeLabels:` 属性),把「范围悄悄退回预发布旧版 → 类型检查重新放行」这条路堵死。 - **面板里的任何渲染错误都不再可能让 tab body 整页失效**:新增本插件自己的错误边界(`src/client/panel-boundary.tsx`,挂在 `sidebar-native.ts` 的 `bodyFor` 最外层):崩溃被就地渲染成一条**可重试**的说明条(`errPanelCrashed` / `panelCrashHint` / 复用 `commonRetry`),标签页本身留着;切换会话(`resetKey` = sessionId)会自动清掉崩溃面。`containPanel(resetKey, Panel, props)` 接收**组件**而不是现成元素,因此「把面板当普通函数调用」这种会让边界失效的写法(本节第一条修 bug 时我自己就先写成了这样)**变成类型错误**;测试另有一道 wiring 守卫(`tests/panel-boundary.spec.ts`:`bodyFor` 必须 `return containPanel(props.sessionId, tab.component, bodyProps)`,且不得再出现 `tab.component(bodyProps)`)。注:React 的服务端渲染器不咨询错误边界(直接 rethrow),因此containment 在 node 环境以**元素结构**断言,而非渲染。 - **在「追问」页点到一条已被归档 / 已删除的追问,会让面板内容整块消失、而且再也调不出来**(本轮实测发现)。根因是**插件自己维护的 localStorage 父子映射不会随 DSH 侧的归档/删除清理**:切换条里的陈旧行照旧可点,点下去就是把面板切到一段**读不出内容**的会话上(正文永远停在「生成中…」);更糟的是这类 id 一旦被拿去导航,`uiWorkspace.openSession` 会**先**把它 retain 成主视图,DSH 自己的导航策略**随后**再把它丢掉(`ui-workspace/src/client/navigation.ts:287-301` 的 `clearArchivedCurrent` → `clearMain`),主视图与整个右栏一起失效——用户正在读的面板随之消失。三处一起修(均为 client 半): - **切换条逐条分类**:新增纯函数 `followUpAvailability` / `isFollowable` / `lastFollowableFollowUp`(`src/client/history-scope.ts`)。已归档 / 已删除的追问**保留可见**(映射行还在,DSH 侧恢复归档后立刻可用)但**置灰不可点**,并标注「已归档 / 已删除」——与「追问记录」的陈旧行同一套语言。面板的默认选中也改为「最新一条**可读**的追问」,不再默认落到一条读不出的会话上。 - **选中项在阅读时被归档**:正文改为明确的说明文案 + 「移除」(只清插件自己的映射,DSH 侧会话不动),输入框、策略、模型座与发送键一并停用。不再有「面板开着却什么都没有、也不知道为什么」的状态。 - **导航入口补围栏**:`sidebar-native.ts` 切屏前先读 `workspaces.list` 的 `archivedSessionIds`(`isArchivedTarget`),命中则**只记一条 warn 并放弃这次手势**,绝不把已归档会话 retain 成主视图;读不到 archive 集时一律当作「未归档」,围栏不会因为读不到输入而开始拒绝打开。 - 分类特意**不复用**「追问记录」的 `sessionStatus`:行标签可以立刻把「feed 里还没有」写成「已删除」,而切换条一旦照此**禁用**,冷启动(`phase !== 'ready'`)时就会把全部追问锁死。`followUpAvailability` 因此多出一个 `unknown`(feed 尚未 hydrate,按可用来处理)。 - **宿主 `useTabInfo` 抛错会让本插件的 tab body 在整页范围内永久失效**。DSH 的 keyed 席位错误边界对崩溃条目的处理是 **abdicate**(`ui-slots/src/index.ts:1541`:`abdicated.add(entry)`)——把这条注册从席位上**永久摘除**,于是**所有会话**的这个 tab 一起变空白,重新打开也回不来(只能刷新页面),正是「内容完全消失且不能再调出」。而 `tab-info.ts:35` 抛的 `tab "" is not committed in session ""` 是宿主自己就可能瞬时产生的状态(布局提交与渲染不一致)。现在 body 读取该 hook 时**捕获并降级**为一条中性的「未挂载」出现实例(`useTabInfoSafely`):这一帧面板渲染成惰性内容,下一次一致提交即自愈;`takeQuoteOnce` 只在真正读到 payload 时才消费,所以随打开带来的引文**不会因为这次降级而丢**。 - 回归用例:`tests/history-scope.spec.ts`(可用性分类 13 例)、`tests/sidebar-native.spec.ts`(已归档目标拒绝导航、hook 抛错仍渲染、引文跨降级仍可交付)、`tests/panel-boundary.spec.ts`(边界状态机 + containment 结构 + wiring 守卫)、`tests/markdown-labels.spec.ts`(类型桩契约),全套 23 文件 / 342 用例。 以下三条都是**在真实部署上实测发现**(现象:侧边栏里看不到 tab),按「上游 → 下游」的顺序排列——前两条如果不修,第三条根本走不到: - **`ctx.inject` 的每个键都是「必需依赖」,不存在可选形式**(**这条是根因**)。上游 cordis 的 `Fiber._refresh` 会遍历 `Object.keys(this.inject)`,只要有一个键没有实现就把 fiber 置为 `INACTIVE`,因此回调**只在全部服务都存在时**才运行。初版写成 `ctx.inject({ sidebarRightTabs: null, betterSidebar: null }, cb)`,语义是「要求原生侧边栏和 better-sidebar **同时**存在」——现实中不成立,于是 fiber 永久 PENDING、回调一次都没跑,插件什么也没注册,**却仍然打印了正常的激活日志**。`{ name: null }` 不是「可选」,而是「必需,并以 `null` 拦截」。 现改为 `ctx.get()` 读取(与 `ui-conversation` 的 `conversation` 同一条路,不需要 inject)+ 监听 cordis 的 **`internal/service`** 事件(`ReflectService.notify` 每次 `provide` 都会 emit)做反应式安装,并且在 `nativeSidebarServicesOf`(`src/client/sidebar-native.ts`)里要求三件套(`sidebarRightTabs` + `sidebarRight` + `slots`)齐备才落定 —— 只有注册表没有席位,tab 会出现但 body 渲染不出来。 - **原生 tab 类型没注册 guide 条目 → 侧边栏里根本看不到它**。原生右侧栏的 `+` 控件**只打开 guide(引导页)**,并不枚举已注册类型;guide 页才是把类型列成可点胶囊的地方(`GuideBody.tsx:96`:点击 → `tab.actions.openTab(entry.kind, { replaceTab: true })`;内置的工作区文件 / 新建终端 / 浏览器三个入口就是同一机制,见 `ui-sidebar-files/definition.tsx:35` 等)。初版只注册了类型与 body,这两个 tab 因此只能由代码打开——正是「没有 tab」。现为两个类型各注册一个 guide 条目(标题 / 说明按语言实时读取),并新增 `askGuideDesc` / `histGuideDesc` 文案。 - **探测只在 `apply` 时做一次 → 后端晚到时永久不注册**。`sidebarRightTabs` 由 `ui-sidebar-right` 自己的 `apply` 发布,两个 fiber 谁先落地属于组装顺序、不是插件可以假设的。初版探测落空后**不再重试**,只留一条容易被忽略的 warn。 三条都补了针对性回归用例(`tests/sidebar-native.spec.ts` 的「guide 条目到达 DSH」「后端迟到时仍会安装,且只装一次」「三件套缺一即视为没有侧边栏」「缺动词的服务当成缺席」)。测试替身也一并改对了:它现在**不提供**任何形式的可选 inject(那正是让前三轮「单测全绿、真机全挂」的原因),服务必须通过 `provide()` 事件到达。 - **原生「提问」打开时用的 kind 与注册时不一致 → 点击必然报错**。原生类型有**两个不能互换的名字**:`id`(实现身份,唯一,body 席位按它查找)与 `kind`(`openTab(kind)` 的派发名,`placeTab` 按它查注册表,查不到就抛)。本插件 `id` 用 tab key(`dsh-sidebar-qa:ask`)、`kind` 用短名(`ask`),而初版把 `openAsk` 的 kind 常量从 **key** 推导,于是注册了 `ask` 却去打开 `dsh-sidebar-qa:ask`: ``` sidebarRight: no tab type is registered as "dsh-sidebar-qa:ask" at SidebarRightController.placeTab (service.ts:369) at Object.openAsk (sidebar-native.ts:226) ``` 现改为**注册时按角色记下每个类型实际使用的 kind**(`kindByRole`,见 `src/client/sidebar-native.ts` 的 `installSidebarTabs`),打开时用它,两者不可能再分叉;未注册时直接告警返回,不再让异常从点击处理器里逃出去。回归用例改为断言「打开的 kind === 注册的 kind」(断言常量抓不到这类漂移)。 - **原生「提问」前先切到引文所属会话**。原生导航面只作用于**已挂载**的会话面板:`openTab` 会取当前绑定的 seat,没有就抛 `sidebarRight: no session surface is mounted`(`ui-sidebar-right/src/client/service.ts:537`)。而引文是在划选时按当时会话捕获的,用户可能已切走,于是这次打开必然失败。现在打开前若目标会话不是当前会话,先经 `ctx.uiWorkspace.openSession` 把它切到屏幕上(`src/client/show-session.ts` 的 `showSessionFirst`)—— 这里原先写的是 `ctx.sessions.open`,一个 `ISessions` 根本不存在的**臆造 API**(该契约里没有任何导航入口,它自己的注释把导航划归 view owner),修掉它这条路径才真的动起来。 - **`SessionListState.current` 这个字段根本不存在 —— 本插件从第一版起就在读一个臆造的字段**(**这是「添加到对话」与选区归属的总根因**)。`SessionListState` 的真实结构是 `{ ids, byId, phase, subagentsByParent, jobsBySession }`(`api/session-controller/src/client/sessions/service.ts:53`),**没有 `current`**,所以 `sessions.list.getSnapshot().current` 恒为 `undefined`。更糟的是**本插件的类型镜像我当初自己加了 `current`**,于是运行时默默返回 `undefined`、编译器也永远报不出来——这个错误因此一直活到了现在。 DSH 自己的「当前会话」定义在 `ui-workspace/src/client/tree.ts:41`:**主视图 retain 的那个会话**(`retainedBy.mainView > 0`)。现已新增纯函数 `src/client/current-session.ts`(`resolveCurrentSessionId`,9 条单测),替换全部 5 处误用:选区归属(`index.tsx`)、composer 目标解析(`draft-insert.ts`)、切会话前后的当前会话判定与接管判定(`show-session.ts` / `sidebar-native.ts`)、追问记录的工作区归属判定(`HistoryPanel.tsx`)。类型镜像里的 `current` 已删除,并按真实结构补齐 `ids` / `phase` / `retainedBy`——删掉假字段后,`tsc` 立刻抓出了最后一处漏改。 - **「添加到对话」在选区没带 sessionId 时被当成不可恢复**。空串来自上一行的假字段。初版把空串当成致命错误直接 `return`,于是**连重试路径也一起被砍掉了**(日志里能看到 `missing-session (selectionSession=)`)。现在空 id 会**在写入时重新解析**,并且允许下一帧重试。 - **「添加到对话」在会话不在屏幕上时必然失败**。写进 composer 需要该会话的 **Agent scope**,而 `ctx.sessions.scope(id)` 只对**当前已挂载**的会话返回值(`session-controller/src/client/sessions/service.ts:502` → `this.scopes.get(id)`);scope 拿不到时该按钮只记一条 warn 并返回 false,而浮层在失败时会**保留自身与选区**——于是用户看到的就是「点了没反应」(这次连浮层都不消失)。 现在按钮先把引文所属会话切到屏幕上(`ctx.uiWorkspace.openSession`,经 `src/client/show-session.ts`),并在 scope 仍未被 retain 时**于下一帧重试一次**(scope 由 React reconciliation retain,不是同步的);失败原因(`missing-session` / `no-conversation-service` / `scope-unavailable` / `threw`)会带 phase 打到 console,不再只有一行含糊的 warn。重试只针对**重试可能修复**的两类(空 id、scope 未 retain),`no-conversation-service` 与 `threw` 不重试,避免噪音翻倍。 - **打开链路的异常不再静默**。浮层在调用之前就把自己关掉了(`SelectionPopover.ask()` 先 `controller.clear()` 再 `onAsk`),因此一旦后半段抛错,用户看到的就是「点了没反应」而没有任何提示。现在 openAsk / openHistory 都会把失败打到 console,并在调用前后各留一条 info 日志,便于区分「没走到」与「走到了但失败」。 - **一批「类型镜像臆造上游成员」的静默故障**。`src/context-types.ts` 是本仓库唯一的声明来源,臆造出来的成员 `tsc` 永远报不出来、运行期读作 `undefined`,同一类故障因此反复发生:`SidebarqaLoaderEntry` 只声明了 `options.name`,而 web-app bundle 把那一行挂成 `id: connection`(`name` 是包名),于是 `/sidebarqa/api` 的信任围栏恒判为 loopback-only,局域网或自定义 Host 的浏览器访问**恒 403**——现在按 `options.id` 匹配,并优先读已求值的 `webRuntime.trustedHosts`(那一行自己的 config 里是**未求值的** `!!js` 表达式,当成数组读会让 `.some()` 抛错、把 403 变成挂起;现在每个来源都做数组校验,失配时拒绝而不是抛)。另外:`slots.has`(上游叫 `spec()`,零调用点)、`HistoryEntry.view`(零调用点)、`HistoryRecord` 的 `{ type: 'chunks' }` 变体(上游是单成员联合,那个「打包增量」在线上根本不存在)、`FollowFrame` 缺 `assistant-stream` 变体(上游可能开始发的第三种帧,读 `frame.event` 会让渲染崩)、以及 `SessionInput.focus()`(上游本就公开,不必经由未公开的 editor 字段手搓)——全部按上游契约改正。 ### 说明(原生侧边栏) - **配置面板已迁入 DSH 官方设置页**:它现在注册成 `settings.section`,在设置页左侧导航里是**一整页**「追问 / Follow-up」(排在 DSH 自带各页之后,`order: 30`),**不再有齿轮弹窗**;`settings.yaml` 的 `sidebarqa` 命名空间仍然有效,两条路写的是同一份配置。之所以不是 `plugins.item`:该席位的契约明文留给 `ui-settings-plugins` 那类 host-plane 配置页、并要求 bundle 配置走 `plugins.bundle.config`,而那个插件页在没有受管 profile 时整页不可用,会把配置入口一起带走。 - **跳转会先把目标会话切到屏幕上**:原生 `openTab` 只作用于**已挂载**的会话 surface,而右栏的 session 绑定由 React effect 在下一次 commit 才重新发布,所以紧跟导航的打开会**静默**打到刚离开的会话。现在的顺序是:先 `ctx.uiWorkspace.openSession`(`src/client/show-session.ts`)把目标会话切到屏幕上,随后用 `openTabIn(sessionId, kind, …)` 定向到目标会话自己的 store —— 该 store 尚未被 adopt 时它是**静默 no-op**,所以**不可能开错会话** —— 在几个帧的有界预算内重发,并以 `active()` 确认,最终失败才 `console.warn`。**有界重试只在需要切换时发生**:目标已经是屏幕上的会话就直接 `openTab`,一次到位。 > 部署提醒:**host 与 client 两半都有改动**(host 侧修了 `/sidebarqa/api` 的信任围栏与 `webRuntime` 读取),且 manifest / 依赖层有变化:`dsh-better-sidebar` peer 被移除,`@deepseek-ai/dsh-client-ui-primitives` 的 devDependency 精确钉到 `0.1.6-alpha.2`、peer 改为下限 `>=0.1.2-alpha.2`,`pnpm-workspace.yaml` 新增 `peerDependencyRules`(lock 随之变化,去掉旧桩那条 katex/micromark 依赖图):升级后请重新 `pnpm install`,并重启 `dsh web`。本轮新修的两条(`MarkdownText` 的 `labels` 与面板错误边界)都是 **client 半**代码,硬刷新浏览器即生效。 ## [0.5.0] - 2026-08-29 ### Added - **划选浮层由一个按钮拆成两个([issue #11](https://github.com/ChenRuoT/dsh-sidebar-qa/issues/11))**: - **「添加到对话」**(新增):把划选文本以 Markdown `>` 引用块**追加**进**当前会话的主输入框**(composer),聚焦并把光标停在引用块下方的空行——**不新建会话、不打开也不展开侧边栏**。适合「引用这段话,我就在主对话里接着说」,与开一个独立追问会话互为轻重两条路。 - **「提问」**:文案与行为**逐字节不变**(存 pending 引文 + `openTab({ type: 'dsh-sidebar-qa:ask' })`)。之所以没有按 issue 原文改名「侧边聊天」,是因为 `dsh-better-sidebar` 0.16.x 自己就有一个叫 Side Chat / 侧边聊天的视图,改名会与上游功能撞车。 - 引用块格式:**逐行**加 `> ` 前缀(选区内的空行渲染成裸 `>`,否则一个引用块会被截断成两个);块尾留一个空行,所以光标落在引用块**外面**——紧贴 `> x` 下一行写字在 Markdown 里属于 lazy continuation,会被吸进引用块里。 - **追加而非覆盖**:输入框里已有的内容一个字都不动,引用块空行分隔接在后面;连点两次会追加两个引用块(刻意不做幂等)。 - 走 DSH `ui-conversation` 的 `conversation` 服务(`ctx.get('conversation')` → `input.for(scope).setDraft`),与 `dsh-better-sidebar` 资源管理器的 `@` 文件引用是同一条路径,**没有任何 DOM hack**。 - 新增纯模块 `src/client/quote-draft.ts`(引用块格式化 + 草稿合并)与薄接线模块 `src/client/draft-insert.ts`,各自带单测(`tests/quote-draft.spec.ts` 21 例、`tests/draft-insert.spec.ts` 12 例)。 ### Changed - `src/context-types.ts` 增补 `conversation` 服务的结构化镜像(`SidebarqaConversationService` / `SidebarqaSessionInput`)并挂上 Context augmentation——与上游的漂移仍然只收敛在这一个文件里。声明它同时让 `ctx.get('conversation')` 返回**有类型**的服务面而不是 `any`(cordis 把 `get` 定义为 `(name: K) => this[K] | undefined`)。 - 新增文案键 `askAddToConversation`(zh `添加到对话` / en `Add to chat`)。`askPopoverButton` 与三条提到「提问」的空状态提示均未改动。 ### 降级契约 `conversation` 服务缺失(没装 ui-conversation 的部署)、会话 scope 解析不到、或调用链上任意一处抛错 → 「添加到对话」记一条 `console.warn` 后**什么都不做,并且保留划选与浮层**,用户可以改点「提问」。插件本身、浮层、以及原有的追问流程都不受影响;`conversation` **没有**进 `inject`,所以它的缺席不会让插件不激活。 > 部署提醒:**仅 client 半改动**,浏览器硬刷新即可,无需重启 `dsh web`。 > 已知限制:输入框里已有的 `@` 引用 chip 会在这次写入中被降级成纯文本——`setDraft` 是 composer 唯一的公开写入路径,它按纯文本重建整棵节点树(`dsh-better-sidebar` 的 `appendToDraft` 有同样的行为)。**输入框为空时(最常见路径)完全不受影响。** ## [0.4.2] - 2026-08-29 ### Changed - **`package.json` 新增 `engines.dsh: ">=0.1.2-alpha.1"`([issue #14](https://github.com/ChenRuoT/dsh-sidebar-qa/issues/14))**:0.4.1 起浏览器侧 RPC 走 DSH 的 Remote 服务(`ctx.remote.session`),该服务首次落地于上游 `dsh-v0.1.2-alpha.1`。此前这个前置条件只写在 release notes 里,机器读不到;现在成为清单里的显式声明,供市场 / CLI / CI 在安装前校验(配套提案见 [dsh-market#404](https://github.com/dsh-market/dsh-market/issues/404))。 > 注意这是**声明而非闸门**:npm / pnpm 的 `engines` 只校验 `node`(及包管理器自身),未知键会被忽略;`dsh plugin --profile web add` 又是 pnpm 的薄转发。真正的安装前拦截需要市场 / CLI 侧消费该字段。在此之前,早于 `0.1.2-alpha.1` 的 DSH 上本插件因 `inject` 解析不到 `remote` 而**静默不激活**(无 UI、无报错),请留在 `0.4.0`。 - **`dsh.plugin.json`(plugin-registry 渠道清单)的 `engines.dsh` 由 `>=0.0.1` 同步为 `>=0.1.2-alpha.1`**:该字段此前是一条陈旧声明(写于插件还没有任何宿主版本要求的时候),按 registry 清单做校验的消费方会读到「任何版本都行」,与 `package.json` 的声明互相矛盾。两份清单现在给出同一个下限。 - **`dsh-better-sidebar` peer 依赖由 `^0.16.0` 放宽为 `>=0.16.0`**:caret 对 `0.x` 只放行同一 minor(`>=0.16.0 <0.17.0`),而上游迭代快于本插件——better-sidebar 发到 `0.17.0` 时本插件会**平白无故解析失败**,尽管并没有任何已知不兼容。改为只声明下限,若将来某个版本确实破坏兼容再收窄上限。 > 部署提醒:本版仅清单与依赖声明改动(`engines.dsh`、peer 范围),无运行时代码变化;已安装用户无需重启。但早于 DSH `0.1.2-alpha.1` 的宿主上插件会**静默不激活**(与 0.4.1 行为相同),请留在 `dsh-sidebar-qa@0.4.0`。 ## [0.4.1] - 2026-08-28 ### Fixed - **版本兼容([issue #12](https://github.com/ChenRuoT/dsh-sidebar-qa/issues/12)):`dsh-better-sidebar` peer 依赖由 `^0.14.0` 调整为 `^0.16.0`**,正式声明兼容 0.16.x(本次 Remote 迁移正源于该版本线的上游重构);0.14.x / 0.15.x 不再被 peer 解析。 - **追问面板整块报错 `dsh-better-sidebar: Cannot read properties of undefined (reading 'sessions')`**:DSH 上游把浏览器侧 RPC 从 `ctx.connection.api` 迁到了**类型化 Remote 服务** `ctx.remote`(`refactor(connection): own RPC transport contracts` 删掉了 `ConnectionHandle.api`;`refactor(client): consume migrated Remote namespaces` 把各命名空间挂成独立 cordis 服务)。本插件的 11 处调用点全部落在那个已消失的对象上,其中 `ModelSelect` 的挂载 effect 是**同步**取值,因此直接被 better-sidebar 的单 tab 错误边界捕获、整块渲染成错误条。现已全量迁移到新接口: | 旧 | 新 | |---|---| | `connection.api.sessions.create / fork / rename / selectModel / prompt` | `ctx.remote.session.*`(信封由 `{result:{ok,…}}` 改为扁平 `{ok,…}`;`prompt` 新增必填 `requestId`,由客户端生成 UUID v4) | | `connection.api.sessions.models({sessionId})` | `ctx.remote.session.modelCatalog()`(Host 级目录,无会话地址)+会话的 `modelSelection` **projection**(当前选择改为**订阅**而非轮询,主对话或 `/model` 改模型会自动同步到座位) | | `connection.api.sessions.history({sessionId})` 每 1.2s 轮询 | `ctx.remote.session.follow()` 实时流(开窗 + 增量帧)+ `session.page()` 向上翻页。**追问回答现在是真流式**,不再等轮询节拍 | - **`inject` 增补 `remote` / `remote.session`**,并且两个 tab 组件改为接收**本插件自己的 ctx**(此前用的是 better-sidebar 传下来的 ctx——cordis 只解析各自 plugin 注入过的服务,而 better-sidebar 并未注入 `remote`,沿用会再次拿到 undefined)。 ### Added - 新增纯模块 `src/client/session-wire.ts`(含 `tests/session-wire.spec.ts`,20 例):Remote 信封拆封、`requestId` 生成、把 wire 上**打包的 assistant delta run**(`chunkrow/text-chunks` 等)还原成逐 seq 的 `assistant/chunk` 事件——因此 `answer.ts` 的折叠逻辑与其全部单测保持零改动——以及重开 follow 时按 seq 合并窗口(避免覆盖已向上翻页的继承历史)。 - projection 读取抽成共享的 `src/client/use-projection.ts`,上下文占用环与模型座位共用一条路径。 > 部署提醒:**仅 client 半改动**,浏览器硬刷新即可,无需重启 `dsh web`。 > 前置条件:本版本要求 DSH 已包含上述 Remote 迁移(0.16.x 系 DSH);在更早的 DSH 上请留在 0.4.0。 > 已知上游问题:`dsh-better-sidebar` 0.16.1 的 Side Chat 视图仍在调用 `ctx.connection.api.sessions.history`,在同一版 DSH 上打开该 tab 会报同样的错——那是上游代码,不在本插件范围内。 ## [0.4.0] - 2026-08-21 ### Added - **中英双语(i18n)**:界面文案与模型侧提示词都跟随 DSH 的语言设置(`locale.preference` > 浏览器语言 > en)**实时切换**,无需刷新。 - 界面词表在 `src/client/locales.ts`(zh 为键集基准,en 由 `Record` 类型标注锁定;单测另有键集平价、空值与占位符一致性检查),并通过 `ctx.locale` 注册进 DSH 的 `sidebarQa` 命名空间。locale 服务为**软依赖**:缺失时插件照常工作,回退浏览器语言。 - 模型侧词表在 `src/prompt-locale.ts`:追问引导语、上下文压缩与标题系统提示,以及提示词**指名引用**的结构标记(`用户:`/`助手:`、`【背景】`、`问题:`)作为一个原子单元同进同退(单测断言这一配对)。 - 已打开的 tab 标题同样跟随切换:better-sidebar 的 tab 标题是持久化定值,面板改为在语言变化时回推 `updateTab` 自愈。 - **明确的回答语言策略**:提示词现在显式要求「用与用户提问相同的语言作答;问题语言不明确时跟随划选文本」——**回答语言跟内容走,不跟界面语言走**(与 DSH 官方 session-title 同款策略)。此前得到中文回答只是「提示词恰好是中文」的副作用。 ### Changed - **追问会话标题去掉「追问」二字**:`❓追问·<主题>` → `❓<主题>`。emoji 本身已足够标记追问会话,去掉后该处不再需要翻译,也不会因切换语言而出现混合语言前缀。**已有会话的标题不会被追溯重命名。** - **占位主题的截断预算按文种自适应**:含 CJK 取 12 字,纯拉丁取 24 字(`TOPIC_MAX_LEN` / `TOPIC_MAX_LEN_LATIN`)——12 个拉丁字符只有两三个词,此前会截在词中间。判定依据是**被划选文本的文种**,不是界面语言。 - **`/sidebarqa/api/context` 与 `/sidebarqa/api/title` 新增可选 `locale` 字段**(`'zh' | 'en'`):host 侧没有语言信号,由 client 下发当前 DSH 语言。**字段缺省等价于 `zh`**,旧版 client 打到新 host 行为与今日逐字节一致;无法识别的语言一律落 en,永不抛错。压缩摘要缓存键加入 locale 维度,切换语言后不会继续命中旧语言的摘要。 - **面板内的宿主错误提示改为「本地化前缀 + 原始英文详情」**(追问失败 / 保存失败 / 模型加载失败),此前直接裸露英文原文。 ### Fixed - 追问记录的相对时间标签此前恒为中文;现在跟随语言(en 使用 `5m` / `3h` / `2d` 等紧凑缩写,规避复数形态)。 > 部署提醒:**host 半与 client 半都有改动**——需重启 `dsh web`,再硬刷新浏览器。只刷新不重启会短暂得到「英文引导语 + 中文压缩提示词」的混合状态(功能正常,仅提示词语言不一致)。 > 已知外观差异:dsh-better-sidebar 插件目录中对本插件的描述仍写作 `❓追问`,属上游只读文案,不影响功能。 ## [0.3.2] - 2026-08-21 ### Changed - **回答/摘要的模型渠道与模型均改为下拉选择**:功能配置面板中「回答模型渠道 / 回答模型 / 摘要模型渠道 / 摘要模型」四行从自由文本输入改为下拉框,选项来自运行时已配置的渠道(新增 `/sidebarqa/api/catalog` 下发渠道与模型目录)。切换渠道时自动联动模型;摘要渠道额外提供「继承被追问会话」空值项。host + client 侧均有改动:需重启 `dsh web` 并硬刷新浏览器。 ### Fixed - **侧边栏切模型会改主对话、思考强度切不动**([issue #10](https://github.com/ChenRuoT/dsh-sidebar-qa/issues/10)):两个独立缺陷。 - **模型座在「新追问」时绑的是被追问的父会话,而座位是写入式的**——为一个还没创建的子会话挑模型,改的却是主对话(挑中的值本来就会通过 `modelOverride` 正常应用到子会话,那次写入纯属误伤)。现在座位按状态分三态:**继续追问**时照旧直接提交到该追问会话;**新追问 + 压缩/裁切**时是**本地草稿**,默认显示配置里的回答模型(子会话真正会用的那个),仅在追问会话建好后应用,**全程不碰主对话**;**新追问 + 全量继承**时**只读置灰**并给出提示——fork 子会话沿用主对话模型正是前缀缓存命中的前提。绑定规则集中在纯函数 `src/client/model-seat.ts`(含单测)。 - **推理强度切换从未发出请求**:`ModelSelect` 的去重守卫只比 provider + model,而「只改强度」恰恰路由不变,于是每一次强度切换都被静默吞掉(DSH 自己的座位让强度走另一条路径绕开该守卫,本插件移植时接错了)。守卫改为纯函数 `isNoopSelection` / `effectiveEffortOf`(`src/client/model-menu.ts`,含单测),在比较前折叠模型广告的 `defaultEffort`,于是勾选态、trigger 标签与去重判定读同一份逻辑。 - **追问子会话的思考强度可能被配置值顶替**:`trySelectModel` 用的是 `override?.reasoningEffort ?? config.answerReasoningEffort`,当面板选择有意表达「跟随提供方默认」时会被配置里的强度(默认 `off`)覆盖,等于给刚选的思考模型强行关思考。改为在强度这一维上 all-or-nothing。 > 已知限制(DSH 上游):`sessions.selectModel` 会无条件把选择同时持久化为**全局默认模型**。因此压缩/裁切追问在给子会话设模型时,仍会改变「新建会话的起始模型」;一个**从未发过请求**的会话也会从该全局默认解析自己的当前模型,可能看起来跟着变。插件侧无 API 可规避(`selectModel` 无 opt-out、不写会话日志,`sessions.models` 只能拉取)。 > 部署提醒:以上均为 client 半改动,硬刷新浏览器即可生效,无需重启 `dsh web`。 ## [0.3.1] - 2026-08-20 ### Fixed - **追问记录中已归档/已删除的会话成为悬空节点**:用户自行归档或删除 DSH 会话后,插件维护的父→子映射(localStorage)不会自动清理,追问记录 tab 会列出点击无效的节点。现在按会话状态实时分类——已归档(读 `workspaces.list` 的 `archivedSessionIds`)与已删除(会话 feed 缺失)的节点**置灰并标注「已归档 / 已删除」**、不可再点击跳转;行尾新增「移除」按钮,将其连同整棵子树从映射中清除(含 titled / collapsed 派生状态,DSH 侧会话本身不受影响)。纯函数集中在 `src/client/history-scope.ts`(`sessionStatus` / `subtreeIds` / `removeSubtree`)与 `store.removeSession`,均含单测。 > 部署提醒:client 半改动,硬刷新浏览器即可生效,无需重启 `dsh web`。 ## [0.3.0] - 2026-08-20 ### Fixed - **侧边栏面板收起时,划选「提问」点击后无可见反应**([issue #6](https://github.com/ChenRuoT/dsh-sidebar-qa/issues/6)):better-sidebar 只对带 `path`/`url` 的**内容型打开**自动展开收起的面板,而「提问」走的是 type-only `openTab`,追问 tab 此前会落在不可见的收起面板里打开成功、用户却看不到任何反馈。「追问」与「追问记录」两个 tab 现在会**自愈展开**收起的面板,覆盖两个时机: - **挂载时**(tab 首次打开):通过 `TabComponentProps.store`(better-sidebar 的 `SidebarStore`)展开——窄视口展开合并抽屉,宽视口按落点展开右侧/底部面板; - **重新激活时**(**用户手动收起面板后再点「提问」**——`openTab` 只聚焦已有 tab、组件不重挂载):通过 `TabDescriptor.onActivate` 回调桥接到已挂载的组件,收到激活信号后再次自愈展开;用户手动收起本身不产生激活事件,不会被误打回。 - 纯逻辑集中在 `src/client/ensure-panel.ts`,激活桥在 `src/client/tab-activation.ts`(均含单测),全程走公开服务契约、不依赖 DOM hack。client 半改动,硬刷新浏览器生效。 ### Added - **三种上下文策略**(对应 [issue #4](https://github.com/ChenRuoT/dsh-sidebar-qa/issues/4)):发起追问时可逐次切换,配置 `historyStrategy` 设默认: - **全量继承**:从主会话分叉出子会话、完整继承上下文,复用 DeepSeek 前缀缓存命中(零压缩损失、更省 token);子会话沿用主会话模型;主对话正在回答时自动降级为「压缩」并在面板提示。 - **压缩**(默认):快速模型压缩较早窗口 + 近期原文保留。 - **机械裁切**:最后 `trimWindowMessages` 条消息原文直取,零 LLM 成本、确定性输出。 - **追问输入框改为 DSH 主对话同款外观**:圆角胶囊卡片 + 上箭头发送键;左侧为**上下文策略**选择 chip,右侧为**模型选择**(与主对话模型座、`/model` 命令共用同一份数据,切换互通)与 **context 占用环**(含系统 / 工具 / 消息 breakdown 面板)。模型选择与占用环在发起新追问时也显示——此时绑定被追问的父会话(占用环反映父会话上下文占用,可据此判断用全量继承还是裁切),继续追问时绑定该追问会话。 - **全量继承追问的对话视图**:追问 tab 默认定位在本次「引用 + 提问」处,继承的主对话历史显示在上方并带分割提示,向上滚动按页加载;压缩 / 裁切等新建会话行为不变。 ### Changed - **精简设置面板**:「功能配置」只保留 8 项常用设置(上下文策略、裁切保留条数、回答/摘要的 provider + model + 思考模式);压缩与标题的内部调参键(`summarizeBudgetTokens`、`recentWindowMessages`、`backgroundWindowMessages`、`titleBudgetTokens`)不再在面板展示,仍可在 `settings.yaml` 的 `sidebarqa` 命名空间配置。 ### Dependencies - peer 依赖升级:`dsh-better-sidebar` `^0.12.0 → ^0.14.0`;`@deepseek-ai/dsh-client-ui-primitives` 与 `cordis` 升至 `rc.8`,适配 **DSH `0.1.0-rc.8`**。rc.7 及更早的 DSH 环境无法解析本插件依赖,需先升级 DSH。 > 部署提醒:host 半改动需重启 `dsh web`;client 半改动硬刷新浏览器即可生效。 ## [0.2.0] - 2026-08-17 ### Added - 「追问记录」tab 限定当前工作区:按当前会话所属工作区的 `sessionIds` 过滤分层树,切换工作区后只看到本工作区的追问记录(归属判定与 DSH runtime 同源,走 `workspaces.list`)。纯函数集中在 `src/client/history-scope.ts`(含单测)。 - 「追问记录」有子追问的节点新增右对齐**折叠按钮**(箭头随折叠状态旋转,折叠状态持久化到 localStorage),左侧显示该对话组**最近访问时间**——复用 DSH 左侧面板的相对时间样式与 `sessions.list.updatedAt` 数据源(`src/client/history-time.ts`,含单测)。 - 「追问记录」点击任意节点跳转对话后,**目标会话的追问记录 tab 保持开启**(无论其原本的 tab 状态如何):跳转后定向在目标会话的侧边栏状态中打开/聚焦追问记录 tab(better-sidebar v0.12+ 的 `openTab(seed, scope)` 定向能力,已打开则聚焦、未打开则新建),来回跳转不再多一步操作。 - **client 半改动,无需重启 `dsh web`,硬刷新浏览器即可生效。** ## [0.1.0] - 2026-08-16 首个公开版本。 ### Added - 划选对话文本 → 浮层「提问」→ 右侧面板内嵌问答,不打断主对话。 - 自动创建同工作区独立 DSH 会话(`❓追问·<主题>`),可继续、可归档、可嵌套追问。 - 快速无思考模型压缩主对话上下文 + 划选引文注入首条消息。 - 「追问记录」tab 按根会话分层树展示。 - 两段式命名:划选首行占位 → 首次回答完成后基于「问题 + 回答」自动重命名。 ### Dependencies - `dsh-better-sidebar`(硬 peer 依赖,未安装时插件不激活)。