Skip to content

v0.5.2

Choose a tag to compare

@github-actions github-actions released this 01 Oct 06:07
· 267 commits to main since this release

MusePi 0.5.2

Platform 下载
macOS (Apple Silicon) MusePi-0.5.2-arm64.dmg
Windows MusePi-0.5.2-setup.exe · MusePi-0.5.2-arm64-setup.exe
Linux x64 MusePi-0.5.2-x86_64.AppImage · MusePi-0.5.2-amd64.deb
Linux arm64 MusePi-0.5.2-arm64.AppImage · MusePi-0.5.2-arm64.deb
Android MusePi-0.5.2-app-debug.apk (debug build, adb 安装)

[0.5.2] - 2026-10-01

Added

  • 文件子系统插件化(插件化第五刀:musepi:file 包):文件子系统从「散置的工具 + 索引」收编为一个内置插件单元(dsh fs 插件族 parity:tool-fs / tool-str-replace-editor / tool-fs-search)——read(read 工具族:文件 / pdf / sqlite / archive 选择器)/ write(write / edit / ast_edit 写入编辑族)/ search(glob / grep / ast_grep 检索族)/ index(工作区文件内容索引扫描)四个真实后端组件独立启停。新增 file-backend deny 通道:组件开关写隐藏键 file.disabledBackends(settings-schema 登记、fail-soft 读取),消费哲学与 terminal/browser 组件一致——read/write/search 被禁后对应工具族不再进入会话工具集(tools/index.ts isToolAllowed 谓词消费,禁用是显式意图、不静默留桩),index 被禁后 daemon 停止后台扫描(server.ts index.scan 门消费;已有 FTS 数据的查询面仍由 index.setEnabled 总开关治理,不越权)。无总开关语义(不发明):file 行只读展示,禁用粒度落到后端组件。extensions.list 组件面如实下发;组件名/描述/kind 标签 i18n 键 zh/en 同步。9 例新契约测试(注册表投影/名单写读/list 下发/谓词映射/扫描门)。

    • EN: File subsystem pluginization (builtin-plugin cut five: the musepi:file package). The file subsystem moves from scattered tools + index to one builtin plugin unit (dsh fs plugin family parity: tool-fs / tool-str-replace-editor / tool-fs-search) — four real backend components toggle independently: read (the read tool family: files / pdf / sqlite / archive selectors) / write (write / edit / ast_edit) / search (glob / grep / ast_grep) / index (the workspace file-content index scan). New file-backend deny channel: component toggles write the hidden key file.disabledBackends (settings-schema registered, fail-soft read), consumed with the same explicit-intent philosophy as the terminal/browser components — denied read/write/search families leave the session tool set (tools/index.ts isToolAllowed predicate; disabling is explicit intent, never a silent stub), and a denied index stops the daemon's background scan (server.ts index.scan gate; querying existing FTS data stays governed by the index.setEnabled master switch, no overreach). No master switch (not invented): the file row is read-only display, disable granularity lands on backend components. extensions.list reports the component plane truthfully; component name/description/kind label i18n keys registered zh/en. 9 new contract tests (registry projection / denylist write+read / list projection / predicate mapping / scan gate).
  • 浏览器后端组件化(插件化第四刀:musepi:browser 包):browser 内置单元从「整工具单组件(tool-deny)」改为三个真实后端组件——launch(脚本启动的 Chromium,browser.headless 控制有头/无头,工具兜底路径)/ attach(接管已有浏览器:CDP 端点、Relay 扩展、cmux、spawn-and-attach)/ gui(托管面板浏览器桥,agent 页面开进右侧面板)。新增 browser-backend deny 通道:组件开关写隐藏键 browser.disabledBackends(settings-schema 登记、fail-soft 读取),browser 工具解析链真实消费——设置链(relay → cdpUrl → cmux → gui → headless)回退跳过被禁环节(不是试过再失败),显式 app.path/cdp_url/relay 指向被禁后端给结构化 DISABLED_BACKEND,三后端全禁给 NO_BACKEND(与终端 provider 组件同哲学:禁用是显式意图,不静默回退)。extensions.list 组件面如实下发;组件名/描述 i18n 键 zh/en 同步(launch/attach/gui);computer 行保持 tool-deny 单组件 + unsetDisabled 包形态,经注册表投影契约测试守。14 例新契约测试(通道写读/list 下发/结构化拒绝/回退语义/全禁报错)。

    • EN: Browser backend componentization (builtin-plugin cut four: the musepi:browser package). The browser builtin unit moves from a single whole-tool component (tool-deny) to three real backend components — launch (scripted Chromium, headed/headless via browser.headless, the tool's fallback path) / attach (take over an existing browser: CDP endpoint, relay extension, cmux, spawn-and-attach) / gui (managed in-app browser bridge; agent pages open in the right pane). New browser-backend deny channel: component toggles write the hidden key browser.disabledBackends (settings-schema registered, fail-soft read), and the browser tool's resolution chain consumes it for real — the settings chain (relay → cdpUrl → cmux → gui → headless) skips denied candidates (never "tried and failed"), explicit app.path/cdp_url/relay pointing at a denied backend yields a structured DISABLED_BACKEND, and all three denied yields NO_BACKEND (same philosophy as the terminal provider components: disabling is an explicit intent, never silently fallen back). extensions.list reports the component plane truthfully; component name/description i18n keys are registered zh/en (launch/attach/gui); the computer row keeps its tool-deny single component + unsetDisabled package shape, guarded by a registry projection contract test. 14 new contract tests (channel write/read, list projection, structured rejection, fallback semantics, all-denied error).

Fixed

  • 会话工作区 slug 归一化(同一目录不再分裂成两个工作区):同一物理目录经不同大小写/符号链接写法(如 C:\Users\... vs c:\users\...)此前会生成两个 slug 目录、在项目页显示为两个工作区。修法:slug 生成改走 normalizePathForComparison(realpath + win32 小写归一),canonical cwd/home/temp 三处统一;现存大小写变体目录复用不重命名(win32 existsSync 实测大小写不敏感会误命中,改经 readdir 精确→模糊两级匹配,首次出现的写法赢得目录名,旧目录继续用);hashed 遗物哈希键保持 resolveEquivalentPath 原始大小写不变(17.2.5-17.2.8 遗物按原始大小写哈希,换键会搁浅旧会话);peekDefaultSessionDir 与 computeDefaultSessionDir 共享同一变体解析,探测/创建永远一致(ghost 对账依赖此一致性);resolveManagedSessionRoot basename 匹配 win32 大小写不敏感,认领旧写法目录(否则被判 unmanaged,GC/归档会漏)。2 例契约测试(win32 条件)。

    • EN: Session-workspace slug normalization (one directory no longer splits into two workspaces). The same physical directory reached via different case/symlink spellings (e.g. C:\Users\... vs c:\users\...) previously produced two slug dirs and showed as two workspaces in the projects tab. Fix: slug generation now goes through normalizePathForComparison (realpath + win32 lowercase normalization), unified across the canonical cwd/home/temp sites; existing case-variant dirs are REUSED, never renamed (win32 existsSync is case-insensitive in practice and false-matches — resolution now does readdir exact→fuzzy two-level matching, first-seen spelling wins the dir name, old dirs keep serving); hashed-legacy hash keys keep the case-preserving resolveEquivalentPath unchanged (17.2.5-17.2.8 artifacts hash the original case — rekeying would strand old sessions); peekDefaultSessionDir and computeDefaultSessionDir share the same variant resolution so probing and creating always agree (the ghost reconciliation depends on this); resolveManagedSessionRoot's basename match is case-insensitive on win32 so it claims old-spelling dirs (otherwise they'd be judged unmanaged and GC/archive would skip them). 2 contract tests (win32-gated).
  • 项目页幽灵目录自动清理(Temp\daemon- 不再赖在列表里)*:幽灵条目真凶不在 daemon 也不在 view store(两处均已对账干净),而在 GUI——项目页列表持久化在 localStorage(musepi-gui-projects),从 session cwd 种子进来的测试/临时工作区(Temp\daemon-viewkey-* 等)删除后永远留在列表。修法:新增只读 RPC fs.stat(FileService,任何错误归一为 exists:false 绝不抛错),侧栏对每个已持久化项目路径探测磁盘存在性,消失的一并从列表/折叠态/忽略墓碑中移除。4 例 fs.stat 契约测试。

    • EN: Ghost folders in the projects tab are cleaned up automatically (Temp\daemon-* no longer lingers in the list). The ghosts lived neither in the daemon nor the view store (both reconcile clean now) but in the GUI: the projects-tab list is persisted in localStorage (musepi-gui-projects), and test/temp workspaces seeded from session cwds (Temp\daemon-viewkey-* etc.) stayed in the list forever after deletion. Fix: a new read-only fs.stat RPC (FileService — any error normalizes to exists:false and never throws), and the sidebar probes every persisted project path against disk existence, dropping vanished ones from the list, collapse state, and dismissal tombstones together. 4 fs.stat contract tests.
  • 死工作区幽灵行自动对账(项目列表不再被已删除的测试/临时工作区刷屏):view store 物化行比 transcript 长寿——测试/临时工作区删除后 store 行仍在,session.list 与侧栏项目页永远列出死工作区(pi-advisor-toggle-/daemon-forkat- 曾刷屏)。knownSessions 合并时新增对账:非 live 且磁盘证据消失的行从合并结果与 view store 一并删除——① transcript 文件消失(扫描缓存内陈旧 sessionFile 路径探测);② store-only 行(activation 物化行,持久层不记路径)的 cwd 对应 slug 产物目录整个消失(新 peekDefaultSessionDir 纯查询推导目录名,无迁移/建目录副作用)。负契约:磁盘文件还在的行保留;目录还在的 store-only 行保留(persist 前 lazy 阶段无法证明消失即不动);live 会话恒保留。另:daemon 启动时一次性清扫 sessions 根下的空 slug 目录(rmdirSync 只删真空目录,有产物的目录抛错跳过)——删掉全部会话后残留的空壳工作区目录不再赖着。4 例契约测试(文件级删除/工作区级删除/两条不误删负契约)。

    • EN: Ghost dead-workspace rows are reconciled automatically (the project list no longer floods with deleted test/temp workspaces). View-store materialized rows outlive their transcripts — once a test/temp workspace was deleted, its store rows stayed behind and session.list / the sidebar project tab listed the dead workspace forever (pi-advisor-toggle-* / daemon-forkat-* once flooded the list). knownSessions now reconciles during the merge: rows that are not live AND whose disk evidence is gone are dropped from the merged result AND from the view store — ① the transcript file vanished (probed via the stale sessionFile path inside the scan cache), ② a store-only row (an activation materialized row; the persistence layer records no path) whose cwd's slug artifact dir disappeared entirely (new peekDefaultSessionDir computes the dir name read-only — no migration, no mkdir side effects). Negative contracts: rows whose disk file survives are kept; store-only rows whose dir survives are kept (a pre-persist lazy session cannot be proven gone, so it stays); live sessions are always kept. Plus: the daemon sweeps empty slug dirs under the sessions root once at startup (rmdirSync only removes truly empty dirs — anything with content throws and stays), so the empty shell of a fully-deleted workspace no longer lingers. 4 contract tests (file-level delete / workspace-level delete / two don't-delete negative contracts).
  • 会话树子行去掉 ├─/└─ 枝线符号:侧栏会话树此前给每个子会话行画 ├─/└─ 连接符,层级已靠缩进与折叠 chevron 表达,连线属冗余视觉噪音。现 treePrefix 只保留缩进空格,flattenTree 的 showConnector/isLast 字段与 SessionRow 对应 props 一并移除(消息树 lib/message-tree 的连接线语义独立,不受影响)。

    • EN: Session-tree child rows drop the ├─/└─ connector glyphs. The sidebar tree drew a connector for every child row even though hierarchy is already expressed by indentation and the fold chevron — the lines were redundant visual noise. treePrefix now renders indentation spaces only; flattenTree's showConnector/isLast fields and the SessionRow props are removed (the message tree's connector semantics in lib/message-tree are independent and unaffected).
  • 孤立的顾问会话顶层行(激活过的顾问/子会话丢失 parentId):顾问 transcript 被点击激活后,daemon 以顾问文件头内部 id 在 view store 落物化行(parent_id = NULL);knownSessions 合并磁盘扫描时 existing 分支只回填 subagent/advisor 标,parentId 被丢弃——session.tree 按 parentId 挂父节点,缺它激活过的顾问行被提为顶层孤立行(磁盘结构完好:父 jsonl + <parentBase>/__advisor.jsonl 都在)。修法:收编循环把父 id 推导(产物目录名 <timestamp>_<parentId> 后缀)移到 existing 分支之前,已有行缺 parentId 时一并回填。3 例契约测试(无 store 行对照 / 有 store 行回填 / 父行不误标)。

    • EN: Orphaned advisor sessions rendered as top-level rows (activated advisor/sub sessions lost their parentId). Clicking an advisor transcript activates it: the daemon materializes a view-store row under the advisor file's header-internal id (parent_id = NULL). knownSessions' merge backfilled only the subagent/advisor flags in the existing-row branch and DROPPED the parentId — session.tree attaches children by parentId, so any activated advisor row was promoted to an isolated root (the disk structure is intact: parent jsonl + <parentBase>/__advisor.jsonl both present). Fix: the parent-id derivation (artifact-dir name suffix <timestamp>_<parentId>) moves ahead of the existing-row branch, which now backfills parentId when the stored row lacks it. 3 contract tests (no-store-row control / store-row backfill / parent row not mis-tagged).
  • 进入顾问/子会话再返回后消息污染所有会话(dup entry id 导致 React 残留 DOM):根因两层。① daemon #doActivate 把 advisor transcript 收编进会话视图时,message 条目按 messageKey(role:timestamp) 重键——同一毫秒的两条「Session update」user 消息生成完全相同的 id;② React 对重复 key 的列表做 reconciliation 时,dup key 的第一个 fiber 被挤出 existingChildren map 而永远不被 unmount,其 DOM 节点残留在 .gui-transcript 容器里——切回父会话后顾问的行叠在转录前面,之后点任何会话都在,重启才消失。修复:daemon 重键循环对 message 条目也加碰撞序号后缀(#n,与原非 message 条目同规则);GUI Transcript 侧加 rowKeys 备忘——首个出现保留裸 id(prepend 分页的行身份依赖),重复 id 加 #n 后缀,两处渲染 map 统一取用。端到端复现脚本(隔离 daemon + playwright 真实点击)验证:返回父会话后转录行数 22→6 干净。

    • EN: Messages polluted every session after visiting the advisor/sub session and coming back (dup entry ids → orphaned React DOM). Two layers to the root cause. ① When the daemon's #doActivate adopts the advisor transcript into the session view, message entries are rekeyed by messageKey(role:timestamp) — two "Session update" user messages in the same millisecond produce the EXACT same id; ② React's reconciliation for duplicate-keyed lists drops the first fiber of a dup pair from the existingChildren map and never unmounts it, leaving its DOM nodes inside .gui-transcript — switching back to the parent rendered the advisor rows on top of the transcript, they stayed through every later session click, and only a restart cleared them. Fix: the daemon rekey loop applies the collision-sequence suffix (#n) to message entries too (same rule as the non-message branch); the GUI Transcript gains a rowKeys memo — first occurrence keeps the bare id (prepend-paging row identity depends on it), duplicates get a #n suffix, and both render maps consume it. Verified end-to-end with the reproduction script (isolated daemon + real playwright clicks): transcript rows after returning to the parent go 22→6, clean.
  • 顾问 transcript 行丢失子代理/顾问标记(view store 同 id 行合并被跳过):knownSessions 合并子代理扫描结果时,merged.has(h.id) 直接 continue——污染事故期间被收编进物化视图 store 的顾问会话行(store 行无层级标记)因此永远拿不到 subagent/advisor 标,session.list/session.tree 把顾问行显示为普通顶层会话。修法:命中已有行时不再跳过,而是把磁盘扫描的层级标记补进已有行(subagent 置真、advisor 按文件名为 __advisor[.<slug>].jsonl 置真、sessionFile 空缺回填)。复现脚本验证:顾问行 subagent:true, advisor:true。

    • EN: Advisor transcript rows lost their subagent/advisor flags (view-store row shadowed the merge). knownSessions skipped merging when merged.has(h.id) was true — the advisor session row adopted into the materialized view store during the pollution incident (store rows carry no hierarchy flags) therefore never received the subagent/advisor tags, and session.list/session.tree showed the advisor row as a plain top-level session. Fix: on an existing row, instead of skipping, backfill the disk scan's hierarchy flags into it (subagent set true, advisor set true when the file is __advisor[.<slug>].jsonl, sessionFile backfilled when missing). Verified with the reproduction script: the advisor row reports subagent:true, advisor:true.
  • 浮层延伸进顶栏/标题栏区域后点击被吞(Electron 拖拽区原生命中规则):输入框「+」添加列表、设置页配置项下拉等浮层向上展开超过 header 后,落在拖拽区内的条目点击无效、变成拖动窗口。根因:Electron 只对拖拽区的后代元素 honoring -webkit-app-region: no-drag,而这些浮层经 portal 挂在 #root 层(拖拽 header 的兄弟节点),不是后代。修法:gui-base.css 新增统一规则,全部交互浮层根(gui-menu-popup / gui-context-menu / gui-palette(-backdrop) / gui-btw-card / gui-rail-tooltip / gui-session-hover-card)显式 no-drag 退出拖拽命中——useFloatingMenu 全仓 80 处消费(含 GuiSelect 设置下拉、ModelSelector、AttachMenu)一处修复全覆盖。

    • EN: Clicks swallowed when a floating overlay extended into the header/titlebar area (Electron drag-region native hit-testing). The composer「+」add list, settings config dropdowns and similar menus open upward past the header, and entries landing inside the drag region did nothing — the click started a window drag instead. Root cause: Electron only honors -webkit-app-region: no-drag on DESCENDANTS of a drag region, while these overlays portal into #root (siblings of the drag header), not descendants. Fix: one consolidated rule in gui-base.css marks every interactive overlay root (gui-menu-popup / gui-context-menu / gui-palette(-backdrop) / gui-btw-card / gui-rail-tooltip / gui-session-hover-card) explicitly no-drag — the single fix covers all 80 useFloatingMenu consumers repo-wide (including the GuiSelect settings dropdown, ModelSelector, and AttachMenu).
  • 侧边栏能力中心直接点击落到插件市场:改为落到「技能」tab(发现分段);插件市场仍经悬停菜单直达。

    • EN: Clicking the sidebar capability-center button landed on the plugins marketplace; it now lands on the "Skills" tab (discover segment). The marketplace stays reachable via the hover menu.
  • 会话地图三处修缮(折叠摘要 / 位置跳变 / 导航轴漂移):① 折叠卡摘要从单行省略号改为与展开态一致的两行 clamp——收起时不再截断丢内容;② 节点卡补 top/left 过渡(与既有 height 过渡同曲线)——折叠/展开引起的布局位移平滑形变,不再瞬移(平移/缩放走 transform 不触发该过渡,无性能副作用);③ 轮次导航轴视口指示改按节点主轴坐标分段线性插值——此前世界坐标线性映射与条目等宽排列在展开节点/分支列存在时双向漂移,视口边缘对准某轮坐标时指示窗也对准该轮条目,点击定位与指示窗共用同一几何。

    • EN: Three session-map fixes (collapsed summary / position jumps / nav-axis drift). ① The collapsed card summary switches from a single-line ellipsis to the same two-line clamp as the expanded state — collapsing no longer truncates content away. ② Node cards gain a top/left transition (same curve as the existing height transition) — layout shifts from folding/unfolding now morph smoothly instead of teleporting (pan/zoom go through transforms and never trigger it, so no perf cost). ③ The turn-nav viewport indicator maps through piecewise-linear interpolation over node main-axis coordinates — the previous linear world-coordinate mapping drifted from the equal-width entry layout whenever expanded nodes or branch lanes existed; a viewport edge aligned with a turn's coordinate now aligns the indicator with that turn's entry, and click navigation shares the same geometry as the indicator.
  • 展开轮内工具活动时头像重复渲染:折叠轮展开后,span 内第一条 assistant 行的前一条 entry 可能是「读取了 N 个文件」这类 custom 汇总行,紧邻判定失效导致该轮再画一个头像,一轮出现两个头像。修法:折叠 span 的头像归 header 行所有——span 内其余行(headerIdx 之后、endIdx 之前)一律不渲染头像,与 openchamber「一轮一个头像」规则一致。

    • EN: Duplicate avatar when expanding a round's tool activity. After unfolding a collapsed round, the first assistant row inside the span could follow a custom summary row (e.g. "read N files"), defeating the adjacency check and painting a second avatar for the same round. Fix: the fold span's avatar is owned by the header row — no other row inside the span (after headerIdx, up to endIdx) renders one, matching the openchamber "one avatar per round" rule.
  • 分支树轨迹视图工具调用显示为省略号:treeTextOf 只提取 text 块,纯工具调用行没有文本 → 90 字硬切片兜底成「…」;CSS 省略号双重截断。重写:assistant 行无文本但有 toolCall 块时返回工具名汇总(单调用 name,多调用 name +N),toolResult 无文本返回空串(保持 verdict 空判定契约),有文本时只留 DOM 400 字上限不再拼省略号。

    • EN: Tool calls in the branch-tree trajectory view rendered as an ellipsis. treeTextOf only extracted text blocks, so pure tool-call rows had no text and fell into the 90-char hard-slice "…" fallback, doubled by the CSS ellipsis. Rewritten: an assistant row with toolCall blocks but no text now returns the tool-name summary (single call name, multiple name +N); a text-less toolResult returns an empty string (preserving the verdict-empty contract); rows with text keep only the 400-char DOM cap without a glued ellipsis.
  • 插件配置表单开关缺旋钮:ConfigFieldRow 的 boolean 开关只渲染 gui-toggle 轨道没放 gui-toggle-knob 旋钮,显示为一块纯色药丸;补上旋钮,与设置页/能力中心开关同规范。

    • EN: The plugin config form's boolean switch rendered the gui-toggle track without the gui-toggle-knob, showing a plain colored pill; the knob is added, matching the settings-page and capability-center switch convention.

Changed

  • 内置插件配置与组件描述全量 i18n(中英双语):daemon 注册表(stt/tts/terminal/browser/computer/lsp 的配置字段 label/description/optionLabels 及组件描述)从硬编码英文改为发 i18n 键,GUI 经 tLoose 解析、缺失回显原文(与 optionLabels 同一约定);缺失 label 的字段(browser.headless/cdpUrl、computer.display/maxWidth、lsp.lazy/shared、terminal.*)补齐。新增 plugin cfg <key> label/desc/opt * 与 ext builtin <name> component <id>[ desc] 两组键,zh/en 词表同步注册(client-core parity 测试守)。

    • EN: Built-in plugin config and component descriptions fully i18n'd (bilingual). The daemon registry (config field label/description/optionLabels for stt/tts/terminal/browser/computer/lsp, plus component descriptions) now emits i18n keys instead of hardcoded English; the GUI resolves them via tLoose with raw-string fallback (same convention as optionLabels). Fields missing labels (browser.headless/cdpUrl, computer.display/maxWidth, lsp.lazy/shared, terminal.*) gain them. Two key families are registered in both zh/en wordlists (guarded by the client-core parity tests): plugin cfg <key> label/desc/opt * and ext builtin <name> component <id>[ desc].
  • 客户端参照类文案清理:用户可见的「同款/parity」参照类表述全部改写为自描述文案(蜂群任务卡片、桌面外壳、定时任务等,zh/en 同步);task-card-style 设置 schema 的 "OMP original (Classic)" 改为 "MusePi original (Classic)"(i18n 键与源字符串此前不匹配,翻译永不命中,已同步两个 settings 词表的键)。

    • EN: Client reference-style copy cleaned up. All user-visible "same-as-X/parity" phrasing is rewritten as self-describing copy (swarm task card, desktop shell, scheduled tasks, etc., zh/en in sync); the task-card-style settings schema's "OMP original (Classic)" becomes "MusePi original (Classic)" (the i18n key and source string previously mismatched, so the translation never resolved — both settings wordlists' keys are updated to match).
  • 插件组件开关实时生效(dsh parity:拨开关即所见):三处断点同修。① 能力中心插件清单的 fetch 上一批改成 mount-only 后丢失了重拉——补回 extensions.changed 订阅(payload.type 判定,照 slot-host 单例同款写法);② 设置页语音分区改接全局 extensions registry 单例(原本地自刷、无事件订阅),插件总开关/组件开关一拨,schema 行、模型选择器、测试入口同帧跟随,不再需要重进页面;③ 引擎级黑名单落地到选择器:daemon 的 voice.disabledEngines 按组件 id 记账,GUI 侧新增组件 id→档位键映射(whisper→fast/balanced/turbo、sensevoice→sensevoice、parakeet→parakeet;kokoro→kokoro、melotts-zh→melotts-zh)与共享 helper hiddenTiersFromComponents,STT/TTS 模型选择器按隐藏档位实时过滤,全部被停用时渲染提示行(新 i18n 键 zh/en 同步);选中档位被隐藏时不再触发自动下载。设置页与插件详情弹层同一 helper、同一存储,双入口零漂移。

    • EN: Plugin component toggles now take effect live (dsh parity: the switch IS the feedback), three breakpoints fixed together. ① The capability center's plugin-list fetch lost its refetch when a previous batch made it mount-only — the extensions.changed subscription is restored (payload.type check, same idiom as the slot-host singleton). ② The settings voice section now consumes the global extensions registry singleton (it previously refreshed only locally with no event subscription), so flipping a plugin master switch or a component switch updates the schema rows, model pickers, and test entries in the same frame — no page re-entry needed. ③ The engine-level denylist reaches the pickers: the daemon's voice.disabledEngines is accounted per component id, and the GUI gains a component-id→tier-key mapping (whisper→fast/balanced/turbo, sensevoice→sensevoice, parakeet→parakeet; kokoro→kokoro, melotts-zh→melotts-zh) plus a shared hiddenTiersFromComponents helper; the STT/TTS model pickers filter hidden tiers in real time, render a hint row when all engines are disabled (new i18n key, zh/en in sync), and no longer auto-download a selected tier that just got hidden. Settings page and plugin detail dialog share one helper and one storage — zero drift between the two entries.
  • 能力中心内切 tab 不跳转(含扩展控制中心「能力中心」CTA):两个根因同修。其一,CapabilityCenterPage 的 tab 是 mount 时读一次的 initialTab——人在能力中心时经侧边栏悬停菜单点另一个 tab 只更新了 state、组件不重挂载,永不跳转;改为受控(tab + onTabChange,app.tsx 单一 state),外部入口(悬停菜单、omp-open-capability 事件、CTA)在页面已挂载时也能切。其二,设置页叠在能力中心之上时(openSettings 保持底层视图挂载,capabilityOpen 仍为 true)点 CTA:viewSwap 的 from===to 早退分支在关闭设置之前 return,设置层一直盖着目标视图——早退分支与常规路径统一改走 closeSettings()(含 150ms 液态玻璃 blur-out),关闭设置本身就是跳转。

    • EN: Switching tabs inside the capability center did nothing (including the "Capability center" CTA in the extension control center); two root causes fixed together. First, the page's tab was an initialTab read once at mount — clicking another tab in the sidebar hover menu while already in the center only updated state and never re-mounted, so no jump; it is now controlled (tab + onTabChange, single state in app.tsx) so external entries (hover menu, omp-open-capability event, CTA) can switch tabs while the page stays mounted. Second, when settings was open on top of the capability center (openSettings keeps the underneath view mounted, so capabilityOpen was still true), clicking the CTA hit viewSwap's from===to early return, which returned BEFORE closing settings — the settings layer kept covering the target view. Both the early-return branch and the normal path now go through closeSettings() (with the 150ms liquid-glass blur-out): closing settings IS the jump.
  • 组件开关报「Unknown method: extensions.setComponentEnabled」:根因是 ExtensionService 路由表登记了 extensions.setConfig / extensions.setComponentEnabled,但 server.ts 巨型 switch 漏挂对应 case,调用永远落到 default 抛错。补齐两个 case;并把门禁补上反向断言——route-coverage 测试此前只查「switch ⊆ 路由表」,新增「路由表 ⊆ switch」方向(服务认领的每条路由必须能被客户端真实调到),变异验证:删掉任一新 case 测试即红。

    • EN: Toggling a plugin component failed with "Unknown method: extensions.setComponentEnabled": the root cause was that ExtensionService's route table registered extensions.setConfig / extensions.setComponentEnabled while the server.ts mega-switch lacked the matching cases, so calls always fell through to the default throw. Both cases added; the gate gains the reverse assertion — route-coverage previously only checked "switch ⊆ route table", now also "route table ⊆ switch" (every service-claimed route must be actually callable by the client), mutation-verified: deleting either new case turns the test red.
  • i18n 重复键不再白屏(fail-safe 化,根除此前两次事故的失效模式):client-core 双语 barrel 的模块加载守卫从「跨域重复 key 直接 throw(一个重复文案键 → 整个 GUI 白屏,已发生两次)」改为容错检测——冲突记入 zhCNDuplicates/enUSDuplicates report 并打 [musepi-i18n] duplicate key: 前缀的 console.error(可检索、不崩溃),词表合并语义不变。把关前移到测试必经路径:i18n parity 断言(跨域零重复 + 域文件数/键总数防缩小守卫 + 合成碰撞用例钉死「不抛错且记录冲突双方」)进 bun test,CI 与包级 check 都无法绕过;参照 dsh locale-dictionary-parity.spec.ts 的「门禁必须长在必经路径 + 防缩小守卫」设计。本次事故键 plugin details(guest 域已持有)从 tools 域移除。

    • EN: Duplicate i18n keys no longer white-screen the GUI (fail-safe, rooting out the failure mode behind two past incidents): the client-core bilingual barrel's module-load guard changes from throwing on a cross-domain duplicate key (one duplicated copy key → whole GUI white screen, twice already) to tolerant detection — collisions are recorded in the zhCNDuplicates/enUSDuplicates reports with a greppable [musepi-i18n] duplicate key: console.error (no crash), merge semantics unchanged. The gate moves onto the mandatory test path: i18n parity assertions (zero cross-domain duplicates + domain-count/key-count anti-narrowing guards + a synthetic-collision case pinning "no throw, both domains recorded") join bun test, unbypassable from CI or package-level checks; design mirrors dsh's locale-dictionary-parity.spec.ts ("gates must live on the mandatory path + anti-narrowing guards"). This incident's key plugin details (already owned by the guest domain) is removed from the tools domain.

Changed

  • 插件列表整卡可点 + 全局文字选中范围治理(dsh parity 两条):① 插件列表条目从「仅名称行可点」改为整条卡片可点——onClick/键盘激活移到卡片容器(role=button + tabIndex + focus-visible 描边),图标/描述/徽章行随之整体可点,行内开关 stopPropagation 不受影响;② 文字选中范围按 dsh 口径治理:gui-base.css 在 html/body 层设 user-select: none(拖过导航/标签页/设置标签/插件卡片不再误起选区、吞点击),输入框、textarea、contenteditable、pre/code 与聊天 Markdown 正文(.tr-md)恢复可选——AI 回复与用户消息保持可复制,xterm 画布自绘选区不受影响。全部窗口(主窗/气泡/桌宠/图钉/托盘菜单)共享 gui.css 单点生效。
    • EN: Two dsh-parity interaction polish items. ① Plugin list rows go from "only the name line is clickable" to whole-card clickable — onClick/keyboard activation moves to the card container (role=button + tabIndex + focus-visible outline), so the icon/description/badge area is all one hit target, while the in-row toggle's stopPropagation is unaffected. ② Selection scope governed to the dsh convention: gui-base.css sets user-select: none at the html/body layer (dragging across nav, tabs, settings labels, or plugin cards no longer starts an accidental selection that swallows clicks), while inputs, textareas, contenteditable surfaces, pre/code, and the chat markdown body (.tr-md) stay selectable — AI replies and user messages remain copyable, and xterm's canvas-drawn selection is unaffected. Applies to every window (main / bubbles / pet / pin / tray menu) through the single gui.css entry.

Added

  • 顾问会话标识与只读保护:__advisor[.<slug>].jsonl transcript 行在 session.list/session.tree 透出 advisor 标记(daemon 打标 + GUI 透传链),侧栏行尾渲染 sparkling 徽章(tooltip「顾问会话」,与子代理徽章同级);打开顾问 transcript 时输入区整体替换为液态玻璃只读说明条(「顾问记录为只读,不能在顾问会话中发送消息」,zh/en),防止向顾问内部记录写入消息。新增 i18n 键两对(zh/en 同步)。

    • EN: Advisor session identity and read-only protection. __advisor[.<slug>].jsonl transcript rows expose an advisor flag through session.list/session.tree (daemon tagging + GUI passthrough chain); the sidebar renders a sparkling badge at row end (tooltip "Advisor session", sibling of the subagent badge); opening the advisor transcript replaces the input area with a liquid-glass read-only notice bar ("Advisor transcript is read-only — you can't send messages here", zh/en), preventing writes into the advisor's internal record. Two new i18n key pairs (zh/en in sync).
  • 会话右键「在迷你对话中打开」(openchamber mini-chat parity):会话树任意行(含子会话)的右键菜单新增「在迷你对话中打开」——Electron 侧经 mini-chat-open IPC 把会话 id 深链进迷你窗(?mini=1&session=<id>,已开的迷你窗带新目标重载并聚焦);mini 渲染端连接建立后一次性消费深链 id 打开目标会话(重连不会重复抢占);非 Electron 环境(浏览器开发)回退为原地打开。托盘/顶栏原有的迷你窗入口不受影响(无参即欢迎态)。

    • EN: "Open in mini chat" in the session context menu (openchamber mini-chat parity). Any tree row (including sub-sessions) gains an "Open in mini chat" item — on Electron the mini-chat-open IPC deep-links the session id into the mini window (?mini=1&session=<id>; an already-open mini window reloads with the new target and focuses); the mini renderer consumes the deep-link id once after the socket is up (a reconnect never re-steals it); non-Electron hosts (browser dev) fall back to opening in place. The existing tray/header mini-window entries are unaffected (no arg = welcome state).
  • 子会话树缩进层级修正(openchamber 文件夹层级语义):侧栏树此前沿用 TUI 紧凑规则——单子链保持父级缩进,一个会话只有一个子代理时子行与父行平级看不出层级;改为子级一律缩进一级(flattenTree 的紧凑规则保留为可选项,TUI 语义不受影响),单子代理的子 transcript 也以 └─ 连接线呈现为嵌套子行。

    • EN: Sub-session tree indent fix (openchamber folder-hierarchy semantics). The sidebar tree previously reused the TUI compact rule — single-child chains kept the parent's indent, so a session with exactly one subagent rendered the child flush with its parent. Children now always indent one level (the compact rule remains an opt-in; TUI semantics unaffected), and a single-subagent child transcript renders as a nested row with the └─ connector.
  • 子会话内嵌预览(openchamber mini-chat 形态的右侧面板落地):agents hub「子会话」分区的每行新增悬停出现的「在面板中预览」动作(picture-in-picture 图标)——点击后该子代理的完整会话就地停靠在右侧面板上层:实时 transcript 轮询(agents.transcript 增量游标)、进度统计(tokens/上下文/费用/工具数/耗时)、kill/revive 生命周期操作与 steer 聊天输入全部可用,无需离开当前主会话;面板头部另加「在主视图打开」升级按钮(external-link 图标)一键把子会话切为主聊天面(与原行点击同等目标,openchamber 返回父级的对偶路径)。行数据补齐 daemon session.agents 下发的 agentId(此前被丢弃),内嵌快照由注册表行合成,覆盖已退出 live roster 的子代理——名册里没有的会话同样可预览。行结构从「button 套 button」改为 div 容器 + 两个独立按钮(合法 HTML),新增 .gui-subsession-open/.gui-subsession-dock 样式(悬停显现、触屏常显、focus-visible 可达)。

    • EN: Embedded sub-session preview (the openchamber mini-chat form, landed in the right panel). Each row in the agents hub "sub sessions" section gains a hover-revealed "preview in panel" action (picture-in-picture icon) — clicking docks that subagent's full conversation over the right pane: live transcript polling (agents.transcript incremental cursor), progress stats (tokens/context/cost/tool count/duration), kill/revive lifecycle actions, and the steer chat input are all available without leaving the current main session. The panel header gains an "open in main view" upgrade button (external-link icon) that switches the sub-session to the main chat surface in one click (the same target as the row click — the dual path of openchamber's back-to-parent). Rows now keep the agentId the daemon's session.agents sends (previously dropped); the embedded snapshot is synthesized from the registry row, covering subagents that have already left the live roster. The row structure changes from "button inside button" to a div container with two independent buttons (valid HTML); new .gui-subsession-open/.gui-subsession-dock styles (hover reveal, always visible on touch, focus-visible reachable).
  • 终端 backend 组件化(musepi:terminal 插件的「包含的组件」面):terminal 内置插件新增 bun-pty / node-pty 两个 provider 组件——组件开关走新增的 terminal-backend deny 通道写隐藏键 terminal.disabledBackends(settings-schema 登记,fail-soft 读取),消费侧在 terminal-provider 解析链真实生效:auto 回退顺序剔除被禁后端(没尝试、不失败回退),显式选中(manifest/settings)被禁后端给结构化 DISABLED_BACKEND(用户显式意图不静默回退),全禁 = NO_BACKEND。auto 保持为解析策略配置值(terminal.provider 字段),不是后端单元,不虚构组件语义。extensions.list 组件面如实下发 enabled/canToggle;契约测试 8 例(写名单不落错键、list 下发、重启用、未声明组件结构化拒绝——含把 auto 当组件拒绝、auto 剔除计数断言、全禁 NO_BACKEND、显式 DISABLED_BACKEND)。

    • EN: Terminal backend components (the "included components" surface of the musepi:terminal plugin). The terminal builtin plugin gains bun-pty / node-pty as provider components — toggles go through the new terminal-backend deny channel into the hidden terminal.disabledBackends key (registered in settings-schema, fail-soft reads), and the consumption side takes effect in the terminal-provider resolution chain for real: auto skips disabled backends (never attempted, not fail-then-fallback), an explicit selection (manifest/settings) of a disabled backend yields a structured DISABLED_BACKEND (explicit user intent never silently falls back), and disabling both yields NO_BACKEND. auto stays a resolution-strategy config value (the terminal.provider field), not a backend unit — no invented component semantics. extensions.list reports the component plane truthfully (enabled/canToggle); 8 contract tests (denylist written to the right key, list plane, re-enable, structured rejection of undeclared components — including rejecting "auto" as a component, auto-skip call-count assertion, all-disabled NO_BACKEND, explicit DISABLED_BACKEND).
  • 子代理会话层级交互补全(openchamber parity 三件套):① 侧栏子会话树可折叠——父行状态方块前渲染 chevron(有子级可点、无子级占位对齐),收起态整棵子树剔除后展平,折叠集合持久化到 localStorage(musepi-gui-session-collapse),搜索时强制展开避免命中行被藏;② 子会话视图左上角新增「父级」液态玻璃返回按钮——数据源为侧栏树中选中节点的父节点(fork 与子代理会话均可返回,app.tsx activeParentSession 一次遍历派生),与右上 Chat/Canvas 浮动条同风格顶置,点击回切父会话;③ 右侧面板 agents 页新增「子会话」分区——经 session.agents 注册表扫描列出子 transcript 行(状态点复用 ag-dot 词表 + 名称 + 相对时间,5s 轮询,fail-open 保留旧行),点击行把整个主视图切到该子会话(与侧栏子行同目标)。新增 i18n 键三个(收起/展开子会话、父级、子会话、打开子代理会话,zh/en 同步)。

    • EN: Subagent session hierarchy interactions completed (openchamber parity, three pieces). ① The sidebar sub-session tree folds: the parent row gets a chevron before its status square (clickable when it has children, a placeholder keeps alignment otherwise), the collapsed subtree is pruned before flattening, the collapsed set persists to localStorage (musepi-gui-session-collapse), and search force-expands so matched rows are never hidden. ② A liquid-glass "Parent" back chip sits at the transcript's top-left in a child session view — the data source is the selected node's parent in the sidebar tree (both fork and subagent sessions can return, derived in one traversal as activeParentSession in app.tsx), styled like the top-right Chat/Canvas floating strip, and clicking it switches back to the parent session. ③ The right panel's agents page gains a "Sub-sessions" section — it lists child transcripts via the session.agents registry scan (status dot reusing the ag-dot vocabulary + name + relative time, 5s polling, fail-open keeps stale rows), and clicking a row switches the whole main view to that sub-session (same target as the sidebar child rows). Three new i18n keys (collapse/expand sub-sessions, parent, sub-sessions, open subagent session), zh/en in sync.
  • 内置设计插件包组织设计稿(dsh bundle 解剖 → MusePi 包清单):docs/review/0.5.0-m2.11-builtin-plugin-package-design.md——dsh 0.2.0-rc.2 真码核实包组织层(一 npm 包 = 一 cordis 插件四件套;base/headless/web-app/sdk-app/acp-app/sdk-minimal 六 bundle 按部署形态切层、cordis.patch.yml 整块覆盖 config、disabled 条件表达式、子插件 - insert: 嵌套、客户端同走 cordis runner),给出我方映射:musepi-base/gui/tui 三 bundle 分层、8 类内置 → 10 个插件包清单(含组件划分与收编序)、patch 行 YAML 方言、builtin-registry 退化为包清单投影,以及接续收编进度表的第三~六刀排期。

    • EN: Built-in design plugin-package organization design doc (dsh bundle anatomy → MusePi package list): docs/review/0.5.0-m2.11-builtin-plugin-package-design.md — dsh 0.2.0-rc.2 verified at the package-organization layer (one npm package = one cordis plugin four-piece set; six bundles base/headless/web-app/sdk-app/acp-app/sdk-minimal split by deployment form, cordis.patch.yml replacing the whole config, disabled condition expressions, nested - insert: sub-plugins, the client running the same cordis runner), then our mapping: three musepi-base/gui/tui bundle layers, the 8 builtin categories → 10 plugin packages (with component splits and adoption order), the patch-row YAML dialect, builtin-registry demoted to a projection of the package manifests, and cuts 3–6 scheduled against the existing adoption progress table.
  • 子代理 transcript 收编为层级子会话行(openchamber parity,名册 stopgap 移除):task/vibe 子代理的 transcript(<agentDir>/sessions/<slug>/<父文件名>/<subId>.jsonl 两层深)此前对侧栏完全不可见,只能经名册展开看一行活动 gist。本轮收编为真实会话行:daemon knownSessions 同 TTL 并列扫两层 glob,子 transcript 以 header id 为会话 id、从父文件名推导 parentId 挂进 session.tree——GUI 侧栏自动嵌套(缩进/连接线复用既有 fork 子树绘制),行尾渲染子代理标记(与 fork 分支标记同级,tooltip「子代理会话」);resolveResumableSession 全局 fallback 末段新增子代理扫描——GUI 侧栏点开子会话行经 subscribe→resume 链即可完整打开(此前永远 Unknown session);session.list 透出 subagent 标记,session.agents 行补 sessionId 字段。昨晚的名册 stopgap UI(行内 chevron + 内联名册 + app.tsx 的拉取/轮询/穿透)整组移除——子树行出现后两者重复;名册样式(.gui-session-agent-*/.gui-tree-agents*)同步删除;session.agents RPC 保留(AgentsPanel 未来可用)。契约测试 5 例(两层扫描命中、畸形文件丢弃、全局 fallback 命中子会话、显式 sessionDir + 关闭 fallback 时不可达、父会话仍走一层扫描)。

    • EN: Sub-agent transcripts are consolidated as hierarchical child session rows (openchamber parity, roster stopgap removed). Task/vibe sub-agent transcripts (two levels deep at <agentDir>/sessions/<slug>/<parentFileBase>/<subId>.jsonl) were previously invisible to the sidebar — only an inline roster could show a one-line activity gist. They are now real session rows: the daemon's knownSessions scans both glob depths under one TTL, uses the transcript header id as the session id, derives parentId from the parent filename, and feeds session.tree — the GUI sidebar nests automatically (indent/connectors reuse the existing fork-subtree drawing), and a sub-agent badge renders at row end (sibling of the fork branch badge, tooltip "Subagent session"). resolveResumableSession's global fallback gains a sub-agent scan segment, so clicking a child row in the sidebar opens it fully through the subscribe→resume chain (previously always "Unknown session"). session.list exposes the subagent flag; session.agents rows gain a sessionId field. Last night's roster stopgap UI (inline chevron + inline roster + app.tsx fetch/poll/passthrough) is removed wholesale — redundant once child rows exist; the roster styles (.gui-session-agent-*/.gui-tree-agents*) are deleted in sync; the session.agents RPC stays (future AgentsPanel). Five contract tests (two-level scan hit, malformed file dropped, global-fallback child hit, unreachable with explicit sessionDir + fallback closed, parent still served by the one-level scan).
  • cordis 收编第四刀:terminal-core builtin 插件单元(设计稿 §2 目标形态落地):终端能力的 backend 注册表收进插件 fiber——新增 daemon/terminal-core-plugin.ts:createTerminalCorePlugin()(apply provide terminal:backends,插件拥有的 TerminalRegistry 实例)+ createTerminalProviderPlugin("bun-pty" | "node-pty") 两个 provider 子单元(inject: ["terminal:backends"] 声明依赖——cordis 排序保证 provider 不先于 core apply,实测先挂 provider 再挂 core 时 fiber pending 至依赖就绪;apply 内 registerBackend 挂 ctx.effect 账本,fiber dispose = backend 反注册,禁用兜底归因(§3.③)的 NO_BACKEND 由此获得真实数据源)。server.ts 收编:删除宿主直调的 new TerminalRegistry() + registerBuiltinTerminalBackends(),改挂三个插件单元;宿主只保留 RPC 薄层 + terminal:deps 宿主能力(envelope seq / 连接 emit / settings——与 DaemonServer 共享编号空间是既有契约),terminal-host-wiring 缩为仅 provide deps。backend 实现工厂(createBunPtyBackend/createNodePtyBackend)从 terminal-provider.ts 导出、与遗留 registerBuiltinTerminalBackends helper 共用防漂移(helper 留给默认注册表消费者与测试)。RPC 名、provider 解析策略(manifest > settings > auto)、TerminalHandle 形状逐点不变;层隔离:同类型 provider 重复挂载 = 后挂者 fiber FAILED(DUPLICATE_BACKEND),core 与先挂者无损。契约测试 5 例(同源实例解析、双 provider 在册、dispose 反注册、DUPLICATE 层隔离、inject 依赖排序)。

    • EN: Cordis adoption, fourth cut: the terminal-core builtin plugin unit lands (design doc §2 target form). The terminal capability's backend registry moves into a plugin fiber — new daemon/terminal-core-plugin.ts: createTerminalCorePlugin() (apply provides terminal:backends, the plugin-owned TerminalRegistry instance) + two provider sub-units createTerminalProviderPlugin("bun-pty" | "node-pty") (inject: ["terminal:backends"] declares the dependency — cordis ordering guarantees a provider never applies before core, verified live: mounting the provider first pends the fiber until the dependency appears; apply registers the backend on the ctx.effect ledger, so fiber dispose unregisters it — the NO_BACKEND behind disable-fallback attribution (§3.③) gains a real data source). server.ts is consolidated: the host-direct new TerminalRegistry() + registerBuiltinTerminalBackends() calls are replaced by mounting the three plugin units; the host keeps only the RPC thin layer + the terminal:deps host capability (envelope seq / connection emit / settings — sharing the numbering space with DaemonServer is an existing contract), and terminal-host-wiring shrinks to providing deps only. The backend impl factories (createBunPtyBackend/createNodePtyBackend) are exported from terminal-provider.ts and shared with the legacy registerBuiltinTerminalBackends helper to prevent drift (the helper stays for default-registry consumers and tests). RPC names, the provider resolution strategy (manifest > settings > auto), and the TerminalHandle shape are unchanged point-by-point; layer isolation: mounting a duplicate provider of the same type FAILEDs the later fiber (DUPLICATE_BACKEND) without touching core or the earlier one. Five contract tests (same-instance resolution, both providers registered, dispose unregisters, DUPLICATE isolation, inject dependency ordering).
  • 能力中心 tab 面板常驻挂载(持久化 + 切入动效)+ 设置侧「插件」tab 摘除:技能/插件/市场三面板(含技能的发现/我安装子分段)常驻挂载、display 切换——切 tab 不再卸载重建,滚动位置、输入态、已拉取数据全部保留,切入面板淡入上浮 180ms(display none→block 重放 keyframes);插件清单改为进入能力中心即预取,切到插件 tab 列表已就绪。设置侧「插件」独立 tab 同步摘除(插件列表由一级能力中心统一承载,设置「扩展与插件」控制中心内部仍含插件泳道),plugins 从 SectionId 退役为能力别名、存量深链落到「扩展与插件」页。

    • EN: Capability-center tab panels stay mounted (persistence + enter animation) + the settings-side "Plugins" tab is removed. The skills / plugins / marketplace panels (including the skills discover/installed sub-panes) stay mounted and switch via display — switching tabs no longer unmounts and rebuilds, so scroll positions, input state, and fetched data are all preserved, and the entering panel fades in and floats up over 180ms (display none→block replays the keyframes); the plugin list is prefetched on entry to the center so it is ready when the plugins tab opens. The settings-side standalone "Plugins" tab is removed in sync (the plugin list is unified under the first-class capability center; the settings "Extensions & plugins" control center still contains plugin lanes), plugins retires from SectionId to a capability alias so stale deep links land on the "Extensions & plugins" page.
  • 能力中心摘遗产扩展 tab + 侧边栏 hub 改悬停菜单 + 自有 provider 品牌改名 MusePi Plugins:① 能力中心的「扩展」tab 移除——遗产 pi/omp 扩展(.pi/.omp extension,不在 .musepi 下)归设置侧「扩展与插件」控制中心管理,能力中心不再重复承载,tab 联合收窄为「技能 | 插件 | 市场」,ExtensionsRuntimeTab 组件删除、全仓无残留引用;② 侧边栏能力中心入口从「主按钮 + chevron 子按钮」改为悬停菜单——行悬停弹出三项(技能 / 插件 / 市场)跳转对应 tab,直接点击落到技能市场,180ms 延迟关闭兼容鼠标跨 portal 迁移;③ 自有插件 provider 品牌名从「MusePi Extensions」改为「MusePi Plugins」(provider id musepi-extensions 保持不变,避免存量数据失效;creator persona 与 musepi-help 技能文档同步;设置侧导航项「扩展」改「扩展与插件」)。

    • EN: Capability center drops the legacy extensions tab + sidebar hub becomes a hover menu + own provider rebranded to MusePi Plugins. ① The capability center's "Extensions" tab is removed — legacy pi/omp extensions (.pi/.omp extensions, not under .musepi) are managed by the Settings "Extensions & plugins" control center instead, and the capability center no longer duplicates them; the tab union narrows to "Skills | Plugins | Marketplace", ExtensionsRuntimeTab is deleted with no dangling references repo-wide. ② The sidebar capability entry changes from "main button + chevron sub-button" to a hover menu — hovering the row pops three items (Skills / Plugins / Marketplace) jumping to the matching tab, while a direct click lands on the marketplace tab, with a 180ms delayed close tolerating mouse migration across the portal. ③ The own-plugin provider brand renames from "MusePi Extensions" to "MusePi Plugins" (the provider id musepi-extensions stays unchanged so existing data keeps working; the creator persona and musepi-help skill doc update in sync; the settings nav item "Extensions" becomes "Extensions & plugins").
  • cordis 收编第三刀:终端 provider 注册表化(registerBackend + provider 插件化挂点,设计稿 §2):新增 daemon/terminal-registry.ts——TerminalRegistry(owner-scoped registerBackend,dsh registerBackend/DUPLICATE_BACKEND 形状 parity):同类型重复注册 = 结构化 DUPLICATE_BACKEND(不允许覆盖式静默顶替);反注册句柄 dispose 只在该注册仍是当前持有者时移除(被顶替的旧注册 dispose 不误删新 backend);getBackend 未注册 = 结构化 NO_BACKEND——「终端后端已停用,原因:插件被禁用」归因(回退保护③)的真实数据源。terminal-provider.ts 静态 PROVIDERS 表改注册表驱动:registerBuiltinTerminalBackends(幂等,builtin bun-pty/node-pty 注册进给定注册表)+ getTerminalProvider(name, registry)——显式 provider 的 open 直查注册表,backend 缺席 = NO_BACKEND(manifest 显式声明严格语义,不静默回退);auto 只在在册 backend 间按 bun-pty → node-pty 回退、跳过缺席者,全缺席 = NO_BACKEND。provider 解析策略(manifest > settings > auto)与 RPC 名、isTerminalProvider 取值域(含 "auto")、TerminalHandle 形状逐点不变。TerminalService 增 opts.registry 注入点(缺省回落共享注册表);server.ts 构造独立注册表、注册 builtin 后端,经 terminal-host-wiring 插件把 terminal:backends / terminal:deps 两键 provide 进 cordis 根 Context(terminal-core 插件化时的 inject 落点,strangler-fig 薄适配层),同一实例注入服务。契约测试 9 例(DUPLICATE/NO_BACKEND 结构化、反注册交错安全、builtin 注册幂等、显式缺席不静默回退、auto 在册回退与全缺席)。模型面终端工具核对结论:BUILTIN_TOOLS 无终端工具可挪(bash-interactive 是 bash 工具的 TUI 辅助),tool-terminal 插件(spawn/send/read/list/signal)留待 terminal-core 插件化刀。

    • EN: Cordis adoption, third cut: terminal provider registry-ized (registerBackend + provider pluginization hook, design doc §2). New daemon/terminal-registry.ts — TerminalRegistry (owner-scoped registerBackend, dsh registerBackend/DUPLICATE_BACKEND shape parity): re-registering the same type is a structured DUPLICATE_BACKEND (no silent override); the unregister handle's dispose removes only while that registration is still the current holder (a superseded registration's dispose never deletes the new backend); an unregistered getBackend is a structured NO_BACKEND — the real data source for the "terminal backend stopped: plugin X disabled" attribution (fallback protection ③). terminal-provider.ts's static PROVIDERS map is now registry-driven: registerBuiltinTerminalBackends (idempotent — registers the builtin bun-pty/node-pty into a given registry) + getTerminalProvider(name, registry) — an explicit provider's open queries the registry directly, a missing backend is NO_BACKEND (strict manifest semantics, no silent fallback); auto degrades only among registered backends in bun-pty → node-pty order, skipping absent ones, and is NO_BACKEND when all are absent. Provider resolution (manifest > settings > auto), RPC names, isTerminalProvider's value domain (including "auto"), and the TerminalHandle shape are unchanged point-by-point. TerminalService gains an opts.registry injection point (falling back to the shared registry); server.ts constructs a dedicated registry, registers the builtin backends, provides the terminal:backends / terminal:deps keys into the cordis root Context via a terminal-host-wiring plugin (the inject landing point for the future terminal-core pluginization, strangler-fig thin adapter), and injects the same instance into the service. Nine contract tests (structured DUPLICATE/NO_BACKEND, unregister interleaving safety, idempotent builtin registration, explicit-missing no-silent-fallback, auto fallback among registered and all-absent). Model-facing terminal tool audit: BUILTIN_TOOLS has no terminal tools to move (bash-interactive is a TUI helper of the bash tool); the tool-terminal plugin (spawn/send/read/list/signal) waits for the terminal-core pluginization cut.
  • 创造模式自举工具环自动激活(creator 预设 settings 轴 + tools.extensionMetaTools)+ 扩展开发技能补 cordis 运行时:回答「创造模式工具集是否完备」的落地缺口——自举工具本就有 11 个(extension_load/reload/status/validate/rollback 生命周期五件套 + ext_define/run/stop/undefine/inspect 会话级动态插件,加常激活的 extensions_list;实现 daemon/extension-lifecycle-tools.ts / daemon/extension-runtime-tools.ts),但此前激活唯一入口是 /extensions 斜杠命令,creator 预设不自激活,且实证发现 defaultInactive custom 工具初始只挂为 xd:// 设备而非顶层工具。本轮:① 新隐藏设置键 tools.extensionMetaTools(settings-schema,默认 false);② SDK 会话初始工具集组装消费该键——开启时 10 个 meta 工具从会话起即顶层激活(并钉住不被 xd:// 分区重新挂载),显式 toolNames 会话不扩列;③ creator 内置预设模板 settings: { "tools.extensionMetaTools": true } + persona 指向自举闭环(写文件 → 加载 → 自查 → 热修 → 回退),BUILTIN_TEMPLATE_REVISION 4→5,LEGACY_TEMPLATES 补 rev4 creator 字面量快照(未动过的旧 creator 预设安全升级链);④ musepi-extension-dev 技能文档新增「自举工具环」「Cordis 插件运行时(组件/子 fiber/兼容性预检 + agentDir compatibility.json 精确版本豁免)」两节,并把「改扩展必须重启 daemon」修正为 HMR 热拔插现实(入口文件 ~500ms watcher 广播、GUI ~1s 刷新、reloadExtension 热载;子模块改动需 touch 入口;只有动发现/加载机制本身才重启)。契约测试 4 例(默认 xd:// 设备态、设置开启顶层激活且不进 xd 分区、显式 false 不激活、显式 toolNames 不扩列)。

    • EN: Creator preset auto-activates the extension bootstrap tool ring (creator preset settings axis + tools.extensionMetaTools) + the extension-dev skill gains the cordis runtime section. This lands the gap behind "is the creator mode toolset complete": the 11 bootstrap tools already existed (extension_load/reload/status/validate/rollback lifecycle quintet + ext_define/run/stop/undefine/inspect session-scoped dynamic plugins, plus the always-active extensions_list; implemented in daemon/extension-lifecycle-tools.ts / daemon/extension-runtime-tools.ts), but the only activation entry was the /extensions slash command, the creator preset did not self-activate, and empirically a defaultInactive custom tool initially mounts only as an xd:// device, not a top-level tool. This round: ① new hidden settings key tools.extensionMetaTools (settings-schema, default false); ② the SDK session-start tool-set assembly consumes it — when on, the 10 meta tools are top-level active from session start (pinned so the xd:// partition cannot remount them), and explicit-toolNames sessions are not widened; ③ the builtin creator preset template gains settings: { "tools.extensionMetaTools": true } + a persona pointing at the bootstrap loop (write file → load → self-check → hotfix → rollback), BUILTIN_TEMPLATE_REVISION bumps 4→5 with a rev4 creator literal snapshot in LEGACY_TEMPLATES (untouched old creator presets upgrade safely); ④ the musepi-extension-dev skill doc gains "Bootstrap tool ring" and "Cordis plugin runtime (components / child fibers / compatibility preflight + agentDir compatibility.json exact-version exemption)" sections, and the "extension changes require a daemon restart" claim is corrected to the HMR hot-plug reality (entry-file changes: ~500ms watcher broadcast, ~1s GUI refresh, reloadExtension hot-load; submodule edits need the entry touched; only changes to the discovery/loading mechanism itself need a restart). Four contract tests (default xd:// device state, setting-on top-level activation without xd partitioning, explicit false stays off, explicit toolNames not widened).
  • 插件兼容性预检 + 精确版本豁免(回退保护②,dsh plugin-compatibility parity):插件装载前三入口统一预检——宿主 runtime load/loadAll、会话装配 loadExtensions、marketplace installPlugin——读 package.json peerDependencies,@musepi/* peer 对照运行时 VERSION 判定(平台单版本线语义;workspace:^/~/* 按运行时版本解析;非法/空区间 = 结构化拒绝——Bun 1.4.2 实测 Bun.semver.satisfies 对非法区间与裸 ^ 都返回 true,判定带 order 语法探针 + 数字字面量双重防护;预发布运行时按 base 版本复检,等价 includePrerelease 意图)。拒绝 = 结构化 DynamicExtensionLoadError(新 code incompatible-version/incompatible-peer/malformed-manifest)+ failed 记录 + 会话装配侧 errors[] 带诊断且先于 import(拒绝路径零副作用);extensions.list 透出 version/compatibility 证据面。豁免登记面 = agentDir 下用户可编辑的 compatibility.json(dsh profile 本地 parity,命名 profile 时天然按 profile 隔离):精确 name@version → 被豁免运行时版本列表,授予必须 acceptRisk 显式知情且只能批当前运行时版本,原子写 0o600,读取 fail-safe(坏文件 = 零豁免不阻塞装配),含被拒记录时拒绝重写保护用户手工内容;新 RPC extensions.setVersionExemption(server.ts case + 路由表双向门禁覆盖)授予/撤销后失效清单缓存扇出 extensions.changed;list 对在役记录的 exempted 标注对照当前清单复核,撤销后管理面讲当下真话(降级 incompatible,不热卸载在役 fiber)。GUI 详情弹层新增兼容性归因段——incompatible 红块(未满足 peer 清单 + 运行时版本 + 授予入口,液态玻璃 confirm 知情确认)、exempted 琥珀块(可撤销),豁免键与判定同源用 manifest name@version。builtin 插件不做预检(随宿主同版本发布)。契约测试 25 例(纯判定/登记面 19 + 会话装配预检 2 + 宿主整链 4,含预发布复检、垃圾区间防护、撤销后标注降级、拒绝零副作用探针)。

    • EN: Plugin compatibility preflight + exact-version exemptions (rollback protection ②, dsh plugin-compatibility parity). All three load entry points — host runtime load/loadAll, session assembly loadExtensions, marketplace installPlugin — read package.json peerDependencies and check @musepi/* peers against the runtime VERSION (single-version-line platform semantics; workspace:^/~/* resolve to the runtime version; invalid/empty ranges are structured refusals — Bun 1.4.2's Bun.semver.satisfies returns true for garbage ranges and a bare ^, so the check layers an order() syntax probe plus a digit-literal guard; prerelease runtimes re-check against their base version, equivalent to includePrerelease intent). A refusal is a structured DynamicExtensionLoadError (new codes incompatible-version/incompatible-peer/malformed-manifest) + a failed record +, on the session-assembly side, an errors[] entry with diagnosis before any import (refused paths have zero side effects); extensions.list surfaces version/compatibility evidence. The exemption registry is a user-editable compatibility.json under agentDir (dsh profile-local parity; naturally per-profile under named profiles): exact name@version keys map to exempted runtime-version lists, granting requires explicit acceptRisk and only approves the current runtime version, writes are atomic at 0o600, reads are fail-safe (a broken file means zero exemptions and never blocks assembly), and files containing rejected records refuse rewriting to protect hand-edited content; the new extensions.setVersionExemption RPC (server.ts case + bidirectional route-coverage gate) invalidates the list cache and fans out extensions.changed after grant/revoke; the list re-checks in-service exempted annotations against the live registry so the management plane tells current truth after a revocation (downgraded to incompatible without hot-unloading the in-service fiber). The GUI detail dialog gains a compatibility attribution block — a red block for incompatible (unmet peer list + runtime version + grant entry behind a liquid-glass informed-confirm) and an amber block for exempted (revocable), with the exemption key sourced from the manifest name@version exactly as the gate computed it. Builtin plugins skip the preflight (shipped at the host version). 25 contract tests (19 pure/store + 2 session-assembly preflight + 4 host end-to-end, covering prerelease re-check, garbage-range guard, post-revoke annotation downgrade, and the refused-zero-side-effect probe).
  • 插件组件组织能力对齐 dsh(musepi.components 清单组件 = 独立装载子 fiber):user 插件可在 package.json musepi.components(omp/pi 块同读)声明组件 {id, description?, entry?}——entry 组件挂为插件 fiber 下的独立 cordis 子 fiber(与插件入口同一 importAndBind 管线,ledgerApi owner 名 <plugin>/<component>,跨组件命令碰撞 = 组件 FAILED 层隔离);entry 缺席的组件为只读展示项。extensions.setComponentEnabled 新增 user 插件径:校验声明 → 真实启停组件 fiber(停用 = dispose + 效果回收,启用 = 重挂)→ 成功才持久化隐藏键黑名单;运行时尚未 reconcile 时先装载再切换。extensions.list 组件面下发声明组件 + enabled + canToggle + 非 ACTIVE 时的 fiberState/effects 真实运行态;GUI 详情弹层组件行显示 fiber 状态行。组件命令宿主可调用(句柄 invoke = 主入口 ∪ 按声明序在役组件)。修一个真实生产 bug:黑名单键 extensions.disabledComponents 与 dotless extensions 数组键点号嵌套冲突,写一次即宿主扩展装载崩溃——改无点号键 disabledExtensionComponents。契约测试 +10(组件生命周期 6 + service 接线 4,含未声明组件拒绝不落盘、首次开关先装载再切换)。

    • EN: Plugin component organization aligned with dsh (musepi.components manifest components = independently loaded child fibers). User plugins can declare components in package.json musepi.components (also read from omp/pi blocks) as {id, description?, entry?} — entry components mount as independent cordis child fibers under the plugin fiber (same importAndBind pipeline as the plugin entry, ledgerApi owner named <plugin>/<component>, cross-component command collisions surface as component-level FAILED isolation); components without an entry are read-only display items. extensions.setComponentEnabled gains the user-plugin path: validate declaration → genuinely start/stop the component fiber (disable = dispose + effect reclamation, enable = remount) → persist the hidden-key denylist only on success; when the runtime hasn't reconciled yet, load first then toggle. extensions.list ships declared components + enabled + canToggle + real fiberState/effects when not ACTIVE; the GUI detail dialog shows a fiber-state line per component row. Component commands are host-callable (handle invoke = main entry ∪ in-service components in declaration order). Fixes a real production bug: the denylist key extensions.disabledComponents collided with the dotless extensions array key via dot-nesting, crashing host extension loading after a single write — renamed to the dotless disabledExtensionComponents. Ten new contract tests (6 component lifecycle + 4 service wiring, including undeclared-component rejection leaving no trace, first toggle loads then toggles).
  • cordis 收编第二刀:user 插件宿主级 fiber 化装载(真实 ExtensionAPI 同源 + register 效果账本):user 动态插件从「探测桩」升级为宿主级真实装载——每个 user 插件 = musepi-dynamic-extensions 组 fiber 下独立子 fiber,运行面与生产会话装载同源*(loader.ts 新增 createConcreteExtensionAPI 唯一构造工厂 + importAndBindExtension 同一条 import/bind 管线,bind 捕获 factory 抛错经新 DynamicExtensionLoadError 透传结构化 code);26 个 register*/on 登记动词经 ledgerApi(Proxy 壳)逐条进 ctx.effect 效果账本(标签如 registerCommand:xxx),unload/reload = fiber.dispose() 按 verb 撤销表反向回收(命令摘除、事件退订、provider 排队注册注销,其他插件无损);跨插件命令名碰撞守卫落地为 FAILED fiber(不留半挂载);loadAll 对账 reconcile(新路径装载、消失路径卸载)返回与 loadExtensions 同形状结果,extension-service.getExtensionRuntimeLoad 改走 fiber 运行时(server.ts 注入,动态 import 避免 index 大环),失败 fail-soft 回退直接装载。HMR 沿用 loader 的单调 ?mtime= 标签口径(spike 证伪裸查询串方案),reload 拾取入口与子模块改写实测通过。契约测试重写 10 例 + 接线测试 3 例(TTL 重入不重复登记、失效 reconcile 效果回收)。

    • EN: Cordis adoption, second cut: user plugins load as host-level fibers with a real ExtensionAPI from the same source + a register* effect ledger. User dynamic plugins upgrade from probe stubs to real host-level loading — each user plugin is an independent child fiber under the musepi-dynamic-extensions group, sharing the exact runtime surface with production session loading (loader.ts gains createConcreteExtensionAPI as the single construction factory + importAndBindExtension on the same import/bind pipeline, with factory throws surfacing a structured code via the new DynamicExtensionLoadError); all 26 register*/on verbs pass through a ledgerApi (Proxy shell) into per-entry ctx.effect ledger entries (labels like registerCommand:xxx), and unload/reload = fiber.dispose() reversing each verb's undo table (commands removed, events unsubscribed, queued provider registrations cancelled, other plugins unaffected); cross-plugin command-name collisions surface as a FAILED fiber (no half-mounted state); loadAll reconciles the discovery manifest against in-service records (new paths loaded, vanished paths unloaded) returning the same shape as loadExtensions, and extension-service.getExtensionRuntimeLoad switches to the fiber runtime (injected from server.ts via dynamic import to avoid the index cycle), with fail-soft fallback to direct loading. HMR reuses the loader's monotonic ?mtime= tag mechanism (the spike disproved raw query-string busting), with reload picking up entry and submodule edits verified live. Contract suite rewritten to 10 cases + 3 wiring cases (no duplicate registration on TTL re-entry, effect reclamation on invalidation reconcile).
  • 侧边栏能力入口合并为 WorkBuddy 式单按钮枢纽:能力中心与扩展中心两个并列导航项合并为一行——主点击进能力中心,尾部 chevron 展开下拉菜单直达「技能 / 插件 / 扩展 / 市场」四个标签页(复用全局玻璃菜单配方,下拉项走 gui-view-opt 行样式);omp-open-capability 命令接受目标 tab 参数。能力中心内新增「扩展」标签页(ExtensionsRuntimeTab):列出用户/项目级 runtime 扩展模块,复用插件详情弹层与启停开关。新增 4 个双语词条(capability hub open / extensions runtime 等)。

    • EN: Sidebar capability entries merge into a WorkBuddy-style single-button hub: the side-by-side CapabilityCenter and ExtensionsCenter nav items become one row — the main click opens CapabilityCenter, a trailing chevron drops a menu with direct jumps to the "Skills / Plugins / Extensions / Marketplace" tabs (reusing the global glass menu recipe, items rendered as gui-view-opt rows); the omp-open-capability command accepts a target tab parameter. CapabilityCenter gains an "Extensions" tab (ExtensionsRuntimeTab) listing user/project-level runtime extension modules, reusing the plugin detail dialog and enable/disable toggle. Four new bilingual entries (capability hub open / extensions runtime, etc.).
  • 会话列表行内展开子代理名册(openchamber parity,session.agents RPC):session.list 逐行返回 agentCount(live 会话取运行中句柄的 sessionFile,历史会话回退列表项路径),AgentRegistry 按产物根目录前缀过滤(排除 advisor,与 registerPersistedSubagents 的注册目录约定同源);新 RPC session.agents 返回名册(id/显示名/kind/状态/最近活动)。GUI 侧会话行 agentCount>0 时出现 chevron(图标+数量),展开内联名册(状态点+名称+活动摘要),点击行进所属会话;custom group、分组列表、noFolder 全部分支均已接线,5s 轮询同步名册刷新。路由覆盖门禁同步登记(route-coverage 反向断言已含)。

    • EN: Session rows expand an inline sub-agent roster (openchamber parity, session.agents RPC): session.list returns per-row agentCount (live sessions read the running handle's sessionFile, historical ones fall back to the list entry's path), with AgentRegistry filtered by artifact-root prefix (advisors excluded, same directory convention as registerPersistedSubagents); the new session.agents RPC returns the roster (id / display name / kind / status / last activity). On the GUI side, rows with agentCount>0 show a chevron (icon + count), expanding an inline roster (status dot + name + activity summary), and clicking an entry enters the owning session; all branches — custom groups, grouped lists, noFolder — are wired, and the 5s poll syncs roster refreshes. The route-coverage gate registers the route (already covered by the reverse assertion).
  • 移除智能体中心页面:侧边栏「智能体中心」导航项、CommandPalette 入口、app.tsx 的 agentsOpen 视图状态与相关 props 全摘——活跃子代理管理由会话内 SubagentPanel 与右栏承担,名册查看由会话行展开承担;AgentsCenterPage.tsx 删除。会话上下文面板的 SubagentPanel 不受影响。

    • EN: Agents center page removed: the sidebar "Agents Center" nav item, CommandPalette entry, app.tsx agentsOpen view state and related props are all gone — active sub-agent management lives in the in-chat SubagentPanel and right rail, roster viewing in session-row expansion; AgentsCenterPage.tsx is deleted. The SubagentPanel used by the session context panel is unaffected.
  • cordis 收编第一刀:builtin 插件单元真实 fiber 化 + 插件管理「运行状态 / 启用条件」落地(dsh fiberPhase / condition parity):① daemon 启动时把 builtin 注册表全部非 annotate 单元挂为 cordis builtin 插件组(musepi-builtin-plugins)下的独立子 fiber——与 user 动态插件同一装配语法、不同信任级(设计稿 §1 第 3 步);每个 fiber apply 提供 builtin:<id> 键(builtin 宿主级 inject 落点),ctx.provide 进效果账本(dsh「everything is an effect」parity),单单元装载失败层隔离(回退保护①)记 FAILED 归因不拖垮组。② extensions.list 新增真实 runtime 面:fiberState(cordis fiber 状态机)+ effects(效果账本计数)+ operational(镜像设置现读的业务运行判定——stt.enabled=false 时语音确实没在跑,与 fiber ACTIVE 两层如实分开,比 dsh fiberPhase 更贴我方架构);builtinRuntime 宿主依赖注入,缺省 {} fail-soft。③ 启用条件(activation)落地为真实可判定语义:镜像设置键(stt.enabled 开启时启用),预设提供的 user 插件走 enabledInPresets——不编造 cordis 表达式字符串。④ 插件详情弹窗 CardFacts 加「运行状态」(液态玻璃质感圆点 + 运行中/已停用 + 非 ACTIVE 时附 fiber 状态名)与「启用条件」两行,仅当有真实数据时渲染;user 插件会话装载、宿主无 fiber,如实不显示。契约测试 +7(builtin 组挂载/检视/provide 解析/dispose 清空/局部 defs + runtime/activation 三态)。

    • EN: Cordis adoption, first cut: builtin plugin units get real fibers + plugin management lands "runtime status / activation condition" (dsh fiberPhase / condition parity). ① At daemon startup every non-annotate builtin registry unit mounts as an independent child fiber under the cordis builtin plugin group (musepi-builtin-plugins) — same assembly syntax as user dynamic plugins, different trust level (design doc §1 step 3); each fiber's apply provides the builtin:<id> key (the host-level inject landing point), ctx.provide lands on the effect ledger (dsh "everything is an effect" parity), and a single unit's mount failure is layer-isolated (fallback protection ①) into a FAILED attribution without dragging down the group. ② extensions.list gains a real runtime plane: fiberState (cordis fiber state machine) + effects (effect ledger count) + operational (a live read of the mirrored setting — with stt.enabled=false voice genuinely isn't running, kept honestly separate from the ACTIVE fiber, closer to our architecture than a raw fiberPhase); builtinRuntime is a host-injected dependency, defaulting to {} fail-soft. ③ Activation conditions land as genuinely decidable semantics: the mirrored settings key ("enabled while stt.enabled is on"), with preset-provided user plugins covered by enabledInPresets — no fabricated cordis expression strings. ④ The plugin detail dialog's CardFacts gains "Runtime status" (liquid-glass dot + Running/Stopped, plus the fiber state name when not ACTIVE) and "Activation condition" rows, rendered only when real data exists; user plugins load per session with no host fiber and honestly show neither. Seven new contract tests (builtin group mount/inspect/provide resolution/dispose clearing/partial defs + three runtime/activation states).
  • 语音设置页插件状态驱动 + dsh「会话插件」启用面(extensions.list 预设轴)+ 插件详情 CardFacts 与面板搜索:① 语音设置页由插件状态驱动——voice:stt / voice:tts 被停用后,对应 schema 行、模型选择卡、测试入口整段隐藏并替换为「插件已停用」说明卡(一键启用回环写 extensions.setEnabled),fail-open:列表不可用时按启用渲染。② extensions.list 新增 dsh「会话插件」轴——解析每个 Agent 预设,只有显式扩展白名单(extensionsExplicit 三态语义,undefined = 全部启用不构成「按预设提供」)的预设才算提供者:返回顶层预设清单(内置预设名走 daemon i18n 回退检测)+ 每个 extension-module 条目的 enabledInPresets「启用于」标签;modesDir 改为宿主依赖注入(缺省回退 $MUSEPI_MODES_DIR / ~/.musepi/modes,测试可隔离),fail-soft 不阻塞清单主数据。③ 插件详情弹窗(PluginDetailDialog)状态段后新增 dsh CardFacts 式 <dl> 信息行——完整名称(code id)/配置状态(含「由 Agent 预设按会话提供」)/启用于(预设标签 join)。④ 插件面板(UnifiedPluginsView)工具栏加搜索框(名称/id/描述大小写不敏感过滤 + 无结果提示),模块行/卡片挂预设提供者标签。契约测试 +2(白名单命中挂标签、未命中/无显式声明不进清单)。

    • EN: Voice settings driven by plugin state + dsh "session plugins" enablement plane (extensions.list preset axis) + plugin detail CardFacts and panel search. ① The voice settings page is plugin-state-driven — with voice:stt / voice:tts disabled, the matching schema rows, model picker cards, and test entries collapse into a "plugin disabled" note card (one-click enable loops through extensions.setEnabled); fail-open: the page renders as enabled when the list is unavailable. ② extensions.list gains the dsh "session plugins" axis — each Agent preset is resolved, and only presets with an explicit extension whitelist count as providers (extensionsExplicit tri-state semantics: undefined = everything enabled, not "provided by preset"): the response gains a top-level preset list (builtin preset names resolved via daemon i18n with fallback detection) plus an enabledInPresets "enabled in" tag on each extension-module entry; modesDir becomes a host-injected dependency (falling back to $MUSEPI_MODES_DIR / ~/.musepi/modes, isolatable in tests) and the plane is fail-soft, never blocking the main list. ③ The plugin detail dialog (PluginDetailDialog) gains a dsh CardFacts-style <dl> block after the status section — full name (code id) / configuration status (including "provided per session by an Agent preset") / enabled in (preset labels joined). ④ The plugins panel (UnifiedPluginsView) toolbar gains a search box (case-insensitive filter over name/id/description + empty-state hint), and module rows/cards show preset-provider tags. Two new contract tests (whitelist hit attaches the tag; misses / non-explicit presets stay out of the plane).
  • 插件详情弹层中文化 + 语音单元模型选择器卡片化 + 语音引擎组件真实启停 + 能力/扩展中心形态分工:① 详情弹层(PluginDetailDialog)整体中文化——wire 契约的 config 字段新增 label/optionLabels(plugin-config.ts fail-soft 解析),ConfigFormRenderer 经 tLoose 解析 label/description/optionLabels,builtin 覆盖表导出共享;voice:stt/tts 的模型与音色字段从通用表单剔除,内嵌与设置页同一 SpeechModelPicker 卡片组件(同一下载通道/存储,零漂移),设置页导航误写成「音色」修正为「语音」(runtime 注册键走 tLoose)。② 语音插件声明 dsh 式「包含的组件」:voice:stt 声明 whisper/sensevoice/parakeet 引擎组件、voice:tts 声明 kokoro/melotts-zh 组件,组件结构升级为 {id, deny: "tool"|"stt-engine"|"tts-engine"};新隐藏设置键 voice.disabledEngines + voice/engine-denylist.ts(sttEngineComponentId/ttsEngineComponentId/readDisabledVoiceEngines/isVoiceEngineDisabled/enabledModels/resolveEnabledModel),extension-service 双通道 list/setComponentEnabled,消费点全接通——stt/tts modelStatus 过滤目录+默认回退、modelDownload 拒绝、stt.transcribe、tts.synthesize、vocalizer 会话、stt-controller,被禁引擎从模型选择(设置页选择器同读此 RPC 自动联动)、下载、转写/合成路径同时消失。契约测试 14 例(含 voice 通道写 voice.disabledEngines 与 tools 正交、禁用引擎真实改变模型解析与回退链)。③ 能力中心保持 dsh 管理导向列表(行内组件计数徽标),扩展中心切发现导向卡片网格,设置页插件分区复用同一 ModuleCard——单一权威组件三处共用。

    • EN: Plugin detail dialog localization + voice-unit model picker cards + real voice-engine component toggles + capability/extensions center layout split. ① The detail dialog (PluginDetailDialog) is fully localized — the wire config contract gains label/optionLabels (fail-soft parsing in plugin-config.ts), ConfigFormRenderer resolves label/description/optionLabels via tLoose, and the builtin display-name table is exported for sharing; voice:stt/tts drop the generic form fields for model/voice and embed the same SpeechModelPicker card component as the settings page (same download channel/storage, zero drift), and the settings nav mislabeled "voice timbre" is corrected to "Voice" (runtime-registered keys go through tLoose). ② Voice plugins declare dsh-style "included components": voice:stt declares the whisper/sensevoice/parakeet engine components and voice:tts the kokoro/melotts-zh components, with the component shape upgraded to {id, deny: "tool"|"stt-engine"|"tts-engine"}; a new hidden settings key voice.disabledEngines plus voice/engine-denylist.ts (sttEngineComponentId/ttsEngineComponentId/readDisabledVoiceEngines/isVoiceEngineDisabled/enabledModels/resolveEnabledModel), dual-channel list/setComponentEnabled in extension-service, and all consumers wired — stt/tts modelStatus filters catalogs + default fallback, modelDownload rejects, stt.transcribe, tts.synthesize, vocalizer sessions, stt-controller: a disabled engine disappears from model selection (the settings-page picker reads the same RPC and syncs automatically), download, and transcription/synthesis paths simultaneously. Fourteen contract tests (including the voice channel writing voice.disabledEngines orthogonally to tools, and disabled engines genuinely changing model resolution and fallback chains). ③ CapabilityCenter keeps the dsh management-oriented list (inline component-count badge), ExtensionsCenter switches to a discovery-oriented card grid, and the settings plugins section reuses the same ModuleCard — one authoritative component shared by all three.
  • 插件管理 dsh 化三件套(列表布局 / 液态玻璃详情弹层 / 设置卡片分区):插件页(扩展中心与能力中心共用)改为 dsh 式布局——顶栏计数 + 「添加插件」按钮、官方/已安装分组标题、行内版本号与组件计数;点击行不再手风琴展开,而是弹出液态玻璃详情弹层(PluginDetailDialog,DialogFrame 承载:状态段/触发词/来源 + 配置表单 + 资源卡,弹层本身日后可插件化);设置页新增「插件」卡片分区(PluginsSection,两列卡片网格,点卡片开同款弹层),settings-nav 注册 plugins 分区(NAV_IDS 测试同步)。新增 10 个双语词条(ext plugins official/installed、ext add plugin、ext plugin components、ext component denied、ext plugin details、ext plugins settings desc 等),i18n 查重通过。

    • EN: Plugin management goes full dsh (list layout / liquid-glass detail dialog / settings card section). The Plugins tab (shared by ExtensionsCenter and CapabilityCenter) switches to the dsh layout — header count + "Add plugin" button, Official/Installed group headers, inline version and component counts; clicking a row no longer accordion-expands but opens a liquid-glass detail dialog (PluginDetailDialog, hosted in DialogFrame: status / triggers / source + config form + resource card; the dialog itself can be plugin-ized later); the settings page gains a "Plugins" card section (PluginsSection, two-column card grid, same dialog on card click) with a plugins section registered in settings-nav (NAV_IDS test updated). Ten new bilingual entries (ext plugins official/installed, ext add plugin, ext plugin components, ext component denied, ext plugin details, ext plugins settings desc, …), dup-key check green.
  • 插件「包含的组件」真实启停(tools.disabled 黑名单 + setComponentEnabled RPC):builtin 注册表单元可声明 components(browser/computer/lsp 各声明自身 tool 组件);extensions.list 挂组件状态(enabled/disabledReason: "tools-denied");新增 extensions.setComponentEnabled RPC——校验组件已在注册表声明 → 读写隐藏设置键 tools.disabled(settings-schema 黑名单数组)→ flush → 失效缓存 + onChanged 扇出;工具准入谓词 isToolAllowed 最前消费黑名单,组件开关对 agent 是真实禁用而非展示层。契约测试 3 例(状态挂载、开关闭环 + 与包级开关正交、未声明拒绝不留痕)。terminal/voice 不声明组件(不发明语义)。

    • EN: Plugin "included components" with real enable/disable semantics (tools.disabled denylist + setComponentEnabled RPC). Builtin registry units may declare components (browser/computer/lsp each declare their own tool component); extensions.list attaches component state (enabled / disabledReason: "tools-denied"); the new extensions.setComponentEnabled RPC validates the component is registry-declared → reads/writes the hidden settings key tools.disabled (a settings-schema denylist array) → flushes → invalidates caches + onChanged fan-out; the tool gate predicate isToolAllowed consumes the denylist first, so component toggles genuinely disable tools for the agent, not just the UI. Three contract tests (state attachment, toggle loop + orthogonality to package toggles, undeclared rejection leaves no trace). terminal/voice declare no components (no invented semantics).
  • GUI 硬编码文案 AST 门禁(dsh verify-client-ui-i18n parity,ratchet 基线收口):新门禁 scripts/verify-client-ui-i18n.ts 用 @babel/parser AST 扫描 desktop-app/client-core 源(排除 i18n 域与 vendor),把 JSX 文本、文案承载属性(label/placeholder/aria-label/…)、文案命名变量/属性/返回值里的硬编码中英句子钉在 CI 必经路径(root check:tools + test:scripts spec 11 例)。ratchet 基线 verify-client-ui-i18n-baseline.json 记录 106 文件 412 条存量债,只能减不能增,未登记文件零配额;单字符非 CJK 符号(版本号 "v" 前缀等)不算文案。这是此前两次「i18n 手滑漏翻/重复」事故的结构性对策,与 check-i18n-dup-keys 互补(一个管词典、一个管引用侧)。

    • EN: Hard-coded GUI copy AST gate (dsh verify-client-ui-i18n parity, ratchet-baselined). The new gate scripts/verify-client-ui-i18n.ts AST-scans desktop-app/client-core sources with @babel/parser (excluding the i18n domains and vendor), pinning hard-coded Chinese/English sentences in JSX text, copy-bearing attributes (label/placeholder/aria-label/…), and copy-named variables/properties/returns onto the mandatory CI path (root check:tools + 11 spec cases in test:scripts). The ratchet baseline verify-client-ui-i18n-baseline.json records 412 legacy violations across 106 files — debt can only shrink, unlisted files have zero quota; single non-CJK glyphs (the "v" version prefix, etc.) don't count as copy. This is the structural answer to the two past "slipped i18n translation/duplicate" incidents, complementing check-i18n-dup-keys (one guards the dictionaries, the other the reference side).
  • 终端/浏览器/电脑控制/LSP 登记为内置插件单元(dsh 子系统插件 parity):builtin 注册表在语音之后补齐四个子系统——terminal:terminal(只读展示:无总开关语义,不发明——终端面板的启停由终端面板自身 UI 决定,配置项 terminal.provider 后端选择/terminal.showImages/terminal.showProgress 照常可改)、browser:browser(开关镜像 browser.enabled)、computer:computer(开关镜像 computer.enabled,schema 默认 false,新增 unsetDisabled 镜像语义:设置从未写入时如实上报 disabled)、lsp:lsp(开关镜像 lsp.enabled)。四单元全部带 dsh 式配置表单,字段键即设置键,与语音单元同一套 list/setConfig 契约;GUI 侧新增 5 个 kind 标签(语音/终端/浏览器/电脑控制/LSP)双语显示名与描述,插件页行内展开即可开关+配置,只读条目不渲染失效开关(此前 readonly 项会显示一个点了没反应的死开关)。

    • EN: Terminal / browser / computer-use / LSP registered as builtin plugin units (dsh subsystem-plugin parity). The builtin registry gains four subsystems after voice — terminal:terminal (read-only display: no master-switch semantics, nothing invented — the terminal panel's own UI owns its visibility, while config fields terminal.provider / terminal.showImages / terminal.showProgress stay editable), browser:browser (toggle mirrors browser.enabled), computer:computer (toggle mirrors computer.enabled, schema default false, with a new unsetDisabled mirror semantic: an untouched setting reports disabled truthfully), and lsp:lsp (toggle mirrors lsp.enabled). All four carry dsh-style config forms whose field keys are the settings keys, on the same list/setConfig contract as the voice units; the GUI adds bilingual kind tags (Voice / Terminal / Browser / Computer Use / LSP) plus descriptions, with toggle + config in the Plugins tab inline expansion, and read-only entries no longer render a dead toggle (previously a readonly row showed a switch that did nothing when clicked).
  • 语音子系统插件化登记(voice:stt / voice:tts,dsh 式内置插件单元):语音输入/播报首次以插件身份进入插件页与能力清单——voice:stt(开关镜像 stt.enabled)与 voice:tts(开关镜像 speech.enabled,vocalizer 每次推送本就现读该键)两个内置单元带 dsh 式配置表单:STT 模型档位/语言提示/静音阈值/提交触发,TTS 本地模型/音色/语速(0.5–2 夹取)/朗读内容模式/自动朗读。关键设计:字段键即设置键——extensions.list 挂注册表声明的 config + 从 settings 现读的 configValues,extensions.setConfig 按声明钳制后经 settings 落盘,插件管理表单与设置页语音分区同一条存储、零漂移;未声明键拒绝、越界夹取、changed 扇出与插件包配置同一契约。GUI 侧新 kind「语音」标签 + 双语显示名/描述(ext builtin stt/tts [desc]),插件 tab 行内展开即可开关+配置。契约测试 5 例(list 挂字段+设置值、setConfig 落盘回读闭环、越界夹取、未声明键拒绝不留痕、setEnabled 镜像总开关状态翻转)。

    • EN: Voice subsystem registered as plugins (voice:stt / voice:tts, dsh-style builtin plugin units). Speech input and readback enter the Plugins tab and capability list for the first time — two builtin units, voice:stt (toggle mirrors stt.enabled) and voice:tts (toggle mirrors speech.enabled, which the vocalizer already re-reads on every push), each carrying a dsh-style config form: STT model tier / language hint / silence threshold / submit trigger; TTS local model / voice / rate (clamped 0.5–2) / read-content mode / auto-read. Key design: the field key IS the settings key — extensions.list attaches the registry-declared config plus live settings-backed configValues, and extensions.setConfig clamps against the declaration and persists via settings, so the plugin management form and the settings page voice section share one storage with zero divergence; undeclared keys are rejected, out-of-range values clamped, and the changed fan-out follows the same contract as plugin-package config. GUI gains the "Voice" kind tag + bilingual display names/descriptions (ext builtin stt/tts [desc]), with toggle + config in the Plugins tab inline expansion. Five contract tests (list attaches fields + setting values, setConfig persist-and-reread loop, out-of-range clamping, undeclared-key rejection leaves no trace, setEnabled mirror flips the master switch state).
  • 插件详情行内展开(dsh 插件管理页 parity)+ 内置插件 i18n + 术语统一:插件 tab(扩展中心与能力中心共用 UnifiedPluginsView)的模块行现在可点击整行头手风琴展开详情——状态(含加载失败制胜相位/遮蔽归因)、触发词、来源与路径,以及清单声明的配置表单(乐观写入 + restart 生效时机提示 + fail-soft 丢弃警告)与资源占用卡;原 PluginManifestSections 组件从 ExtensionsCenter 迁入 UnifiedPluginsView 成为单一权威,能力清单 detail pane 反向引用,杜绝双份漂移;条目状态标签(pluginStateLabel)同理收归一处。builtin 注册表 7 个内置插件单元(蜂群任务卡片/桌壳/三个魔术关键词/内置主题包/工具卡片渲染器)的硬编码英文 displayName/description 由 GUI 侧 i18n 覆盖(ext builtin <name>[ desc] 键,未命中回退原文,不改 daemon 避免 TUI/GUI 分叉),zh/en 双语词条补齐。术语收口:插件相关文案统一「插件」(扩展模块→插件模块、"已发现的扩展插件列表"→"已发现的插件列表"、空态与设置页描述同步),能力清单保持「能力」。

    • EN: Plugin detail inline expansion (dsh plugin-management parity) + builtin plugin i18n + terminology unification. Module rows in the Plugins tab (ExtensionsCenter and CapabilityCenter share UnifiedPluginsView) now expand an accordion detail on row-header click — status (load-failure winning phase / shadowing attribution), trigger, source and path, plus manifest-declared config forms (optimistic writes + restart-timing hints + fail-soft dropped-field warnings) and the resource footprint card. The PluginManifestSections component moves from ExtensionsCenter into UnifiedPluginsView as the single authority, with the capability-list detail pane importing it back — no more duplicated drift; the entry state label (pluginStateLabel) is centralized the same way. The builtin registry's 7 units (swarm task card / desktop shell / three magic keywords / built-in theme pack / tool card renderers) get GUI-side i18n coverage over their hardcoded English displayName/description (ext builtin <name>[ desc] keys, falling back to the original text when missing; daemon untouched to avoid TUI/GUI divergence), with zh/en entries added. Terminology: plugin-facing copy unifies on "plugin" (extension module → plugin module, "discovered extension plugins" → "discovered plugins", empty-state and settings descriptions aligned) while the capability list keeps "capability".
  • 工具注册表时间线升级:模型可见注入(dsh developer/message 回灌 parity 落地):地基从「纯 UI 标记」升级为完整 dsh 语义——agent-loop 在 sync 之后、组请求之前比对 wire 工具名,有 diff 时①立即把 developer 角色通知(formatToolRegistryNotice 单一文本权威:Tool added: X / Tool removed: Y 逐行)注入当前上下文,模型在携带新工具集的同一请求即知晓变化(pi-ai 的 developer 角色全 provider 序列化,Kimi/OpenAI 家族落为 developer/system);②会话树里的 tool_registry 条目不再被 session-context 排除,convertToLlm 的 custom 分支把它重建为同文本 developer 消息——live 注入与 resume/压缩重建逐字一致,重建后模型不丢工具集知识。前缀缓存只在追加处向前增长,无扰动。dsh 对 DeepSeek 原生 tool_addition/tool_removal wire 块的路由分化(不支持路由剥离 developer 消息只换声明表)在我们 OpenAI 兼容家族上由 developer 角色统一覆盖。契约测试 4 例(live 同请求到达、基线/无变化静默、重建同文本、空 diff 丢弃)。

    • EN: Tool-registry timeline upgrade: model-visible injection (dsh developer/message backfill parity lands). The foundation upgrades from "pure UI marker" to full dsh semantics — after sync and before assembling the request, the agent loop diffs wire tool names; on a diff it ① immediately injects a developer-role notice (formatToolRegistryNotice as the single text authority: one Tool added: X / Tool removed: Y line each) into the live context, so the model learns the change in the very request that carries the new tool set (pi-ai serializes the developer role across providers; the Kimi/OpenAI family lands it as developer/system), and ② the session tree's tool_registry entry is no longer excluded by session-context — convertToLlm's custom branch rebuilds it as the same developer message text, so live injection and resume/compaction rebuilds are byte-identical and the model never loses tool-set knowledge after a rebuild. The provider prefix cache only grows forward at the append point, zero disturbance. dsh's route split (native tool_addition/tool_removal wire blocks for DeepSeek; strip-developer-and-swap-declarations for unsupported routes) is covered uniformly on our OpenAI-compatible family by the developer role. Four contract tests (live same-request delivery, baseline/no-change silence, rebuild identity, empty-diff drop).
  • 工具注册表时间线地基(热插拔实时反映,dsh request/header parity):agent-loop 现在逐模型请求比对实际携带的 wire 工具名快照——扩展/插件在两步请求之间被启停导致工具集增删时,diff 经新 onToolRegistryChange 回调(agent 公开 addToolRegistryChangeListener)上报;会话侧落 custom_message 条目(tool_registry:GUI/TUI 时间线渲染「工具已更新 +N/−M + 涉及工具名」注入行、轨迹视图映射 system 事件)。首个请求只建立基线不触发(初始工具集已由 session_init 记录)。模型可见注入随下一刀落地(见上条)。

    • EN: Tool-registry timeline foundation (hot-toggle real-time reflection, dsh request/header parity): the agent loop now snapshots the wire tool names actually carried by each model request — when an extension/plugin is toggled between steps and the tool set changes, the diff is reported via the new onToolRegistryChange callback (exposed on Agent as addToolRegistryChangeListener); the session journals a custom_message entry (tool_registry: GUI/TUI timelines render a "tools updated +N/−M + affected tool names" injection row, the trajectory view maps it to a system event). The first request of a run only establishes the baseline (the initial tool set is already recorded at session_init). Model-visible injection lands in the follow-up cut (see above).
  • 插件页与能力清单口径统一(builtin 插件单元纳入插件 lane):扩展中心「插件」标签此前严格过滤 kind === "extension-module",而 8 项 builtin 注册表条目(task-card 风格/桌壳/魔术关键词×3/主题/工具卡片渲染)的 kind 各不相同,且本机无真实扩展模块,导致能力清单显示 8 项而插件页显示「未加载插件」的矛盾。现在 isPluginLaneEntry(UnifiedPluginsView 导出)成为单一权威口径——extension-module 全收 + musepi-extensions provider 且 native 层且非 skill 的 builtin 条目(排除 skill 避免与能力清单整屏重复);ExtensionsCenter 计数与模块行 kind 标签同口径,空态文案改写为如实说明 builtin 插件单元也汇总于此。能力清单与插件页不再各说各话。

    • EN: Plugin tab unified with the capability list (builtin plugin units join the plugin lane): the extension center's Plugins tab previously filtered strictly on kind === "extension-module", while the 8 builtin registry entries (task-card style / desktop shell / magic keywords ×3 / theme / tool-card renderer) carry assorted kinds — and with no real extension modules installed locally, the capability list showed 8 while the Plugins tab claimed "no plugins loaded". isPluginLaneEntry (exported from UnifiedPluginsView) is now the single authority — all extension-module entries plus builtin entries from the musepi-extensions provider at native level excluding skills (skills are excluded to avoid duplicating the whole capability list); ExtensionsCenter's count and module-row kind tags use the same rule, and the empty-state copy now honestly explains that builtin plugin units are tallied here too. The two views no longer contradict each other.
  • 插件清单字段正名:musepi 成为 package.json 权威清单字段(musepi.extensions / musepi.config / musepi.resources),omp/pi 保留只读兼容——全部读取点统一为 musepi 优先(extensions 加载器、插件 installer/loader、spike 动态运行时、discovery helpers),回归测试切到 musepi 主 fixture 并补「遗留 omp 字段仍可读」用例锁定 fallback;docs/extensions-dev.md §13 双语与 bundled skill 同步。

    • EN: Plugin manifest field canonicalized: musepi is now the authoritative package.json manifest field (musepi.extensions / musepi.config / musepi.resources), with omp/pi kept as read-only legacy fallback — every read site resolves musepi first (extensions loader, plugin installer/loader, spike dynamic runtime, discovery helpers); regression tests switched to musepi fixtures plus a "legacy omp block still readable" case locking the fallback; docs/extensions-dev.md §13 (both languages) and the bundled skill updated accordingly.
  • M2-2.9 spike ① 动态插件运行时试点(⑥ cordis 插件化):真实 user 扩展目录经 cordis 装配内核动态装载——CordisDynamicExtensionRuntime 在 DaemonHostContext 根的 musepi-dynamic-extensions 组 fiber 下为每个扩展挂独立子 fiber,四能力实测通过:加载(package.json omp.extensions 入口发现 + apply 内 factory 运行)、隔离(跨扩展重名命令 = 结构化碰撞拒绝 + 教学式文案,卸载互不移除登记)、生命周期(每次登记 = ctx.effect 效果账本一条带标签 effect,fiber.dispose() 反向回收 interval/命令/事件订阅;失败 fiber 立即 dispose 不留半挂载)、检视(fiber 状态机 + getEffects() 诊断树 + 错误归因 + 探针日志)。HMR 实测结论(Windows + Bun 1.4.2):裸查询串与 Bun.plugin onLoad 均不能击穿同进程模块缓存,可靠口径 = 整包暂存复制(reload 拾取含子模块的整包最新代码,watch 触发粒度契约不变);cordis 依赖图级 HMR 在本栈不可用,AGENTS.md HMR 条保留我方 watcher + 补丁层。DaemonHostContext 增加通用 plugin() 装配口(mount 重构复用)。试点报告 docs/review/0.5.0-m2-spike1-dynamic-plugin-runtime-report.md。契约测试 7 例。

    • EN: M2-2.9 spike 1 dynamic-plugin-runtime pilot (⑥ cordis plugin-ization). Real user extension directories load dynamically through the cordis assembly kernel — CordisDynamicExtensionRuntime hangs an independent child fiber per extension under the musepi-dynamic-extensions group fiber on the DaemonHostContext root, with all four capabilities verified: loading (package.json omp.extensions discovery + factory run inside apply), isolation (cross-extension name collisions get structured rejection with teaching errors; unloading one extension never removes another's registrations), lifecycle (every registration is a labeled effect on the ctx.effect ledger; fiber.dispose() unwinds intervals/commands/event subscriptions in reverse; failed fibers are disposed immediately, never left half-mounted), and inspection (fiber state machine + getEffects() diagnostics + error attribution + probe log). HMR findings (Windows + Bun 1.4.2): bare query strings and Bun.plugin onLoad cannot bust the same-process module cache; the reliable mechanism is stage-copy the whole package per reload (picks up entry AND submodule edits; watcher trigger granularity unchanged); cordis dependency-graph HMR is unavailable in this stack, so the AGENTS.md HMR contract keeps our watcher + patch layer. DaemonHostContext gains a generic plugin() mount point (mount refactored onto it). Pilot report at docs/review/0.5.0-m2-spike1-dynamic-plugin-runtime-report.md. Seven contract tests.
  • 扩展运行时读取清单配置(⑥ cordis 插件化第三刀):扩展 API 新增 pi.config.get(key) / pi.config.getAll()——运行时消费链路收口。清单声明的 meta 按 API 实例缓存(HMR 重载自然刷新),存储值每次现读 <agentDir>/extensions/plugin-config.json(进程级缓存 + 写穿透),restart 语义三档落地:none 现读立即可见、session 加载快照、daemon 重启;与第二刀的表单/setConfig 写入共用同一条 pi-wire 钳制链路(未声明键返回 undefined、坏存储值钳制回退)。extensions/types.ts 补 M2-2.4 六字段能力缝声明;docs/extensions-dev.md(含 .zh-CN.md 双语 + i18n.yaml 配对)新增 §13「插件清单配置」;bundled skill musepi-extension-dev 同步「插件配置」节。新增契约测试 4 例(默认值/未声明键 undefined、坏存储值钳制且 rogue 键过滤、无 config 声明读空表、写入后同进程重读)。

    • EN: Extensions read manifest-declared config at runtime (⑥ cordis plugin-ization, third cut). The extension API gains pi.config.get(key) / pi.config.getAll() — the runtime-consumption link is wired. Manifest-declared meta is cached per API instance (HMR reload refreshes naturally) while stored values are re-read on each call from <agentDir>/extensions/plugin-config.json (process-level cache + write-through), landing the three-tier restart semantics: none = visible immediately on live reads, session = load-time snapshot, daemon = service restart; this shares the same pi-wire clamping path as the second cut's form/setConfig writes (undeclared keys return undefined, corrupt stored values clamp back to defaults). extensions/types.ts gains the M2-2.4 six-field capability-seam declaration; docs/extensions-dev.md (with .zh-CN.md bilingual pair + i18n.yaml) gains §13 "Plugin manifest config"; the bundled musepi-extension-dev skill gains a matching "Plugin config" section. Four new contract tests (defaults / undeclared key undefined, corrupt stored value clamped with rogue keys filtered, no-config-declaration reads an empty table, same-process re-read after write).
  • 插件配置写入链路:管理页表单真正落盘(⑥ cordis 插件化第二刀):扩展中心配置表单的每次修改经新 RPC extensions.setConfig 落盘——daemon 以扩展 manifest 声明的字段为准(未声明的键直接拒绝,防写垃圾键),值经 coerceConfigFieldValue 钳制后写入 <agentDir>/extensions/plugin-config.json(进程级缓存 + 写穿透,损坏文件按空存储 fail-soft),失效扩展缓存并扇出 extensions.changed;extensions.list 同帧下发钳制后的 configValues,表单初始值 = 清单默认值 ← 存储值。GUI 乐观更新、失败回滚该键并内联报错,成功按字段 restart 声明提示「立即/新会话/重启服务后生效」(运行时消费随 cordis 试点落地)。取值钳制上收 @musepi/pi-wire 成为 daemon/GUI 单一权威,删去 desktop-app 的重复实现。新增契约测试 17 例(wire 取值 7、存储 6、服务级写入闭环 4:声明写入→钳制落盘→list 回读、越界夹取、未声明键拒绝不留痕、changed 广播计数)。

    • EN: Plugin config write path: management-page form values actually persist (⑥ cordis plugin-ization, second cut). Every form change lands via the new extensions.setConfig RPC — the daemon validates against the extension manifest's declared fields (undeclared keys are rejected outright to stop garbage writes), coerces values with coerceConfigFieldValue, and writes <agentDir>/extensions/plugin-config.json (process-level cache + write-through; a corrupt file degrades to empty fail-soft), then invalidates the extension cache and fans out extensions.changed; extensions.list ships the coerced configValues in the same frame, so the form seeds from manifest defaults ← stored values. The GUI updates optimistically, rolls the key back with an inline error on failure, and on success hints the field's restart semantics ("immediately / new sessions / after service restart" — runtime consumption lands with the cordis pilot). Value coercion moves up into @musepi/pi-wire as the single authority for daemon and GUI; the duplicated desktop-app copy is removed. Seventeen new contract tests (wire coercion 7, store 6, service-level write loop 4: declared write → clamped persist → list round-trip, out-of-range clamping, undeclared-key rejection leaves no trace, changed-broadcast counting).
  • 插件化第一刀:清单 config/resources 契约 + 扩展中心 dsh 式管理页段落(⑥ cordis 插件化):扩展的 package.json 可在 omp/pi 字段下声明 config(声明式配置字段表)与 resources(资源占用卡),daemon 侧逐扩展 fail-soft 校验后经 extensions.list 下发,扩展中心详情页据此渲染三段式管理视图——配置表单(boolean→开关行、number→NumberStepper 区间步进、select→≤4 项 Segmented / 更多 GuiSelect、string/path→文本行,restart 声明渲染生效时机提示)、资源卡(磁盘/内存/首次准备/模型下载)、坏字段丢弃警告。校验契约在 @musepi/pi-wire 新增 plugin-config 模块(daemon 与 GUI 共用,不引入 schemastery);manifest 是用户可写 JSON,坏字段进结构化 errors 被丢弃、好字段照常通过,绝不拖垮扩展登记(回退保护①)。GUI 取值一律经 coerceConfigValue 钳制回退默认值,存储坏值不会透出表单。契约测试 21 例(wire 8 + daemon manifest 6 + GUI 取值 7)。

    • EN: Plugin-ization first cut: manifest config/resources contract + dsh-style management sections in the extension center (⑥ cordis plugin-ization). An extension's package.json can declare config (declarative field table) and resources (resource footprint card) under its omp/pi block; the daemon validates each extension fail-soft and ships the result via extensions.list, and the extension center detail pane renders the three management sections — a config form (boolean → toggle row, number → NumberStepper with range/step, select → Segmented for ≤4 options / GuiSelect beyond, string/path → text row, restart hints showing when a change takes effect), a resources card (disk / memory / first-time setup / model downloads), and a dropped-fields warning. The validation contract lands in a new @musepi/pi-wire plugin-config module (shared by daemon and GUI, no schemastery); the manifest is user-writable JSON — bad fields go to structured errors and are dropped while good fields pass, never dragging down extension registration (fallback protection ①). GUI reads are always clamped by coerceConfigValue back to declared defaults so corrupt stored values never surface. Twenty-one contract tests (wire 8 + daemon manifest 6 + GUI coercion 7).
  • 「新功能」公告弹窗的桌宠穿戴用户装饰:版本更新公告里的庆祝桌宠(PetSprite)此前渲染默认裸装——现在经 usePetDecor() 挂上用户当前选的配饰与光泽,与主界面桌宠观感一致。

    • EN: The "what's new" announcement overlay's celebrating pet now wears the user's decor: the PetSprite previously rendered the default bare look and now mounts the active accessory and gloss via usePetDecor(), consistent with the main-surface pet.
  • 输入框「性能与用量」统计条(M1.10c,dsh-v0.2.0-rc.1 StatsPills parity):输入框下方新增两颗 pill——时间 pill(轮次/步数 · 总 tokens · 模型用时 · 工具用时 · TTFT · tokens/s)与用量 pill(输入/输出/缓存读写 · 缓存命中率 · 成本),点击各自弹出明细浮层(useFloatingMenu 单例互斥);ContextRing 不动,统计行经 ComposerFrame 新 belowRow 插槽渲染在输入卡外(设计稿 docs/gui-design.md §5w)。数据源零新 RPC:token 数字复用 session.contextUsage 持久投影(分页/压缩安全),时间数字走可见窗口 fold 纯函数 deriveWindowStats(八字段镜像 dsh,memo 比较器守流式重渲染);设置 → 聊天 → 「interface and input」新增 详细/简洁 档位(简洁档只留速度与缓存命中),localStorage + 跨标签页 storage 事件同步。纯函数 15 例契约测试。

    • EN: composer "Performance & usage" stats row (M1.10c, dsh-v0.2.0-rc.1 StatsPills parity): two pills ride below the composer — a time pill (turns/steps · total tokens · model time · tool time · TTFT · tokens/s) and a usage pill (input/output/cache read+write · cache hit · cost), each opening a detail popup (useFloatingMenu singleton mutex). The ContextRing stays put; the row renders outside the input card via ComposerFrame's new belowRow slot (design doc docs/gui-design.md §5w). Zero new RPCs: token figures reuse the durable session.contextUsage projection (paging/compaction-safe), time figures come from the visible-window fold pure function deriveWindowStats (eight fields mirroring dsh, memo comparator guarding streaming re-renders); Settings → Chat → "interface and input" gains a Detailed/Compact density (compact keeps only speed and cache hit), synced over localStorage + the cross-tab storage event. Fifteen contract tests on the pure functions.
  • 预设设置页卡片化 + 「查看配置」继承展开只读视图(dsh 0.2.0 预设设置页信息架构 parity):设置 → 智能体 → 预设分为「内置预设 / 自定义预设」两卡片区,默认预设高亮,卡片名称统一走 resolveModeLabel 取名链(永不裸 id);每张卡片新增「查看配置」入口,弹窗只读展示继承展开结果——诊断警告(继承环/超深截断/悬空引用)→ 继承链逐层 → 提示词区块(按 order)→ 扩展白名单(全部启用/仅内置核心/chips)→ settings 覆盖键值表 → 标志位(promptComplete 及声明者/runtimeContext/默认模型档位)。合并语义逐条镜像 daemon resolveMode(拓扑序、同名子胜父、后者胜、complete 唯一来源),环/悬空/超深只记标记不抛错(读路径容错);零新 RPC——链闭包由 modes.list 摘要行一次算出、链上全部层并行 modes.get。编辑/新建/复制/删除能力全部保留。纯函数 19 例契约测试(线性链/菱形/环/自环/悬空/截断/string 快捷展开/complete 缩减)。

    • EN: preset settings page card-ified + a "View config" resolved-inheritance read-only view (dsh 0.2.0 preset settings IA parity): Settings → Agent → Presets splits into "Built-in presets" / "Custom presets" card groups with the default visually highlighted, and every card name goes through the resolveModeLabel chain (never a bare id); each card gains a "View config" entry whose dialog shows the resolved inheritance — diagnostic warnings (cycle / over-depth truncation / dangling refs) → layer-by-layer chain → prompt sections (by order) → extension whitelist (all/core-only/chips) → settings override key-values → flags (promptComplete + declaring source / runtimeContext / default model role). Merge semantics mirror daemon resolveMode line by line (topo order, child-wins-by-name, last-wins, complete = sole source); cycles/dangling/over-deep are recorded, never thrown (read-path tolerance); zero new RPCs — the chain closure is computed from modes.list summary rows and every layer is fetched in parallel via modes.get. All edit/create/duplicate/delete flows are preserved. Nineteen contract tests on the pure functions (linear/diamond/cycle/self-cycle/dangling/truncation/string-shorthand expansion/complete reduction).
  • 右侧面板分栏 + 标签页跨列拖拽(dsh 0.2.0 双栏 parity):右侧面板可一分为二——头部新增分栏开关,双栏时同屏显示两组「标签条 + 内容体」,单列布局与分栏布局共享同一 renderPanelBody 渲染闭包,任何表面在两种形态下观感一致。数据模型升 schema v2:PanelTab 增 column 字段,扁平 tabs 数组保持「先列 0 后列 1」分组不变式(既有邻接回落/预算驱逐逻辑零改动继续成立);状态增 split + 每列显示 columnActive。一个 DndContext 横跨两列:同列拖拽走 reorder,跨列或落空白区走 movePanelTabToColumn(被移动标签加入目标列切片末尾并带走全局焦点——浏览器单例守卫按 activeId 选宿主列,焦点不跟会错位);合并分栏时列 1 全部并入列 0 保序,全局焦点保留为唯一显示。浏览器单例守卫:非全局聚焦列里的 browser 标签显示占位按钮(点击即切焦点)。持久化 v1(前分栏)载荷迁移为全列 0。panel-tabs 契约测试扩至 32 例(分组不变式/跨列 reorder no-op/焦点跟随/v1 迁移/分栏往返)。

    • EN: Right-panel split columns + cross-column tab drag (dsh 0.2.0 two-column parity): the right panel can divide in two — a split toggle in the header shows two tab strips + bodies side by side, and both the single-column and split layouts render through one shared renderPanelBody closure so every surface looks identical in either shape. The data model moves to schema v2: PanelTab gains a column field and the flat tabs array keeps the column-0-first grouping invariant (existing neighbour fallback / eviction-budget logic keeps holding unchanged); state gains split + per-column display columnActive. One DndContext spans both columns: same-column drags route to reorder, cross-column or blank-area drops route to movePanelTabToColumn (the moved tab joins the end of the target column's slice and takes global focus — the browser singleton guard keys on activeId to pick its host column, so focus must follow); merging collapses every column-1 tab into column 0 in order, keeping the global focus as the single display. Browser singleton guard: a browser tab in the non-focused column renders a placeholder button (click to focus). v1 (pre-split) payloads migrate to all-column-0. panel-tabs contract tests grow to 32 cases (grouping invariant / cross-column reorder no-op / focus follows / v1 migration / split round-trip).
  • 预设菜单选项补描述行(dsh 预设菜单信息 parity):WelcomeComposer 预设选项从单行升级为「名称 + 描述」两行结构;resolveModeDescription() 走 mode {id} description 词表链(回显返回 null,无描述退化为单行);四个内置预设补描述键(工作=执行编码任务/对话=轻讨论问答/设计=界面视觉设计/创作=创意发散),双语同步。

    • EN: Preset menu options gain a description row (dsh preset-menu information parity): WelcomeComposer preset options upgrade from a single row to a name + description two-line structure; resolveModeDescription() walks the mode {id} description i18n chain (returns null on echo — entries without a description degrade to a single row); the four builtin presets gain description keys (work = execute coding tasks / chat = lightweight Q&A / design = UI & visual design / creator = creative exploration), bilingual in sync.
  • 单栏直接拖标签分栏(dsh rc.2 拖放分栏 parity):分栏不再需要先点「分栏」按钮——单列布局下拖起标签即点亮右半身「拖到此处分栏」落区(PanelSplitZone,虚线高亮 + 双语提示),落入即 setSplit(true) 并把标签送进第二列;落区与标签条共享同一 DndContext/碰撞判定(指针包含纪律沿用),拖拽含入判定不再把落区当作独立高亮态互抢。ContextPanel 非分栏分支重构为自带 DndContext(分栏分支的 splitSensors/panelSplitCollision 上提共享),SurfaceTabStrip 无关 import 清理。

    • EN: Drag a tab straight into a split (dsh rc.2 drop-to-split parity). Splitting no longer requires clicking the split button first — in the single-column layout, lifting a tab lights up a right-half "drop here to split" zone (PanelSplitZone, dashed highlight + bilingual hint); dropping there calls setSplit(true) and sends the tab into column 2. The zone shares one DndContext / collision discipline with the strip (the pointer-containment rule carries over), so the containment check no longer treats the zone as a competing highlight. The ContextPanel non-split branch is restructured to own its DndContext (the split branch's splitSensors/panelSplitCollision are hoisted and shared), and the unrelated SurfaceTabStrip import is cleaned up.
  • 文件面板树/预览分签 + 隐藏文件开关 + 行内打开与引用(dsh 文件树 parity):文件树与文件预览拆成两个独立标签页——树占位签不再被首个真实文件吞掉(upsert 例外:files 占位即树浏览器,真实实例在旁新开一签),预览头「返回」激活树签;面板标签页 body 本就独立挂载,树签↔文件签切换会卸载重挂,未保存编辑缓冲改走模块级 bufferCache 闭环(卸载时按预览路径暂存脏缓冲、重挂 openPreview 自动恢复编辑态并重报脏点到标签条)。工具栏新增「显示隐藏文件」开关(ScanEye/EyeOff 双态,git-prefs 同款持久化 + 跨面事件同步;daemon 扫描本就返回点文件,过滤纯 GUI 侧按文件名 . 前缀)。树行悬停动作区新增「在应用中打开」(dsh rc.2 ui-open-in-app parity:文件夹开资源管理器、文件开默认应用,行尾悬停浮现)。预览头新增「引用文件到输入框」(Quote 图标,走 musepi-gui-insert-text 共享通道把工作区相对路径以 @path 插入输入框草稿)。panel-tabs 契约用例随新契约改写(files 占位保留、其他表面仍替换)。

    • EN: File-pane tree/preview split tabs + hidden-files toggle + row open action & cite (dsh file-tree parity). The tree and the file preview become two independent tabs — the tree placeholder is no longer consumed by the first real file (upsert exception: the files placeholder IS the tree browser, a real instance opens next to it) and the preview's back button activates the tree tab. Panel tab bodies mount independently, so a tree↔file tab switch unmounts and remounts the pane; unsaved editor buffers now close the loop through the module-level bufferCache (dirty buffer stashed by preview path at unmount, restored into edit mode by openPreview on remount, dirty dot re-reported to the strip). The toolbar gains a "show hidden files" toggle (ScanEye/EyeOff states, git-prefs-style persistence + cross-surface event sync; the daemon scan already returns dotfiles — filtering is a pure GUI-side .-prefix name filter). Tree rows gain a hover "open with app" action (dsh rc.2 ui-open-in-app parity: folders open Explorer, files open the default app, revealed at the row's right edge on hover). The preview header gains "cite file in composer" (Quote icon — pushes the workspace-relative path into the composer draft as @path via the shared musepi-gui-insert-text channel). panel-tabs contract cases rewritten for the new contract (files placeholder kept, other surfaces still replace).

Fixed

  • 回合冻结用时三套键空间统一为回合起点键(P0-2,交接单 docs/review/0.5.1-defect-handoff.md):daemon、GUI store、collab 客人/宿主客户端此前按三种不同键写 roundDurations——daemon 按回合起点 ts(最后一条 user 消息或 display 的顾问笔记)、GUI store 与两个客户端按末条 assistant ts(其中 collab 两端还拿 Date.now() 减 user ts 混时钟);而 Transcript 三处读侧全用 assistant ts 键 → daemon 恢复快照里的冻结用时在会话视图永远查不到(轨迹检视器因读侧做了「起点键优先、assistant 键回退」的归一才幸免)。修复:① pi-wire 新增共享 helper roundDurationRecord(锚定规则唯一实现:isTurnStartEntry + wire 时钟末条 − 起点,时钟回拨返回 null),daemon 视图与全部三个 GUI 写侧统一调用;② collab 两端的 Date.now() 混时钟算法随统一一并消除;③ 读侧新增纯函数 roundTimerKeyByRow(每回合末条 assistant 行 → 回合起点键),Transcript 行级/回合头/流式幽灵行三处读侧统一走「起点键 + 旧 assistant 键回退」(旧快照可读性保留);④ 轨迹检视器读侧本就归一,零改动。契约测试:wire helper 6 例、render-units 4 例(多步回合/顾问锚/纯工具轮)、guest client 3 例(起点键、wire 时钟定值、每轮累积)、store 2 例(实时冻结 + daemon 播种同键可读);既有断言 assistant 键的测试改写为新契约(失败模式即旧键空间:daemon 值永远不可见)。

    • EN: Round-freeze durations unified onto the turn-start key (P0-2, handoff doc docs/review/0.5.1-defect-handoff.md). The daemon, the GUI store and the collab guest/host clients wrote roundDurations under THREE different keys — the daemon keyed by turn-start ts (last user message or displayed advisor note), the GUI store and both collab clients by the LAST assistant ts (with the collab pair additionally mixing clocks via Date.now() - userTs) — while all three Transcript read sites looked up assistant-ts keys, so frozen totals from a daemon-restored snapshot could never be found in the conversation view (the trajectory viewer survived only because its read side normalized turn-start-first with an assistant-key fallback). Fix: ① new shared pi-wire helper roundDurationRecord (single implementation of the anchor rule: isTurnStartEntry + last wire-clock entry minus start, null on clock skew), now called by the daemon view and all three GUI write sides; ② the collab pair's mixed-clock Date.now() math eliminated by the same unification; ③ new pure read-side function roundTimerKeyByRow (each turn's LAST assistant row → turn-start key), with all three Transcript read sites (row-level, turn header, ghost stream row) unified to turn-start-primary plus the legacy assistant-key fallback (pre-anchor snapshots stay readable); ④ the trajectory viewer's read side was already normalized — untouched. Contract tests: 6 for the wire helper, 4 for render-units (multi-step round / advisor anchor / tool-only round), 3 for the guest client (turn-start key, deterministic wire-clock value, per-round accumulation), 2 for the store (live freeze + daemon-seeded key served through the same registry); the existing tests asserting assistant keys were rewritten to the new contract (their failure mode IS the old key space: daemon values forever invisible).
  • 全局事件与会话 journal 水位门串线(P0-1,交接单 docs/review/0.5.1-defect-handoff.md):daemon 全局广播(extensions.changed / modes.changed / STT-TTS 下载进度等)此前以 kind:"event" 下发——与会话 journal 记录同 kind,而 GUI store 对该 kind 执行 seq 水位门。全局 seq 是独立计数空间:新会话水位 0 时首个全局事件 seq 恰好为 1 → 水位被推进 → 后续 seq 1 的真实 journal 记录被误判「重放」静默丢弃;seq 越过水位+1 时进重排缓冲 + 触发 catchup + 10s 空洞计时器 → 下载进度风暴可把整会话反复打回重订阅。修复:① daemon EventService 广播原语与 extensions.notification 改发 kind:"global-event"(seq 保留——帧识别要求数字 seq——但不再进任何会话水位语义);② GUI app.tsx 事件路由对 global-event 显式早退(消费方本就只走 rpc.addEventListener 按 payload.type 过滤,不受影响);③ session-store.apply 对 global-event 兜底直弃。契约测试:daemon 侧 kind 分离 + 客户端「全局事件先于首条 journal 记录不丢消息」「40 连发下载进度零 catchup 零 resync」;既有 design-systems 广播契约测试更新为新 kind。

    • EN: Global events were cross-wired into the session journal watermark gate (P0-1, handoff doc docs/review/0.5.1-defect-handoff.md). Daemon global broadcasts (extensions.changed / modes.changed / STT-TTS download progress) went out as kind:"event" — the same kind as session journal records, which the GUI store watermark-gates. The global seq is an independent counter: on a fresh session (watermark 0) the first global event's seq is exactly 1 → the watermark advanced → the real journal record seq 1 was then misjudged as a "replay" and silently dropped; any seq beyond watermark+1 landed in the reorder buffer, triggered catchup and armed the 10s gap timer → a download-progress storm could repeatedly throw whole sessions back into re-subscribe. Fix: ① the daemon EventService broadcast primitive and extensions.notification now emit kind:"global-event" (seq kept — frame detection requires a numeric seq — but it enters no session watermark semantics); ② the GUI app.tsx event router early-returns on global-event (consumers only ever listened via rpc.addEventListener filtering on payload.type, unaffected); ③ session-store.apply drops global-event as defense-in-depth. Contract tests: daemon-side kind separation + client-side "a global event before the first journal record doesn't lose the message" and "a 40-burst download-progress storm causes zero catchups and zero resyncs"; the existing design-systems broadcast contract test updated to the new kind.
  • desktop 启动裸 exit 3 的根因定位 + 启动器韧性(用户 2026-09-29):bun run desktop 在「dist fresh — skipping build」后直接 exited with code 3、零输出。排查链:真实二进制 --version 同样 exit 3(故障①)→ ELECTRON_RUN_AS_NODE=1 正常(exe 完好)→ --no-sandbox 下露出真错误 ProcessSingleton: Lock file can not be created! Error code: 5(故障②)→ userData 目录普通写入正常、SeCreateSymbolicLinkPrivilege 已从令牌消失(系统日志显示 22:01 HONOR PCManager 重置过策略)。根因:Electron 的单实例锁在 Windows 上是符号链接(Chromium 内部实现,无法选择不用),创建需要「创建符号链接」用户权限;管家类软件/策略重置会静默回收它,开发者模式救不了(Chromium 不走特权创建标志)。更阴的是故障②的退出码:绕过沙箱后 Chromium 打出 Lock file 错误,但进程以 0 退出(单实例守卫干净退出)——裸看退出码是"成功",窗口却从未出现。修复(仅 dev 循环,发布版沙箱不动):desktop-run.mjs 改为韧性启动——Electron 10 秒内 exit 3 自动以 --no-sandbox 重试一次并说明原因;stderr 改为管道扫描(转发同时滚动匹配),任何快速退出只要命中 ProcessSingleton 字样即打印双根因 + 修复指引并以 1 退出,不再把误导性的 0 转发给调用方;配套新增 scripts/grant-symlink-privilege.ps1——右键管理员运行一次把 SeCreateSymbolicLinkPrivilege 授回当前用户(secedit 只动 USER_RIGHTS 区域:导出→增补 SID→回配),注销重登生效(重启系统通常同时治好沙箱与权限两项)。

    • EN: desktop startup's bare exit 3 root-caused + launcher resilience (user 2026-09-29). bun run desktop printed "dist fresh — skipping build" then exited with code 3 with zero output. Root-caused to TWO independent OS-level failures (concurrent on this machine — the system log shows HONOR PCManager resetting a policy scheme at 22:01): ① Chromium sandbox init killed silently — electron --version exits 3 with no output sandboxed, fine under --no-sandbox; ② SeCreateSymbolicLinkPrivilege has vanished from the user token — Electron's single-instance lock is a SYMLINK on Windows (a Chromium-internal implementation; there is no opting out), and creating it needs the "Create symbolic links" user right; once PC-manager "optimizers" / policy resets revoke it, creation fails with ACCESS DENIED, and Developer Mode does not help (Chromium never passes the unprivileged-create flag). The nastier twist is failure ②'s exit code: with the sandbox bypassed, Chromium logs ProcessSingleton: Lock file can not be created! Error code 5 but the process exits 0 (the single-instance guard quits cleanly) — a bare exit-code check reads "success" while no window ever appeared. Fix (dev loop only; release builds keep the sandbox): desktop-run.mjs becomes resilient — an Electron exit 3 within 10s auto-retries once with --no-sandbox and explains why; stderr is piped through a scanner (forwarded while rolling-matched), and any fast exit whose output hits the ProcessSingleton pattern prints both root causes + remediation and exits 1 instead of forwarding the misleading 0; plus a new scripts/grant-symlink-privilege.ps1 — run once as administrator to grant SeCreateSymbolicLinkPrivilege back to the current user (secedit touches only the USER_RIGHTS area: export → append the SID → re-apply), effective after sign-out/sign-in (a reboot usually cures both the sandbox and the privilege).
  • 浏览器标签右键菜单 + theme-color 标签样式修复(用户 2026-09-29:网页标签不能右键关闭、个别网页标签样式显示不正常):标签条每个标签支持右键——关闭 / 关闭其他(单标签时禁用)/ 复制标签页 / 复制网址,走共享 ContextMenu(useFloatingMenu 玻璃壳、指针锚点、全局菜单互斥)。网页 theme-color 不再整块实底 + 强制黑白文字(部分站点主题色与界面明暗冲突,实底标签读起来就是"样式坏了")——改为低透明度晕染(18% alpha 背景 + 32% alpha 内描边环),玻璃标签质感与文字可读性都保住;hexLuminance 对比度启发式随实底方案一并下线。

    • EN: Browser tab context menu + theme-color tab style fix (user 2026-09-29: web tabs can't be right-click closed, some web tabs render with broken styling). Every tab in the strip now has a right-click menu — close / close others (disabled with a single tab) / duplicate tab / copy URL — via the shared ContextMenu (useFloatingMenu glass shell, pointer anchor, global menu mutex). A page's theme-color no longer paints the tab as a full-bleed solid with forced black/white text (some sites' theme colors clash with the UI's light/dark and the solid tab just read as "broken styling") — it becomes a low-alpha tint (18% alpha background + 32% alpha inset ring) so the glass tab keeps both its material and legible text; the hexLuminance contrast heuristic leaves with the solid approach.
  • 跨列拖拽标签鬼畜闪烁(dsh dockkit 指针包含判定 parity):分栏后拖拽标签在列间移动时「over」目标来回跳变、两列标签条逐帧互抢动画(用户 2026-09-29:鬼畜闪烁难以正确切换)。根因是 closestCenter 距离判定:被拖 chip 同时接近「小标签矩形」与「大列落区」时,最近者随指针微移在两者之间振荡。修法对齐 dsh ui-dockkit 的 pointerWithin 纪律——只把指针包含在内的 droppable 作为候选,面积最小者胜出(标签优先于包含它的列区),指针一旦进入另一列该列独占全部候选:每个指针位置一个稳定判定,零振荡。

    • EN: Cross-column tab drag flicker fixed (dsh dockkit pointer-containment parity). With the panel split, dragging a tab across columns made the "over" target flip back and forth and both strips' animations fight frame by frame (user 2026-09-29: violent flicker, hard to land a switch). Root cause: the closestCenter distance heuristic — a dragged chip sitting between a small tab rect and the big column zone oscillates between the two as the pointer micro-moves. The fix mirrors dsh ui-dockkit's pointerWithin discipline: only droppables whose rect CONTAINS the pointer are candidates, the smallest area wins (a tab beats the column zone holding it), and once the pointer crosses into the other column that column owns every candidate — one stable verdict per pointer position, zero oscillation.
  • ContextRing 液态玻璃落在圆环本体(按钮玻璃化回退):上一轮把 .gui-context-ring-btn 并进 footer-pill 玻璃家族方向有误(用户 2026-09-29:指的是圆环本身加粗改液态玻璃质感,不是整体变图标)——本次回退按钮壳的玻璃材质(退出五组家族选择器,按钮恢复透明点击区,仅悬停时圆环轻放大),把玻璃做进圆环:弧 stroke 2.5→3.4 加粗、按状态色走渐变 stroke(浅色端 45% 白混入)+ 同色柔光 drop-shadow、轨道改磨砂半透明发丝——材质在环上,不在按钮盘上。

    • EN: ContextRing liquid glass lands on the ring itself (button glass reverted). Glassing the .gui-context-ring-btn into the footer-pill family last round was the wrong direction (user 2026-09-29: the RING itself should be thicker with a liquid-glass feel, not the whole thing turned into an icon) — this reverts the button shell's glass material (leaves all five family selector groups; the button is a transparent hit target again, with a subtle ring-only scale-up on hover) and puts the glass into the ring: arc stroke 2.5→3.4 thicker, tone-colored gradient stroke (light end mixed 45% white) + a soft chromatic drop-shadow, and a frosted translucent track — the material lives on the ring, not on a button disc.
  • 白屏事故修复:i18n 跨域重复键 + 新增静态门禁:「性能与用量」统计条新增词表时,input tokens / output tokens 两键与 settings 域既有键重复——client-core i18n barrel 在模块加载时对跨域重复键硬抛,整个 renderer bundle 加载失败,主窗与托盘(tray-menu.html 独立 entry、共享同一 bundle)全部白屏;bun run check 的类型检查每域独立 satisfies,结构上抓不到跨域重复,门禁全绿却运行时必崩。修复:general 域删除两重复键(stats-pills 经 t() 回落 settings 域既有「输入 token / 输出 token」,语义不变);新增 scripts/check-i18n-dup-keys.ts 静态扫描三个语言 barrel(client-core zh-CN / en-US、TUI zh-CN)的跨域键交集,已挂入 check:tools(变异验证:塞回事故键即 exit 1)。

    • EN: White-screen incident fixed: duplicate cross-domain i18n key + a new static gate. Adding the "Performance & usage" stats-row keys put input tokens / output tokens in general, duplicating existing settings-domain keys — the client-core i18n barrel throws on cross-domain duplicates at module load, so the entire renderer bundle failed and both the main window and the tray (tray-menu.html is a separate entry sharing the same bundle) went white; bun run check's per-domain independent satisfies type check structurally cannot see cross-domain duplicates, so every gate was green while the app crashed at runtime. Fix: the two duplicate keys are removed from the general domain (stats-pills' t() falls back to the settings-domain "输入 token / 输出 token" — same semantics); a new scripts/check-i18n-dup-keys.ts statically scans the three language barrels (client-core zh-CN / en-US, TUI zh-CN) for cross-domain key intersections and is wired into check:tools (mutation-verified: re-inserting the incident key exits 1).
  • 新功能面板(what's-new)两处渲染缺陷:①底部按钮行此前直接挂在卡片根、复用无内边距的 .gui-onboarding-actions,按钮贴死卡片圆角底边被 overflow:hidden 裁切——新增作用域规则给按钮行独立内边距+顶部分隔线,成为标准 dialog footer;②面板头部的球体桌宠此前不传配饰配置,用户给桌面宠物配的耳机/音符在新功能页显示为裸球——改走 usePetDecor()(与聊天头像、桌面宠物窗同源),配饰与光泽设置即时同步。

    • EN: Two rendering defects in the what's-new panel. ① The footer button row hung directly off the card root reusing the padding-less .gui-onboarding-actions, so the buttons kissed the card's rounded bottom edge and got clipped by overflow:hidden — a scoped rule now gives the row its own padding plus a top hairline, making it a standard dialog footer. ② The header orb pet never received the decoration config, so the headphones/note accessory the user picked for the desktop pet rendered as a bare ball here — it now resolves through usePetDecor() (same source as the chat avatar and the desktop pet window), keeping accessory and gloss in sync live.
  • 内置预设中文名补全 + 预设取名链收口:zh 词表 preset {id} name 四个内置预设(work/chat/design/creator)此前是英文原值("Work" 等),TUI 与 daemon modes.list 中文环境下仍显示英文;GUI 侧 WelcomeComposer 预设 chip 与选项菜单、设置页预设列表此前直接用 daemon 裸 label,绕过 resolveModeLabel 取名链(GUI i18n mode {id} → catalog → 兜底)。修复:zh 词表补翻译(工作模式/对话模式/设计模式/创作模式,与 GUI mode {id} 词表逐字一致);三处显示点(预设 chip、chip 菜单、设置页列表)全部改走 resolveModeLabel,与 ContextPanel/SessionHoverCard 同名同链(mode-label.ts 契约注释由「两处」扩为「三处」)。用户自建/扩展预设不受影响(t() 回显 key 时回落 catalog label)。

    • EN: Builtin preset Chinese names completed and the preset naming chain closed. The zh locale left all four builtin preset {id} name values in English ("Work" etc.), so the TUI and daemon modes.list still showed English under a Chinese locale; on the GUI side, the WelcomeComposer preset chip, its option menu, and the settings preset list rendered daemon raw label directly, bypassing the resolveModeLabel chain (GUI i18n mode {id} → catalog → fallback). Fix: the zh table gains the translations (工作模式/对话模式/设计模式/创作模式, verbatim identical to the GUI mode {id} table); all three display points (preset chip, chip menu, settings list) now go through resolveModeLabel, same name/same chain as ContextPanel and SessionHoverCard (the mode-label.ts contract comment grows from "two places" to "three"). User-created/extension presets are unaffected (when t() echoes the key, the catalog label is used).
  • 「性能与用量」统计条中文裸键 + 居中 + 浮窗信息增强:①浮窗此前在中文环境显示裸词表 key(stats turns steps / model time 等 9 键 zh 从未补过)——zh general 域补齐 10 键、en 补 1 键;②统计条此前靠右对齐,改居中(输入框下方视觉重心稳定);③浮窗补两行——未缓存输入(input − cacheRead)与本次会话花费(复用 ContextRing 的 formatSpend),并加宽至 210px。

    • EN: "Performance & usage" stats row: raw zh keys + centering + popover info. ① The popover showed raw i18n keys in Chinese (stats turns steps / model time — 9 zh keys were never added) — the zh general domain gains 10 keys, en gains 1. ② The row was right-aligned; it now centers (stable visual weight under the composer). ③ The popover gains two rows — uncached input (input − cacheRead) and this-session spend (reusing ContextRing's formatSpend) — and widens to 210px.
  • ContextRing 按钮液态玻璃化:.gui-context-ring-btn 此前 background: transparent 游离于玻璃体系外(浅色主题下尤其突兀)——并入 footer-pill 玻璃家族五组选择器(基材/泽/hover/active 与统计 pill、模型胶囊同款),成员规则删自声明背景。

    • EN: The ContextRing button goes liquid-glass: .gui-context-ring-btn used to float outside the glass system with background: transparent (especially jarring in the light theme) — it now joins the footer-pill glass family across all five selector groups (base/sheen/hover/active identical to the stats pills and the model capsule), and the member rule drops its self-declared background.
  • desktop 启动偶发崩溃(updater 在非 Electron 宿主下把整个启动炸掉)+ 启动日志瘦身:某次 bun run desktop 中 launcher shim 被并发 bun install 重写踩中、main.cjs 落在纯 Node 宿主下运行,require("electron").app 为 undefined,electron-updater 的 autoUpdater getter 在模块加载期直接抛 Cannot read properties of undefined (reading 'getVersion'),整个 GUI 启动失败并滚出 65 行崩溃栈。修复双保险:① updater.cjs 头部加宿主守卫——非 Electron 宿主不再 require electron-updater,导出禁用态 stub(state.status="error" + 一句话诊断),GUI 其余部分照常启动;② main.cjs 头部加 fail-fast 守卫,宿主不对时一行说明立即退出,不再向下游炸出莫名其妙的错误。同时治理启动日志:desktop 改为 scripts/desktop-run.mjs——dist 新鲜(晚于全部打包输入的 mtime,覆盖本包 src/public/html 与 @musepi 各 workspace 源码)时跳过全量重建,一行说明直接起 Electron(MUSEPI_FORCE_BUILD=1 强制重建,旧全量行为保留为 desktop:rebuild);build:fix 的逐文件刷屏改为每阶段一行汇总(VERBOSE=1 保留明细)。

    • EN: Occasional desktop startup crash (updater killed the whole launch under a non-Electron host) + slimmer startup log. In one bun run desktop run the launcher shim was caught mid-rewrite by a concurrent bun install, main.cjs landed on a plain-Node host where require("electron").app is undefined, and electron-updater's autoUpdater getter threw Cannot read properties of undefined (reading 'getVersion') at module-load time — the whole GUI launch died with a 65-line crash dump. Two-layer fix: ① updater.cjs gains a host guard at the top — outside Electron it skips requiring electron-updater entirely and exports a disabled stub (state.status="error" + a one-line diagnosis), so the rest of the GUI still starts; ② main.cjs gains a fail-fast guard that prints a one-line explanation and exits immediately when the host is wrong, instead of failing downstream with a cryptic error. The startup log is governed too: desktop now runs scripts/desktop-run.mjs — when dist is fresh (newer than every bundler input's mtime, covering this package's src/public/html entries and all @musepi workspace sources) the full rebuild is skipped and Electron starts with a one-line notice (MUSEPI_FORCE_BUILD=1 forces; the old full behavior survives as desktop:rebuild); build:fix's per-file chatter becomes one summary line per phase (VERBOSE=1 keeps the detail).

Changed

  • 分栏顶栏改版 + 标签条「+」surface 菜单(dsh TabStrip 行内工具组 parity):分栏后顶栏不再多出带「分栏」文字的独立控制行(用户 2026-09-29:标签条被挤换行)——该整行删除,+ / 合并分栏 / 最大化并入每列自己的标签条行内右侧工具组:标签 chips 盒为唯一收缩部分,工具按钮恒定宽度,标签条永不换行;+ 从「仅浏览器直开」升级为 surface 选择菜单(点按列出全部 primary 表面,选中的在该列新开标签页,浏览器额外开 about:blank),单列与分栏两种形态同一行为,消灭非浏览器表面上的死按钮。

    • EN: Split-pane header redesign + strip "+" surface menu (dsh TabStrip in-row tool group parity). Splitting no longer adds a separate control line carrying the "分栏" text (user 2026-09-29: the strip was pushed into wrapping) — that whole row is gone; + / merge / maximize move into each column's own tab-strip row as a right-end tool group: the chip box is the only shrinking part, tool buttons keep their width, and the strip never wraps. The + upgrades from a browser-only direct action to a surface picker (lists every primary surface; the pick opens a new tab in that column, browser additionally opens about:blank), behaving identically in single-column and split shapes and killing the dead button on non-browser surfaces.
  • 右栏 rail 改为纯启动器语义(dsh 0.2.0 rail parity):rail 图标按钮不再与「当前激活标签页实例」绑定——删 --active 强调色与 aria-pressed(溢出菜单项、扩展 tab 同删),悬停 tooltip、git 角标、⌘ 序号提示全部保留;点击行为维持 open-or-focus(upsert 去重聚焦),与 dsh「rail 打开内容、不做实例切换指示」的语义对齐(用户 2026-09-29:rail 不该像是某个实例的标签)。

    • EN: The right rail becomes a pure launcher (dsh 0.2.0 rail parity). Rail icon buttons are no longer bound to the active tab instance — the --active emphasis and aria-pressed are removed (overflow items and extension tabs alike) while hover tooltips, the git badge, and the ⌘ number hints stay; clicking keeps open-or-focus (upsert dedup + focus), matching dsh's "the rail opens content, it is not an instance indicator" semantics (user 2026-09-29: the rail must not read as some instance's tab).
  • design 预设五区块吸收 workdsh creation-prompts 两代演进方法(修订 3→4):role 区块新增「判断可验证三要素」——每个设计判断写明依据什么证据(内容层级/阅读顺序/品牌基准)、用什么尺度衡量(对比度/间距节奏/信息密度)、什么条件下改变结论,禁写「美观」「高级」这类无法判定的词;workflow 区块新增从用户参考与例子逐条倒推资源(效果对应什么版式/组件/素材,缺口先索取)、以真实渲染通过为产物完成标准、交付前自检清单(简报无未知项/渲染通过/manifest 齐全/风格未越界);brief 区块新增参考提取职责——截图/链接/描述要逐个说明提取什么(配色/版式/组件/文案语气),不把参考当装饰;artifact 区块补「写完文件不等于完成」的证据链(manifest 真实可解析、entry 真实可渲染、预览面板必须能识别);boundary 区块补修改既有界面的编辑保护(先读现状、局部修改、不推倒重来、不覆盖用户定制)。内置模板修订 3→4;LEGACY_TEMPLATES 全部冻结为字面量快照——rev2 档原用 spread 引用当前模板,模板一变历史档即漂移、真实老用户文件比对不上、升级链断裂(本次一并修复并留注释纪律);新增 rev3→rev4 升级路径回归测试(含「被用户改过的文件不升级」守护)。

    • EN: The design preset's five blocks absorb the workdsh creation-prompts two-generation method (revision 3→4). The role block gains the "verifiable judgment triad" — every design judgment states its