diff --git a/.agents/skills/release-notes/SKILL.md b/.agents/skills/release-notes/SKILL.md index 200659235..ef24c637b 100644 --- a/.agents/skills/release-notes/SKILL.md +++ b/.agents/skills/release-notes/SKILL.md @@ -79,7 +79,7 @@ description: 发布 desktop(kimi-code-app)新版本前,生成更新弹窗 - 逐条对译,不增不删,不补充中文原文没有的信息(原文含糊就保持含糊,交给 review)。 - 用户面向的短句,一条一句、每条 ≤ 15 词;同一小节内句式节奏保持一致(如同为 `Fixed ...` 起手)。中文已是压缩后的一句话,英文不得再拉长。 - 修复类 `Fixed ...`;新功能类直接说用户得到什么(`Updates can now download in the background.`)。不出现文件名、内部包名、PR 号。 -- 产品术语对齐现有英译:翻译前先查 `packages/web-i18n/src/locales/en/` 里相应说法(如 会话→session、工作区→workspace、思考→thinking、设置→Settings),保持一致。 +- 产品术语对齐现有英译:翻译前先查 `packages/app-i18n/src/locales/en/` 里相应说法(如 会话→session、工作区→workspace、思考→thinking、设置→Settings),保持一致。 - 拿不准如何翻译的产品名/功能名保留英文原词(如 Plan mode、Thinking)。 ## 红旗 diff --git a/.changeset/config.json b/.changeset/config.json index f3453d0c8..b41bfd1c9 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -12,10 +12,10 @@ "ignore": [ "kimi-code-web", "@moonshot-ai/vite-preset", - "@moonshot-ai/web-core", - "@moonshot-ai/web-i18n", - "@moonshot-ai/web-markdown", - "@moonshot-ai/web-ui" + "@moonshot-ai/app-core", + "@moonshot-ai/app-i18n", + "@moonshot-ai/app-markdown", + "@moonshot-ai/app-ui" ], "snapshot": { "useCalculatedVersion": true, diff --git a/.gitignore b/.gitignore index bf804da83..0b349f8c8 100644 --- a/.gitignore +++ b/.gitignore @@ -12,9 +12,9 @@ coverage .ci-signing/ .env mac_keys -packages/web-ui/src/assets/fonts/NotoSansSC\[wght\].woff2 -packages/web-ui/src/assets/fonts/SpecialGothic\[wdth,wght\].woff2 -packages/web-ui/src/assets/fonts/SchibstedGrotesk\[wght\].woff2 -packages/web-ui/src/assets/fonts/SchibstedGrotesk-Italic\[wght\].woff2 -packages/web-ui/src/assets/fonts/ZalandoSans\[wdth,wght\].woff2 -packages/web-ui/src/assets/fonts/ZalandoSans-Italic\[wdth,wght\].woff2 +packages/app-ui/src/assets/fonts/NotoSansSC\[wght\].woff2 +packages/app-ui/src/assets/fonts/SpecialGothic\[wdth,wght\].woff2 +packages/app-ui/src/assets/fonts/SchibstedGrotesk\[wght\].woff2 +packages/app-ui/src/assets/fonts/SchibstedGrotesk-Italic\[wght\].woff2 +packages/app-ui/src/assets/fonts/ZalandoSans\[wdth,wght\].woff2 +packages/app-ui/src/assets/fonts/ZalandoSans-Italic\[wdth,wght\].woff2 diff --git a/AGENTS.md b/AGENTS.md index d155296a2..ace057c59 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ ## 硬约束 - **依赖方向 `code-app → kimi-code` 单向**:desktop 只经 `@moonshot-ai/*` 包名 import kimi-code 的 packages 源码,禁止跨包相对路径 import;`kimi-code` 不得 import `code-app`。 -- `apps/web` 只依赖 `@moonshot-ai/{web-core,web-i18n,web-markdown,web-ui}` 共享包,不直接 import kimi-code 的包。 +- `apps/web` 只依赖 `@moonshot-ai/{app-core,app-i18n,app-markdown,app-ui}` 共享包,不直接 import kimi-code 的包。 - **不改包名**:`kimi-code-web`、`kimi-code-app`。 - **两端逐步分叉是既定方向**:desktop 的原生功能(`window.kimiDesktop` 桥接)只在 `apps/desktop` 实现,web 保留原 daemon 实现、不回填;原生路径必须带无桥降级(探测不到桥时回退旧实现)。分叉清单在 `apps/desktop/docs/native-todos.md`——改两端共有的文件前先查它,手动同步副本时保留 desktop 侧的分叉块。 - **开发顺序**:两端共有的 UI 改动优先在 `apps/desktop` 开发,完成后再同步到 `apps/web`(desktop 专属原生功能除外,见上条)。 @@ -27,7 +27,7 @@ - `apps/desktop`:Electron 壳(`kimi-code-app`);简介见 `apps/desktop/README.md`,原生功能分叉清单见 `apps/desktop/docs/native-todos.md`。新测试:主进程进 `tests/main/`,renderer 进 `tests/renderer/`。 - `apps/web`:浏览器 Web UI(`kimi-code-web`,Vue 3 + Vite + vue-i18n)。dev 时 Vite 把 `/api/v1`(REST + WS)代理到 `KIMI_SERVER_URL`(默认 `http://127.0.0.1:58627`)。 -- `packages/*`:`@moonshot-ai/{web-core,web-i18n,web-markdown,web-ui}` + `vite-preset`(exports→src,被 apps/web 与 desktop renderer 复用);共享字体产物在 `web-ui/src/assets/fonts`(gitignored),由 `scripts/prepare-fonts.mjs` 自动准备。 +- `packages/*`:`@moonshot-ai/{app-core,app-i18n,app-markdown,app-ui}` + `vite-preset`(exports→src,被 apps/web 与 desktop renderer 复用);共享字体产物在 `app-ui/src/assets/fonts`(gitignored),由 `scripts/prepare-fonts.mjs` 自动准备。 - `kimi-code/`:git submodule(核心仓)。`kimi-code/packages/*` 提供 `kap-server`、`agent-core-v2`、`kimi-code-sdk` 等源码。 - `scripts/sync-web-to-kimi-code.mjs`:`apps/web/dist` → `/apps/kimi-code/dist-web`(`KIMI_CODE_REPO` 必传,指定目标 checkout)。 - `KIMI CODE LOGO/` + `scripts/build-brand-icons.mjs`:品牌源文件与 desktop 图标资源的生成脚本(`pnpm build:icons`);`apps/desktop/build/` 的图标与 desktop 组件内联品牌标是产物,勿手改。**web 品牌已分叉**:`apps/web` 的侧栏 / onboarding / favicon 用回旧版小蓝标,不由该脚本生成(脚本刻意不再写 apps/web),勿用机器人标冲掉。 diff --git a/README.md b/README.md index 588c138ca..b65b7c1d0 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ OAuth 授权完成页唤起桌面端窗口(登录态由 daemon 轮询完成, - `apps/desktop`:Electron 桌面端(`kimi-code-app`) - `apps/web`:浏览器 Web UI(`kimi-code-web`) -- `packages/*`:web 共享包(web-core / web-i18n / web-markdown / web-ui / vite-preset) +- `packages/*`:web 共享包(app-core / app-i18n / app-markdown / app-ui / vite-preset) - `kimi-code/`:核心仓 submodule(CLI / server / agent / packages) - `KIMI CODE LOGO/`:品牌设计源文件(SVG/PNG),下游图标资源由 `pnpm build:icons` 生成 - `scripts/`:字体准备(`prepare-fonts.mjs`)、品牌图标生成(`build-brand-icons.mjs`)、web 产物同步(`sync-web-to-kimi-code.mjs`)等 diff --git a/apps/desktop/docs/native-todos.md b/apps/desktop/docs/native-todos.md index 983a5efb8..5918395ec 100644 --- a/apps/desktop/docs/native-todos.md +++ b/apps/desktop/docs/native-todos.md @@ -40,7 +40,7 @@ - 实现:「用 xxx 打开」全链路在主进程、不走 daemon REST。`src/main/open-in.ts` 负责应用目录检测(macOS only:`/Applications` + `~/Applications` 存在性;Finder/Terminal 系统恒有)与启动(`open -a ` / Finder 裸 `open ` / Terminal 用 `open -b com.apple.Terminal`,各 app 的文件夹打开能力已对 Info.plist 核实;Xcode 未声明 folder 注册,纯目录可能只弹提示——已知限制)。IPC:`kimi:open-in-list` / `kimi:open-in`(`ipc-channels.ts` + `ipc.ts`,参数在 handler 校验),preload 白名单加 `listOpenInApps` / `openInApp`(`preload.test.ts` 同步)。renderer 侧 `src/renderer/lib/nativeOpenIn.ts`:`canOpenInNative()` 探测(桥缺方法也判 false),列表/打开失败一律归 false/[](无桥降级=隐藏入口,不回退 REST,用户明确决定)。`ChatHeader.vue` 挂载 `OpenInMenu.vue`(desktop 分叉块:import/onMounted/模板 `showOpenIn` 块,整目录 re-copy 时需保留);`OpenInMenu.vue` 与 web 版整体分叉重写:紧凑 pill(左=当前选中应用图标,点击只打开不写盘;右=caret 展开菜单,菜单项点击才=打开+选中),菜单项带彩色图标。选中态单源:菜单点击与设置页写同一个 key `kimi-web.open-in.default-target`(`saveDefaultOpenInTarget`,模块级响应式 ref `useDefaultOpenInTarget`,改动同 tick 同步到 pill 与设置页),未选择时显示第一个可用(`resolveOpenInTarget`)。注意 pill 自带 `-webkit-app-region: no-drag`(ChatHeader 的 scoped no-drag 规则到不了子组件内部,不写会被 macOS 拖拽区吃掉点击);菜单打开期间在 capture 阶段吞掉 Escape(ConversationPane 的 document 级 bubble 监听会把 Esc 当中断运行,capture 先执行,Esc 只关菜单)。设置项在 `SettingsDialog.vue` 通用页独立的「打开方式」分组(desktop 分叉块,`openInAppOptions` 为 null 即整组隐藏),选项即检测到的应用列表。 - 测试:`tests/main/open-in.test.ts`(13 用例:平台门控、检测、argv 构造、失败回传);`tests/main/preload.test.ts` 白名单;`src/renderer/lib/nativeOpenIn.test.ts`(16 用例:桥探测、列表过滤、打开回传、默认目标持久化、快捷解析优先级)。 - 遗留:打开失败暂无 UI 反馈(静默);「打开文件 / 在 Finder 显示」(FilePreview 的 `openWorkspaceFile`/`revealWorkspaceFile`)仍走 daemon REST,未原生化。 - - 图标:彩色官方图标从本机 app bundle 的 `.icns` 提取为 128px PNG(`src/renderer/assets/app-icons/`,11 个),经 `lib/nativeOpenIn.ts` 的 `openInAppIcon(id)` 映射;菜单项与快捷按钮用 `` 渲染(nominative use),设置页 Select 靠 web-ui `Select` 新增的 `option.icon` 字段(可选、向后兼容,web 同步受益)。 + - 图标:彩色官方图标从本机 app bundle 的 `.icns` 提取为 128px PNG(`src/renderer/assets/app-icons/`,11 个),经 `lib/nativeOpenIn.ts` 的 `openInAppIcon(id)` 映射;菜单项与快捷按钮用 `` 渲染(nominative use),设置页 Select 靠 app-ui `Select` 新增的 `option.icon` 字段(可选、向后兼容,web 同步受益)。 - 已知限制:仅 macOS / Windows(2026-07 已补 Windows 侧:VS Code / VS Code Insiders / Cursor / Zed 走 LOCALAPPDATA/ProgramFiles 固定路径探测;File Explorer 恒有;Terminal 优先 `wt.exe`、不可用时回退 Windows PowerShell;Git Bash 走 Git for Windows 的 LOCALAPPDATA/ProgramFiles 路径探测;启动全部 detached spawn——explorer.exe 成功也常返回非零退出码,不看 exit code;图标为从 exe / 安装包资源提取的官方图标);Linux 返回空目录并隐藏入口。 - [x] **文件导出走保存对话框**(已完成,desktop 专属) @@ -82,7 +82,7 @@ - [x] **内置终端(Electron 原生,不走 server)**(已完成,desktop 专属) - 实现:PTY 由主进程 `node-pty` 直接托管,与内嵌 server / daemon 终端服务无关(方案见 `docs/plans/2026-07-27-desktop-native-terminal.md`)。主进程 `src/main/terminal.ts`:`createTerminalManager(deps)` DI 纯函数 + 模块级单例(`initTerminalManager` 在 `registerIpcHandlers` 里装真 deps——locale/homedir/existsSync/push 通道),`node-pty` 动态 import 懒加载(坏了只挂单个终端不挂主进程);shell 解析 `resolveDefaultShell`(POSIX `$SHELL` 兜底 `/bin/zsh`·`/bin/sh`,Windows pwsh→powershell→cmd 链,**Windows 路径恒走 `path/win32`**——宿主无关,否则 mac 宿主上探测路径拼成混合分隔符;COMSPEC 永不作首选)+ `defaultShellEnv`(GUI 启动缺 `LANG`/`LC_ALL` 时按 `app.getLocale()` 补 `zh_CN/en_US.UTF-8`,防终端中文乱码);cwd 只收 renderer 传的工作区 root(`isDirectory` 校验,无效回退 home),cols/rows 钳 1–500。生命周期:`app.ts` before-quit、`window.ts` 的 `render-process-gone`(先 bump generation 再杀——在途 create 回来即 superseded,不给死页面留孤儿)与 **`did-finish-load`**(新文档提交后才 kill——导航开始时不杀,reload 失败/中止时旧页面与 PTY 都保留)调 `killAllTerminals()`——终端状态不跨 renderer 重载保留。IPC 六个 channel(`ipc-channels.ts`):create 走 invoke(校验 `event.sender === win.webContents` + payload;**handler 先 `await startShellEnvProbe()`**——memoized 启动探测,GUI 启动的首个 PTY 也继承完整 PATH/Homebrew/NVM,不必等下一个 tab;**await 前后捕获并复查 renderer generation**——探测可达数秒,期间 reload 会 bump gen 且 stale 清扫已跑完,此时 spawn 必成无主孤儿,变则抛错),input/resize/close 走 send(逐字段校验),output/exit 为 renderer event(`RendererEventChannel` 已登记)。preload 白名单加 `createNativeTerminal` / `nativeTerminalInput` / `nativeTerminalResize` / `closeNativeTerminal` / `onNativeTerminalOutput` / `onNativeTerminalExit`(`preload.test.ts` 同步)。 - - renderer(全部 desktop-only 新文件,不同步 web):`composables/useNativeTerminal.ts` 模块级单例状态机(按 sessionId 分桶的 tabs/activeTabId/open + error;桥探测 `canUseNativeTerminal` 缺桥整体 no-op;输出/退出订阅一次按 tab id 全局路由——后台 session 的视图保持挂载持续接收输出;closeTab 修 active;restartTab 先建后杀保位置复用 cwd;**终端状态按 sessionId 分桶**(`switchSession` 由 App.vue watch activeSessionId 驱动,draft 态共用 `__draft__` 桶)——切换 session 只换可见桶,旧桶的 PTY 与 xterm 视图全部保留(scrollback、全屏 TUI 画面都在),切回来原样恢复;**bucket 懒创建**(浏览 session 不占 LRU 名额;**创建即打 lastUsed 时间戳**——switchSession 只盖已存在的桶,不盖的话新桶 lastUsed=0 会被当成最老的首选受害者),上限 `MAX_TERMINAL_SESSIONS`(10)**只计有 tab/在途的「活」桶**(空桶——常驻 draft 或 tab 全关的——不占名额不顶掉别人的 PTY;**落地即重戳**——create 落地让桶变活,在 newTabInto push 后补一次驱逐检查,否则活桶数永远凑不齐触发线),LRU 驱逐最老桶并杀其 PTY;**draft 桶按工作区分键**(`__draft__:`,App.vue 计算——`activeSessionId` 在 draft 态是空串不是 null);draft 发首条消息/技能启动/`/goal`/`/btw`/侧聊开关/composer goal 落成 session 时经 `migrateDraftTo(from, to)` 把 draft 桶 **re-key** 到新 session(**目标桶已存在则合并**——draft 对象作为存活者吸收目标 tabs(create 窗口内用户已在新 session 开过终端时,不能把 draft 的 PTY 丢弃在旧 key 下;newTabInto/restartTab 的 await 后守卫经 `resolveHome`——仅「仍注册」(migration re-key 同一对象)或「merge 转发」(`mergedInto` 指向存活对象)可落地;**被驱逐/销毁的桶绝不落地,即使后来同名重建了新桶**(身份比较会把合并后的在途 PTY 误当已驱逐杀掉,纯 key 查找又会把已销毁桶的旧 PTY 挂进同名新桶);**inflight 守卫经 `adoptInflight` 对全部继承 promise 重新武装**——`Promise.allSettled` 汇总且 finally 挂存活对象,只留一个 promise 会让 ensureTab 在另一 spawn 未落时误判空闲多开 PTY,外来 promise 的 finally 又清错对象);迁移锚定创建调用**返回的 sessionId**——`startSessionAndSendPrompt`/`startSessionAndActivateSkill`/`startSessionAndOpenSideChat`/`createGoal`/`openSideChatTab` 均返回 `string | null`(**建 session 之后的失败路径也返回 createdId**——session 已存在且被选中,不返回会把 draft 桶丢在旧 key),属共享逻辑已同步 apps/web;不用 sessions 差集——另一客户端并发插入会选错对象;后三者在 client 内部建 session,App.vue 统一经 `runSessionCreatingAction`/`handleCreateGoal` 包装;在途 create 一并带走,不会落进隐藏桶;**无工作区 draft `__draft__:none` 不迁移**——其 PTY 跑在 fallback cwd 而非新选目录,`addWorkspace` 成功即直接 destroy(不论有无 pending prompt));归档 session(确认框与 ⌥⌘A 快捷键两路都清,且确认会话真的消失才动手)、删除工作区(其全部 session,按 root 兜底匹配)与 **WS `sessionDeleted`/`workspaceDeleted`/`sessionUpdated(archived)`**(远端/其他客户端删除或归档,绕过 App.vue 回调——`useKimiWebClient.ts` 事件分支照 unpin 先例调 `destroySession`;workspaceDeleted 按 workspaceId 或 cwd=root 匹配其全部 session 并清该工作区 draft 桶;归档广播是 sessionUpdated 而非删除,session 只是离开侧栏但 PTY 会继续跑,同样连坐)经 `destroySession` 连坐杀 PTY;create/restart 的 await 返回后先校验桶未被驱逐、再按 id 重定位(防误删邻居);输出缓冲:xterm 订阅前的 PTY 输出按 tab 缓存(上限 1000 chunk),首个订阅者回放,防首个 prompt 丢失;early-exit:tab 入列前到达的 exit 按 id 暂存,push 时应用(秒退的 PTY 不会以 running 卡住);主进程侧 close/killAll 后 PTY 的迟到 output/exit 不再推送(registry 守卫);write/resize 包 try/catch 兜死进程竞态(conpty resize 可抛);首 tab 创建经 `ensureTab` 去重(在途 guard 只清自己那个 promise)generation 计数防竞态——reset 期间 resolve 的在途 create 直接 kill 不进 tab);`components/terminal/TerminalPanel.vue`(tab 条 + 新建/重启/收起,纯展示——**无 tab 时 + 号走 `ensureTab`**(在途去重),防冷启动连点 spawn 多个 shell;**新建后聚焦**——+ 号 await 后 activateAndFocus,空态 ensureTab 经单个 pending watcher(重试先 stop 旧的、卸载清理,失败/挂起不累积)在首个 tab 落地时聚焦,否则按钮焦点留在 .tp 里、打字无处可去;**渲染所有含 tab 的桶**——每个 session 的 xterm 视图 v-show 保活不卸载,只显示当前桶的激活 tab;tab 结构为容器 div + 并列 activate/close 双 button(无嵌套交互控件),tablist 实现 roving tabindex + ←/→/Home/End 键盘模型);`TerminalView.vue`(每 tab 一个 xterm,v-show 保活,active/面板重开时 refit+focus(首次挂载即 active 也显式 focus,否则键盘焦点留在 composer);初始化照 `components/Terminal.vue` 范本——fonts.ready 后测量、FitAddon 100ms 防抖、canvas 调色板 token 派生(`getComputedStyle` 读 `--color-*`,亮暗翻转重算;magenta/cyan/black 走新增 `--color-term-magenta/cyan/black` token(惯例色值,web-ui style.css 三个 scheme 块各一份——black 不再映射 --color-bg,亮色下 `\x1b[30m` 曾白底白字消失);字号经探针元素解析 `--md-b2` 的 px(`data-font-scale` 变化经 MutationObserver 重设 `term.options.fontSize` + refit),行高 1.1 无合适 token 保留终端密度)——数据面换原生桥;退出提示走 `terminal.processExited*` i18n;**焦点联动菜单挂起**:xterm focusin/focusout 经专用通道 `kimi:menu-terminal-focus` 驱动 `menu.ts` 的双模式挂起(与录制挂起互相独立不覆盖;**reload 中止恢复**——did-start-navigation 快照 focus 态,did-fail-(provisional-)load 且旧文档仍在时还原,否则旧页面 xterm 还聚焦着但 Windows 按键已重新武装,Ctrl+C 等被抢直到重新聚焦;**OS 全局快捷键联动**——`menu.ts` 的 `onTerminalMenuFocus` 钩子在 ipc.ts 接 `shortcuts.ts` 的 `setGlobalShortcutTerminalFocus`,globalShortcut 挂起模型改 recording/terminal 双标志,否则 Ctrl 系 summon 绑定被系统先吞、PTY 收不到)——terminal 模式在 macOS 只剥自定义 Ctrl-only accelerator(role 的 ⌘ 系与 PTY 的 Ctrl 键无冲突;renderer 侧同步放行——xterm 键盘过滤器与 App dispatcher 都经 `terminalPassesChordToPty` 判定(macOS 放 Ctrl-only、Windows/Linux 放全部,toggle 恒除外),否则自定义 Ctrl+C 这类绑定仍偷走 SIGINT),Windows/Linux 经 `registerAccelerator: false` 摘掉全部按键注册(含 Edit 的 Ctrl+C/V/A/Z,否则 SIGINT 到不了 PTY)但**role 原生行为与 click 全部保留**(菜单点按照常,不像录制式纯 label 那样整体失效);面板收起时 watcher 显式清焦点标志;xterm 的 `attachCustomKeyEventHandler` 对命中注册表 global action 的按键返回 false 放行冒泡(⌃\` 关面板、⌘N/⌘K 等在终端聚焦时仍生效,不让 xterm 吞成 PTY 控制字符);异步初始化(动态 import + fonts.ready)后复查 unmounted/host 防快速关闭时 open 抛错);`TerminalResizeHandle.vue`(水平拖拽条;键盘可达:tabindex + ↑/↓ 步进 + aria-valuenow/min/max)。 + - renderer(全部 desktop-only 新文件,不同步 web):`composables/useNativeTerminal.ts` 模块级单例状态机(按 sessionId 分桶的 tabs/activeTabId/open + error;桥探测 `canUseNativeTerminal` 缺桥整体 no-op;输出/退出订阅一次按 tab id 全局路由——后台 session 的视图保持挂载持续接收输出;closeTab 修 active;restartTab 先建后杀保位置复用 cwd;**终端状态按 sessionId 分桶**(`switchSession` 由 App.vue watch activeSessionId 驱动,draft 态共用 `__draft__` 桶)——切换 session 只换可见桶,旧桶的 PTY 与 xterm 视图全部保留(scrollback、全屏 TUI 画面都在),切回来原样恢复;**bucket 懒创建**(浏览 session 不占 LRU 名额;**创建即打 lastUsed 时间戳**——switchSession 只盖已存在的桶,不盖的话新桶 lastUsed=0 会被当成最老的首选受害者),上限 `MAX_TERMINAL_SESSIONS`(10)**只计有 tab/在途的「活」桶**(空桶——常驻 draft 或 tab 全关的——不占名额不顶掉别人的 PTY;**落地即重戳**——create 落地让桶变活,在 newTabInto push 后补一次驱逐检查,否则活桶数永远凑不齐触发线),LRU 驱逐最老桶并杀其 PTY;**draft 桶按工作区分键**(`__draft__:`,App.vue 计算——`activeSessionId` 在 draft 态是空串不是 null);draft 发首条消息/技能启动/`/goal`/`/btw`/侧聊开关/composer goal 落成 session 时经 `migrateDraftTo(from, to)` 把 draft 桶 **re-key** 到新 session(**目标桶已存在则合并**——draft 对象作为存活者吸收目标 tabs(create 窗口内用户已在新 session 开过终端时,不能把 draft 的 PTY 丢弃在旧 key 下;newTabInto/restartTab 的 await 后守卫经 `resolveHome`——仅「仍注册」(migration re-key 同一对象)或「merge 转发」(`mergedInto` 指向存活对象)可落地;**被驱逐/销毁的桶绝不落地,即使后来同名重建了新桶**(身份比较会把合并后的在途 PTY 误当已驱逐杀掉,纯 key 查找又会把已销毁桶的旧 PTY 挂进同名新桶);**inflight 守卫经 `adoptInflight` 对全部继承 promise 重新武装**——`Promise.allSettled` 汇总且 finally 挂存活对象,只留一个 promise 会让 ensureTab 在另一 spawn 未落时误判空闲多开 PTY,外来 promise 的 finally 又清错对象);迁移锚定创建调用**返回的 sessionId**——`startSessionAndSendPrompt`/`startSessionAndActivateSkill`/`startSessionAndOpenSideChat`/`createGoal`/`openSideChatTab` 均返回 `string | null`(**建 session 之后的失败路径也返回 createdId**——session 已存在且被选中,不返回会把 draft 桶丢在旧 key),属共享逻辑已同步 apps/web;不用 sessions 差集——另一客户端并发插入会选错对象;后三者在 client 内部建 session,App.vue 统一经 `runSessionCreatingAction`/`handleCreateGoal` 包装;在途 create 一并带走,不会落进隐藏桶;**无工作区 draft `__draft__:none` 不迁移**——其 PTY 跑在 fallback cwd 而非新选目录,`addWorkspace` 成功即直接 destroy(不论有无 pending prompt));归档 session(确认框与 ⌥⌘A 快捷键两路都清,且确认会话真的消失才动手)、删除工作区(其全部 session,按 root 兜底匹配)与 **WS `sessionDeleted`/`workspaceDeleted`/`sessionUpdated(archived)`**(远端/其他客户端删除或归档,绕过 App.vue 回调——`useKimiWebClient.ts` 事件分支照 unpin 先例调 `destroySession`;workspaceDeleted 按 workspaceId 或 cwd=root 匹配其全部 session 并清该工作区 draft 桶;归档广播是 sessionUpdated 而非删除,session 只是离开侧栏但 PTY 会继续跑,同样连坐)经 `destroySession` 连坐杀 PTY;create/restart 的 await 返回后先校验桶未被驱逐、再按 id 重定位(防误删邻居);输出缓冲:xterm 订阅前的 PTY 输出按 tab 缓存(上限 1000 chunk),首个订阅者回放,防首个 prompt 丢失;early-exit:tab 入列前到达的 exit 按 id 暂存,push 时应用(秒退的 PTY 不会以 running 卡住);主进程侧 close/killAll 后 PTY 的迟到 output/exit 不再推送(registry 守卫);write/resize 包 try/catch 兜死进程竞态(conpty resize 可抛);首 tab 创建经 `ensureTab` 去重(在途 guard 只清自己那个 promise)generation 计数防竞态——reset 期间 resolve 的在途 create 直接 kill 不进 tab);`components/terminal/TerminalPanel.vue`(tab 条 + 新建/重启/收起,纯展示——**无 tab 时 + 号走 `ensureTab`**(在途去重),防冷启动连点 spawn 多个 shell;**新建后聚焦**——+ 号 await 后 activateAndFocus,空态 ensureTab 经单个 pending watcher(重试先 stop 旧的、卸载清理,失败/挂起不累积)在首个 tab 落地时聚焦,否则按钮焦点留在 .tp 里、打字无处可去;**渲染所有含 tab 的桶**——每个 session 的 xterm 视图 v-show 保活不卸载,只显示当前桶的激活 tab;tab 结构为容器 div + 并列 activate/close 双 button(无嵌套交互控件),tablist 实现 roving tabindex + ←/→/Home/End 键盘模型);`TerminalView.vue`(每 tab 一个 xterm,v-show 保活,active/面板重开时 refit+focus(首次挂载即 active 也显式 focus,否则键盘焦点留在 composer);初始化照 `components/Terminal.vue` 范本——fonts.ready 后测量、FitAddon 100ms 防抖、canvas 调色板 token 派生(`getComputedStyle` 读 `--color-*`,亮暗翻转重算;magenta/cyan/black 走新增 `--color-term-magenta/cyan/black` token(惯例色值,app-ui style.css 三个 scheme 块各一份——black 不再映射 --color-bg,亮色下 `\x1b[30m` 曾白底白字消失);字号经探针元素解析 `--md-b2` 的 px(`data-font-scale` 变化经 MutationObserver 重设 `term.options.fontSize` + refit),行高 1.1 无合适 token 保留终端密度)——数据面换原生桥;退出提示走 `terminal.processExited*` i18n;**焦点联动菜单挂起**:xterm focusin/focusout 经专用通道 `kimi:menu-terminal-focus` 驱动 `menu.ts` 的双模式挂起(与录制挂起互相独立不覆盖;**reload 中止恢复**——did-start-navigation 快照 focus 态,did-fail-(provisional-)load 且旧文档仍在时还原,否则旧页面 xterm 还聚焦着但 Windows 按键已重新武装,Ctrl+C 等被抢直到重新聚焦;**OS 全局快捷键联动**——`menu.ts` 的 `onTerminalMenuFocus` 钩子在 ipc.ts 接 `shortcuts.ts` 的 `setGlobalShortcutTerminalFocus`,globalShortcut 挂起模型改 recording/terminal 双标志,否则 Ctrl 系 summon 绑定被系统先吞、PTY 收不到)——terminal 模式在 macOS 只剥自定义 Ctrl-only accelerator(role 的 ⌘ 系与 PTY 的 Ctrl 键无冲突;renderer 侧同步放行——xterm 键盘过滤器与 App dispatcher 都经 `terminalPassesChordToPty` 判定(macOS 放 Ctrl-only、Windows/Linux 放全部,toggle 恒除外),否则自定义 Ctrl+C 这类绑定仍偷走 SIGINT),Windows/Linux 经 `registerAccelerator: false` 摘掉全部按键注册(含 Edit 的 Ctrl+C/V/A/Z,否则 SIGINT 到不了 PTY)但**role 原生行为与 click 全部保留**(菜单点按照常,不像录制式纯 label 那样整体失效);面板收起时 watcher 显式清焦点标志;xterm 的 `attachCustomKeyEventHandler` 对命中注册表 global action 的按键返回 false 放行冒泡(⌃\` 关面板、⌘N/⌘K 等在终端聚焦时仍生效,不让 xterm 吞成 PTY 控制字符);异步初始化(动态 import + fonts.ready)后复查 unmounted/host 防快速关闭时 open 抛错);`TerminalResizeHandle.vue`(水平拖拽条;键盘可达:tabindex + ↑/↓ 步进 + aria-valuenow/min/max)。 - 布局:`App.vue` 加第二条 grid 行 `minmax(0,1fr) auto`,终端 section `grid-column: 3; grid-row: 2`——只占会话列下方的格子,**侧栏与右侧面板 `grid-row: 1 / -1` 通高**(VS Code 布局:面板属编辑区而非全窗);高度 0 ↔ `var(--terminal-h)` 过渡(同 aside 机制;`--terminal-h` 命令式写入,同 `--preview-w` 先例);首次打开才挂载、之后保持挂载(xterm scrollback 不因收起丢失);高度存 `kimi-web.terminal-panel-height`(min 120 / max 视口 60%——上限为响应式 computed,窗口缩放双向跟随且存量高度随之 clamp);收起时 section 置 `inert`(内部控件与 xterm textarea 退出 tab 序,实例保留);mobile 布局下 section 只 `display:none` 不卸载(scrollback 不丢),toggle 在 mobile 与无桥时为 no-op;终端内 Esc 不触发全局关面板(App.vue onGlobalKeydown 对 `.terminal-host` 内事件放行;ConversationPane 文档级 onKeyDown 同守卫——Esc 中断/find/select-all 在终端聚焦时全失效,tmux 的 Ctrl+A 前缀也可用;该守卫在共享文件里双端同步、web 无 .terminal-host 恒 no-op,vim/fzf 可用)。共享 `useResizable.ts` 加 `axis: 'y'` 选项(默认 'x' 零行为变化)与 **cap 响应式收缩钳制**(watch resolved max,收缩且非拖拽中则把已提交宽度钳到 cap——否则下次拖拽/键盘步进从陈旧的超 cap 值起算,面板跳回超高)**已同步 apps/web**(两端文件一致,非分叉)。 - 入口:`ctrl+`` 快捷键(⌃`,对齐主流编辑器的终端切换键;⌘` 是 macOS 系统窗口切换键故避让——keymap 注册表 `toggleTerminal`,进 MENU_SYNCED_ACTIONS——菜单 accelerator 跟随用户绑定、冲突检测跨 scope)+ View 菜单「切换终端 / Toggle Terminal」双语项(click 必须存在:accelerator 会遮蔽 renderer keydown)+ **ChatHeader 终端图标按钮**(OpenIn 菜单右侧,面板打开时带 open 高亮态;`terminalStore.available` 桥探测控制显隐,web 恒不渲染);Windows 自定义标题栏的 View 菜单经 `windowsMenuTemplate` 自动继承。i18n `terminal.*` 与 `shortcuts.actions.toggleTerminal.*` 在共享包(web 不使用无副作用)。 - 测试:`tests/main/terminal.test.ts`(15 用例:shell 解析两平台/LANG 修补/create 参数钳制/cwd 回退/输出退出路由/输入截断/close 幂等/killAll);`tests/main/preload.test.ts`(白名单 + 6 通道 + 双向畸形丢弃);`tests/main/menu.test.ts`(View 终端项双语 + accelerator 跟随 + 已接线);`tests/renderer/useNativeTerminal.test.ts`(9 用例:桥探测/无桥 no-op/首开自动建 tab/输出退出按 id 路由/closeTab 修选中/restart 保位/运行态门控/持久化);`tests/renderer/useResizable.test.ts` +1(y 轴 clientY/reverse/row 光标);keymap 注册表 sanity 自动覆盖新 action。 @@ -125,7 +125,7 @@ - 测试:`tests/main/updater.test.ts`(7 用例:dev no-op、autoDownload 配置与定时检查、状态流、下载失败 error + 重试、后台失败静默、回滚清态、动作状态守卫);`tests/renderer/useUpdateStatus.test.ts`(10 用例:含 skip/visibility/快照竞态);`tests/main/preload.test.ts` 白名单 + 通道断言同步。 - 手动检查更新(设置 → 高级,2026-07 补):主进程 `UpdateController.check()` 用一次性监听器竞速 update-available / update-not-available / error + promise rejection + 30s 超时,resolve `UpdateCheckResult`(available+version / latest / error+message;dev 无 controller 时 `requestUpdateCheck()` 返回 unsupported);新 IPC `kimi:update-check` + preload `checkForUpdates()`(`asUpdateCheckResult` 结构校验)。renderer `useUpdateStatus` 加 `canCheck`(桥有 `checkForUpdates` 方法才 true,旧版 desktop 桥降级 false)与 `check()`(无桥 resolve unsupported);SettingsDialog 高级页「检查更新」行 `v-if canCheck`(web 恒隐藏,非分叉块,两端文件仍一致),结果行内 hint 显示(检查中…/已是最新/发现新版本→侧栏 pill 入口/不支持/失败)。测试同步:updater +4 用例(available/latest/error+rejection/超时+监听器清理)、useUpdateStatus +4、preload +1(响应校验兜底)。 - 后台自动下载 + 设置开关(2026-07 补):新增 **opt-in** 的后台自动下载——设置 → 高级新增「自动下载更新」Switch 行(**默认关**,`v-if canToggleAutoDownload` 桥特性探测,web 恒隐藏,两端 SettingsDialog 仍一致),开启后发现新版本即后台下载,下完侧栏 pill 显示「重启更新」,自然退出/重启时静默装(autoInstallOnAppQuit 原有);关闭即维持点击下载的手动模式(现状)。**设置存主进程** `ui-state.json` 的 `updateAutoDownload` 字段(缺失默认 false;主进程是消费者——启动 10s 首查就要读到,renderer 推送有时序空洞,故不走 localStorage;ui-state.ts 抽 `writeUiState` 读-合并-写复用)。**状态机不变,可见性收口 renderer**:`useUpdateStatus.visible` 自动模式仅 downloaded 显示(后台下载与失败静默,下次定时检查自恢复重试),手动模式维持原规则(含本次跳过)。开关边界:开→关只翻标志不取消进行中下载(electron-updater 无公开 cancel API);关→开时若 available/error 态立即触发下载(`UpdateController.setAutoDownload` 复用 `download()` 的状态守卫)。`check()` 一次性监听器补 `update-downloaded`——已下载版本手动检查 electron-updater 重发该事件而非 update-available,此前会 30s 超时误报失败。菜单「检查更新…」分流:已下载(不分模式)→「立即重启/稍后」(requestUpdateInstall;downloaded 判断必须在模式分流之前,否则手动模式下会落到被 downloaded 状态守卫 no-op 的「立即下载」死按钮,2026-07 codex review 实锤);自动模式下载中→单「好」+后台下载文案(避免再给一个 no-op 的「立即下载」按钮);手动模式未下载→「立即下载/稍后」照旧;MENU_STRINGS +3 双语(updateAutoDownloading / updateReadyToInstall / restartNow,ok 复用)。SettingsDialog 检查结果文案按模式分流(已 downloaded→「已就绪」、自动模式→「后台下载中」、手动→原侧栏入口文案)。新 IPC `kimi:update-get-auto-download` / `kimi:update-set-auto-download`;preload `getUpdateAutoDownload`(响应非 boolean 兜底 false)/ `setUpdateAutoDownload`(入参校验,畸形丢弃)。测试:updater +4(默认手动/显式开启、setAutoDownload 翻转三态、error 态 enable 重试、check 已下载不超时)、ui-state +2、useUpdateStatus +3(自动模式可见性、toggle 翻转即时生效、canToggle 探测;fakeBridge 默认手动模式保住既有 skip 用例语义)、preload +2(白名单/通道、响应兜底)。 - - 更新弹窗双语 changelog(2026-07 补):更新弹窗展示该版本中英双语更新说明。**存储**:CDN 版本目录 `desktop/binaries//changelog.{zh,en}.md`(2026-07 布局调整挪入 `binaries/` 子目录,过渡期同步往旧路径 `desktop//` 传 changelog 副本供未升级客户端拉取,两个版本后停传;不动 latest*.yml 与 electron-updater feed;旧版本目录无此文件 → 404 → 静默降级,弹窗隐藏区块,天然向后兼容);仓内 `release-notes//` git 存档,由 `.agents/skills/release-notes` skill 生成(从 `apps/desktop/CHANGELOG.md` 抽中文案 + 翻译英文,发布人 review 后 commit);`publish-desktop-cdn.sh` 在版本目录上传步后追加上传这两个 md(缺失警告不阻断,补传 = 生成后 `--artifacts-only` 重跑)。**拉取**:主进程 updater.ts 在 `update-available` / `update-downloaded`(上轮已下载的更新无前置 announce,首知版本在此)后按版本号拼 URL `net.fetch` 预拉(`RELEASE_NOTES_BASE` 与 electron-builder.config.cjs 的 publish URL 同源;每版本只拉一次——空结果/失败会解除标记、允许后续事件重试,因为发布允许先切指针后补传 changelog,2026-07 codex review 实锤;双语言并行、单语言失败不拖另一个;整体永不 reject——changelog 不阻塞更新);拉到后仅当 status 仍指向该版本才合并进 `UpdateStatus.releaseNotes { zh?, en? }` 推送;`setStatus` 规则改为 releaseNotes 跟随 version(同版本流转继承、跨版本/idle 清除)。preload `asUpdateStatus` 逐字段校验(junk 只丢字段不丢 status)。**展示**:UpdateIndicator 弹窗 meta 行下新增「更新内容 / What's new」区块,`@moonshot-ai/web-markdown` 的 Markdown 组件渲染(desktop/web 均已有依赖),按 i18n locale 选 zh/en、缺失 fallback 另一语言、都没有隐藏;两端文件已同步(web 无桥恒不渲染)。测试:updater +7(合并推送/每版本一次/竞态丢弃/空与失败静默/回滚清除/无注入/生产 fetch URL 与容错)、preload +1(releaseNotes 字段级校验)、useUpdateStatus +1(直通)×2 端。 + - 更新弹窗双语 changelog(2026-07 补):更新弹窗展示该版本中英双语更新说明。**存储**:CDN 版本目录 `desktop/binaries//changelog.{zh,en}.md`(2026-07 布局调整挪入 `binaries/` 子目录,过渡期同步往旧路径 `desktop//` 传 changelog 副本供未升级客户端拉取,两个版本后停传;不动 latest*.yml 与 electron-updater feed;旧版本目录无此文件 → 404 → 静默降级,弹窗隐藏区块,天然向后兼容);仓内 `release-notes//` git 存档,由 `.agents/skills/release-notes` skill 生成(从 `apps/desktop/CHANGELOG.md` 抽中文案 + 翻译英文,发布人 review 后 commit);`publish-desktop-cdn.sh` 在版本目录上传步后追加上传这两个 md(缺失警告不阻断,补传 = 生成后 `--artifacts-only` 重跑)。**拉取**:主进程 updater.ts 在 `update-available` / `update-downloaded`(上轮已下载的更新无前置 announce,首知版本在此)后按版本号拼 URL `net.fetch` 预拉(`RELEASE_NOTES_BASE` 与 electron-builder.config.cjs 的 publish URL 同源;每版本只拉一次——空结果/失败会解除标记、允许后续事件重试,因为发布允许先切指针后补传 changelog,2026-07 codex review 实锤;双语言并行、单语言失败不拖另一个;整体永不 reject——changelog 不阻塞更新);拉到后仅当 status 仍指向该版本才合并进 `UpdateStatus.releaseNotes { zh?, en? }` 推送;`setStatus` 规则改为 releaseNotes 跟随 version(同版本流转继承、跨版本/idle 清除)。preload `asUpdateStatus` 逐字段校验(junk 只丢字段不丢 status)。**展示**:UpdateIndicator 弹窗 meta 行下新增「更新内容 / What's new」区块,`@moonshot-ai/app-markdown` 的 Markdown 组件渲染(desktop/web 均已有依赖),按 i18n locale 选 zh/en、缺失 fallback 另一语言、都没有隐藏;两端文件已同步(web 无桥恒不渲染)。测试:updater +7(合并推送/每版本一次/竞态丢弃/空与失败静默/回滚清除/无注入/生产 fetch URL 与容错)、preload +1(releaseNotes 字段级校验)、useUpdateStatus +1(直通)×2 端。 - [x] **内嵌 server 遥测(agent-core-v2 CloudAppender)**(已完成,desktop 专属) - 实现:新增 `src/main/telemetry.ts`——kap-server 自身不接任何 telemetry appender(agent-core-v2 的事件全落 null appender),`server.ts` 在 `startServer` 返回后经 `handle.core`(DI Scope)装 `CloudAppender`(上报 `telemetry-logs.kimi.com`,批量 + PII 脱敏 + 失败磁盘兜底),身份 `app_name=kimi-code-desktop` / `ui_mode=desktop` / `client_version=app.getVersion()`,deviceId 由 host headers 首次创建并与 telemetry 共用(与 CLI 共享 `/device_id`,首启发 `first_launch`);长驻进程显式 `startPeriodicFlush()` + `retryDiskEvents()`;应用退出时 `app.ts` 的 before-quit 屏障只 await `telemetry.shutdown()`(发 `exit` 事件 + 自带 3s 超时 flush)再放行 quit——不 await 整个 server close(02f0bb62 曾因 quit 卡顿去掉屏障,后按此有界形式恢复)。consent 是宿主职责:读 config `telemetry` 键(opt-out,与设置页隐私开关同一个键,改动下次启动生效)+ 补查 `KIMI_DISABLE_TELEMETRY`(v2 链路不读该 env,desktop 照 v1 真值表自查);接线失败只记日志,不影响 server 启动。仅内嵌 server 模式(外部 server 的 appender 归对面管)。宿主层事件经 `src/main/track.ts` 的 `trackDesktopEvent` 发出(`telemetry.ts` 接线后安装 impl,接线前的事件缓冲至多 200 条、接线后回放,关闭后清空)。**事件契约本地定义在 `src/main/telemetry-events.ts`(独立数据目录文件;renderer 可发事件的契约在 `src/shared/track-events.ts`,由此 re-export 保持单一目录),经无类型 `ITelemetryService.track` 发出,刻意不进 kimi-code 的 `events.ts` 注册表**——kimi-code 是公开仓而 desktop 未发布,注册宿主事件(托盘/原生菜单/updater 等产品面)会提前曝光;`track2` 的注册表约束只是编译期的,运行期管线对事件名零校验,wire 格式一致,发布后可一行切回 `track2` 并把契约机械迁入 `events.ts`。主进程事件(9):`embedded_renderer_load_result`(仅 embedded server 已就绪后的 renderer `loadURL` 成/败 + duration_ms + error_class;不代表完整启动成功率)、`app_crashed`(log.ts 崩溃守卫,kind + error_name)、`update_status_changed`(updater.ts 状态转移去重,progress 不刷)、`menu_action`(menu.ts **仅主进程独占项**:检查更新 / 帮助文档 / 控制台;转发 renderer 的菜单项由 `action_invoked{source:'menu'}` 覆盖,不双计)、`tray_action`(tray.ts open-session / show-window / quit,带 pending_count 快照)、`global_shortcut_invoked` + `global_shortcut_register_failed`(shortcuts.ts,invalid / conflicted)、`window_lifecycle`(window.ts 监听真实 shown / hidden / closed)、`native_ipc_used`(ipc.ts 仅在成功处理 dialog-open/save、open-in、vibrancy、show-window 后上报;启动同步与能力查询不计);renderer 事件经下条目的 `kimi:track` 通道汇入同一管线。 @@ -186,7 +186,7 @@ - 测试:`tests/renderer/nativeWorkspaceDrop.test.ts` 新增 4 用例(文件/文件夹分流、无路径文件夹不上传也不产出路径、非 file item 双侧跳过、items 空时回退 dataTransfer.files)。 - [x] **macOS 毛玻璃侧栏(窗口 vibrancy)+ 辅助功能开关**(已完成,desktop 专属,macOS only) - - 材质:`window.ts` 的 `vibrancyWindowOptions(platform)` 纯函数出窗口配置——darwin 恒为 `vibrancy:'menu'` + `visualEffectState:'inactive'`(钉住扁平渲染,不随焦点漂移)+ 透明 `backgroundColor`,其余平台不透明 `#0b0b0c`。**关闭态启动走"创建后立即移除"**:窗口仍带材质创建(保证 `visualEffectState` 在创建时存入,Electron 之后每次 SetVibrancy 都会重放,已对 v43.1.1 源码核实),`createWindow` 紧接着 `applyWindowVibrancy(false)`(首帧前,无闪烁)——不依赖"关闭态创建时 option 是否生效"的解读分歧。运行时切换同走 `applyWindowVibrancy`(`setVibrancy('menu'|null)` + `setBackgroundColor`)。钉住 `inactive` 的原因(2026-07 实测):默认 followWindow 下聚焦窗口的材质高度透光(暗色 ≈ #535353 带壁纸渗透),发白发奶,且会在聚焦/失焦两幅面孔间跳变;`inactive` 是其扁平压底渲染(≈ #282829 暗 / #E7E7E7 亮,测量值记在 web-ui style.css 的 `--color-sidebar-tint` 注释里)。 + - 材质:`window.ts` 的 `vibrancyWindowOptions(platform)` 纯函数出窗口配置——darwin 恒为 `vibrancy:'menu'` + `visualEffectState:'inactive'`(钉住扁平渲染,不随焦点漂移)+ 透明 `backgroundColor`,其余平台不透明 `#0b0b0c`。**关闭态启动走"创建后立即移除"**:窗口仍带材质创建(保证 `visualEffectState` 在创建时存入,Electron 之后每次 SetVibrancy 都会重放,已对 v43.1.1 源码核实),`createWindow` 紧接着 `applyWindowVibrancy(false)`(首帧前,无闪烁)——不依赖"关闭态创建时 option 是否生效"的解读分歧。运行时切换同走 `applyWindowVibrancy`(`setVibrancy('menu'|null)` + `setBackgroundColor`)。钉住 `inactive` 的原因(2026-07 实测):默认 followWindow 下聚焦窗口的材质高度透光(暗色 ≈ #535353 带壁纸渗透),发白发奶,且会在聚焦/失焦两幅面孔间跳变;`inactive` 是其扁平压底渲染(≈ #282829 暗 / #E7E7E7 亮,测量值记在 app-ui style.css 的 `--color-sidebar-tint` 注释里)。 - 开关(默认开,辅助功能动机):**主进程持久化**——`ui-state.ts` 的 `vibrancy` 键(`isVibrancyEnabled` 缺省/畸形均按 true),窗口创建时先读它,renderer 不存在前材质就已就位,无启动闪烁。IPC:`kimi:vibrancy`(写,send + 布尔校验,handler 里持久化 + live-apply)与 `kimi:get-vibrancy`(读,invoke);preload 白名单加 `setVibrancy` / `getVibrancy`(后者只认显式 false,畸形回默认 true)。 - renderer:`composables/useVibrancy.ts` 单例(照 useFullscreen 模式,无桥恒 true 零影响)。**关键 class 拆分**:`macos-desktop` 只管红绿灯避让/拖拽区等 macOS 布局,`vibrancy`(``、`App.vue` 根、`.side` 三处绑定同一 ref)只管"让材质透出"的上色规则——`style.css` 根链透明、`App.vue` `.app` 背景透明、`Sidebar.vue` 侧栏 tint + 子面透明;关闭 vibrancy 时布局(红绿灯)必须原样保留,所以两套 class 绝不能合并。`main.ts` 启动同步加 `macos-desktop` + `initVibrancy()`。**关闭态启动防闪烁**:偏好经 `rendererUrl` 的 `kimi_vibrancy=0|1` 查询参数**每次启动都注入**(protocol.ts,照 `kimi_onboarded` 先例——ui-state 主进程持有,URL 注入不依赖 origin localStorage;恒注入是为了让 SPA 内部 pushState 丢 query 后的 reload 能区分"开启"与"参数丢失"),`initVibrancy` 同步读参并镜像进 sessionStorage(desktopFlag 模式)决定首帧 class——**sessionStorage 优先于 URL pin**(设置切换写 sessionStorage,而地址栏里的 boot query 可能是旧值,SPA 或整页 reload 时不能让旧 query 盖过当次选择),异步桥读仅作确认;否则关闭过的用户启动会先闪一帧 tint 再跳回。设置行在 `SettingsDialog.vue` 通用页 Appearance(`v-if="isMacosDesktop"`,web 恒不渲染;i18n `settings.vibrancy[Hint]` 在共享包,web 不使用)。 - **web 无对应物**:浏览器没有窗口材质;`useVibrancy.ts` 不同步 apps/web(同 useFullscreen 先例);分叉块:`App.vue`(import + binding + `.app.macos-desktop.vibrancy` CSS)、`Sidebar.vue`(import + binding + tint CSS)、`SettingsDialog.vue`(import + 设置行)、`main.ts`(initVibrancy),整目录 re-copy 时需保留。 @@ -197,7 +197,7 @@ - renderer 侧统一入口 `src/renderer/lib/log.ts`(`logInfo/logWarn/logError`):console 镜像恒在(devtools 可见性不变)+ 运行时探测 `window.kimiDesktop.log` 转发,无桥(web)/老桥缺方法自动退化为纯 console,桥异常静默——日志绝不能影响业务。**两端同文件、零分叉**(运行时桥探测分流),`apps/web/src/lib/log.ts` 是同一份。`debug/trace.ts` 的 window error / unhandledrejection 也经它 always-on 落盘(不受 `?debug=1` 门控)——白屏/未捕获异常在打包版从此有记录。既有 25 处裸 `console.*` 全部收口到该 lib(desktop `[kimi-code]` / web `[kimi-web]` 品牌前缀分叉照旧保留);`useModelProviderState` 的 desktop 分叉块(直连 console.error 五处 vs web 走 `pushOperationFailure`)同步收口。 - 主进程顺带补关键路径日志:app ready(version/platform/arch/packaged)与 quitting、内嵌 server listening 端口、updater 状态迁移(available / download started / downloaded / install / download failed,原裸 `console.warn` 收口为 `log.warn`)、`openExternal` 失败(URL 经 `redactUrlForLog` 脱敏)、`render-process-gone`(渲染进程崩溃/被杀死,此前唯一线索是用户看到白屏)。 - 测试:`tests/main/renderer-log.test.ts`(13 用例:payload 校验、脱敏、截断、恶意输入、限流 + 汇总行);`tests/main/preload.test.ts`(白名单 + 通道 + 畸形不发送);`tests/renderer/log.test.ts` 与 `apps/web/test/log.test.ts`(各 4 用例:桥转发、多参 detail、无桥/老桥 console-only、桥抛错静默)。 - - **session 导出带桌面日志**:导出请求新增 `desktop: true` 标记(renderer 按 `isDesktop` 发送,web 恒不发),server 端(kimi-code 仓)读到标记后自行把 `/logs/kimi-code-desktop.log` 打包为 zip 里的 `logs/kimi-desktop.log` 并登记 manifest——日志内容不经过 renderer/HTTP。client(web-core)对不认识该字段的旧版 server(strict schema 40001)自动去掉标记重试一次。kimi-code 侧改动:protocol/kap-server 两份 schema 加 `desktop` 可选字段、kap-server 路由透传 `includeDesktopLog`、agent-core-v2 导出服务照 global log 同款模式打包(缺文件静默跳过)。 + - **session 导出带桌面日志**:导出请求新增 `desktop: true` 标记(renderer 按 `isDesktop` 发送,web 恒不发),server 端(kimi-code 仓)读到标记后自行把 `/logs/kimi-code-desktop.log` 打包为 zip 里的 `logs/kimi-desktop.log` 并登记 manifest——日志内容不经过 renderer/HTTP。client(app-core)对不认识该字段的旧版 server(strict schema 40001)自动去掉标记重试一次。kimi-code 侧改动:protocol/kap-server 两份 schema 加 `desktop` 可选字段、kap-server 路由透传 `includeDesktopLog`、agent-core-v2 导出服务照 global log 同款模式打包(缺文件静默跳过)。 ## 共享组件 UI 分叉 @@ -205,13 +205,13 @@ - **自定义供应商管理已两端同步**(2026-08,原 #93 desktop-first):`components/settings/ProvidersPanel.vue`、`AddProviderFlow.vue`、`ProviderForm.vue`、`lib/providerForm.ts`(+测试)、`useModelProviderState.ts` 的供应商重写、`SettingsDialog.vue` 的 providers tab、onboarding 的自定义供应商卡(`OnboardingWizard`/`OnboardingLoginStep` 的 `addProvider` emit 与卡片)现已两端一致(仅品牌头/日志前缀差异),旧 `ProviderManager.vue` 已从 web 删除。后续两端共用改动照常 desktop 先行、同步 web;web 侧无 track/loginSource 块(desktop 侧保留)。 - **设置页「程序坞图标」行(desktop macOS only)** - - 实现:通用页外观组**末尾**新增 `components/settings/DockIconPicker.vue`(desktop 专属组件,**不复用 web-ui 的 SegmentedControl**,不同步 web;控件模式已登记进 `DesignSystemView.vue` §03 的 SegmentedControl 之后,带活体 demo,web 的 DesignSystemView 不涉及):**两个选项「默认 / 黑色」**直接渲染应用图标瓷砖(BrandLogo 强制 variant,白砖 / 黑砖),分组容器 + 滑动选中 pill 复刻分段控件视觉语言;瓷砖去描边并用 n=4.5 超椭圆 clip-path(与打进 .icns 的圆角同形),底下垫随主题反转的文字色 wash 保持可读。**刻意不提供「跟随系统」**——避免与上一行的明暗主题设置混淆(早期版本有 `auto`,后经 `kimi:os-appearance` 系列 IPC 跟随真实 OS 外观,已整体拆除:picker 的 OS 外观查询/订阅、主进程 `defaults read`/`osascript` 探测与 `nativeTheme` + `AppleInterfaceThemeChangedNotification` 监听、`kimi:os-appearance[-changed]` 通道与 preload 的 `getOsAppearance`/`onOsAppearanceChanged` 全部移除)。状态在 desktop 专属 `lib/dockIconChoice.ts`(**不同步 web**,同 keymap.ts 先例;`kimi-web.dock-icon-choice` key 只加在 desktop 的 `lib/storage.ts`):localStorage 持久化 + 经桥 `setDockIconChoice`(IPC `kimi:dock-icon-choice`)推给主进程,模块加载时做一次启动首推;取值仅 `'light'|'dark'`,**存量 `auto` 与任何异常值都回落 `'light'`**(与打进 .icns 的静态图标一致)。主进程 `dock-icon.ts` 存 `currentChoice`,每次 IPC 推送时镜像写入 `/ui-state.json`(`ui-state.ts` 的 `getDockIconChoice`/`saveDockIconChoice`,同 vibrancy 先例——renderer localStorage 是本地存储正本但要等窗口 boot 才经 IPC 到达,主进程这份只是启动种子,推送永远覆盖它),`initDockIcon` 启动先读种子再 apply(读不到回落 `'light'`,与 .icns 一致),之后仅在 IPC 推送时重设 Dock 图标;preload 与 IPC 均校验取值。行可见条件 `isMacosDesktop && canSetDockIconChoice()`(桥探测)。 + - 实现:通用页外观组**末尾**新增 `components/settings/DockIconPicker.vue`(desktop 专属组件,**不复用 app-ui 的 SegmentedControl**,不同步 web;控件模式已登记进 `DesignSystemView.vue` §03 的 SegmentedControl 之后,带活体 demo,web 的 DesignSystemView 不涉及):**两个选项「默认 / 黑色」**直接渲染应用图标瓷砖(BrandLogo 强制 variant,白砖 / 黑砖),分组容器 + 滑动选中 pill 复刻分段控件视觉语言;瓷砖去描边并用 n=4.5 超椭圆 clip-path(与打进 .icns 的圆角同形),底下垫随主题反转的文字色 wash 保持可读。**刻意不提供「跟随系统」**——避免与上一行的明暗主题设置混淆(早期版本有 `auto`,后经 `kimi:os-appearance` 系列 IPC 跟随真实 OS 外观,已整体拆除:picker 的 OS 外观查询/订阅、主进程 `defaults read`/`osascript` 探测与 `nativeTheme` + `AppleInterfaceThemeChangedNotification` 监听、`kimi:os-appearance[-changed]` 通道与 preload 的 `getOsAppearance`/`onOsAppearanceChanged` 全部移除)。状态在 desktop 专属 `lib/dockIconChoice.ts`(**不同步 web**,同 keymap.ts 先例;`kimi-web.dock-icon-choice` key 只加在 desktop 的 `lib/storage.ts`):localStorage 持久化 + 经桥 `setDockIconChoice`(IPC `kimi:dock-icon-choice`)推给主进程,模块加载时做一次启动首推;取值仅 `'light'|'dark'`,**存量 `auto` 与任何异常值都回落 `'light'`**(与打进 .icns 的静态图标一致)。主进程 `dock-icon.ts` 存 `currentChoice`,每次 IPC 推送时镜像写入 `/ui-state.json`(`ui-state.ts` 的 `getDockIconChoice`/`saveDockIconChoice`,同 vibrancy 先例——renderer localStorage 是本地存储正本但要等窗口 boot 才经 IPC 到达,主进程这份只是启动种子,推送永远覆盖它),`initDockIcon` 启动先读种子再 apply(读不到回落 `'light'`,与 .icns 一致),之后仅在 IPC 推送时重设 Dock 图标;preload 与 IPC 均校验取值。行可见条件 `isMacosDesktop && canSetDockIconChoice()`(桥探测)。 - **web 不改**:浏览器没有 Dock;web 的 `SettingsDialog.vue` 无此行、无此 import,`DockIconPicker.vue` 与 `lib/dockIconChoice.ts` 均不存在于 apps/web,整目录 re-copy 时需保留 desktop 侧分叉块。 - 测试:`tests/main/dock-icon.test.ts`(路径 + `isDockIconChoice` + 非 darwin 平台守卫);`tests/main/preload.test.ts` 白名单 + 合法/非法值断言;`tests/main/ui-state.test.ts`(`dockIconChoice` 回读 / 非法与存量 `auto` 值判 undefined / 保留其它 key)。 - **侧边栏用户区(UserMenu.vue,2026-07;含原侧栏设置入口「内部测试」badge,现挂触发按钮用户名右侧)** - 实现:侧栏 footer 的全宽「设置」按钮被 `components/UserMenu.vue` 取代——已登录显示 24px 圆形头像(`@error` 回退 user 图标,URL 变化重置失败态)+ 昵称,未登录显示 user 图标 +「未登录」;点击向上弹出 §03 菜单(手写 fixed 定位:`left`/`width` 跟随触发器并用 ResizeObserver 在侧栏拖宽时同步,竖直方向 `top`/`bottom` 成对显式设置(未用写 `'auto'`)——class 里的 `top:0` 与 bottom 并存曾把菜单拉成全高;底边以 `bottom` 锚定在触发器上方 4px,空间不足向下 flip;`max-height: calc(100vh - 16px)` + `overflow-y: auto` 兜底;外点 / capture 阶段 Esc / window resize 关闭)。菜单内容:套餐用量 / 明暗 / 语言三个模块均为 macOS 风格 hover flyout 子菜单——父行(模块图标 + label + faint 当前值 + 固定 chevron-right,`aria-haspopup` + `aria-expanded`)`mouseenter` 打开子菜单并同时关闭其它(`openSubmenu: 'usage'|'theme'|'language'|null` 单值状态机),键盘可达与 hover 同语义(`@focus` 展开、`@blur` 走同一 250ms 宽限、`@click` 幂等展开、`@keydown` 的 Enter/Space/→ 展开并 preventDefault;不做全键盘 ↑↓ 导航——全仓既有缺口,Esc 仍整体关闭不分级),`mouseleave` 起 250ms hover-intent 宽限关闭、进入子菜单面板取消计时;子菜单独立 `` + Menu 表面 + menu-pop(宽度内容自适应 `width: max-content`——下限沿用 Menu 自带的 `min-width: 180px` §03 约定,`maxWidth` 内联钳到父菜单宽度、用量 reset hint 超长时 ellipsis;`left` = 父菜单 rect.right + 4px、右缘按实际渲染宽度判断、溢出视口翻到左侧、top 对齐父行并 8px clamp;父菜单 ResizeObserver 重定位时直接关子菜单,不做跟随;外点判定含 `.user-submenu`)。用量面板 = 三态 + `usagePercent` 百分比行(打开父菜单时 `client.getUsage()` 逻辑不变,重试按钮不关菜单);明暗 / 语言面板选项点击 = 应用选择、check 移动,子菜单与整个菜单保持打开(不自动关;`lib/planUsage.ts` 的 `findUsageByWindow` / `MAX_USER_LEVEL = 30` / `shouldShowUpgrade` 不变)→ 升级项(`window.open` 官网升级页,music 图标;`v-if="showUpgrade"`——仅 `userLevel < MAX_USER_LEVEL (30)` 显示,`shouldShowUpgrade` 纯函数与测试在 `lib/planUsage.ts`)→ 设置(desktop 右侧常驻 `Kbd` 键帽快捷键 hint(与侧栏新建会话/搜索同款呈现,但非 hover 才显):`resolvedBindingKeys('openSettings')` 数组直接喂 caps,跟随用户自定义绑定,未分配不显示;web 端没有 openSettings 键位绑定,故无此 hint)→ 退出登录(普通 MenuItem——菜单项去红色,确认弹窗的 danger 确认键不变;`useConfirmDialog` 二次确认;设置页的退出确认改在 `App.vue` 的 `confirmLogout`,SettingsDialog 不动)。菜单经 `` 渲染——`.col` 的 `container-type: inline-size` 会捕获 position:fixed(Windows 标题栏下会错位)。菜单图标全部 Kimi 集合(histogram / translate / follow-system / sign-out / music 从设计资产拷入两端 `icons/kimi/`,`log-out` 映射由 ri 换回 Kimi SignOut,`user`/`settings`/`log-in`/chevron/check/light-mode/dark-mode 本为 Kimi;升级项由 remix sparkles 换成 music 后菜单内已无 remix——`sparkles` 名称因 SwarmTool/ChatDock/Composer/toolMeta 仍在用而保留注册)。 - - 关联修复(评审跟进,两端同改):`useWorkspaceState.checkAuth` 的 fire-and-forget userinfo 加请求代际门控(`userInfoFetchGeneration` 在 checkAuth **入口**递增、以调用发起顺序为序——后发起者赢,先发起的慢 /auth 不会反被当成最新;重叠 checkAuth 时旧调用的迟到响应不再覆盖、旧 rejection 不再清空最新调用写入的 profile;非 authenticated 分支无需再手动作废——入口递增已天然作废在途请求;原有登出 status 守卫保留为第二重),desktop `tests/renderer/workspaceStateManagedUserInfo.test.ts` 与 web `test/workspace-state.test.ts` 各补 3 个竞态用例(旧请求晚到 resolve 不覆盖 / 晚到 reject 不清空 / 第一次调用的 /auth 更慢时 profile 仍以第二次为准)。移动端同款 Esc 穿透修复:`BottomSheet.vue` 新增 `closeOnEsc` prop(默认 true,与 web-ui Dialog 的 `close-on-esc` 对称),`MobileSettingsSheet.vue` 绑 `:close-on-esc="!isConfirmOpen"`(`useConfirmDialog` 暴露的确认框打开态,`SettingsDialog.vue` 的 Esc 守卫用同一状态源),退出登录确认框打开时按 Esc 不再连带关闭移动端设置页;用量 flyout 的 ARIA role 按内容切换(`:role="openSubmenu === 'usage' ? 'dialog' : 'menu'"`——用量面板是状态行 + 重试 Button 而非 MenuItem,用 Menu 自带的 `role?: 'menu'|'dialog'` prop)。 + - 关联修复(评审跟进,两端同改):`useWorkspaceState.checkAuth` 的 fire-and-forget userinfo 加请求代际门控(`userInfoFetchGeneration` 在 checkAuth **入口**递增、以调用发起顺序为序——后发起者赢,先发起的慢 /auth 不会反被当成最新;重叠 checkAuth 时旧调用的迟到响应不再覆盖、旧 rejection 不再清空最新调用写入的 profile;非 authenticated 分支无需再手动作废——入口递增已天然作废在途请求;原有登出 status 守卫保留为第二重),desktop `tests/renderer/workspaceStateManagedUserInfo.test.ts` 与 web `test/workspace-state.test.ts` 各补 3 个竞态用例(旧请求晚到 resolve 不覆盖 / 晚到 reject 不清空 / 第一次调用的 /auth 更慢时 profile 仍以第二次为准)。移动端同款 Esc 穿透修复:`BottomSheet.vue` 新增 `closeOnEsc` prop(默认 true,与 app-ui Dialog 的 `close-on-esc` 对称),`MobileSettingsSheet.vue` 绑 `:close-on-esc="!isConfirmOpen"`(`useConfirmDialog` 暴露的确认框打开态,`SettingsDialog.vue` 的 Esc 守卫用同一状态源),退出登录确认框打开时按 Esc 不再连带关闭移动端设置页;用量 flyout 的 ARIA role 按内容切换(`:role="openSubmenu === 'usage' ? 'dialog' : 'menu'"`——用量面板是状态行 + 重试 Button 而非 MenuItem,用 Menu 自带的 `role?: 'menu'|'dialog'` prop)。 - **desktop 分叉块**:track import + 三处调用(主题切换 `settings_changed{source_panel:'user_menu'}`、语言切换同事件 `key:'language'`、升级 `upgrade_clicked`;契约 `src/shared/track-events.ts` 同步登记新事件与 source_panel 枚举值,`main/telemetry-events.ts` 同步)+ 「内部测试」Badge(desktop 生产包 `isDesktop && import.meta.env.PROD`,挂在触发按钮用户名右侧,登录 / 未登录态都渲染)+ 设置项快捷键 hint(`resolvedBindingKeys` / `isAppleShortcutPlatform` import 与 `settingsShortcutHint` computed;web 无 openSettings 快捷键,不渲染)。web 版不渲染 Badge 与快捷键 hint、不 import `Badge`、无 track 调用;UPGRADE_URL 的 `from` 归因参数两端分叉(desktop `kimi_code_desktop` / web `kimi_code_web`),re-copy 同步时各自保留;其余结构两端一致。 - 整目录 re-copy 同步时需保留 desktop 侧的 track import/调用、Badge import/模板与快捷键 hint 块。 diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 5bf72ae80..d99f2d578 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -37,10 +37,10 @@ "@moonshot-ai/kimi-code-oauth": "workspace:^", "@moonshot-ai/kimi-code-sdk": "workspace:^", "@moonshot-ai/vite-preset": "workspace:*", - "@moonshot-ai/web-core": "workspace:*", - "@moonshot-ai/web-i18n": "workspace:*", - "@moonshot-ai/web-markdown": "workspace:*", - "@moonshot-ai/web-ui": "workspace:*", + "@moonshot-ai/app-core": "workspace:*", + "@moonshot-ai/app-i18n": "workspace:*", + "@moonshot-ai/app-markdown": "workspace:*", + "@moonshot-ai/app-ui": "workspace:*", "@rive-app/canvas": "2.38.5", "@vitejs/plugin-vue": "^5.2.4", "@xterm/addon-fit": "^0.11.0", diff --git a/apps/desktop/src/renderer/App.vue b/apps/desktop/src/renderer/App.vue index f21b612ff..e62eeafac 100644 --- a/apps/desktop/src/renderer/App.vue +++ b/apps/desktop/src/renderer/App.vue @@ -35,25 +35,25 @@ import { useSidebarLayout } from './composables/useSidebarLayout'; import { useFilePreview, type DetailTarget } from './composables/useFilePreview'; import { useDetailPanel } from './composables/useDetailPanel'; import { useIsMobile } from './composables/useIsMobile'; -import { openDialogCount } from '@moonshot-ai/web-ui'; +import { openDialogCount } from '@moonshot-ai/app-ui'; import type { SwarmMember } from './composables/swarmGroups'; import ServerAuthDialog from './components/ServerAuthDialog.vue'; -import { initServerAuth, onAuthRequired } from './lib/serverAuth'; +import { initServerAuth, onAuthRequired } from '@moonshot-ai/app-core/lib'; import { canPickWorkspaceDirectory, createAddWorkspaceEntry, pickWorkspaceDirectory, } from './lib/nativeWorkspacePicker'; import type { AppConfig, ThinkingLevel } from './api/types'; -import { commitLevel, effectiveThinkingLevel, segmentsFor } from './lib/modelThinking'; +import { commitLevel, effectiveThinkingLevel, segmentsFor } from '@moonshot-ai/app-core/lib'; import { modelDisplayName, subagentEffortSuffix } from './lib/modelDisplay'; -import { stripSkillPrefix } from './lib/slashCommands'; -import { ActionToast, Icon, IconButton } from '@moonshot-ai/web-ui'; -import { isMacosDesktop, isWindowsDesktop } from './lib/desktopFlag'; +import { stripSkillPrefix } from '@moonshot-ai/app-core/lib'; +import { ActionToast, Icon, IconButton } from '@moonshot-ai/app-ui'; +import { isMacosDesktop, isWindowsDesktop } from '@moonshot-ai/app-core/lib'; import WindowsTitleBar from './components/window/WindowsTitleBar.vue'; import TerminalPanel from './components/terminal/TerminalPanel.vue'; import TerminalResizeHandle from './components/terminal/TerminalResizeHandle.vue'; -import { selectContentsOf } from './lib/transcriptSelectAll'; +import { selectContentsOf } from '@moonshot-ai/app-core/lib'; import { useFullscreen } from './composables/useFullscreen'; import { useNativeTerminal, nativeTerminalDraftKey } from './composables/useNativeTerminal'; import { runWhenInitialized, useTrayAttention } from './composables/useTrayAttention'; @@ -69,7 +69,7 @@ import { useDefaultOpenInTarget, } from './lib/nativeOpenIn'; import { track } from './lib/track'; -import { openUpgrade } from './lib/upgrade'; +import { openUpgrade } from '@moonshot-ai/app-core/lib'; import { setSessionIntent } from './lib/session-intent'; import type { SessionCreatedSource } from '../shared/track-events'; import { isAppActionId, type AppActionId } from '../shared/action-ids'; @@ -2121,7 +2121,7 @@ function openPr(url: string): void { /* macOS desktop (hidden title bar): the right panel's header row continues the conversation header's 48px top strip, so it joins the window-drag region. Interactive controls inside opt out (same pattern as ChatHeader). Cross- - component rule (the header is web-ui's PanelHeader), so it lives here. */ + component rule (the header is app-ui's PanelHeader), so it lives here. */ .app.macos-desktop .global-preview .ui-panel-header { -webkit-app-region: drag; } diff --git a/apps/desktop/src/renderer/api/bootstrap.ts b/apps/desktop/src/renderer/api/bootstrap.ts index 3cd06a6e7..0c55d3d38 100644 --- a/apps/desktop/src/renderer/api/bootstrap.ts +++ b/apps/desktop/src/renderer/api/bootstrap.ts @@ -1,13 +1,13 @@ -// apps/web src/api/bootstrap.ts — composes the DaemonKimiWebApi (web-core) with +// apps/web src/api/bootstrap.ts — composes the DaemonKimiWebApi (app-core) with // the apps/web tracer, credential store, and agent projector, and exposes the // shared singleton. // -// This is the only module that knows both sides: web-core's api client and +// This is the only module that knows both sides: app-core's api client and // apps/web's debug/trace + serverAuth + projector. Everything else consumes the // composed `api` (or the back-compat `getKimiWebApi()` accessor). -import { DaemonKimiWebApi } from '@moonshot-ai/web-core/api'; -import type { CredentialStore, Tracer } from '@moonshot-ai/web-core/contracts'; +import { DaemonKimiWebApi } from '@moonshot-ai/app-core/api'; +import type { CredentialStore, Tracer } from '@moonshot-ai/app-core/contracts'; import { traceRestFailure, traceRestRequest, @@ -17,7 +17,7 @@ import { traceWsOut, traceKeyEvent as recordKeyEvent, } from '../debug/trace'; -import { getCredential, markAuthRequired } from '../lib/serverAuth'; +import { getCredential, markAuthRequired } from '@moonshot-ai/app-core/lib'; import { readKimiApiConfig } from './config'; import { createAgentProjector } from './daemon/agentEventProjector'; import type { KimiWebApi } from './types'; diff --git a/apps/desktop/src/renderer/api/config.ts b/apps/desktop/src/renderer/api/config.ts index e4e864fb0..fb023139d 100644 --- a/apps/desktop/src/renderer/api/config.ts +++ b/apps/desktop/src/renderer/api/config.ts @@ -1,11 +1,11 @@ // apps/web src/api/config.ts — reads Vite env + window, resolves the server // origin, and derives the stable client identity for the DaemonKimiWebApi. // -// Pure URL builders (buildRestUrl / buildWsUrl) live in @moonshot-ai/web-core; +// Pure URL builders (buildRestUrl / buildWsUrl) live in @moonshot-ai/app-core; // this module keeps only the consumer-runtime concerns (window / import.meta.env // / storage) that the package deliberately does not own. -import { safeGetString, safeSetString, STORAGE_KEYS } from '../lib/storage'; +import { safeGetString, safeSetString, STORAGE_KEYS } from '@moonshot-ai/app-core/lib'; import { DESKTOP_PRODUCT_NAME, DESKTOP_UI_MODE } from '../../shared/identity'; const CLIENT_ID_KEY = STORAGE_KEYS.clientId; diff --git a/apps/desktop/src/renderer/api/daemon/agentEventProjector.ts b/apps/desktop/src/renderer/api/daemon/agentEventProjector.ts index 78e7895dc..bcd123cc2 100644 --- a/apps/desktop/src/renderer/api/daemon/agentEventProjector.ts +++ b/apps/desktop/src/renderer/api/daemon/agentEventProjector.ts @@ -26,10 +26,10 @@ import type { AppTask, } from '../types'; import { i18n } from '../../i18n'; -import { logError } from '../../lib/log'; +import { logError } from '@moonshot-ai/app-core/lib'; import { toolLabel, toolSummary } from '../../lib/toolMeta'; -import { toAppMessageContent } from '@moonshot-ai/web-core/api'; -import type { AgentProjector, ProjectMeta, WireMessageContent } from '@moonshot-ai/web-core/api'; +import { toAppMessageContent } from '@moonshot-ai/app-core/api'; +import type { AgentProjector, ProjectMeta, WireMessageContent } from '@moonshot-ai/app-core/api'; // Subagent turns share the parent session id: their turn / step / delta / tool // frames stream over the SAME session channel, each tagged with the subagent's @@ -494,7 +494,7 @@ function buildUsageSnapshot(state: SessionState): AppSessionUsage { // AgentProjector // --------------------------------------------------------------------------- -// ProjectMeta + AgentProjector contracts live in @moonshot-ai/web-core/api +// ProjectMeta + AgentProjector contracts live in @moonshot-ai/app-core/api // (imported above); this is the apps/web implementation (i18n + toolMeta). export function createAgentProjector(): AgentProjector { diff --git a/apps/desktop/src/renderer/api/errors.ts b/apps/desktop/src/renderer/api/errors.ts index 3a34ef1dd..ee82944f2 100644 --- a/apps/desktop/src/renderer/api/errors.ts +++ b/apps/desktop/src/renderer/api/errors.ts @@ -1,5 +1,5 @@ // apps/web src/api/errors.ts — re-export daemon error types + guards from -// web-core so existing `import … from '../api/errors'` sites keep working. +// app-core so existing `import … from '../api/errors'` sites keep working. export { DaemonApiError, DaemonNetworkError, @@ -7,4 +7,4 @@ export { isDaemonApiError, isDaemonNetworkError, isFileTooLargeError, -} from '@moonshot-ai/web-core/api'; +} from '@moonshot-ai/app-core/api'; diff --git a/apps/desktop/src/renderer/api/index.ts b/apps/desktop/src/renderer/api/index.ts index f9e2e8b1e..aedd150a1 100644 --- a/apps/desktop/src/renderer/api/index.ts +++ b/apps/desktop/src/renderer/api/index.ts @@ -1,5 +1,5 @@ // apps/web src/api — thin barrel. The api client + transport now live in -// @moonshot-ai/web-core; this module exposes the apps/web-composed singleton +// @moonshot-ai/app-core; this module exposes the apps/web-composed singleton // (see ./bootstrap) so existing `import { getKimiWebApi } from '../api'` sites // keep working unchanged. export { api, getKimiWebApi } from './bootstrap'; diff --git a/apps/desktop/src/renderer/api/types.ts b/apps/desktop/src/renderer/api/types.ts index b9fab74a2..d65f0754b 100644 --- a/apps/desktop/src/renderer/api/types.ts +++ b/apps/desktop/src/renderer/api/types.ts @@ -1,5 +1,5 @@ -// apps/web src/api/types.ts — re-export the api types from web-core so the many +// apps/web src/api/types.ts — re-export the api types from app-core so the many // existing `import … from '../api/types'` (and '../../api/types') sites keep // working without per-file rewrites. The single source of truth now lives in -// @moonshot-ai/web-core/api. -export * from '@moonshot-ai/web-core/api'; +// @moonshot-ai/app-core/api. +export * from '@moonshot-ai/app-core/api'; diff --git a/apps/desktop/src/renderer/components/FilePreview.vue b/apps/desktop/src/renderer/components/FilePreview.vue index 8c6e6538d..65ff0ce31 100644 --- a/apps/desktop/src/renderer/components/FilePreview.vue +++ b/apps/desktop/src/renderer/components/FilePreview.vue @@ -3,11 +3,11 @@ diff --git a/apps/desktop/src/renderer/components/HighlightedCode.vue b/apps/desktop/src/renderer/components/HighlightedCode.vue index ceef2498a..89ccc5085 100644 --- a/apps/desktop/src/renderer/components/HighlightedCode.vue +++ b/apps/desktop/src/renderer/components/HighlightedCode.vue @@ -7,8 +7,8 @@ diff --git a/apps/desktop/src/renderer/components/settings/PlanUsageCard.vue b/apps/desktop/src/renderer/components/settings/PlanUsageCard.vue index 8b9f715f8..3f6d96987 100644 --- a/apps/desktop/src/renderer/components/settings/PlanUsageCard.vue +++ b/apps/desktop/src/renderer/components/settings/PlanUsageCard.vue @@ -15,10 +15,10 @@ import { moneyParts, usagePercent, usageSeverity, -} from '../../lib/planUsage'; +} from '@moonshot-ai/app-core/lib'; import { track } from '../../lib/track'; import PlanUpgradeCard from './PlanUpgradeCard.vue'; -import { Button, Spinner } from '@moonshot-ai/web-ui'; +import { Button, Spinner } from '@moonshot-ai/app-ui'; const props = defineProps<{ onFetchUsage: () => Promise; diff --git a/apps/desktop/src/renderer/components/settings/ProviderForm.vue b/apps/desktop/src/renderer/components/settings/ProviderForm.vue index 0d4ddf825..086767f23 100644 --- a/apps/desktop/src/renderer/components/settings/ProviderForm.vue +++ b/apps/desktop/src/renderer/components/settings/ProviderForm.vue @@ -20,7 +20,7 @@ import { validateProviderForm, type ProviderFormState, } from '../../lib/providerForm'; -import { Banner, Button, Icon, IconButton, Input, Select } from '@moonshot-ai/web-ui'; +import { Banner, Button, Icon, IconButton, Input, Select } from '@moonshot-ai/app-ui'; const props = defineProps<{ mode: 'add' | 'edit'; @@ -445,7 +445,7 @@ function removeModelRow(index: number): void { .pf-key-wrap { position: relative; } .pf-key-wrap :deep(.ui-input) { padding-right: calc(var(--icon-button-sm) + var(--space-2)); } .pf-key-eye { position: absolute; right: var(--space-1); top: 50%; transform: translateY(-50%); } -/* Same label styling as web-ui's Field, plus a required marker. */ +/* Same label styling as app-ui's Field, plus a required marker. */ .pf-field-label { font-family: var(--font-ui); font-size: var(--text-sm); diff --git a/apps/desktop/src/renderer/components/settings/ProvidersPanel.vue b/apps/desktop/src/renderer/components/settings/ProvidersPanel.vue index f9e4a79e6..5ce11acf2 100644 --- a/apps/desktop/src/renderer/components/settings/ProvidersPanel.vue +++ b/apps/desktop/src/renderer/components/settings/ProvidersPanel.vue @@ -13,7 +13,7 @@ import { useKimiWebClient } from '../../composables/useKimiWebClient'; import { isManagedOAuthProvider, providerModelRows } from '../../lib/providerForm'; import AddProviderFlow from './AddProviderFlow.vue'; import ProviderForm from './ProviderForm.vue'; -import { Badge, Button, Icon, Spinner } from '@moonshot-ai/web-ui'; +import { Badge, Button, Icon, Spinner } from '@moonshot-ai/app-ui'; const { t } = useI18n(); const client = useKimiWebClient(); diff --git a/apps/desktop/src/renderer/components/settings/SecondaryModelPicker.vue b/apps/desktop/src/renderer/components/settings/SecondaryModelPicker.vue index 231c4a274..8717053d5 100644 --- a/apps/desktop/src/renderer/components/settings/SecondaryModelPicker.vue +++ b/apps/desktop/src/renderer/components/settings/SecondaryModelPicker.vue @@ -17,8 +17,8 @@ ', 'script'); - expect(out).not.toContain(' diff --git a/apps/web/src/components/HighlightedCode.vue b/apps/web/src/components/HighlightedCode.vue index ceef2498a..89ccc5085 100644 --- a/apps/web/src/components/HighlightedCode.vue +++ b/apps/web/src/components/HighlightedCode.vue @@ -7,8 +7,8 @@ diff --git a/apps/web/src/components/settings/PlanUsageCard.vue b/apps/web/src/components/settings/PlanUsageCard.vue index 87a672421..96c6d6dfb 100644 --- a/apps/web/src/components/settings/PlanUsageCard.vue +++ b/apps/web/src/components/settings/PlanUsageCard.vue @@ -15,9 +15,9 @@ import { moneyParts, usagePercent, usageSeverity, -} from '../../lib/planUsage'; +} from '@moonshot-ai/app-core/lib'; import PlanUpgradeCard from './PlanUpgradeCard.vue'; -import { Button, Spinner } from '@moonshot-ai/web-ui'; +import { Button, Spinner } from '@moonshot-ai/app-ui'; const props = defineProps<{ onFetchUsage: () => Promise; diff --git a/apps/web/src/components/settings/ProviderForm.vue b/apps/web/src/components/settings/ProviderForm.vue index 0d4ddf825..086767f23 100644 --- a/apps/web/src/components/settings/ProviderForm.vue +++ b/apps/web/src/components/settings/ProviderForm.vue @@ -20,7 +20,7 @@ import { validateProviderForm, type ProviderFormState, } from '../../lib/providerForm'; -import { Banner, Button, Icon, IconButton, Input, Select } from '@moonshot-ai/web-ui'; +import { Banner, Button, Icon, IconButton, Input, Select } from '@moonshot-ai/app-ui'; const props = defineProps<{ mode: 'add' | 'edit'; @@ -445,7 +445,7 @@ function removeModelRow(index: number): void { .pf-key-wrap { position: relative; } .pf-key-wrap :deep(.ui-input) { padding-right: calc(var(--icon-button-sm) + var(--space-2)); } .pf-key-eye { position: absolute; right: var(--space-1); top: 50%; transform: translateY(-50%); } -/* Same label styling as web-ui's Field, plus a required marker. */ +/* Same label styling as app-ui's Field, plus a required marker. */ .pf-field-label { font-family: var(--font-ui); font-size: var(--text-sm); diff --git a/apps/web/src/components/settings/ProvidersPanel.vue b/apps/web/src/components/settings/ProvidersPanel.vue index f9e4a79e6..5ce11acf2 100644 --- a/apps/web/src/components/settings/ProvidersPanel.vue +++ b/apps/web/src/components/settings/ProvidersPanel.vue @@ -13,7 +13,7 @@ import { useKimiWebClient } from '../../composables/useKimiWebClient'; import { isManagedOAuthProvider, providerModelRows } from '../../lib/providerForm'; import AddProviderFlow from './AddProviderFlow.vue'; import ProviderForm from './ProviderForm.vue'; -import { Badge, Button, Icon, Spinner } from '@moonshot-ai/web-ui'; +import { Badge, Button, Icon, Spinner } from '@moonshot-ai/app-ui'; const { t } = useI18n(); const client = useKimiWebClient(); diff --git a/apps/web/src/components/settings/SecondaryModelPicker.vue b/apps/web/src/components/settings/SecondaryModelPicker.vue index 231c4a274..8717053d5 100644 --- a/apps/web/src/components/settings/SecondaryModelPicker.vue +++ b/apps/web/src/components/settings/SecondaryModelPicker.vue @@ -17,8 +17,8 @@