C6 removed apes:tasks as a duplicate of the host session panel; in practice the two answer different questions (host panel = switch workspaces, plugin tab = who is running right now), so the tab is restored and the info layer goes back to five types.
tasks: live session list (current / running / subagent markers) + showInactive toggle via pluginSettings['apes:tasks']; messages: placeholder that states the host has no message feed yet; both get badge counters, and the whole layer now honours the visible gate (auto-collapse) so hidden tabs stop waking on session events.
zh/en/ja dictionaries in sync (AGENTS 5); wiki 02/03/07 reverted from three to five tabs; decision record in docs/plans/2026-10-11-apes-tab-revival.md.
eslint was red at HEAD (5b715ac) in files untouched by this branch: unused statSync/dirname (check-plugin-api.mjs), unused PrefsSchema (src/index.ts, tests/smoke.spec.ts) and two Function types (tests/smoke.spec.ts). No behaviour change.
The Subagent/任务管理 page pinned on a permanent '加载中…' spinner
whenever the host's per-parent subagent catalog (subagentsByParent)
never hydrated for the root session — rootCatalog stayed undefined
while the session mirror already listed the children, so
summaryBackedLoading was true forever (SubagentView.tsx summaryBacked
gate). Root cause: a host-side hydrate gap (e.g. the
agent/disposed listener TypeError breaking the refresh chain) leaves
setSubagentCatalogOpen unanswered, and the UI had no escape.
Add a 5s hydrate timeout: once setSubagentCatalogOpen has had a chance
to land and the root catalog is still un-hydrated, render the known
children from the session mirror as CLICKABLE summary rows
(SummaryFallbackRows) instead of the infinite spinner. The timer resets
on root switch / catalog hydrate / page hidden. mode is inferred from
the summary's running flag (best-effort transport hint).
Verified: lint 0 / typecheck 0 / test 131 files·1385 passed / build 0.
Rollback: git revert <this-sha> or reset SubagentView.tsx to 99bef0f.
Every side-card switch was dead: settings.update returned OK and
cordis.patch.yml was written, yet the running plugin never saw the new value
and nothing reported an error.
Root cause: the plugin declared Config with the plain npm `schemastery`
(3.18.0), which has no volatile support. The host resolves plugin config via
`runtime.Config['~standard'].validate()` — i.e. the plugin's OWN schemastery —
so `meta.volatile` nodes were never wrapped in cosmokit volatile references.
`cordis-plugin-loader`'s `_commitVolatile()` bails out as soon as
`volatileEntries(fiber.config)` is empty, so every volatile-only edit was
silently dropped: no in-place update, no plugin restart.
Fix: import z from '@deepseek-ai/schemastery' (peer provided by the host at
mount; pinned 3.18.4 as a devDependency for the dev tree), matching the
repo's existing pnpm-workspace convention for @deepseek-ai/* peers.
Verified: probe reports tabsIsVolatile=true and revision climbing 0->6 across
six writes, each read-back matching the written value (previously the value
was frozen and revision stuck at 1).
Also in this change:
- apes tab ids lose their colon (apes:tasks -> apesTasks, ...): the host's
settings projection dropped map keys containing a colon.
- sidebar prefs ns follows the profile entry id (`better-sidebar`).
- subagent rows degrade to summary rows when the root catalog never hydrates.
- docs/feedback-0.2.0-rc2-contract-gaps.md: four host contract gaps, gap 4
documenting the silent volatile-write drop above.
Gates: typecheck 0, lint 0, build 0, test 131 files / 1385 passed.
0.2.0-rc.2 withdrew every catalog/subagent seam this fork was written
against, and all three calls degraded to silent no-ops:
openSubagent(address) -> gone
setSubagentCatalogOpen(id, open) -> gone
refreshSubagents(id) -> gone
The host's own subagent UI (dsh-client-ui-subagent) rebuilds a parent's
child catalog with `sessions.refreshProjections(parentSessionId)`, so
that is the only kick that still exists. Wire it into `observe()` and the
catalog retry button, keeping the legacy names as fallbacks for older
hosts. Without it the root catalog never hydrated: the page sat on
"loading" or degraded to synthesized summary rows.
Also document what the investigation settled:
- docs/subagent-state-enumeration.md: the full state model (catalog
child / diagnostic / session / job / sidechat) and proof that this
fork already renders it 1:1 with upstream.
- docs/plugin-conventions.md: the hard rules another sidebar plugin
must follow (host schemastery, volatile prefs, ns = profile entry id,
no colons in tab ids, never read a volatile field directly) plus the
withdrawn-API replacement table and a pre-release checklist.
- docs/feedback-0.2.0-rc2-contract-gaps.md: gaps 2/3 reframed. The ask
is documenting the official paths (open(childSessionId) /
refreshProjections(id)), not adding new APIs.
Gates: typecheck 0, lint 0, build 0, test 131 files / 1385 passed.
The host's own catalog puts an "open beside" action on every non-current
child row; this port had never wired it. Add it: the row grows a hover
button that opens the child's chat as a `subagentchat` resource in the
native right Sidebar, so a user can watch a child work without giving up
the pane he is in (the in-pane jump stays the plain row click).
- src/client/native/subagent-chat.ts: the resource-address codec, copied
from @deepseek-ai/dsh-client-ui-subagent rather than imported (the
package exports no helpers and the plugin must not reach into it).
- native/surface.ts: carry `kind` / `preferNewPane` on an open.
- SubagentView: `openChildAside` via `ctx.get?.('sidebarRight')` — an
optional probe, since a fake ctx without `get` otherwise crashes the
whole render (9 specs caught it) — threaded through all three
CatalogRows call sites.
- locales: `subagentOpenAside` in zh/en plus all 20 third-language
dictionaries (tests/locales.spec.ts enforces key-set parity).
- docs: the conventions page now documents the resource-open recipe.
Gates: typecheck / lint / build / test 132 files 1390 passed /
check:consumer-types, plus the repo's runtime-contract symbol check.
The aside button could hit `subagent/unauthorized: subagent mode does not
match the supplied address`. The host's session-controller (dsh-api-session-
controller validateAddress) compares the `mode` in the resource address
against the child's durable descriptor mode and rejects any mismatch. The
address `mode` came from the row, and on the degraded path the row's mode is
a best-effort guess from the summary `running` flag (running => continuable)
— so a child that is genuinely one-shot but still running was guessed as
continuable and the very open it was meant to trigger 401'd.
Two layers, so it holds on both the real-catalog and the degraded path:
- subagentChatAddress: only pass a mode through when it is one of the two
durable values (`one-shot` / `continuable`). Anything else is omitted,
which the controller reads as `mode: "unknown"` and therefore skips the
comparison — the safest form when the caller does not actually know.
- openChildAside: prefer the host's real descriptor mode for the child
(the `subagentsByParent` entry carries the durable mode) over the address
mode, falling back to the address only when the real catalog never arrived.
Gates: typecheck / lint / build / test 132 files 1392 passed.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
背景
C6 曾把
apes:tasks当作「与宿主会话面板重复」删除。实际用下来两者回答的不是同一个问题:宿主面板按工作区分组,服务的是「换项目」;插件 tab 服务的是「此刻谁在跑」。这个 PR 把信息层恢复成五个 tab,并补上 v2.0.0 只规划未落地的 badge / visible 两件增量。改了什么(
66ee395feat)apes:tasks(order 55) /agents(60) /apps(65) /messages(67) /profile(70)apes:tasks:实时会话列表(当前 / 运行中 / 子智能体标记)+showInactive开关,通道pluginSettings['apes:tasks'](showInactive=false只按running过滤,与 v2.0.0 保真)apes:messages:占位,如实说明宿主当前没有 message feed(保真 v2.0.0)visible门控:隐藏的 tab 不再被会话事件唤醒(auto-collapse)02/03/07三 tab → 五 tab;决策记录docs/plans/2026-10-11-apes-tab-revival.md(记 C6→C7 反演理由)顺带(
99bef0fchore(lint))清掉 HEAD 上本就存在、与本改动无关的 6 条 lint(
check-plugin-api.mjs未用statSync/dirname、src/index.ts与tests/smoke.spec.ts未用PrefsSchema、smoke 里两个Function类型),独立成 commit 便于单独 revert。零行为变更。验证(本机实测,
99bef0f干净工作树)pnpm run lintpnpm run typecheckpnpm run buildpnpm exec vitest runtests/apes-tabs.spec.ts26 用例)pnpm run check:consumer-types挂载冒烟(真实宿主 + 无头渲染)
打包 → scratch profile
dsh plugin add→ 真实dsh web --port 0→ Playwright,全程scripts/e2e-mount.sh。@deepseek-ai/dsh@0.2.0-rc.2(npmlatest,与 peer^0.2.0-rc.2同 tuple)Playwright does not support chromium on mac13),故临时加channel: 'chrome'跑,跑完已git checkout还原,未进本 PR结果:本分支 3 passed / 4 failed;基线
5a715ac(origin/main)在同环境、同 DSH 版本下复跑,失败集合逐条相同:5a715acmount.e2e.ts:144挂载 + 内置 tab 扫描mount.e2e.ts:561插件 chrome 挂载 + 稳定 data 属性mount.e2e.ts:594标准 WCO 几何驱动条mount.e2e.ts:636opt-in shell preset(settings.update400)mount.e2e.ts:664custom scheme 注入用户样式perf.e2e.ts:113mount latency / longtask / bundle costperf.e2e.ts:182底部条拖拽帧率结论:本 PR 不引入新的挂载失败;「打包产物经真实 DSH 挂载后不 crash」的直接证据(
:561chrome 挂载 + 稳定 data 属性)两侧都绿。4 条红的归因见 #2:lane 的宿主契约仍停在 0.1.5-rc.2 ——
tests/e2e/*与.github/workflows/ci.yml最后同步于 2026-10-035a7e213(v0.19.1 re-base),其后的 0.2.0-rc.2 对齐提交(df6f99c/f4c44d1/a6dd8aa)没有回带 lane,CI 至今钉@deepseek-ai/dsh@0.1.5-rc.2(与 peer^0.2.0-rc.2不一致)。回滚
git revert 99bef0fgit reset --hard 5a715ac(origin/main 当前头 = 本分支父提交)13 files changed, +603 / −72
✅ 真根因找到并修复:「设置开关点了不生效」
结论:不是 UI 问题、不是我们注册的 ns 问题——插件的 Config 用了 npm 公版
schemastery,导致宿主 loader 把每一次 volatile 写入静默丢弃。根因链(宿主源码逐层定位)
settings.update落盘后,靠configEditor.edit()→reconcileProfilePatches()→cordis-plugin-loader条目update()volatileOnly后进_commitVolatile(),第一行:const refs = volatileEntries(fiber.config); if (!refs.length) return true;createVolatile()的引用对象;引用由schemastery的Schema.resolve()遇到meta.volatile时创建(@deepseek-ai/schemastery有该分支,公版没有)cordis的resolveConfig()走runtime.Config['~standard'].validate()⇒ 用的是插件自己那份 schemasteryschemastery@3.18.0(volatile相关代码 0 处)⇒ 解析结果无任何 volatile 引用 ⇒refs.length === 0⇒ 既不就地更新、也不重启插件 ⇒ 写入被彻底丢弃(无任何报错)硬证据(探针实测)
config.tabsEnabled是否 volatile 引用false(write符号undefined)true(function)revisioncordis.patch.yml修复(最小改动)
src/config.ts:import z from 'schemastery'→import z from '@deepseek-ai/schemastery'package.json:移除dependencies.schemastery;peerDependencies加@deepseek-ai/schemastery: ^3.18.1(宿主 mount 时提供,符合本 repopnpm-workspace.yaml既有约定);devDependencies钉3.18.4门禁
锚点
feat/apes-tab-revival,commitaa8f417(含回滚锚点,git revert aa8f417即可回退)043340e随本次一并提交
apes:tasks→apesTasks)——宿主 settings 投影会丢弃含冒号的 map keybetter-sidebar)docs/feedback-0.2.0-rc2-contract-gaps.md:4 条宿主契约缺口,缺口 4 即本次静默丢写的完整根因🔴 待办
—— Hermes@mac13
新提交
35d33c7:catalog 用宿主的refreshProjectionshydrate(缺口 2/3 闭环)结论:0.2.0-rc.2 撤掉了本 fork 依赖的全部三个 catalog/subagent 接口,且都是静默 no-op(app 内全量 grep 0 命中):
sessions.openSubagent(address)sessions.open(childSessionId)sessions.setSubagentCatalogOpen(id, open)sessions.refreshProjections(id)sessions.refreshSubagents(id)权威样板 = 宿主自带的
@deepseek-ai/dsh-client-ui-subagent/lib/client.js:945-1005:本次改动:把
observe()与 catalog 重试按钮接到sessions.refreshProjections(旧名保留为老宿主回退)。此前 root catalog 从不 hydrate ⇒ 页面卡「加载中」或降级为合成 summary 行。顺带交付三份文档
docs/subagent-state-enumeration.md— 完整状态枚举(catalog 行 / diagnostic / 会话行 / job / sidechat)+ 实证本 fork 已与上游 1:1 对齐(枚举上限由宿主 3 个 StateDot state 决定,非插件缺口)docs/plugin-conventions.md— 给其他侧边栏插件的操作规范(host schemastery / volatile 偏好 / ns = 条目 id / tab id 不用冒号 / volatile 字段不可直读)+ 已撤 API 替代表 + 发版自检清单docs/feedback-0.2.0-rc2-contract-gaps.md— 缺口 2/3 的诉求改写为「文档化这两条官方路径」,而不是「补 API」(避免宿主重造已存在的能力)门禁:typecheck 0 / lint 0 / build 0 / test 131 files, 1385 passed
锚点:
35d33c7(基线aa8f417)—— Hermes@mac13
The host's own catalog puts an "open beside" action on every non-current child row; this port had never wired it. Add it: the row grows a hover button that opens the child's chat as a `subagentchat` resource in the native right Sidebar, so a user can watch a child work without giving up the pane he is in (the in-pane jump stays the plain row click). - src/client/native/subagent-chat.ts: the resource-address codec, copied from @deepseek-ai/dsh-client-ui-subagent rather than imported (the package exports no helpers and the plugin must not reach into it). - native/surface.ts: carry `kind` / `preferNewPane` on an open. - SubagentView: `openChildAside` via `ctx.get?.('sidebarRight')` — an optional probe, since a fake ctx without `get` otherwise crashes the whole render (9 specs caught it) — threaded through all three CatalogRows call sites. - locales: `subagentOpenAside` in zh/en plus all 20 third-language dictionaries (tests/locales.spec.ts enforces key-set parity). - docs: the conventions page now documents the resource-open recipe. Gates: typecheck / lint / build / test 132 files 1390 passed / check:consumer-types, plus the repo's runtime-contract symbol check.✅ 已接入官方的「侧栏打开子会话聊天」(
subagentchat)来源:宿主官方
dsh-client-ui-subagent的catalogActions.openChildAside(client.js:226/953,行内按钮 bind 在:308)。做法(不改宿主、纯插件侧):
src/client/native/subagent-chat.ts(新):地址编解码,逐字节对齐官方 ——dsh-resource://subagentchat/session/<childId>?parent=<parentId>&mode=<mode>ctx.get?.('sidebarRight')?.openResource(addr, { kind: 'subagentchat', preferNewPane: true })title+aria-label同一文案ctx.get?.())——直接ctx.get()会让无get的假 ctx 在渲染期崩掉整棵子树(复现:9 个渲染测试全挂)门禁:typecheck / lint / build / test 132 files 1390 passed / check:consumer-types / 运行时契约符号校验,五项全绿。
commit:
6b4a29b(27 files, +220/−7),基线35d33c7;回滚git revert 6b4a29b。规范:
docs/plugin-conventions.md已补该配方,并已同步到 wiki「侧边栏插件操作规范」。署名 Hermes@mac13
🔧 修了刚才那个
subagent mode does not match the supplied address根因(读了宿主
dsh-api-session-controller的validateAddress):服务端把资源地址里的mode跟子代理持久化描述符的真实 mode 比对,不等就抛subagent/unauthorized。我的地址mode取自行上值,而降级路径(宿主目录 2.5s 没 hydrate)的行 mode 是从 summary 的running推断的(running→continuable)。一个真实是 one-shot 但还在跑的子 Agent 就被猜成 continuable → 它本该触发的打开 401 了。两层修复(真实目录 + 降级两条路都覆盖):
subagentChatAddress:只有one-shot/continuable两个合法值才透传mode;否则省略 → 服务端按mode:"unknown"处理、跳过比对(最稳)。openChildAside:优先用宿主真实目录subagentsByParent里该子的持久化 mode,查不到才回退地址 mode。门禁:typecheck / lint / build / test 132 files 1392 passed(+2 用例锁定 mode 省略行为)。已部署 + 重启(pid 33354)。
commit:见下条(回滚
git revert该提交)。署名 Hermes@mac13
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.