diff --git a/.agents/skills/gen-changesets/SKILL.md b/.agents/skills/gen-changesets/SKILL.md index 15471a032..1c93e34c1 100644 --- a/.agents/skills/gen-changesets/SKILL.md +++ b/.agents/skills/gen-changesets/SKILL.md @@ -11,6 +11,8 @@ description: Use when generating changesets in the kimi-code repository, includi All other `@moonshot-ai/*` packages are treated as internal packages, including `@moonshot-ai/kimi-code-sdk`, `agent-core`, `kosong`, `kaos`, `kimi-code-oauth`, `kimi-telemetry`, and `migration-legacy`. +`@moonshot-ai/pi-tui` is a special internal package: it is a private fork (`private: true`) that is never published, but it keeps its own changelog through changesets. It is an exception to Core Rule 4 — see the dedicated section below. + ## Core Rules 1. **Inspect the actual changes first.** Use `git status` / `git diff --name-only` to identify which packages were actually changed. @@ -176,6 +178,41 @@ Add the server-hosted web UI, including chat layout and session list behaviors. Add the server REST and WebSocket APIs that power the web UI. ``` +## `@moonshot-ai/pi-tui` changes + +`@moonshot-ai/pi-tui` is a vendored fork that lives in `packages/pi-tui`. It is `private: true` and is never published, but it is **not** ignored by changesets: changesets versions it and writes `packages/pi-tui/CHANGELOG.md` so the fork keeps its own history. Because it is bundled into the CLI like other internal packages, it is an exception to Core Rule 4 — do **not** list `@moonshot-ai/kimi-code` for a change that only touches pi-tui. + +- Changes that only affect pi-tui (build, package, strict-mode cleanup, renderer fixes): list `@moonshot-ai/pi-tui` only. No CLI changeset. +- If the same change is also user-visible in the CLI (for example a terminal rendering fix that CLI users can see), add a **separate** changeset that lists `@moonshot-ai/kimi-code` with CLI-focused wording, in addition to the pi-tui changeset. Do not mix both packages in one frontmatter — the two changelogs need different wording. + +pi-tui-only change: + +```markdown +--- +"@moonshot-ai/pi-tui": patch +--- + +Export the package manifest so the bundled binary can locate its native assets. +``` + +pi-tui change that is also visible in the CLI (two separate changesets): + +```markdown +--- +"@moonshot-ai/pi-tui": patch +--- + +Clamp the differential render to the visible viewport so scrolling up during streaming no longer jumps to the top. +``` + +```markdown +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix the transcript jumping to the top when scrolling up through history during streaming output. +``` + ## Red Flags - You are about to write `major` without asking the user. @@ -188,3 +225,4 @@ Add the server REST and WebSocket APIs that power the web UI. - The wording claims more than the diff actually did. - The CLI wording mentions internal package names, class names, or PR numbers. - The entry includes real internal identifiers instead of neutral placeholders. +- A change that only touches `@moonshot-ai/pi-tui` lists `@moonshot-ai/kimi-code` instead of `@moonshot-ai/pi-tui`, or mixes both packages in one frontmatter. diff --git a/.changeset/add-gui-store-api.md b/.changeset/add-gui-store-api.md deleted file mode 100644 index 0bca72941..000000000 --- a/.changeset/add-gui-store-api.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@moonshot-ai/protocol": patch -"@moonshot-ai/server": patch -"@moonshot-ai/kimi-code": patch ---- - -Add a server-side key-value store API for persisting web UI preferences to the user's data directory. diff --git a/.changeset/close-abandoned-tool-exchanges.md b/.changeset/close-abandoned-tool-exchanges.md new file mode 100644 index 000000000..94f397435 --- /dev/null +++ b/.changeset/close-abandoned-tool-exchanges.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix sessions silently dropping later user messages after a turn was interrupted between a tool call and its result. diff --git a/.changeset/compaction-forward-plan.md b/.changeset/compaction-forward-plan.md new file mode 100644 index 000000000..69765325f --- /dev/null +++ b/.changeset/compaction-forward-plan.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Have context-compaction notes capture a forward plan for the remaining work — upcoming steps, settled decisions, and foreseeable obstacles — instead of only the immediate next step, so the agent continues more coherently after auto-compaction. diff --git a/.changeset/dedupe-duplicate-tool-call-ids.md b/.changeset/dedupe-duplicate-tool-call-ids.md new file mode 100644 index 000000000..ab358a693 --- /dev/null +++ b/.changeset/dedupe-duplicate-tool-call-ids.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix requests being rejected by strict providers when the model emits duplicate tool call ids. diff --git a/.changeset/double-esc-undo.md b/.changeset/double-esc-undo.md deleted file mode 100644 index a48970c97..000000000 --- a/.changeset/double-esc-undo.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@moonshot-ai/kimi-code": patch ---- - -Add a double-Esc shortcut to open the undo selector. Press Esc twice while idle to undo. diff --git a/.changeset/fix-at-mention-slash-arg.md b/.changeset/fix-at-mention-slash-arg.md deleted file mode 100644 index 64c373cd0..000000000 --- a/.changeset/fix-at-mention-slash-arg.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@moonshot-ai/kimi-code": patch ---- - -Fix @ file mentions not opening when typed inside a slash command argument. diff --git a/.changeset/fix-login-shell-path.md b/.changeset/fix-login-shell-path.md new file mode 100644 index 000000000..80695da31 --- /dev/null +++ b/.changeset/fix-login-shell-path.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Enrich PATH from the user's login shell at startup, so shell commands find user-installed tools (e.g. Homebrew's `gh`) even when kimi-code was launched without the full profile PATH. diff --git a/.changeset/fix-shell-mode-slash-completion.md b/.changeset/fix-shell-mode-slash-completion.md deleted file mode 100644 index 9078ab1d2..000000000 --- a/.changeset/fix-shell-mode-slash-completion.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@moonshot-ai/kimi-code": patch ---- - -Show file path completions when typing `/` in shell mode (`!`). diff --git a/.changeset/fix-web-duplicate-workspaces.md b/.changeset/fix-web-duplicate-workspaces.md deleted file mode 100644 index b389fcae7..000000000 --- a/.changeset/fix-web-duplicate-workspaces.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@moonshot-ai/agent-core": patch -"@moonshot-ai/kimi-code": patch ---- - -Fix duplicate workspaces showing in the web sidebar when the same folder is registered more than once. diff --git a/.changeset/fix-windows-update-console-flash.md b/.changeset/fix-windows-update-console-flash.md new file mode 100644 index 000000000..26df3a34c --- /dev/null +++ b/.changeset/fix-windows-update-console-flash.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Keep automatic background updates from flashing a console window on Windows. diff --git a/.changeset/fix-windows-upgrade-spawn.md b/.changeset/fix-windows-upgrade-spawn.md new file mode 100644 index 000000000..c4a8678b7 --- /dev/null +++ b/.changeset/fix-windows-upgrade-spawn.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix `kimi upgrade` failing on Windows with a spawn error when installing the new version. diff --git a/.changeset/language-matching-prompt.md b/.changeset/language-matching-prompt.md new file mode 100644 index 000000000..7d0805f45 --- /dev/null +++ b/.changeset/language-matching-prompt.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Promote the language-matching rule to a dedicated section in the system prompt, so replies and reasoning consistently follow the user's language through long English tool output, while repository artifacts keep project conventions. diff --git a/.changeset/pi-tui-paste-burst.md b/.changeset/pi-tui-paste-burst.md new file mode 100644 index 000000000..579cf45df --- /dev/null +++ b/.changeset/pi-tui-paste-burst.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/pi-tui": patch +--- + +Add a paste-burst fallback that treats Enter as a newline during rapid non-bracketed multi-line input bursts. diff --git a/.changeset/plugin-commands.md b/.changeset/plugin-commands.md deleted file mode 100644 index 35e3b02e7..000000000 --- a/.changeset/plugin-commands.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@moonshot-ai/kimi-code": minor ---- - -Plugins can now provide slash commands via a `commands` field in their manifest, registered as `:` and invoked with `$ARGUMENTS` expansion. diff --git a/.changeset/ttft-client-server-split.md b/.changeset/ttft-client-server-split.md deleted file mode 100644 index 3e8b5c7ef..000000000 --- a/.changeset/ttft-client-server-split.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@moonshot-ai/kimi-code": patch ---- - -Split LLM streaming timing in the session log and `KIMI_CODE_DEBUG=1` output into client vs. API-server portions, so slow turns can be attributed without parsing the wire log. Time-to-first-token splits into the API-server portion (network + server) and the client portion (in-process request building); the decode window splits into time awaiting tokens from the server and time the client spends processing each streamed chunk. diff --git a/.changeset/tui-paste-burst-config.md b/.changeset/tui-paste-burst-config.md new file mode 100644 index 000000000..d5d3899b5 --- /dev/null +++ b/.changeset/tui-paste-burst-config.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Add a TUI preference to keep rapid multi-line pastes from submitting line by line when bracketed paste is unavailable. Set `disable_paste_burst = true` in `tui.toml` to turn it off. diff --git a/.changeset/web-design-system-route.md b/.changeset/web-design-system-route.md new file mode 100644 index 000000000..fa80e48c8 --- /dev/null +++ b/.changeset/web-design-system-route.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Rebuild the web design-system easter egg as an in-app overlay that uses the app's real design tokens, so it stays in sync instead of drifting as a separate copy. diff --git a/.changeset/web-telemetry-toggle.md b/.changeset/web-telemetry-toggle.md deleted file mode 100644 index 34165369d..000000000 --- a/.changeset/web-telemetry-toggle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@moonshot-ai/kimi-code": patch ---- - -Always show the usage-data opt-out toggle in the web settings with a clearer label and description. diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml new file mode 100644 index 000000000..6c993662b --- /dev/null +++ b/.github/workflows/desktop-build.yml @@ -0,0 +1,170 @@ +name: desktop-build + +# Builds the Kimi Desktop (Electron) installers for macOS, Windows and Linux. +# Each runner builds the matching-platform SEA backend first, then packages it +# with electron-builder. +# +# macOS is signed with a Developer ID certificate + notarized (so it opens on +# any Mac without the "app is damaged" Gatekeeper block) when `sign-macos` is +# true and the Apple secrets are configured. Windows/Linux ship unsigned in v1. +# +# Triggered two ways: +# - workflow_dispatch: manual ad-hoc builds from the Actions tab. +# - workflow_call: called by release.yml to attach installers to a release. +on: + workflow_dispatch: + inputs: + sign-macos: + description: 'Sign + notarize macOS (needs Apple secrets)' + required: false + type: boolean + default: true + retention-days: + description: 'Artifact retention in days' + required: false + type: number + default: 5 + upload-artifact-prefix: + description: 'Prefix for uploaded artifact name' + required: false + type: string + default: 'kimi-desktop' + workflow_call: + inputs: + sign-macos: + description: 'Sign + notarize macOS (needs Apple secrets)' + required: false + type: boolean + default: false + retention-days: + description: 'Artifact retention in days' + required: false + type: number + default: 7 + upload-artifact-prefix: + description: 'Prefix for uploaded artifact name' + required: false + type: string + default: 'kimi-desktop' + secrets: + APPLE_CERTIFICATE_P12: + required: false + APPLE_CERTIFICATE_PASSWORD: + required: false + APPLE_NOTARIZATION_KEY_P8: + required: false + APPLE_NOTARIZATION_KEY_ID: + required: false + APPLE_NOTARIZATION_ISSUER_ID: + required: false + +permissions: + contents: read + +jobs: + desktop: + name: Desktop installer (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: macos-15 + target: darwin-arm64 + - os: macos-15-intel + target: darwin-x64 + - os: windows-2025-vs2026 + target: win32-x64 + - os: ubuntu-24.04 + target: linux-x64 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build Kimi web assets + # The SEA blob embeds apps/kimi-code/dist-web; build the web app and + # stage its assets before producing the native executable. + # KIMI_WEB_DESKTOP=1 bakes the internal-build banner into the web bundle + # (see apps/kimi-web/src/components/InternalBuildBanner.vue); only the + # desktop sets this flag, so the CLI `kimi web` stays banner-free. + env: + KIMI_WEB_DESKTOP: '1' + run: | + pnpm --filter @moonshot-ai/kimi-web run build + node apps/kimi-code/scripts/copy-web-assets.mjs + + - name: Build native executable (local profile) + # The Electron app signs the SEA itself (electron-builder, inside-out), + # so the native build stays unsigned here. + run: pnpm --filter @moonshot-ai/kimi-code run build:native:sea + + - name: Setup macOS keychain (Developer ID) + if: runner.os == 'macOS' && inputs.sign-macos + uses: ./.github/actions/macos-keychain-setup + with: + certificate-p12: ${{ secrets.APPLE_CERTIFICATE_P12 }} + certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + + - name: Prepare CSC_NAME for electron-builder (macOS) + if: runner.os == 'macOS' && inputs.sign-macos + shell: bash + run: | + # electron-builder rejects the "Developer ID Application: " prefix in + # CSC_NAME; strip it so the certificate matches by team name + ID. + name="${APPLE_SIGNING_IDENTITY}" + name="${name#Developer ID Application: }" + echo "CSC_NAME=$name" >> "$GITHUB_ENV" + + - name: Prepare notarization API key (macOS) + if: runner.os == 'macOS' && inputs.sign-macos + shell: bash + env: + APPLE_NOTARIZATION_KEY_P8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }} + run: | + set -euo pipefail + key_path="$RUNNER_TEMP/notary-AuthKey.p8" + printf '%s' "$APPLE_NOTARIZATION_KEY_P8" | base64 -d > "$key_path" + echo "APPLE_API_KEY=$key_path" >> "$GITHUB_ENV" + + - name: Build & package desktop app + shell: bash + env: + # macOS signing is driven by env: when sign-macos, electron-builder + # signs with the keychain's Developer ID and notarizes via the notary + # API key; otherwise it builds unsigned. + CSC_IDENTITY_AUTO_DISCOVERY: ${{ (runner.os == 'macOS' && inputs.sign-macos) && 'true' || 'false' }} + CSC_KEYCHAIN: ${{ env.APPLE_KEYCHAIN_PATH }} + KIMI_DESKTOP_NOTARIZE: ${{ (runner.os == 'macOS' && inputs.sign-macos) && 'true' || 'false' }} + APPLE_API_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} + APPLE_API_ISSUER: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} + run: pnpm --filter @moonshot-ai/kimi-desktop run dist + + - name: Cleanup macOS keychain + if: always() && runner.os == 'macOS' && inputs.sign-macos + uses: ./.github/actions/macos-keychain-cleanup + + - name: Upload installers + uses: actions/upload-artifact@v7 + with: + name: ${{ inputs.upload-artifact-prefix }}-${{ matrix.target }} + retention-days: ${{ inputs.retention-days }} + path: | + apps/kimi-desktop/dist-app/*.dmg + apps/kimi-desktop/dist-app/*.zip + apps/kimi-desktop/dist-app/*.exe + apps/kimi-desktop/dist-app/*.AppImage + apps/kimi-desktop/dist-app/*.deb + if-no-files-found: ignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1f74dc9d9..16b9c0f12 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -97,6 +97,22 @@ jobs: APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} + desktop-artifacts: + name: Desktop release artifact + needs: release + if: needs.release.outputs.kimi_native_release == 'true' + uses: ./.github/workflows/desktop-build.yml + with: + upload-artifact-prefix: kimi-desktop + retention-days: 7 + sign-macos: true + secrets: + APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_NOTARIZATION_KEY_P8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }} + APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} + APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} + publish-native-assets: name: Publish native release assets needs: diff --git a/.oxlintrc.json b/.oxlintrc.json index 877553f49..003359f31 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -150,6 +150,7 @@ "node_modules/", "apps/*/scripts/", "docs/smoke-archive/", + "packages/pi-tui/", "*.generated.ts" ] } diff --git a/HANDOVER-kimi-web-table-width.md b/HANDOVER-kimi-web-table-width.md new file mode 100644 index 000000000..c3ac05613 --- /dev/null +++ b/HANDOVER-kimi-web-table-width.md @@ -0,0 +1,149 @@ +# 接手文档:kimi-web 聊天表格宽度 + +> **当前状态(未提交,用户先用着)**:表格在阅读列(≈760px)内**单元格换行**显示—— +> 不横向滚动、不溢出、不裁切,但**没有比正文更宽**。这是目前效果最干净、可以先用的版本。 +> +> **仍未达成的目标**:让宽表格像 Manus 那样**突破到比正文更宽的列**再换行。两次突破尝试 +> (容器单位 `cqw`、固定 px `@container` 断点)都没做出满意效果,已回退/停用,见 §4、§6。 +> +> **只改了 1 个文件**:`apps/kimi-web/src/components/chat/Markdown.vue`(表格样式块)。 +> typecheck / check:style 通过。 + +## 1. 背景与目标 + +`apps/kimi-web` 聊天里 markstream 渲染的 markdown 表格,默认被钉在阅读列宽内 +(`width:100%` + `table-layout:fixed`),宽表格要么被挤成很窄的列、要么表格内横向滚动。 + +用户想要的最终效果(参照 Manus 截图): + +1. 表格**比正文阅读列更宽**(横向铺开),但 +2. 单元格**换行**,所以表格**永不横向滚动**(表格自身和对话面板都不滚),且 +3. 正文 / 代码 / diff / 工具块 / thinking 等其它块仍限在 760px 阅读列内。 + +> 第 1 点(撑得比正文宽)目前**没做到**——见 §6。当前上线的是"列内换行"的降级版。 + +## 2. 当前实现(列内换行版,正在用) + +`Markdown.vue` ` +`; + +function loadingHtml(): string { + return `${SCREEN_STYLE} +
+

正在启动 Kimi 本地服务…

+

首次启动可能需要几秒。

`; +} + +function errorHtml(message: string): string { + const safe = message.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>'); + return `${SCREEN_STYLE} +

无法启动本地服务

+

${safe}

+

查看日志:${serverLogPath()}

+

菜单 → Kimi Code Desktop → 重试连接,或先检查日志。

`; +} + +// --- server auth token -------------------------------------------------------- + +/** On-disk filename of the daemon's persistent bearer token (under KIMI_CODE_HOME). */ +const SERVER_TOKEN_FILE = 'server.token'; + +/** + * Read the daemon's bearer token so the web UI can authenticate without showing + * the manual token dialog on a fresh launch. Returns undefined when the token + * cannot be read (the web UI then falls back to the dialog). + */ +function readServerToken(): string | undefined { + try { + const token = readFileSync(join(kimiHome(), SERVER_TOKEN_FILE), 'utf-8').trim(); + return token.length > 0 ? token : undefined; + } catch { + return undefined; + } +} + +// --- connect flow ------------------------------------------------------------- + +async function connect(win: BrowserWindow): Promise { + await win.loadURL(dataUrl(loadingHtml())); + try { + const { origin } = await ensureServer(resolveSeaPath()); + process.stdout.write(`[kimi-desktop] connected to ${origin}\n`); + if (!win.isDestroyed()) { + // Append a desktop marker so the web UI shows the internal-build banner + // even when it is served by an already-running shared daemon (the desktop + // reuses the local daemon rather than starting a private one). Carry the + // server token in the `#token=` fragment — like `kimi web` does — so the + // web UI can authenticate without falling into the manual token dialog on + // a fresh launch. + const token = readServerToken(); + const fragment = token === undefined ? '' : `#token=${encodeURIComponent(token)}`; + await win.loadURL( + `${origin}/?kimi_desktop=1&platform=${process.platform}${fragment}`, + ); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`[kimi-desktop] ensureServer failed: ${message}\n`); + if (!win.isDestroyed()) { + await win.loadURL(dataUrl(errorHtml(message))); + } + } +} + +function createWindow(): void { + const win = new BrowserWindow({ + ...loadBounds(), + minWidth: 720, + minHeight: 480, + backgroundColor: '#0b0b0c', + title: 'Kimi Code Desktop', + // macOS: hide the native title bar and float the traffic lights over the + // content; the web UI reserves a draggable strip at the top to clear them. + // 'default' on other platforms (they keep their native title bar). + titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', + webPreferences: { + contextIsolation: true, + nodeIntegration: false, + }, + }); + mainWindow = win; + // Keep the window title as the product name. The web page sets document.title + // ("Kimi Code Web"), which would otherwise replace it. + win.webContents.on('page-title-updated', (event) => { + event.preventDefault(); + }); + win.on('close', () => { + saveBounds(win); + }); + win.on('closed', () => { + if (mainWindow === win) { + mainWindow = null; + } + }); + void connect(win); +} + +// --- native menu -------------------------------------------------------------- + +function buildMenu(): void { + const isMac = process.platform === 'darwin'; + const appMenu: MenuItemConstructorOptions = { + label: 'Kimi Code Desktop', + submenu: [ + ...(isMac ? [{ role: 'about' as const }, { type: 'separator' as const }] : []), + { + label: '重试连接', + click: () => { + if (mainWindow !== null) { + void connect(mainWindow); + } else { + createWindow(); + } + }, + }, + { + label: '打开服务日志', + click: () => { + void shell.openPath(serverLogPath()); + }, + }, + { type: 'separator' }, + isMac ? { role: 'quit' } : { role: 'close' }, + ], + }; + + const template: MenuItemConstructorOptions[] = [ + appMenu, + { role: 'editMenu' }, + { + label: 'View', + submenu: [ + { role: 'reload' }, + { role: 'forceReload' }, + { role: 'toggleDevTools' }, + { type: 'separator' }, + { role: 'resetZoom' }, + { role: 'zoomIn' }, + { role: 'zoomOut' }, + { type: 'separator' }, + { role: 'togglefullscreen' }, + ], + }, + { role: 'windowMenu' }, + ]; + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +} + +// --- app lifecycle ------------------------------------------------------------ + +function main(): void { + // The shared daemon is deliberately left running on quit — it self-exits ~60s + // after the last client disconnects, so we never tear down a server another + // client (CLI / browser / TUI) may still be using. + app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } + }); + + void app.whenReady().then(() => { + buildMenu(); + createWindow(); + app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); + }); +} + +main(); diff --git a/apps/kimi-desktop/src/main/sea-path.ts b/apps/kimi-desktop/src/main/sea-path.ts new file mode 100644 index 000000000..8703d72b1 --- /dev/null +++ b/apps/kimi-desktop/src/main/sea-path.ts @@ -0,0 +1,45 @@ +import { join } from 'node:path'; + +import { app } from 'electron'; + +// The bundled backend targets the same 6 platform/arch pairs the kimi-code +// native SEA build supports (apps/kimi-code/scripts/native/native-deps.mjs). +const SUPPORTED_TARGETS = new Set([ + 'darwin-arm64', + 'darwin-x64', + 'linux-arm64', + 'linux-x64', + 'win32-arm64', + 'win32-x64', +]); + +/** `-` triple for the current process, validated against the SEA targets. */ +export function currentTarget(): string { + const target = `${process.platform}-${process.arch}`; + if (!SUPPORTED_TARGETS.has(target)) { + throw new Error(`No bundled Kimi server for this platform: ${target}`); + } + return target; +} + +function executableName(): string { + return process.platform === 'win32' ? 'kimi.exe' : 'kimi'; +} + +/** + * Absolute path to the bundled SEA backend executable. + * + * - packaged: `/bin//kimi[.exe]` — placed there by + * electron-builder `extraResources`. + * - dev: `apps/kimi-code/dist-native/bin//kimi[.exe]` — produced by + * `pnpm -C apps/kimi-code build:native:sea`. In dev `app.getAppPath()` is + * `apps/kimi-desktop`, so the sibling app is one level up. + */ +export function resolveSeaPath(): string { + const target = currentTarget(); + const exe = executableName(); + if (app.isPackaged) { + return join(process.resourcesPath, 'bin', target, exe); + } + return join(app.getAppPath(), '..', 'kimi-code', 'dist-native', 'bin', target, exe); +} diff --git a/apps/kimi-desktop/tsconfig.json b/apps/kimi-desktop/tsconfig.json new file mode 100644 index 000000000..596e2cf72 --- /dev/null +++ b/apps/kimi-desktop/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src"] +} diff --git a/apps/kimi-desktop/tsdown.config.ts b/apps/kimi-desktop/tsdown.config.ts new file mode 100644 index 000000000..c58e8866d --- /dev/null +++ b/apps/kimi-desktop/tsdown.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'tsdown'; + +// The Electron main process is loaded as CommonJS (`out/main.cjs`). All sources +// under src/main are bundled into a single file; `electron` stays external +// (provided by the Electron runtime) and Node built-ins are external by default. +export default defineConfig({ + entry: { main: 'src/main/index.ts' }, + format: ['cjs'], + platform: 'node', + target: 'node20', + outDir: 'out', + clean: true, + dts: false, + fixedExtension: true, + deps: { neverBundle: ['electron'] }, +}); diff --git a/apps/kimi-web/AGENTS.md b/apps/kimi-web/AGENTS.md index bd7940fef..19180fe1c 100644 --- a/apps/kimi-web/AGENTS.md +++ b/apps/kimi-web/AGENTS.md @@ -4,13 +4,22 @@ Package-local rules for `apps/kimi-web` (`@moonshot-ai/kimi-web`). ## What it is -The browser web UI for Kimi Code — a peer to the TUI in `apps/kimi-code`. It talks to the local server over REST + WebSocket under `/api/v1`. Stack: Vue 3 + Vite 6 + TypeScript (strict) + Tailwind v4 + vue-i18n v11. There is no client router and no Pinia; state lives in composables/refs and provide/inject. +The browser web UI for Kimi Code — a peer to the TUI in `apps/kimi-code`. It talks to the local server over REST + WebSocket under `/api/v1`. Stack: Vue 3 + Vite 6 + TypeScript (strict) + vue-i18n v11. (Tailwind was removed; all styling is via design tokens in `src/style.css` + scoped component styles.) There is no client router and no Pinia; state lives in composables/refs and provide/inject. + +## Design system (normative — required when modifying the UI) + +- **Before changing any component, style, layout, or theme, read the design system view at `src/views/DesignSystemView.vue` (open it as an overlay: long-press the sidebar logo).** It is the canonical design system and visual spec for this app (tokens §02, primitives §03, chat §04, theme rules §05, style rules §06). It consumes the product tokens from `src/style.css` directly, so it stays in sync with the app. New and modified UI must match it. +- **Use the primitives in `src/components/ui/`.** The library covers Button, IconButton, Badge, Pill, Card, Input/Select/Textarea/Field, Dialog, Spinner, MoonSpinner, Link, Menu/MenuItem, SegmentedControl, Tabs, Switch, Checkbox, Avatar, EmptyState, Divider, Tooltip, Banner, Sheet, Skeleton, CommandBar, TopBar. One semantic = one component — do not hand-roll a bespoke button/badge/dialog/input for a single screen. When a primitive replaces an element, **delete the old scoped CSS** (do not append override blocks). +- **Use the tokens, not ad-hoc values.** Colors, fonts, radii, spacing, shadows, z-index, and motion come from the CSS custom properties in `src/style.css` (catalogued in the design-system view §02). Canonical names are `--color-*` / `--radius-*` / `--space-*` / `--text-*` / `--font-*` / `--z-*` / `--shadow-*` / `--ease-*` / `--duration-*` / `--weight-*` / `--leading-*`. A small set of layout/focus tokens keep the `--p-` prefix: `--p-focus-ring`, `--p-selection`, `--p-ic-sm/md/lg`, `--p-sidebar-w`, `--p-content-max/-wide`, `--p-bp-sm/-md`. +- **The moon spinner (🌑…🌘) is reserved** for the chat "waiting for the agent's first response" state only, and is rendered solely by `ui/MoonSpinner.vue`; every other loading state uses the plain `Spinner`. +- **Run `pnpm --filter @moonshot-ai/kimi-web check:style`** (`scripts/check-style.mjs`) — it enforces the §06 anti-pattern rules (no-gradient, no-glassmorphism except TopBar `frost`, no-emoji-icon except moon, no-hardcoded-hex/font, radius/z/weight from scale). Do not add new violations. +- **Verify visually.** For any UI change, render it in the browser (light + dark, plus hover/focus states) and confirm it matches the design-system view and introduces no regression before considering it done. Build/typecheck/check-style are necessary but not sufficient. ## Layout (`src/`) - `main.ts` — bootstrap (creates the app, installs i18n, mounts `#app`). `App.vue` — root component, holds most app state. - `api/` — server client. `index.ts` exposes the `getKimiWebApi()` singleton; `config.ts` builds REST/WS URLs; `daemon/` holds the wire client (`http.ts`, `ws.ts`, `wire.ts`, `mappers.ts`, `agentEventProjector.ts`, `eventReducer.ts`). -- `components/` — SFCs grouped by area: `chat/` (conversation/chat UI), `settings/` (settings & configuration), `dialogs/` (modal dialogs & sheets), `mobile/` (mobile-specific shell), plus shared layout components at the top level. +- `components/` — SFCs grouped by area: `chat/` (conversation/chat UI), `settings/` (settings & configuration), `dialogs/` (modal dialogs & sheets), `mobile/` (mobile-specific shell), `ui/` (design-system primitives — see "Design system" above), plus shared layout components at the top level. - `composables/` — reusable state logic, `useX` naming (`useKimiWebClient`, `useIsDark`, `usePaneLayout`, …). - `lib/` — pure helpers (`parseDiff`, `slashCommands`, `sessionRoute`, `toolMeta`, …). - `i18n/` — vue-i18n setup plus locale namespaces. @@ -40,6 +49,7 @@ All via `pnpm --filter @moonshot-ai/kimi-web …`: - `build` — production build into `dist/`. - `typecheck` — `vue-tsc --noEmit`. - `test` — `vitest run` (pure logic tests only; no jsdom / component tests). +- `check:style` — design-system §06 anti-pattern guard (`scripts/check-style.mjs`). - There is **no `lint` script** in this package; linting runs at the repo root via oxlint. ## Gotchas / hard rules diff --git a/apps/kimi-web/design/queue-composer-mockup.html b/apps/kimi-web/design/queue-composer-mockup.html new file mode 100644 index 000000000..6a7afc19d --- /dev/null +++ b/apps/kimi-web/design/queue-composer-mockup.html @@ -0,0 +1,547 @@ + + + + + +kimi-web · 消息队列 UX 高保真原型 + + + +
+
+

消息队列 UX · 高保真原型

+

把「发送」和「中断」解耦,把队列从隐藏面板搬进对话流。下面是运行中 + 排队的核心场景,可交互(试着输入回车、点 ×、点排队项编辑、点“出队一条”)。

+
+ +
+ +
+
+

相对现在的改动(4 点)

+
A
发送永远是发送 — 运行中也不再变成“中断”,Enter / 按钮都是入队。
+
B
Stop 独立按钮 — 与发送平级、运行时固定显示,急停仍一键可达。
+
C
队列内联进对话流 — 作为半透明待发送气泡,看得见、点得动。
+
D
可编辑 / 可排序 / 可删除 — 点气泡编辑、拖拽排序、× 删除。
+
+ +
场景 · 运行中(agent 正在改写中间件,你又追加了 3 条)
+ +
+
+
+ +
帮我把登录接口改成 JWT,并补上对应的单元测试。
+ +

我看了一下 src/auth 的结构,当前是基于 session cookie 的实现,先读取相关文件。

+ +
+ + read_file + src/auth/session.ts + 0.2s +
+
+ + read_file + src/auth/middleware.ts + 运行中… +
+
🌔正在改写认证中间件…
+ + +
+
+ + + 队列 · 3 + + 当前回合结束后自动逐条发送 +
+
+
+
+ + +
+
+ +
+
+ + yolo + + plan + +
+
+ kimi-k2· thinking + + + + + + +
+
+
+ + 运行中:Enter 加入队列 · Ctrl+S 立即插入当前回合 +
+
+ +
+ + + + 提示:在输入框里打点什么按 Enter,会作为新气泡进队列。 +
+
+
+ + +
+ +
对照 · 空闲态(没有队列、没有运行中的 turn)
+
+
+
这个项目用的是什么测试框架?
+

用的是 vitest,纯逻辑测试,没有 jsdom / 组件测试。测试文件跟被测模块放在一起。

+
+
+
+ +
+
+ + yolo +
+
+ kimi-k2· thinking + + + + +
+
+
+
+
+ + +
+ + + + diff --git a/apps/kimi-web/design/sidebar-show-more-demo.html b/apps/kimi-web/design/sidebar-show-more-demo.html new file mode 100644 index 000000000..d42cf7c5d --- /dev/null +++ b/apps/kimi-web/design/sidebar-show-more-demo.html @@ -0,0 +1,649 @@ + + + + + +kimi-web · 侧栏「展开更多」Demo + + + +
+

侧栏「展开更多」Demo

+ + + +
+ +
+
+
方案 A · 加载更多 + 整组折叠
(点工作区标题折叠整组)
+ +
+
+
方案 B · 加载更多 + 组内展开/收起
(收回到第一页)
+ +
+
+

怎么看这个 demo

+
    +
  • 左右两个侧栏是同一组数据(kimi-code 工作区共 23 个会话,每页 5 条),可独立点按。
  • +
  • 顶部 按钮样式 切换「当前实现」与「规范合规」,两套样式同时作用在两个方案上,方便对比。
  • +
  • 第二个工作区 docs 用来看「上面的列表展开后会把下面的工作区顶下去」——这就是想收起的动机。
  • +
+ +

方案 A:加载更多 + 整组折叠

+
    +
  • 只有 加载更多 一个按钮,拉下一页追加。
  • +
  • 收起靠 点工作区标题(整组会话全部隐藏)。这是 §07 已经定义的折叠。
  • +
  • 改动最小,完全符合现有规范。
  • +
  • 一折就整组全收,连第一页也看不见。
  • +
+ +

方案 B:加载更多 + 组内展开/收起

+
    +
  • 加载超过第一页后,多一个 收起 按钮;收回后变成 展开
  • +
  • 收起只回到第一页(前 5 条),数据不丢、不重新请求。
  • +
  • 能收回第一页之外的会话,缓解把下面工作区顶下去的问题。
  • +
  • §07 目前没有定义这个组内控件,需要扩展规范。
  • +
+ +

样式差异(切到「当前实现」看)

+
    +
  • 当前:裸 button —— mono 字体、无圆角、hover 文字变 accent、无焦点环,且文字比 session 标题偏右 8px
  • +
  • 设计稿:做成「会话行同款」的紧凑列表控件 —— 行首留空使文字与 session 标题精确对齐font-uiradius-md、26px 行高、hover 只出 sunken 底、:focus-visible 焦点环。
  • +
+

规范参考:design-system.html §07(侧栏)/ §02(token / 字体)/ §08(焦点环)。

+
+
+ + + + diff --git a/apps/kimi-web/design/undo-exit-demos.html b/apps/kimi-web/design/undo-exit-demos.html new file mode 100644 index 000000000..de6bb3c11 --- /dev/null +++ b/apps/kimi-web/design/undo-exit-demos.html @@ -0,0 +1,398 @@ + + + + + +用户消息 undo 退场动画 · 10 个候选 + + + +
+

用户消息 undo 退场动画 · 10 个候选

+

点击每条气泡预览退场效果;点击「重播」或「全部重置」可再次观看。气泡样式对齐真实 .u-bub(右对齐、accent-soft、非对称圆角)。

+
+ +
+ + 复杂度:低 = 纯 transform/opacity/filter;中 = 需 clip-path / perspective / 多 filter;高 = 需 @property + mask。 +
+ +
+ +
+
+

01 柔雾消散

+

原地失焦 + 轻微缩小。最克制、最贴设计系统的「安静」气质。

+ opacity · blur · scale +
+
撤销这条消息,看看效果
+
+ +
+
+

02 向右滑出

+

向右平移并淡出,方向明确(与「撤回」语义一致)。性能最好。

+ translateX · opacity +
+
撤销这条消息,看看效果
+
+ +
+
+

03 缩小淡出

+

向右锚点缩小并淡出。比柔雾多一点「被收走」的感觉,但仍然克制。

+ scale · opacity(origin right) +
+
撤销这条消息,看看效果
+
+ +
+
+

04 横向收束

+

沿右边缘把气泡压扁到 0。像把消息「抽走」,方向感强。

+ scaleX · opacity(origin right) +
+
撤销这条消息,看看效果
+
+ +
+
+

05 向上折起

+

3D 向上翻折消失,立体、有层次。需父级 perspective。

+ rotateX · opacity · perspective +
+
撤销这条消息,看看效果
+
+ +
+
+

06 裁切擦除

+

用 clip-path 从右向左裁掉。干净、利落,无明显位移。

+ clip-path inset · opacity +
+
撤销这条消息,看看效果
+
+ +
+
+

07 浮起消散 低~中

+

向上浮起、阴影放大、淡出。像消息「飘走」,轻盈。

+ translateY · box-shadow · opacity +
+
撤销这条消息,看看效果
+
+ +
+
+

08 墨迹晕开

+

失焦 + 提亮 + 去饱和,像墨迹在水里晕开。柔和、有质感。

+ blur · brightness · grayscale · opacity +
+
撤销这条消息,看看效果
+
+ +
+
+

09 粒子点阵消散

+

mask 点阵逐点收缩,气泡「碎成点」再消失。最接近「粒子消散」。

+ @property --k · radial-gradient mask +
+
撤销这条消息,看看效果
+
+ +
+
+

10 散焦放大

+

轻微放大 + 强失焦,像被一阵风吹散。比柔雾更「爆」一点。

+ scale · blur · opacity +
+
撤销这条消息,看看效果
+
+ +
+ + + + diff --git a/apps/kimi-web/index.html b/apps/kimi-web/index.html index 40e54a320..dff0375e4 100644 --- a/apps/kimi-web/index.html +++ b/apps/kimi-web/index.html @@ -8,14 +8,11 @@ + users can get a color-scheme/font flash before useKimiWebClient mirrors + the data attributes. Mirrors applyColorSchemeToDocument. --> @@ -18,7 +19,7 @@ const { t } = useI18n(); - +
{{ t('app.connecting') }}
@@ -36,7 +37,7 @@ const { t } = useI18n(); height: 100dvh; min-width: 100vw; min-height: 100dvh; - z-index: 1000; + z-index: var(--z-toast); display: flex; align-items: center; justify-content: center; @@ -53,31 +54,12 @@ const { t } = useI18n(); .gload-logo { width: 128px; height: auto; - color: var(--ink); + color: var(--color-text); animation: gload-pop 0.55s cubic-bezier(0.22, 1, 0.36, 1) both; } -/* slim indeterminate progress bar */ -.gload-bar { - width: 150px; - height: 3px; - border-radius: 3px; - background: var(--line); - overflow: hidden; - position: relative; -} -.gload-bar-fill { - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 40%; - border-radius: 3px; - background: var(--blue); - animation: gload-slide 1.1s ease-in-out infinite; -} .gload-text { font-family: var(--mono); - font-size: calc(var(--ui-font-size) - 2.5px); + font-size: var(--text-base); color: var(--muted); letter-spacing: 0.04em; } @@ -85,12 +67,9 @@ const { t } = useI18n(); from { opacity: 0; transform: translateY(6px) scale(0.96); } to { opacity: 1; transform: translateY(0) scale(1); } } -@keyframes gload-slide { - 0% { left: -42%; } - 100% { left: 102%; } -} @media (prefers-reduced-motion: reduce) { .gload-logo { animation: none; } - .gload-bar-fill { animation-duration: 2.4s; } } + +.gload-text { font-family: var(--sans); } diff --git a/apps/kimi-web/src/components/InternalBuildBanner.vue b/apps/kimi-web/src/components/InternalBuildBanner.vue new file mode 100644 index 000000000..e2638aaad --- /dev/null +++ b/apps/kimi-web/src/components/InternalBuildBanner.vue @@ -0,0 +1,56 @@ + + + + + + diff --git a/apps/kimi-web/src/components/ResizeHandle.vue b/apps/kimi-web/src/components/ResizeHandle.vue index 39ff4a25c..b37a8c246 100644 --- a/apps/kimi-web/src/components/ResizeHandle.vue +++ b/apps/kimi-web/src/components/ResizeHandle.vue @@ -69,7 +69,7 @@ watch(dragging, (d) => emit('update:dragging', d)); touch-action: none; /* sits over the 1px column border so the whole 4px strip is grabbable */ margin: 0 -2px; - z-index: 5; + z-index: var(--z-sticky); } .rh-bar { position: absolute; @@ -79,6 +79,6 @@ watch(dragging, (d) => emit('update:dragging', d)); } .rh:hover .rh-bar, .rh.dragging .rh-bar { - background: var(--blue); + background: var(--color-accent); } diff --git a/apps/kimi-web/src/components/ServerAuthDialog.vue b/apps/kimi-web/src/components/ServerAuthDialog.vue index 94cebac63..44c008649 100644 --- a/apps/kimi-web/src/components/ServerAuthDialog.vue +++ b/apps/kimi-web/src/components/ServerAuthDialog.vue @@ -2,14 +2,16 @@ + it up. The overlay uses a tokened translucent backdrop and the card follows + the unified v2 dialog look. --> diff --git a/apps/kimi-web/src/components/Sidebar.vue b/apps/kimi-web/src/components/Sidebar.vue index adf31d559..0a31ae5fc 100644 --- a/apps/kimi-web/src/components/Sidebar.vue +++ b/apps/kimi-web/src/components/Sidebar.vue @@ -3,17 +3,30 @@ The old workspace rail and workspace tabs have been removed; workspace switching, folding and renaming all live in the group header. -->