MusePi

MusePi GUI 实现笔记(契约与坑)

English | 中文

状态:活文档(2026-08-06 建立,从 gui-design.md 拆出)——packages/gui / packages/desktop-web 实现的事实记录:daemon RPC 契约、IPC 形状、算法语义、踩坑与验证方法。与实现同步,实现文件为准。

设计风格规范(布局/token/动效/组件模式)见 docs/gui-design.md

1. daemon 生命周期

musepi serve 由 Electron main detached spawn(daemon.cjs),GUI 退出后存活、重启 GUI 只重连不复用新代码——daemon 代码变更后必须重启它。实例菜单(header)有 「重启 daemon」 项(daemon-restart IPC):lsof 找监听 pid → SIGTERM → 等端口释放 → 重新 spawn → 等绑定 → GUI 自动重连(boot 链)。lsof 参数必须合成单 token -tiTCP:<port>(拆分会被当文件名)。主进程代码(main.cjs/daemon.cjs)不热加载——改动后必须重启 Electron 实例。

1b. 空闲回顾(recap,daemon 契约)

TUI recap.enabled/recap.idleSeconds(schema tab interaction、group Notifications)在 daemon 会话的完整对齐实现:

1c. 暂停(daemon 契约,2026-08-20)

暂停分两级,状态都只活在 daemon 侧,重连/重启 GUI 不丢:

1d. 连接恢复(2026-08-20,合盖睡眠唤醒冻结修复)

触发:Electron 在系统睡眠时断开 renderer↔daemon 的 WebSocket(electron#19993,localhost 同受);唤醒后必须重连。恢复链三层:

1e. 自定义供应商模型发现(models.discover,2026-08-22)

「获取可用模型」按钮的 daemon 契约:配置期对草稿端点的一次性询问,不写任何配置、不入缓存。

2. 扩展控制中心(daemon 契约)

设置「扩展」tab + 侧栏「扩展」入口共用 components/ExtensionsCenter.tsx,TUI /extensions parity。UI 形态(见 gui-design.md 无——此处只记契约):顶部 provider tabs(buildProviderTabs 排序:ALL → 有内容的 enabled → disabled → 空,disabled 灰显可点)+ 左侧 provider→kind(计数)→item 三级树(provider 级开关走 setProviderEnabled,native/内置节点只读;kind 折叠记忆)+ 右侧详情(名称/类型/描述/触发/来源 via X (等级)/路径/状态/指令内容/raw inspector 折叠)。

daemon RPC:

语义:状态三色点 绿 active / 灰 disabled / 橙 shadowed(详情显 shadowedBy)。provider 关闭后其条目从列表消失(loadCapability 层行为,与 TUI 同源,不是 bug)。内置判定 provider native omp-managed builtin-defaults。

3. Git 设置与功能(实现)

4. 会话设置与清理(算法)

5. 伙伴实现细节(双窗口:pet.html + bubble.html,2026-08-11 更新)

6. CSS 反模式清单(全部踩过坑)

  1. 自定义属性自引用/循环(--border: var(--border))→ guaranteed-invalid,静默消失。同坑复发:浮层 scrim 想写 --gui-glass-overlay: max(95%, var(--gui-glass-overlay)) 是自引用,静默回退继承值——必须直接覆盖 background 本身。
  2. calc 长度×百分比(calc(28px * 100%))→ 非法,静默回退 0。
  3. transform 动画 keyframes 替换静态 transform(translate(-50%,-50%) + scale 关键帧)→ 动画期间锚点丢失跳位;纯 opacity 或把完整 transform 写进关键帧。
  4. backdrop-filter 首帧闪烁 → 两阶段挂载(先 opacity 0 上屏,下一帧加动画类)。
  5. 挂载即动画会杀死磨砂(2026-08-06 实测,与 4 同根):带 transform: scalegui-menu-in 若在挂载帧直接播放(ContextMenu/Pop 旧实现),Chromium 真实屏幕合成器跳过 backdrop 采样,且动画结束后不重采样——菜单永久渲染成普通半透明(背后文字直接透出,无磨砂);useFloatingMenu 的两阶段(挂载帧无动画类,下一帧 rAF 加 --entered)不受影响。CDP 截图(offscreen 合成)仍显示模糊,是最大误导源——曾据此误判”transparent 窗口 blur 全失效”(electron#30412 是长期未解决的独立问题,但本 GUI 的菜单 blur 一直可修),错误地用 95% scrim 覆盖全部浮层(用户立刻发现”全都不是磨砂了”,已回退)。修复:ContextMenu/Pop 两阶段进入(--pending/--entered 类),Pop 调用方类移除自带 animation。验证浮层磨砂必须真实屏幕截图(screencapture -l 窗口 ID),CDP 截图/计算样式不算数。 全浮层统一(2026-08-11):共享 hook useTwoPhaseEnter(active)(gui/src/lib/use-two-phase-enter.ts,两 rAF 后返回 --entered 后缀,close 时重置)——接入此前越轨的 5 处:Board 放大/小组件任务/引导遮罩/⌘K 命令面板(改常驻挂载+退出动画)/选区工具条(补入场退场);base opacity:0 + --entered 使 motion-off 自然瞬现。这些点此前是挂载帧直接播 gui-fade-in(纯 opacity,无 scale)——属本条风险类的轻度变体,未在真实屏幕实测失效,但契约上违反两阶段,统一后消除差异。
  6. flex 子项缺 min-width:0 → 内容撑破/尺寸不一致;flex 子项 margin-inline:auto 会击败 stretch。
  7. popup/浮层被祖先 overflow/transform 裁剪 → portal 到 body。
  8. rAF 节流闩锁未在帧回调内释放 → 后续事件被吞。
  9. SVG stroke 渐变必须 gradientUnits="userSpaceOnUse"(2026-08-06 图标渲染实测):stroke="url(#g)" 配默认 objectBoundingBox 单位在 Chromium 渲染器(Chrome/Electron headless)整条 stroke 渲染为空白,同渐变 fill 正常;显式坐标 + userSpaceOnUse 即恢复。涉及 app 图标 SVG 渲染(build/icon.svg)时必踩。
  10. 无层(unlayered)规则压制所有 @layer 规则(2026-08-06 侧栏 tabstrip 贴边根因):desktop-web base.css 的 *{margin:0} 是 unlayered,Tailwind v4 utility 全部在 @layer utilities——cascade 里 unlayered 恒胜所有 layer,于是侧栏 mx-2.5/mt-3/ml-auto 等全部 margin utility 静默算成 0px(胶囊贴左边缘、右侧按钮簇紧跟胶囊、mt-3 间距消失),而 padding utility 正常(没有 universal padding reset),症状极具迷惑性。修复:reset 移入 @layer base,前置空 @layer theme/base/components/utilities {} 块钉死顺序(tailwind CLI 会把 @layer a,b,c; 语句规范成这种空块形式,构建幂等)。教训:GUI 里 Tailwind margin utility 不生效先查 unlayered universal margin reset
  11. 误截断源文件的恢复路径(2026-08-06 事故记录):head -c <N>字节截断大 CSS(8251 行 ≈ 370KB),git 无 WIP 提交、无 sourcemap、无 TM 快照时,唯一完整恢复源是上次 bun run build 的 dist 压缩 CSS(含全部规则):①选择器集合 diff(HEAD vs dist)枚举 WIP 规则;②从 dist 提取每条规则(压缩单行,按 { 平衡扫描 + 向前回溯选择器列表起点,合并逗号组合块);③@media 内规则记录上下文;④@keyframes 逐一比对(注意 ag-*/tr-*/tv-*/spin 等属 desktop-web,勿混入 gui.css);⑤格式化重排后追加,带恢复注释。恢复后功能等价,格式/注释丢失。教训:大文件截断前先 wc -c;给重要 CSS 定期 git add(index blob 可救)。
  12. legacy keyframes 烤静态 transform 在 flow 布局下错位(2026-08-11):面板 keyframes 烤 translateX(-50%)(旧 absolute 居中残留),新布局已 transform: none(flow + margin 居中)——动画播放时把元素左移半宽,”先露右半再突现左半”。教训:改布局定位方式时必须同步审计 keyframes 里的静态 transform;动画与静态布局解耦用 flat keyframes(只动位移/缩放/模糊的相对量)
  13. 动画中 getBoundingClientRect 含 transform(2026-08-11):scale(0.98) 入场动画中 rect 是缩小值——内容驱动窗口按它报告会把窗口定小,animationend 后跳变。内容尺寸报告对动画中元素用 offsetWidth/offsetHeight(布局盒);检测 el.getAnimations().some(a => a.playState === "running")
  14. 锁宽测量 width:"auto" 覆盖 CSS max-content(2026-08-11):morph 测量目标宽度时 style.width = "auto" 对块级元素=撑满包含块(覆盖 CSS width:max-content),toW 退化为容器宽 → 宽度过渡静默跳过(高度正常,视觉”只缩高不缩宽”再跳变)。必须 style.width = ""(删 inline 声明回 CSS 值)
  15. 浮层卡面 class 双应用 = 嵌套双画圆角(2026-08-15,自定义强调色选色器):useFloatingMenu(…, { className }) 的 class 落在外层 portal 容器,内容组件根若带同一卡面 class(ColorPickerPanel 根 gui-color-picker),两层同时拿到磨砂+圆角+阴影 → 背景多画一层圆角容器(div.gui-menu-popup.gui-color-picker.gui-menu-popup--entered > div.gui-color-picker)。规则:卡面 class 只能出现在一层——传 className 时内容平铺(proj/todo/queue/creds 类);不传时内容根自持卡面(quota/context/color-picker 类)。验证:document.querySelectorAll('.gui-color-picker').length === 1 且外层 computed border-radius: 0background: transparent

7. macOS 应用图标处理(2026-08-06 查证 + 修复)

三条独立路径,规则完全不同:

  1. 打包版 Dock / Finder 图标 = bundle 内 Contents/Resources/icon.icns(electron-builder 默认 buildResources/icon.icns = build/icon.icns)。LaunchServices 解析 bundle 时自动应用系统 squircle 遮罩 → icns 必须全出血 1024 方形、绝不能自画圆角(否则双重圆角)。生成链:SVG → 1024 PNG → iconutil -c icns(10 档 16-1024 @1x/@2x)。release 打包后手动同步 bundle icns(md5 一致)。
  2. dev 模式 Dock 图标 = app.dock.setIcon(image):官方语义只是”贴图到 NSDockTile”,不经过 LaunchServices,系统遮罩不生效 → 传全出血方形 PNG 就是方角(用户看到”图标是方的”的根因)。dev 必须用预圆角 PNG(build/icon-dock.png)。填充率以 kimi 实测为准 = 80.5%(2026-08-06 /Applications/Kimi.app/Contents/Resources/icon.icns 解出 1024,alpha bbox x100-923):深色卡 824/1024 居中 + 四角 superellipse n=5 切圆 + 四周对称透明边距。全出血 100% 会让 Dock 里图标比 kimi 大 ~24%(用户”始终大一点”根因;92% 内缩版仍 >80.5%);”禁止内缩”的上一版结论错误——kimi 官方桌面资产就是卡中卡,与邻位 app 视觉统一优先。Python/numpy 生成。打包版 icns 同样带 80.5% 边距(卡中卡是设计,不是遮罩错误)。
  3. 窗口 icon(BrowserWindow icon)= Linux/Windows 窗口 chrome;macOS 上 Cmd+Tab/窗口预览由系统按 bundle/Dock 图标渲染(全出血 + 系统遮罩),窗口预览显示”大方块”是窗口内容预览而非图标,系统行为。

splash 内嵌 logo(src/vendor/logo.png)= UI 内 img,系统遮罩不适用,同样要预圆角版本(512,与 icon-dock 同源同参数);.gui-splash-logo 不能再加 CSS border-radius(双重圆角)。换图标 = 三处同步:build/icon.icns(打包)+ build/icon-dock.png(dev Dock)+ src/vendor/logo.png(splash/内嵌)。

8. 验证工作流(改 UI 的必做项)

9. 平台适配(2026-08-11)

10. 受管浏览器(Proma 吸收,2026-08-11)

右侧 Browser 工具从”独立 webview”升级为受管浏览器:Electron 主进程持有 WebContentsView(每 tab 一个),agent 的 browser 工具通过本地 CDP 桥驱动同一个实例——用户在面板里看到的页面就是 agent 操作的页面,登录状态天然共享(Proma browser-controller 模式)。

架构

配置(settings-schema + 设置 → 工具 → Grep & Browser)

踩坑(验证过的)

  1. about:blank 初始态 debugger 挂死:对未完成初始加载的 webContents debugger.attach 后所有命令永久 pending,导航后才活。修复:backgroundThrottling: false + loadURL("about:blank") 强制 renderer 启动 + whenDebuggerReady()(等 did-finish-load)后再 attach。
  2. Target.setAutoAttach(waitForDebuggerOnStart:true)转发到真实 debugger 会 wedged:Electron 单会话 debugger 无子 target;page 会话里拦截 setAutoAttach/setDiscoverTargets/runIfWaitingForDebugger 本地应答 {}
  3. Page.captureScreenshot 在 webContents.debugger 上超时:改拦截 capturePage()(Proma 同款)。
  4. tab 级 attachedToTarget(page) 事件必须带消息级 sessionId(scope 到 tab 会话),否则 puppeteer 的 #targetsIdsForInit 永不完成、connect() 死等。

验证

边界项落地(2026-08-12,均 E2E 验证)

10.1 最佳实践(使用 + 工程)

使用(desktop)

工程

12. 用量视图(usage.reports / 托盘 / ContextRing,2026-08-16)

daemon RPC usage.reports(server.ts,session.askAnswer 之后):会话态(params.sessionId → live session 的 fetchUsageReports)+ 全局态(无 sessionId,空态 composer 用 ensureRegistry() 起 registry)。返回 { reports, unreportedAccounts, disabledCredentials, reloginDeadlines, activeAccount? } —— 与 TUI /usage 同源(usage-shared.ts 共享聚合),activeAccount 仅会话态有(● 标记)。

数据形状:每凭据一个 UsageReport(provider + limits[] + metadata);同 provider 多凭据 → GUI 端必须合并,否则:

合并算法(gui/src/components/composer/usage-panel.tsx UsageProviderSection,托盘 tray-menu-main.tsx buildUsageRows 同款):

托盘菜单(gui-tray-menu):固定窗口高 TRAY_MENU_HEIGHT = 440(main.cjs)——不要动态 resize(tray-menu:set-size IPC 已删);内容内部滚动(__scroll flex:1 + overflow-y:auto);footer padding 10px 10px 14px(按钮距窗底有呼吸感)。窗口自身 acrylic(DWM),页面内容 chrome 即可。

用量缓存(packages/ai AuthStorage.fetchUsageReports,GUI/TUI/托盘共用):SQLite cache 表磁盘持久化 + 5min TTL(USAGE_REPORT_TTL_MS,±25% 抖动防 per-IP 429 fan-out)+ 上游失败 last-good 兜底(24h)+ in-flight 合并(多界面并发请求只打一次上游)。daemon 重启后缓存仍在;冷缓存首次查看会阻塞上游一轮(合并保证只一轮)。

验证套路:组件级 headless(bun build 临时 entry + 桩 electronAPI.trayMenu/props)→ 断言 DOM 列序/合计/无 key 警告;真实托盘需重启 Electron(主进程改动不热重载)。

13. slash 补全排序(2026-08-16)

gui/src/lib/slash-rank.ts rankSlashEntries(entries, query, guiNative)(会话 Composer use-completion.ts + WelcomeComposer 共用):

14. 轨迹 Overview 时间轴 + 选择检视(2026-08-21,DSH Trajectory 吸收)

设计规范见 gui-design.md §1(轨迹时间轴与检视)。此处记数据契约与坑。

数据契约

组件行为

验证

packages/gui/test/trajectory.test.ts 10 用例(tsMs 提取 / turn 时序 / roundDurations Map+数组形态 / 未命中不闭合 / 范围判定 / usage·duration·ttft 提取);tsgo -p tsconfig.json --noEmit 全绿;bun run build:bundle 通过。CDP/截图验证按 §8 工作流。

15. 消息树数据 seam(/tree 语义,2026-08-21)

命名规范化见 docs/gui-design.md §0(会话列表 / 消息树 / 轨迹 术语表)。此处记数据契约:

16. 转录自定义消息渲染 + 流式 markdown 契约(2026-08-22)

desktop-web components/transcript/Transcript.tsxcustom_message 分支按 entry.customType 分派,已处理:

customType 渲染 数据来源
collab-prompt 用户行 + 来源 badge details.from
ttsr TtsrBlock(警告折叠) details.rules[]
irc:* tr-irc details.message/body
async-result .tr-async-result 卡片(每 job 一行「✓ 后台任务已完成 [type] id (耗时)」) details.jobs[]
advisor AdvisorBlock(severity 色 rail + badge、blocker 计数、>3 条折叠) details.notes[]
其他 默认 tr-custom(chip customType + content markdown)

铁律:模型-facing 模板(<system-notice><advisory severity=…>)只存在于 content(给 LLM 的 payload),GUI 渲染器只读 details.* 干净文本,绝不把模板正文渲染给用户——async-result/advisor 都是 2026-08-22 补上 GUI 渲染(此前落入默认 tr-custom,把 <system-notice>/<advisory> XML 原样显示,与 TUI 的 buildAsyncResultBlock/createAdvisorMessageCard 不对齐)。

流式 markdown 时机(答「是不是结束后才渲染」——不是):

17. OTA 更新渠道 + 三合一按钮 run 级 working 语义(2026-08-22)

OTA 更新渠道(GitHub release 资产重定向,bitfun parity)

三处同源,统一走 /releases/latest/download/update-manifest.json(302 到最新 release 资产,无 api.github.com 限流):

位置 用途 默认值
gui/electron/updater.cjs 主进程 OTA 检查(checkForUpdates)+ 更新说明拉取(fetchManifestNotes RELEASE_MANIFEST_URL 常量
gui/package.json update.manifestUrl 打包时覆盖 notes 拉取默认 同 URL
daemon/server.ts updates.check daemon 侧版本/notes 探测(遗留——UpdateToast 已改走 updater-notes;RPC 保留对等) 同 URL(硬编码)

三合一发送/停止按钮:run 级 working(turn 级边界陷阱)

SendOrStopButtongui/src/components/composer/action-buttons.tsx)idle 显示发送箭头,working 态变胶囊 + 点阵 bloom + 两标签(「工作中」/「停止」,hover 互换)。关键陷阱

更新提示 toast(bitfun DailyAppUpdateGate parity)

发布产物与 CLI 关系(2026-08-23 实测确认)

dmg 自包含 daemon,不依赖 bun run setup 污染系统daemonCommand()electron/daemon.cjs)解析顺序——① PATH 上的 musepi(仅当用户单独装过)→ ② 打包版 Resources/app.asar.unpacked/vendor/daemon/musepi(120M 完整 CLI 二进制,workflow 的 “Build daemon binary + Stage 进 vendor + asarUnpack vendor/daemon/” 保证必达)** → ③ dev 模式 bun src/cli.ts serve

发布链路要点

18. 近期落地特性(2026-08-24 → 2026-08-26)

早期章节之后落地的契约、RPC 形状与坑;设计意图在 docs/gui-design.md §5g,分特性规格见所列文档。

OTA 经 electron-updater 更新(v0.4.4,2026-08-24)

docs/ota-update-design.md。把 §17 的「前往下载」改为 下载 → 校验 → 安装 → 重启(electron-updater v6.4.1 + GitHub provider):

扩展 P3/P4 接缝(plugin-design.md P 层)

自 2026-08-25 核对(P3 ❌ / P4 service ❌)后已落地。在 extensibility/extensions/

看板 widget 数据代理缺口 + 调度引擎(board-dashboard.md)

widget.data 代理 RPC 未实现(行情卡为静态默认值;数据源 agent 代理是剩余 M4 工作);调度执行引擎已在 GUI 侧实现desktop-web task-run 执行引擎 + BoardPage 30s poll 消费 data.task.schedule;手动 run 走同一执行器而非 setTimeout)。

TUI /trace + /tree

/tree(结构投影)与 /trace(同一 entry 树的时间/成本投影)都已在 TUI 落地——tree-selector.ts TreeProjection = "tree"|"trace"/trace slash 命令(builtin-session.tsshowTraceSelector)。数据源 = tree-selector 的 SessionEntry 树 + live AssistantMessage.usage/.duration/.ttft(零新数据依赖)。GUI 侧消息树 seam(message-tree.ts buildMessageTree)仍为向前兼容路径。方案:docs/tui-trace-plan.md

musepi ps CLI

cli-commands.ts 注册 pscommands/ps.tsrunPsCommand):action list|info|logs|stop|kill|restart;flag -a/--all-j/--json--plain--dir--global-f/--follow--head-n/--lines--grep--timeout。从 harness 外部查看/控制 daemon-broker 托管进程(机器全局 --global 作用域,如 browser-relay)。

遥测指标更名 → pi.musepi.agent.*

telemetry-export.ts OTel 指标/属性统一命名空间 pi.musepi.agent.*(counter:chat.cost.estimated_usdrunsstepschat.callstool.callserrors;histogram:chat.durationtool.duration;另有 pi.musepi.agent.run.completed 事件)。chat token 记录仍打 pi.gen_ai.agent.id/pi.gen_ai.agent.nametest/otel-signals-probe.ts 验证。

win32 磨砂玻璃修复(2026-08-26)

gui-base.csshtml:root, html:root body { background: transparent }(此前被忽略、带不透明 var(--bg) 的一层);[data-platform="win32"] .gui-main, [data-platform="linux"] .gui-main { backdrop-filter:none }(模糊来自窗口材质);[data-platform="win32"][data-theme="light"] scrim 22–58%。

dsh-desktop 兼容链(运行时 serve 渲染器 + host-mode,2026-08-27)

dsh-desktop 对齐目标是壳包装运行时提供的渲染器,而非捆绑 Electron 耦合的 UI。MusePi 的三半:

坑位:tsgo 下 server.port 类型为 number | undefinedconst port = server.port ?? options.port);host 视图是 v1 最小recap/approval-request/ask-request 事件忽略、sendUiResponse no-op、subagent chat/kill 走 agents.* rpc);desktop-web 绝不能 import @musepi/gui(分层:collab-proto ← desktop-web ← gui),故 host client 放在 desktop-web,GUI 在需要处自己供应 slot-host。

Frame 叠加 + host 富交互(2026-08-27):compat 壳以 ?shell=1 通知渲染器 — desktop-web/src/lib/compat-shell.ts isCompatShell() 精确匹配值 "1";页面随即加 .sh-app--compatpadding-top:48px)+ .compat-titlebar(48px fixed 拖拽条,-webkit-app-region: drag),Electron 以 compatUrl + "?shell=1" 加载并沿用既有 titleBarStyle:"hidden" + titleBarOverlay:{height:48}(win32/linux)。纯浏览器中无效(无 OS 控件)。host 视图现已接线富交互:HostClientask-requestuiRequest(Composer 渲染,sendUiResponsesession.askAnswer,经 #askReqIds 桥接 daemon 字符串 id ↔ composer 数字 reqId)、approval-requestapprovalRequestcomponents/shell/ApprovalCard.tsx 渲染允许/拒绝 → tool.approve/tool.deny)、recap→notice。GuestSnapshot/SessionClient 新增 approvalRequest + respondApproval;collab guest 两处保持 null/no-op。

Compat slot host(serve 注入,2026-08-28):desktop-web 保持被动渲染器——musepi serve?shell=1(Electron compat)请求根路径时,把 compatSlotHostScript() 注入 </head> 前:脚本开自己的 daemon WS(读 /__daemon.json),调 extensions.list,过滤 slot:"transcript.node" 组件,blob-import 已编译 ESM(react 绑定 window.MusePiReact,desktop-web 入口同 GUI 一样暴露),然后 window.MusePiCompatHost.register(slot, entryKinds, Component, extensionId)。Transcript 在未注入 renderTranscriptNode 时(独立页/ compat 页)回退查该注册表按 kind 分派——纯浏览器 guest 无注册表,内建渲染不受影响。注入缝在 static-web.ts;seam 是 Transcript 条目行的 data-entry-kind/data-entry-id(被动的 DOM 锚,React 树不改)。

desktop-shell 扩展 + Shell 三模式(2026-08-28 增补):Electron 壳是一等扩展(kind:"desktop-shell", id desktop-shell:shell, builtin-registry.ts)——extensions.list 顶层 shell: { enabled, mode, webUrl }shell.enabled/shell.mode 设置键驱动,webUrl 来自 daemon --web-port);extensions.setEnabled("desktop-shell:shell", { enabled, mode }) 切换开关与模式并管理 web.port 发现文件(壳 probeWeb() 读它 loadURL compat or 本地 bundle)。注入脚本按 registry.shell.mode 选 slot 集合:compatibility=transcript.node;extended/enhanced 加 composer.dock/panel.tab.workbench/statusbar——desktop-web 的 CompatSlotHostlib/compat-slot-host.tsx,memo 化)按 slot 渲染,App 挂 composer.dock(composer 上方)/statusbar(底部)/workbench 面板(HeaderBar GuestPanel)。桌面 host 视图标 sh-app--host

19. GUI 吸收轮(2026-08-29):git 图谱 / 浮动状态卡 / 奖励弹窗 / 最大化层级

侧栏排序 / tab 动效 / 切换卡顿打磨(2026-08-29,用户反馈第二轮)

20. Windows NSIS 快捷方式持久化(2026-08-30)

症状:OTA 更新后桌面快捷方式消失。根因(两层):

  1. electron-builder KeepShortcuts 保留机制——首次安装向 HKCU\Software\<APP_GUID> 写入 KeepShortcuts=true(GUID = appId 的 UUID v5,multiUser.nshINSTALL_REGISTRY_KEY)。后续安装时 installSection.nsh 读它:带 KeepShortcuts=true 且 exe 存在 → 走保留分支(仅在新旧路径不同时改名;oldLink == newLink不重建)。用户/清理工具删掉的桌面快捷方式,任何后续更新都不会重建。
  2. createDesktopShortcut:"always" 对更新无效:它只定义 RECREATE_DESKTOP_SHORTCUT(NsisTarget.js),该分支被 ${ifNot} ${isUpdated} 门控——electron-updater 以 --updated 参数拉起安装器(NsisUpdater.js 参数 ["--updated","/S",...]),所以更新永远跳过桌面重建,即便设了 “always”。

修复(open-design custom-NSIS parity):packages/gui/release/ensure-shortcuts.nsh 定义 customInstall 宏——installSection.nsh 在文件安装完成后调用它(!ifmacrodef customInstall)。它无条件 CreateShortCut 桌面 + 开始菜单快捷方式(绕过 keepShortcuts/isUpdated 门控)并通知壳。经 packages/gui/package.jsonnsis.include 接线。

验证(真实安装器,0.4.7):

21. 任务中心加固(2026-09-03):cron 契约、时区语义、运行历史

任务中心页(gui/src/components/ScheduledTasksPage.tsx + TaskCenterViews.tsx)是 daemon cron 存储(~/.musepi/crons.json + crons.runs.json;调度器在 coding-agent/src/daemon/server.ts,合并/校验/下次运行在 daemon/crons.ts)的唯一 GUI 客户端。

cron.* RPC 契约

crons.changed 广播(新增)

每次 cron 变更与运行开始/结束后,daemon 在 events.subscribe 流上广播 { type: "crons.changed", at }(与 extensions.changed 同 seq 机制;payload 只带时间戳——客户端重拉 cron.list)。消费方:ScheduledTasksPage(即时刷新)与 app 级运行完成通知。两者均保留轮询兜底(页面 30s / app 20s);collab guest 收不到广播,维持纯轮询。

调度语义(crons.ts)