The slash catalog follows a blank session's preset switch
斜杠目录跟随空会话的 preset 切换
Presets moved the rows that decide what a session's `/` menu contains. The Web composition disables host-plane `skill-filesystem`, `tool-skill`, `plan-mode`, and `command-compact`; a preset supplies them, so which commands and skills exist is a property of the session's composition rather than of the deployment. Both browser catalogs cache per session — `CommandDirectory` in `dsh-client-ui-commands`, the single-fligh
English
Problem
Presets moved the rows that decide what a session's / menu contains. The Web composition disables host-plane skill-filesystem, tool-skill, plan-mode, and command-compact; a preset supplies them, so which commands and skills exist is a property of the session's composition rather than of the deployment.
Both browser catalogs cache per session — CommandDirectory in dsh-client-ui-commands, the single-flight fetch map in dsh-client-ui-skill — and the composer warms both at scope birth, under whatever preset the session was created with. The hero chip then lets the user recompose the still-blank session, and neither cache had an invalidation edge for that: commands/change is registry-wide and connection/reset needs a reconnect. agentPresets.recompose re-parents the agent's scope onto a standing mount that may already exist, so it registers nothing and the registry-wide signal never fires for it.
The menu therefore kept serving the composition the session no longer ran. Switching down left compact, plan, and every project skill listed; switching up left the narrower catalog — the four host-plane rows and the client's own model contribution — with no skills at all, which is what the bug report described. The catalog only healed when an unrelated registry change or a reconnect happened to invalidate it.
Decision
The switch's commit point is the logged agent-preset/selected event. The preset owner re-emits that commit as the client-safe cordis owner event agent-preset/selected(sessionId, agentPreset), the host stream forwards it verbatim, and each catalog subscribes directly through ctx.remote.$on: ui-commands soft-refreshes the key (the old snapshot keeps serving the open menu until the new one lands), while ui-skill invalidates it (aborting an in-flight prewarm, so a warm racing the switch cannot publish the stale catalog).
The owner event is per session and carries no catalog, only the preset id. ui-agent-preset folds it into the session row because the agentPresets.select echo reaches only the client that issued the switch and the row is what the session header labels itself from (and what the hero chip compares the next pick against).
Deriving the owner event from the logged event rather than from the RPC handler's return keeps one authority for "this session's composition changed": every connected client observes the switch, not only the tab that issued it, and a client that is not the switcher never has to infer it from a registry signal that will not come.
Alternatives considered
Invalidate in the client's own agentPresets.select callback. Smallest change, and the preset is locked after the first turn, so the hero chip is the only place a switch can originate. Rejected because the invalidation would then live in the surface that happens to issue the RPC rather than at the commit point: a second tab on the same blank session keeps a stale menu, and any future host-side recomposition has no signal at all.
Derive the client event from the existing session/event mux frame. The logged event already reaches every subscribed client, so no new wire type would be needed. Rejected on face separation: narrowing event.type to agent-preset/selected requires the SessionEventMap augmentation, and the only ways to load it in the Client program are a project reference to dsh-agent-presets — which drags the host ctx.sessions merge into a program that publishes its own — or a cast that defeats the discriminant.
Reuse forwarded commands/change. It is the existing catalog-invalidation event, but it is registry-wide, carries no session, and says nothing about skills; a client would repull every session's commands and still never refresh a skill catalog.
Consequences
The forwarding allowlist gains the preset owner's typed event, and every catalog a preset decides has one place to subscribe: a future per-session surface derived from the composition invalidates on the same signal instead of inventing another. The owner event remains a second publication of a logged fact, so a future switch path that recomposes without logging would go unannounced. ui-commands stays soft (the open menu never blanks) while ui-skill drops its entry outright, because a skill catalog has no partial-serve mode; a menu opened inside the refetch window shows no skills for that instant rather than the wrong ones.
Testing
api-proxy-agent-preset.spec.ts asserts the committed switch is forwarded once with the session and its new preset; the ui-agent-preset, ui-commands, and ui-skill specs assert that direct Remote subscriptions merge the row or repull only the recomposed session. The agent-preset-selection web e2e seeds a project skill and, after the hero chip applies minimal, asserts the / menu drops compact, plan, and the skill while keeping the host-plane rows — the assembled-application evidence that the panel follows the composition.
That e2e also stopped reading its staged-pick assertion off the serialized session list: the seeded session records minimal too, so the substring answered before the switch had landed. It now addresses the live session by id.
Related
Reaching the host on a SECOND switch is a separate defect with its own cause and fix: the session-row identity guard. Until it landed, agent-preset-selection.e2e.ts could only exercise the first switch — the invalidation edge here is direction-blind, but the switch it reacts to has to happen.
中文
问题
preset 把决定 / 菜单内容的那些行搬走了。Web 组装禁用了宿主面的 skill-filesystem、tool-skill、plan-mode 和 command-compact,改由 preset 提供,因此一个会话有哪些命令和技能,是它自身组成的属性,而不是部署的属性。
浏览器侧两份目录都按会话缓存——dsh-client-ui-commands 的 CommandDirectory,dsh-client-ui-skill 的 single-flight 拉取表——并且 composer 在 scope 出生时就按会话创建时的 preset 预热了它们。随后 hero 上的 chip 允许用户重组这个仍为空的会话,而两份缓存都没有对应的失效边:commands/change 是注册表级的,connection/reset 需要重连。agentPresets.recompose 只是把 agent 的 scope 重新挂接到一个可能已经存在的常驻挂载上,不产生任何注册,注册表级信号因此永远不会为它触发。
于是菜单继续提供会话已经不再运行的那套组成。向下切换后 compact、plan 和全部项目技能仍列在菜单里;向上切换后留在原地的是更窄的目录——四条宿主面行加客户端自己的 model 贡献——而且完全没有技能,这正是 bug 报告描述的现象。只有当某个无关的注册表变化或一次重连恰好使其失效时,目录才会自愈。
决策
这次切换的提交点是落账的 agent-preset/selected 事件。preset owner 将该提交重新发为 client-safe 的 cordis owner 事件 agent-preset/selected(sessionId, agentPreset),宿主流原样转发它,两份目录各自通过 ctx.remote.$on 直接订阅:ui-commands 软刷新该键(新快照落地前,旧快照继续服务已打开的菜单),ui-skill 让它失效(并中止在途的预热,使一次与切换赛跑的 warm 无法发布过期目录)。
该 owner 事件按会话粒度,不携带目录,只带 preset id。ui-agent-preset 会把它折进会话行,因为 agentPresets.select 的回执只会到达发起切换的那个客户端,而会话头部标签正是以这一行为准(hero chip 比较下一次选择时读的也是它)。
从落账事件而不是 RPC 处理器的返回值派生 owner 事件,使「这个会话的组成变了」只有一个权威来源:每个已连接的客户端都能观察到这次切换,而不只是发起它的那个标签页;不是发起方的客户端也无需从一个根本不会到来的注册表信号里去推断。
考虑过的替代方案
在客户端自己的 agentPresets.select 回调里就地失效。 改动最小,而且第一轮之后 preset 就锁定,hero 上的 chip 是切换唯一可能的发起处。否决理由是失效逻辑会落在恰好发起 RPC 的那个界面上,而不是提交点:同一个空会话在第二个标签页里仍是过期菜单,将来任何宿主侧的重组也完全没有信号。
从既有的 session/event mux 帧派生客户端事件。 落账事件本来就会送达每个已订阅的客户端,不需要新增协议类型。因面(face)分离而否决:把 event.type 收窄到 agent-preset/selected 需要 SessionEventMap 增补,而在 Client 程序里加载它只有两条路——引用 dsh-agent-presets 工程,那会把宿主的 ctx.sessions 合并拖进一个自己也发布同名服务的程序;或者用一次类型断言绕过判别式。
复用转发的 commands/change。 它是既有的目录失效事件,但它是注册表级的、不带会话、也与技能无关;客户端会把每个会话的命令都重拉一遍,却依然永远刷不新技能目录。
后果
转发名单加入了 preset owner 的类型化事件,而每一份由 preset 决定的目录从此有了统一的订阅点:将来任何从组成派生的按会话界面,都在同一个信号上失效,而不必再发明一个。owner 事件仍是落账事实的第二次发布,因此将来若出现一条不落账就重组的切换路径,它将无人宣告。ui-commands 保持软失效(已打开的菜单不会变空),而 ui-skill 直接丢弃该项,因为技能目录没有「部分可服务」的状态;在重拉窗口内打开的菜单,那一瞬间显示的是没有技能,而不是错误的技能。
测试
api-proxy-agent-preset.spec.ts 断言已提交的切换恰好转发一次,并带上会话与新 preset;ui-agent-preset、ui-commands 与 ui-skill 的 spec 断言直接 Remote 订阅会合并会话行或只重拉被重组的会话。agent-preset-selection web e2e 播种一个项目技能,并在 hero chip 应用 minimal 之后断言 / 菜单丢掉了 compact、plan 和该技能,同时保留宿主面的那几行——这是面板跟随组成的整装应用证据。
同一条 e2e 也不再从序列化后的会话列表里读它的 staged-pick 断言:被播种的会话同样记录着 minimal,子串匹配在切换落地之前就会通过。现在它按 id 寻址那个活跃会话。
Related
第二次切换能否到达宿主是另一个缺陷,有各自的成因与修复:会话行的标识判定。在它落地之前,agent-preset-selection.e2e.ts 只能演练第一次切换——这里的失效边对方向无感,但它所响应的那次切换必须真的发生。