mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-10 01:39:25 +00:00
Merge remote-tracking branch 'origin/kimi-code-v2' into kimi-code-v2
This commit is contained in:
commit
33fbbcb451
787 changed files with 66468 additions and 13587 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
5
.changeset/close-abandoned-tool-exchanges.md
Normal file
5
.changeset/close-abandoned-tool-exchanges.md
Normal file
|
|
@ -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.
|
||||
5
.changeset/compaction-forward-plan.md
Normal file
5
.changeset/compaction-forward-plan.md
Normal file
|
|
@ -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.
|
||||
5
.changeset/dedupe-duplicate-tool-call-ids.md
Normal file
5
.changeset/dedupe-duplicate-tool-call-ids.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Fix requests being rejected by strict providers when the model emits duplicate tool call ids.
|
||||
|
|
@ -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.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Fix @ file mentions not opening when typed inside a slash command argument.
|
||||
5
.changeset/fix-login-shell-path.md
Normal file
5
.changeset/fix-login-shell-path.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Show file path completions when typing `/` in shell mode (`!`).
|
||||
|
|
@ -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.
|
||||
5
.changeset/fix-windows-update-console-flash.md
Normal file
5
.changeset/fix-windows-update-console-flash.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Keep automatic background updates from flashing a console window on Windows.
|
||||
5
.changeset/fix-windows-upgrade-spawn.md
Normal file
5
.changeset/fix-windows-upgrade-spawn.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Fix `kimi upgrade` failing on Windows with a spawn error when installing the new version.
|
||||
5
.changeset/language-matching-prompt.md
Normal file
5
.changeset/language-matching-prompt.md
Normal file
|
|
@ -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.
|
||||
5
.changeset/pi-tui-paste-burst.md
Normal file
5
.changeset/pi-tui-paste-burst.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": minor
|
||||
---
|
||||
|
||||
Plugins can now provide slash commands via a `commands` field in their manifest, registered as `<plugin>:<command>` and invoked with `$ARGUMENTS` expansion.
|
||||
|
|
@ -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.
|
||||
5
.changeset/tui-paste-burst-config.md
Normal file
5
.changeset/tui-paste-burst-config.md
Normal file
|
|
@ -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.
|
||||
5
.changeset/web-design-system-route.md
Normal file
5
.changeset/web-design-system-route.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
170
.github/workflows/desktop-build.yml
vendored
Normal file
170
.github/workflows/desktop-build.yml
vendored
Normal file
|
|
@ -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
|
||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@
|
|||
"node_modules/",
|
||||
"apps/*/scripts/",
|
||||
"docs/smoke-archive/",
|
||||
"packages/pi-tui/",
|
||||
"*.generated.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
149
HANDOVER-kimi-web-table-width.md
Normal file
149
HANDOVER-kimi-web-table-width.md
Normal file
|
|
@ -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` `<style scoped>` 表格块:
|
||||
|
||||
```css
|
||||
.md :deep(.table-node) { /* <table>:列宽按内容、可换行 */
|
||||
width: auto !important;
|
||||
max-width: 100% !important;
|
||||
table-layout: auto !important;
|
||||
}
|
||||
/* 表格在阅读列内换行;装不下(不可断的长内容)才滚动兜底 */
|
||||
.md :deep(.table-node-wrapper) {
|
||||
width: max-content;
|
||||
max-width: 100% !important; /* 100% = 阅读列宽,封顶 → 换行 */
|
||||
overflow-x: auto !important;
|
||||
}
|
||||
.md :deep(.table-node th),
|
||||
.md :deep(.table-node td) {
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
```
|
||||
|
||||
要点:
|
||||
- **换行靠的是"去掉旧代码的 `white-space:nowrap`"**。markstream 单元格默认就是
|
||||
`white-space:normal`(见 `node_modules/markstream-vue/dist/index.px.css`),旧代码特意加
|
||||
`nowrap !important` 才不换行;现在不设 white-space,就回到 markstream 默认的换行。
|
||||
- `max-content` + `max-width:100%`:列宽按内容撑,但整体不超过阅读列 → 到列宽就换行。
|
||||
- `.a-msg` 保持 HEAD 的 `width:94%`;`.content-wrap` 保持 `max-width:760px`,都没动。
|
||||
|
||||
## 3. DOM / CSS 结构与关键约束
|
||||
|
||||
```
|
||||
.con (container-type:inline-size; position:relative) ← 不裁剪;@container 的查询根
|
||||
└ .chat-layout
|
||||
└ .panes.chat-scroll { overflow-y:auto } ← 决定性裁剪点
|
||||
└ .content-wrap { max-width:760px; margin:auto } ← 居中阅读列(桌面恒 align-center)
|
||||
└ .chat (flex column; padding:16px 14px)
|
||||
└ .a-msg (width:94%; align-self:flex-start) ← 左对齐
|
||||
├ .msg → Markdown(.md) → .markdown-renderer
|
||||
│ └ .node-slot { width:100% } ← markstream,不裁剪
|
||||
│ └ .node-content { width:100% } ← markstream,不裁剪
|
||||
│ └ .table-node-wrapper ← 表格(突破点,若要撑宽)
|
||||
│ └ table.table-node
|
||||
└ .think / .tool-group / .tool / .a-msg-ft
|
||||
```
|
||||
|
||||
**两条硬约束(踩过的坑):**
|
||||
|
||||
1. **`.panes` 是裁剪点。** 它有 `overflow-y:auto`,按 CSS 规则另一方向的 `visible` 会被强制算成
|
||||
`auto`,所以 `.panes` 实际两个方向都裁剪。表格若比面板宽,`.panes` 就会横向滚动
|
||||
(macOS 悬浮滚动条静止时看不见,容易误以为是"被裁掉")。后代无法靠 CSS 逃出它
|
||||
(`position:fixed` 除外,但脱流、不随消息滚动,不可用)。
|
||||
2. **`.node-content` / `.node-slot` 不裁剪**(只有 `width:100%`,无 overflow),所以更宽的表格
|
||||
wrapper 能正常"顶出"它们向右显示——如果 wrapper 宽度真的生效的话(见 §4 两次失败)。
|
||||
|
||||
markstream 的 `.table-node-wrapper` 原始样式:
|
||||
`{ position:relative; max-width:100%; overflow-x:auto; ... }`,所以覆盖它的 `max-width` / `overflow-x`
|
||||
需要 `!important`。
|
||||
|
||||
## 4. 撑宽尝试为什么失败(避免重复试错)
|
||||
|
||||
目标是"表格突破 760 列、显示得比正文宽、再换行"。试过两种给 wrapper 更大宽度的写法,都不行:
|
||||
|
||||
1. **容器单位 `cqw`**:`max-width: calc((100cqw + 760px)/2 - 48px)`。
|
||||
`cqw`(容器宽度单位)在本仓库**别处从没用过**,实测**没生效**——`max-width` 没卡住,
|
||||
`width:max-content` 就一路撑到不换行的完整宽度、溢出面板(`.panes` 横滚)。
|
||||
(注意:更早还踩过 `(100cqw+760)/2` 里 `760` 不带单位 → `长度+纯数字` 非法 → 整条声明被丢弃,
|
||||
但即便修正带单位,`cqw` 依旧没做出效果。)
|
||||
2. **固定 px `@container (min-width)` 断点**:`@container (min-width:960px){ ... max-width:800px }`
|
||||
等按面板宽分档给固定上限。容器查询本身在本仓库可靠(Sidebar 等在用),语法也过了 lint,
|
||||
但实际视觉效果用户仍不满意(表格没有明显变宽 / 或档位不合适),已撤掉。
|
||||
|
||||
> 结论:单纯改 `.table-node-wrapper` 的 `max-width` 这条路,没能稳定做出"比正文宽且换行填满"的效果。
|
||||
> 下次重做建议换思路(见 §6)。
|
||||
|
||||
## 5. 涉及文件
|
||||
|
||||
| 文件 | 关键位置 |
|
||||
|---|---|
|
||||
| `apps/kimi-web/src/components/chat/Markdown.vue` | `<style scoped>` 内 `.table-node` / `.table-node-wrapper` / 单元格块(**本任务唯一改动**) |
|
||||
| `apps/kimi-web/src/components/chat/ConversationPane.vue` | `.con { container-type:inline-size; --read-max:760px }`、`.content-wrap`、`.panes`(裁剪源)——**未改** |
|
||||
| `apps/kimi-web/src/components/chat/ChatPane.vue` | `.a-msg { width:94% }`——**未改**(本任务) |
|
||||
|
||||
## 6. 若要重做"撑得比正文宽"
|
||||
|
||||
难点:让表格 wrapper 拿到一个"比 760 列宽、但不超过面板"的宽度,且这个宽度要**真的生效**并触发换行。
|
||||
几个可试方向(按推荐度):
|
||||
|
||||
| 方向 | 说明 | 风险 |
|
||||
|---|---|---|
|
||||
| A. 撑开 `.a-msg` + 让表格 `max-content` | `.a-msg` 改 `width:fit-content; min-width:100%`,其余直接子元素限 760px;表格 `max-content` 顶宽。曾实现过,问题是超面板时 `.panes` 横滚、且正文与宽表格同屏排版怪。 | 中:需解决"超面板横滚"和排版 |
|
||||
| B. JS 量面板宽写进 CSS 变量 | `ResizeObserver` 量 `.con` 宽度,写 `--pane-w`,表格 `max-width: calc((var(--pane-w)+760px)/2 - …)`。绕开 `cqw` 不生效的问题。 | 中:引入 JS,但最可控 |
|
||||
| C. Teleport 到 `.panes` 外 | 宽表格用 Vue Teleport 渲到滚动容器外、JS 定位覆盖到消息处。 | 高:定位+滚动同步脆;且表格是 markstream 第三方 DOM,流式重渲染会打架 |
|
||||
| D. 页面级滚动重构 | 去掉 `.panes` 内滚动,整页纵向滚。 | 高:要重写滚动到底/加载更多/新消息提示等 |
|
||||
|
||||
**重做第一步**:先在浏览器量清楚——用户常见宽表格的自然宽度、以及侧栏展开/收起时
|
||||
`.con`(面板)实际多宽。若表格自然宽度基本 ≤ 面板宽,方向 A/B 就够;否则必然要在
|
||||
"表格内滚动 / 面板滚动 / 换行压缩"三者里选一个让步(§3 硬约束决定纯 CSS 无法三者全免)。
|
||||
|
||||
## 7. 验证
|
||||
|
||||
```bash
|
||||
export PATH="$HOME/.nvm/versions/node/v24.16.0/bin:$PATH"
|
||||
pnpm --filter kimi-web typecheck # 通过
|
||||
pnpm --filter kimi-web check:style # baseline,本文件无新增告警
|
||||
```
|
||||
|
||||
浏览器肉眼(本环境不能渲染):
|
||||
- 让 agent 输出一个多列宽表格。
|
||||
- 当前版本预期:表格在阅读列内**换行**显示,不横向滚动、不溢出;但**不会比正文宽**。
|
||||
- macOS 是悬浮滚动条,判断"有没有横滚"别只看静止截图,拖一下或缩小窗口确认。
|
||||
|
||||
## 8. 与本任务无关的未提交改动(不要误改 / 误回退)
|
||||
|
||||
`git diff` 里还有这些**非本任务**的改动:
|
||||
|
||||
- `ConversationPane.vue`:平滑滚动守卫(`smoothScrollUntil` / `handleEditMessage` / `scrollToBottom` 调整)。
|
||||
- `ChatPane.vue`:`.u-copy` 的 `justify-content:center` + `.u-copy svg` 的 `translateY(1.3px)`。
|
||||
- `apps/kimi-web/src/i18n/…`、`lib/desktopFlag.ts`、`components/chat/ConversationToc.vue` 等仓库里早已存在的杂项。
|
||||
|
||||
## 9. 收尾
|
||||
|
||||
表格改动只在 `Markdown.vue` 一个文件。若就用当前"列内换行"版收尾,按 `gen-changesets` 技能
|
||||
生成 changeset(scope: web,描述:markdown 表格单元格换行、避免被挤成窄列)即可。
|
||||
3
apps/kimi-code/.gitignore
vendored
3
apps/kimi-code/.gitignore
vendored
|
|
@ -6,3 +6,6 @@ agents/
|
|||
# next to it keeps `#/generated/vis-web-asset` type-resolvable on a fresh
|
||||
# clone (before any build has produced the `.ts`).
|
||||
src/generated/vis-web-asset.ts
|
||||
|
||||
# Copied from packages/pi-tui/native at build time by scripts/copy-native-assets.mjs
|
||||
native/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,144 @@
|
|||
# @moonshot-ai/kimi-code
|
||||
|
||||
## 0.22.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1304](https://github.com/MoonshotAI/kimi-code/pull/1304) [`0fc0ae3`](https://github.com/MoonshotAI/kimi-code/commit/0fc0ae380b09aa96aad0eff1ae66f239e061d01a) - When large images are compressed, tell the model the original and delivered image details. Keep the original image available, and support cropped or full-resolution reads for fine details.
|
||||
|
||||
- [#1315](https://github.com/MoonshotAI/kimi-code/pull/1315) [`b40bb71`](https://github.com/MoonshotAI/kimi-code/commit/b40bb7139939eb2ba734ce5dd4871b894d7033e8) - Fix TUI rendering bugs that caused the screen to go blank and the input box to disappear.
|
||||
|
||||
- [#1303](https://github.com/MoonshotAI/kimi-code/pull/1303) [`2639786`](https://github.com/MoonshotAI/kimi-code/commit/2639786ce578f15c020a2c11c344797dae18de61) - Fix the TUI crashing when the terminal is resized to a very narrow width while the input contains CJK or emoji text.
|
||||
|
||||
- [#1315](https://github.com/MoonshotAI/kimi-code/pull/1315) [`b40bb71`](https://github.com/MoonshotAI/kimi-code/commit/b40bb7139939eb2ba734ce5dd4871b894d7033e8) - Clear the screen fully when starting a new session via /new, /clear, or a session switch.
|
||||
|
||||
- [#1301](https://github.com/MoonshotAI/kimi-code/pull/1301) [`c3653a1`](https://github.com/MoonshotAI/kimi-code/commit/c3653a1c50ffa3856484599e132980628eb9fca4) - Show an up arrow on the web composer send button.
|
||||
|
||||
- [#1290](https://github.com/MoonshotAI/kimi-code/pull/1290) [`3ea84a5`](https://github.com/MoonshotAI/kimi-code/commit/3ea84a56e4dfdeaddd58add5b269be0342f3f986) - Fix the session search dialog showing a horizontal scrollbar for long session titles or snippets.
|
||||
|
||||
- [#1316](https://github.com/MoonshotAI/kimi-code/pull/1316) [`5322c63`](https://github.com/MoonshotAI/kimi-code/commit/5322c638895a934c1ce220fefed54f5077d2a49e) - Fix web tooltips that could get stuck on screen when their trigger element is removed while open.
|
||||
|
||||
- [#1319](https://github.com/MoonshotAI/kimi-code/pull/1319) [`e8ab7ca`](https://github.com/MoonshotAI/kimi-code/commit/e8ab7ca78661de7f00a8196444be1db93e7c14b4) - Fix the sidebar session row shifting its title and status badges when hovered.
|
||||
|
||||
- [#1293](https://github.com/MoonshotAI/kimi-code/pull/1293) [`6a469b3`](https://github.com/MoonshotAI/kimi-code/commit/6a469b3e07022e56b29b1fd8a7c58df36b2111fe) - Refresh the web UI icon set and unify the message copy and undo button hover states and tooltips.
|
||||
|
||||
- [#1311](https://github.com/MoonshotAI/kimi-code/pull/1311) [`b40649b`](https://github.com/MoonshotAI/kimi-code/commit/b40649b2ae7a4b6a0aea04e32eba200555393064) - Remove duplicate newline-shortcut handling from the prompt editor.
|
||||
|
||||
- [#1317](https://github.com/MoonshotAI/kimi-code/pull/1317) [`78a058a`](https://github.com/MoonshotAI/kimi-code/commit/78a058acd2fc91de5cca0c1d66d415ee35884889) - Remove the experimental micro compaction feature and its toggle from the experiments panel.
|
||||
|
||||
- [#1283](https://github.com/MoonshotAI/kimi-code/pull/1283) [`ea55911`](https://github.com/MoonshotAI/kimi-code/commit/ea55911062eefcb0414cfddb84c8a4494c45f363) - Improve compaction handoff summaries for more reliable resumed sessions. They now keep the latest intent, key tool results, decisions, open questions, and context to re-check.
|
||||
|
||||
- [#1295](https://github.com/MoonshotAI/kimi-code/pull/1295) [`77eb3a9`](https://github.com/MoonshotAI/kimi-code/commit/77eb3a9fe40c93fa32e335f07160b8128355bab6) - Save shell commands to input history and recall them in bash mode. Press Up on an empty `!` prompt to browse previous shell commands.
|
||||
|
||||
- [#1316](https://github.com/MoonshotAI/kimi-code/pull/1316) [`5322c63`](https://github.com/MoonshotAI/kimi-code/commit/5322c638895a934c1ce220fefed54f5077d2a49e) - Trim redundant and incorrect tooltips in the web UI.
|
||||
|
||||
- [#1320](https://github.com/MoonshotAI/kimi-code/pull/1320) [`444e6b1`](https://github.com/MoonshotAI/kimi-code/commit/444e6b15f0e53b6c4d75d1bfdc0b35639dce6f4c) - Fix the web UI becoming sluggish after opening many sessions.
|
||||
|
||||
- [#1322](https://github.com/MoonshotAI/kimi-code/pull/1322) [`5441ad1`](https://github.com/MoonshotAI/kimi-code/commit/5441ad1838a5cfa1f3df0ca2ee1524e1433fb513) - Let the web sidebar collapse an expanded workspace session list back to its first page.
|
||||
|
||||
## 0.22.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1243](https://github.com/MoonshotAI/kimi-code/pull/1243) [`ace7901`](https://github.com/MoonshotAI/kimi-code/commit/ace79010669d19ad175bc25443b6efb41ca2e2ac) - Automatically compress oversized images before they reach the model. Whatever the source — pasted into the CLI, uploaded from the web/desktop client, sent over ACP, read via `ReadMediaFile`, or returned by an MCP tool — images are downsampled (longest edge ≤ 2000px) and re-encoded to fit a per-image byte budget, cutting vision-token cost and avoiding provider image-size errors. Screenshots stay lossless PNG and only degrade to JPEG when the byte budget cannot otherwise be met. Compression runs as an input-stage step at each ingestion point (while the content part is built), and guards against decompression bombs by skipping absurdly large pixel/byte payloads before decoding. Best-effort: if it fails for any reason the original image is sent unchanged.
|
||||
|
||||
- [#1262](https://github.com/MoonshotAI/kimi-code/pull/1262) [`c070fbe`](https://github.com/MoonshotAI/kimi-code/commit/c070fbeddeb1c147d8859a76046f9465f696c9cb) - Add model alias overrides so manual thinking effort levels and model metadata survive provider catalog refreshes. Set them under `[models."<alias>".overrides]`.
|
||||
|
||||
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Refresh the web UI with a new design system, including updated colors, typography, spacing, light and dark palettes, restyled tooltips, and subtle enter/exit and expand/collapse animations.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Show draft pull requests with a distinct draft status instead of displaying them as open.
|
||||
|
||||
- [#1254](https://github.com/MoonshotAI/kimi-code/pull/1254) [`7859b0a`](https://github.com/MoonshotAI/kimi-code/commit/7859b0afe8898852806e5a0c21b9dd52cb82f834) - Fix the transcript jumping to the top when scrolling up through history during streaming output.
|
||||
|
||||
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Fix plan, swarm, and goal modes being shared across sessions in the web UI; each session now keeps its own toggles.
|
||||
|
||||
- [#1264](https://github.com/MoonshotAI/kimi-code/pull/1264) [`003733c`](https://github.com/MoonshotAI/kimi-code/commit/003733c751584ce30d8ebae4f5e608f0df049d32) - Hide the unsupported Off option in the /model thinking switcher for always-on models that already expose multiple effort levels.
|
||||
|
||||
- [#1272](https://github.com/MoonshotAI/kimi-code/pull/1272) [`54703d9`](https://github.com/MoonshotAI/kimi-code/commit/54703d9457dcda7bc782301fc2dbb41a2c8d7293) - Release pasted images and streaming timers once they are no longer shown, so memory stops growing in long sessions.
|
||||
|
||||
- [#1272](https://github.com/MoonshotAI/kimi-code/pull/1272) [`54703d9`](https://github.com/MoonshotAI/kimi-code/commit/54703d9457dcda7bc782301fc2dbb41a2c8d7293) - Fix the terminal being left in raw mode with a hidden cursor and disabled flow control after a crash or abrupt exit.
|
||||
|
||||
- [#1265](https://github.com/MoonshotAI/kimi-code/pull/1265) [`8cfb165`](https://github.com/MoonshotAI/kimi-code/commit/8cfb1657ad7bf525269df4ab6cf5c12aa1d406a9) - Reduce the default TUI transcript window to keep long sessions responsive.
|
||||
|
||||
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Replace the Explore and Native theme options with a single chat layout and a Blue or Black accent-color setting.
|
||||
|
||||
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Show time, duration, connection, and stack details in web error and warning toasts.
|
||||
|
||||
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Fix an active workspace showing only its five most recent sessions on load, so it now keeps loading older sessions from the last 12 hours.
|
||||
|
||||
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Reduce the web composer's default height for a more compact empty state, and fix ArrowUp recalling the previous message while editing a multi-line draft; ArrowUp now recalls only from the very start of the text and is disabled in the expanded editor.
|
||||
|
||||
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Fix the Thinking-by-default setting not taking effect, so new sessions correctly start with thinking enabled.
|
||||
|
||||
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Fix spurious errors from the web question, approval, and task actions when the action was already complete, and add loading feedback so each click is acknowledged immediately.
|
||||
|
||||
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Show queued prompts inline below the running turn in the web chat, and split Stop into its own button so Send no longer interrupts.
|
||||
|
||||
- [#1278](https://github.com/MoonshotAI/kimi-code/pull/1278) [`bbda90a`](https://github.com/MoonshotAI/kimi-code/commit/bbda90af846ca66232158d2e9605d3d59a7e3a49) - Hide the conversation outline when there is not enough room to expand its labels, so it no longer clips against the window edge.
|
||||
|
||||
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Show the conversation outline as one entry per user query that expands into a labeled list on hover.
|
||||
|
||||
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Remove the fade-out animation when undoing a message in the web chat.
|
||||
|
||||
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Improve session search with a Cmd/Ctrl+K palette that filters by title, workspace, and last prompt with highlighted matches. Press Cmd+K or Ctrl+K to open it.
|
||||
|
||||
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Group consecutive tool calls into a collapsible stack with per-tool renderers, including diff line-count chips for edits and inline previews for image, video, and audio results.
|
||||
|
||||
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Use one consistent modal dialog for confirmations in the web UI (archive session, delete workspace, delete provider, undo message, and mode toggles).
|
||||
|
||||
- [#1258](https://github.com/MoonshotAI/kimi-code/pull/1258) [`b905dd4`](https://github.com/MoonshotAI/kimi-code/commit/b905dd49108c567d0fecd38a096808c121672795) - Add workspace sorting by manual order or last-edited time, plus collapse-all and expand-all controls, to the sidebar.
|
||||
|
||||
## 0.21.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1256](https://github.com/MoonshotAI/kimi-code/pull/1256) [`0cc02ac`](https://github.com/MoonshotAI/kimi-code/commit/0cc02ac67d465d1d4d7fe070422bab17053cdaa3) - Keep the waiting spinner visible while encrypted reasoning streams, fixing a blank spinner-less gap before the first response text appears.
|
||||
|
||||
## 0.21.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- [#1204](https://github.com/MoonshotAI/kimi-code/pull/1204) [`5cb80ce`](https://github.com/MoonshotAI/kimi-code/commit/5cb80ce879406d239048c32d61202778cb860e58) - Plugins can now provide slash commands via a `commands` field in their manifest, registered as `<plugin>:<command>` and invoked with `$ARGUMENTS` expansion.
|
||||
|
||||
- [#1214](https://github.com/MoonshotAI/kimi-code/pull/1214) [`86e0c92`](https://github.com/MoonshotAI/kimi-code/commit/86e0c9201ed58c7c1ce5543b1dfb47a4cf5117f6) - Rework conversation compaction:
|
||||
|
||||
- Keep only recent user prompts plus a single user-role summary; drop assistant and tool messages.
|
||||
- Repair tool_use/tool_result adjacency before sending, fixing a strict-provider HTTP 400 when a tool call and its result became non-adjacent.
|
||||
- Merge consecutive user turns for strict providers (Gemini/Vertex), fixing an HTTP 400 ("roles must alternate") after compaction or when a turn is steered in right after a tool result.
|
||||
- Micro-compaction now defaults off.
|
||||
|
||||
- [#1132](https://github.com/MoonshotAI/kimi-code/pull/1132) [`108299b`](https://github.com/MoonshotAI/kimi-code/commit/108299be3cdffc31a23f64efd3ff5ba50976b412) - Refactor the thinking effort system
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- [#1231](https://github.com/MoonshotAI/kimi-code/pull/1231) [`ceb27f5`](https://github.com/MoonshotAI/kimi-code/commit/ceb27f5e449e177493f320d90e292487a8fc3410) - Add a server-side key-value store API for persisting web UI preferences to the user's data directory.
|
||||
|
||||
- [#1220](https://github.com/MoonshotAI/kimi-code/pull/1220) [`ec51324`](https://github.com/MoonshotAI/kimi-code/commit/ec51324230484f2ebaad1ab0aebf2e38f531d914) - Add a double-Esc shortcut to open the undo selector. Press Esc twice while idle to undo.
|
||||
|
||||
- [#1223](https://github.com/MoonshotAI/kimi-code/pull/1223) [`80e6888`](https://github.com/MoonshotAI/kimi-code/commit/80e6888e34e4362247c0eac5b77340df014ba286) - Fix @ file mentions not opening when typed inside a slash command argument.
|
||||
|
||||
- [#1233](https://github.com/MoonshotAI/kimi-code/pull/1233) [`020992c`](https://github.com/MoonshotAI/kimi-code/commit/020992c286f0f6bff6a038a7c7bd7e9db639e3c9) - Force-exit headless runs (`kimi -p`) so a stray ref'd handle left over from the run can't keep a completed run alive until an external timeout, and bound prompt cleanup so a wedged shutdown step can't hang shutdown.
|
||||
|
||||
- [#1225](https://github.com/MoonshotAI/kimi-code/pull/1225) [`659062d`](https://github.com/MoonshotAI/kimi-code/commit/659062d11cc272fe631fc6d4faf64d0e0b1a0142) - Show file path completions when typing `/` in shell mode (`!`).
|
||||
|
||||
- [#1236](https://github.com/MoonshotAI/kimi-code/pull/1236) [`bfe8e6a`](https://github.com/MoonshotAI/kimi-code/commit/bfe8e6ace3cda76b1991bf29c25b9444611d5512) - Fix adding a workspace by path in the web UI failing silently when the daemon rejects the path; it now shows an error instead of a broken workspace.
|
||||
|
||||
- [#1221](https://github.com/MoonshotAI/kimi-code/pull/1221) [`a3f9cec`](https://github.com/MoonshotAI/kimi-code/commit/a3f9cec8a975f11e37e992e42f954789ed394207) - Fix duplicate workspaces showing in the web sidebar when the same folder is registered more than once.
|
||||
|
||||
- [#1241](https://github.com/MoonshotAI/kimi-code/pull/1241) [`8ac337a`](https://github.com/MoonshotAI/kimi-code/commit/8ac337a2b2ac800aa79a373459308abb6c9e63bb) - Stop a malformed message history from permanently bricking a session on strict providers (Anthropic). The request is repaired before sending — orphaned tool calls are closed and empty/whitespace-only text blocks dropped — and if the provider still rejects its structure, it is resent once with a wire-compliant rebuild.
|
||||
|
||||
- [#1228](https://github.com/MoonshotAI/kimi-code/pull/1228) [`42e37eb`](https://github.com/MoonshotAI/kimi-code/commit/42e37eb898b722829d2ec83e909525ff18e336a5) - 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.
|
||||
|
||||
- [#1234](https://github.com/MoonshotAI/kimi-code/pull/1234) [`882cf35`](https://github.com/MoonshotAI/kimi-code/commit/882cf355a9cb45bb5b3424a27b953bde8e106bb0) - Hide the provider management dialog in the web UI until the server supports it.
|
||||
|
||||
- [#1226](https://github.com/MoonshotAI/kimi-code/pull/1226) [`7f05f58`](https://github.com/MoonshotAI/kimi-code/commit/7f05f589e7bc77a2f26463a41317ff7087e3c3a0) - Add Mermaid diagram rendering to the web chat. Fenced `mermaid` blocks in assistant responses now render as diagrams. KaTeX math and Mermaid diagram parsing also run in Web Workers to keep the UI responsive during live streaming.
|
||||
|
||||
- [#1232](https://github.com/MoonshotAI/kimi-code/pull/1232) [`aa6b0d0`](https://github.com/MoonshotAI/kimi-code/commit/aa6b0d065ee888056c3812781483ddb74739897f) - Always show the usage-data opt-out toggle in the web settings with a clearer label and description.
|
||||
|
||||
- [#1234](https://github.com/MoonshotAI/kimi-code/pull/1234) [`882cf35`](https://github.com/MoonshotAI/kimi-code/commit/882cf355a9cb45bb5b3424a27b953bde8e106bb0) - Fix the web workspace rename not persisting after a page refresh.
|
||||
|
||||
## 0.20.3
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@moonshot-ai/kimi-code",
|
||||
"version": "0.20.3",
|
||||
"version": "0.22.1",
|
||||
"description": "The Starting Point for Next-Gen Agents",
|
||||
"license": "MIT",
|
||||
"author": "Moonshot AI",
|
||||
|
|
@ -28,6 +28,7 @@
|
|||
"files": [
|
||||
"dist",
|
||||
"dist-web",
|
||||
"native",
|
||||
"scripts/postinstall.mjs",
|
||||
"scripts/postinstall",
|
||||
"README.md"
|
||||
|
|
@ -48,7 +49,7 @@
|
|||
"provenance": true
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm -C ../kimi-web run build && tsdown && node scripts/copy-web-assets.mjs",
|
||||
"build": "pnpm -C ../kimi-web run build && tsdown && node scripts/copy-native-assets.mjs && node scripts/copy-web-assets.mjs",
|
||||
"prebuild": "node scripts/build-vis-asset.mjs",
|
||||
"catalog:update": "node scripts/update-catalog.mjs --out dist/built-in-catalog.json",
|
||||
"smoke": "node scripts/smoke.mjs",
|
||||
|
|
@ -75,17 +76,16 @@
|
|||
},
|
||||
"optionalDependencies": {
|
||||
"@mariozechner/clipboard": "^0.3.9",
|
||||
"koffi": "^2.16.0",
|
||||
"node-pty": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@earendil-works/pi-tui": "^0.74.0",
|
||||
"@moonshot-ai/acp-adapter": "workspace:^",
|
||||
"@moonshot-ai/kimi-code-oauth": "workspace:^",
|
||||
"@moonshot-ai/kimi-code-sdk": "workspace:^",
|
||||
"@moonshot-ai/kimi-telemetry": "workspace:^",
|
||||
"@moonshot-ai/kimi-web": "workspace:^",
|
||||
"@moonshot-ai/migration-legacy": "workspace:^",
|
||||
"@moonshot-ai/pi-tui": "workspace:^",
|
||||
"@moonshot-ai/server": "workspace:^",
|
||||
"@moonshot-ai/vis-server": "workspace:^",
|
||||
"@moonshot-ai/vis-web": "workspace:*",
|
||||
|
|
@ -94,6 +94,7 @@
|
|||
"chalk": "^5.4.1",
|
||||
"cli-highlight": "^2.1.11",
|
||||
"commander": "^13.1.0",
|
||||
"jimp": "^1.6.1",
|
||||
"pathe": "^2.0.3",
|
||||
"postject": "1.0.0-alpha.6",
|
||||
"semver": "^7.7.4",
|
||||
|
|
|
|||
38
apps/kimi-code/scripts/copy-native-assets.mjs
Normal file
38
apps/kimi-code/scripts/copy-native-assets.mjs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { cp, mkdir, rm, stat } from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const repoRoot = resolve(appRoot, '../..');
|
||||
const source = resolve(repoRoot, 'packages/pi-tui/native');
|
||||
const target = resolve(appRoot, 'native');
|
||||
|
||||
// pi-tui ships platform-specific native helpers only for darwin/win32;
|
||||
// Linux has no native helper, so there is nothing to copy for it.
|
||||
const PLATFORMS = ['darwin', 'win32'];
|
||||
|
||||
async function assertPrebuilds(platform) {
|
||||
const dir = resolve(source, platform, 'prebuilds');
|
||||
try {
|
||||
const info = await stat(dir);
|
||||
if (!info.isDirectory()) {
|
||||
throw new Error('not a directory');
|
||||
}
|
||||
} catch {
|
||||
throw new Error(
|
||||
`pi-tui native prebuilds were not found at ${dir}. Build or restore packages/pi-tui first.`,
|
||||
);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
await rm(target, { recursive: true, force: true });
|
||||
await mkdir(target, { recursive: true });
|
||||
|
||||
for (const platform of PLATFORMS) {
|
||||
const srcPrebuilds = await assertPrebuilds(platform);
|
||||
const dstPrebuilds = resolve(target, platform, 'prebuilds');
|
||||
await cp(srcPrebuilds, dstPrebuilds, { recursive: true });
|
||||
}
|
||||
|
||||
console.log(`Copied pi-tui native prebuilds to ${target}`);
|
||||
|
|
@ -17,9 +17,7 @@ export const NATIVE_TARGETS = Object.freeze(
|
|||
SUPPORTED_TARGETS.map((t) => {
|
||||
const deps = resolveTargetDeps(t);
|
||||
const clipboardTarget = deps.find((d) => d.id === 'clipboard-target')?.resolvedName;
|
||||
const koffiNativeFile = deps.find((d) => d.id === 'koffi')?.nativeFileRelatives?.[0];
|
||||
const koffiTriplet = koffiNativeFile?.match(/koffi\/([^/]+)\/koffi\.node$/)?.[1] ?? null;
|
||||
return [t, { clipboardPackage: clipboardTarget, koffiTriplet }];
|
||||
return [t, { clipboardPackage: clipboardTarget }];
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
|
@ -161,16 +159,19 @@ async function collectPackageFiles({
|
|||
packageName,
|
||||
packageRoot,
|
||||
includeNativeFiles,
|
||||
includeEntryJs = true,
|
||||
nativeFileRelatives = [],
|
||||
}) {
|
||||
const packageJsonPath = join(packageRoot, 'package.json');
|
||||
const packageJson = await readJson(packageJsonPath);
|
||||
const selected = new Set([packageJsonPath]);
|
||||
|
||||
const entry = resolvePackageEntry(packageRoot, packageJson);
|
||||
if (entry !== null) {
|
||||
selected.add(entry);
|
||||
await addRuntimeDependencyFiles(packageRoot, entry, selected);
|
||||
if (includeEntryJs) {
|
||||
const entry = resolvePackageEntry(packageRoot, packageJson);
|
||||
if (entry !== null) {
|
||||
selected.add(entry);
|
||||
await addRuntimeDependencyFiles(packageRoot, entry, selected);
|
||||
}
|
||||
}
|
||||
|
||||
for (const nativeFileRelative of nativeFileRelatives) {
|
||||
|
|
@ -250,6 +251,7 @@ export async function collectNativeAssets({ appRoot, target }) {
|
|||
packageName: dep.resolvedName,
|
||||
packageRoot,
|
||||
includeNativeFiles: dep.collect === 'native-files',
|
||||
includeEntryJs: dep.collect !== 'native-file-only',
|
||||
nativeFileRelatives: dep.nativeFileRelatives,
|
||||
});
|
||||
const result = await packageManifestEntries({
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const optionalRuntimeRequires = new Set([
|
|||
'utf-8-validate',
|
||||
]);
|
||||
const optionalRelativeRuntimeRequires = new Set(['./crypto/build/Release/sshcrypto.node']);
|
||||
const handledNativeRuntimeRequires = new Set(['koffi']);
|
||||
const handledNativeRuntimeRequires = new Set();
|
||||
|
||||
function isAllowedSpecifier(specifier) {
|
||||
if (builtins.has(specifier) || specifier.startsWith('node:')) return true;
|
||||
|
|
|
|||
|
|
@ -27,13 +27,16 @@ const clipboardSubpackageByTarget = Object.freeze({
|
|||
'win32-x64': '@mariozechner/clipboard-win32-x64-msvc',
|
||||
});
|
||||
|
||||
const koffiTripletByTarget = Object.freeze({
|
||||
'darwin-arm64': 'darwin_arm64',
|
||||
'darwin-x64': 'darwin_x64',
|
||||
'linux-arm64': 'linux_arm64',
|
||||
'linux-x64': 'linux_x64',
|
||||
'win32-arm64': 'win32_arm64',
|
||||
'win32-x64': 'win32_x64',
|
||||
// pi-tui ships platform-specific native helpers (no Linux build):
|
||||
// - darwin: Shift-modifier detection for Terminal.app Shift+Enter
|
||||
// - win32: enable ENABLE_VIRTUAL_TERMINAL_INPUT so Shift+Tab is distinguishable
|
||||
const piTuiNativeFileByTarget = Object.freeze({
|
||||
'darwin-arm64': ['native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node'],
|
||||
'darwin-x64': ['native/darwin/prebuilds/darwin-x64/darwin-modifiers.node'],
|
||||
'linux-arm64': [],
|
||||
'linux-x64': [],
|
||||
'win32-arm64': ['native/win32/prebuilds/win32-arm64/win32-console-mode.node'],
|
||||
'win32-x64': ['native/win32/prebuilds/win32-x64/win32-console-mode.node'],
|
||||
});
|
||||
|
||||
export function isSupportedTarget(target) {
|
||||
|
|
@ -45,13 +48,15 @@ export function isSupportedTarget(target) {
|
|||
* @property {string} id — stable internal id used for parent refs
|
||||
* @property {(target: string) => string} name
|
||||
* — npm package name (may depend on target)
|
||||
* @property {'js-only'|'native-files'|'js-and-native-file'|'virtual'} collect
|
||||
* @property {'js-only'|'native-files'|'js-and-native-file'|'native-file-only'|'virtual'} collect
|
||||
* @property {string|null} parent
|
||||
* — id of another registered dep this nests under (for pnpm),
|
||||
* or null for top-level (resolvable from app root)
|
||||
* @property {(target: string) => string[]} [nativeFileRelatives]
|
||||
* — explicit list of .node files relative to package root
|
||||
* (used by 'js-and-native-file'; native-files mode auto-scans *.node)
|
||||
* (used by 'js-and-native-file' and 'native-file-only';
|
||||
* native-files mode auto-scans *.node). 'native-file-only' collects
|
||||
* package.json + these .node files but skips the package entry JS.
|
||||
*/
|
||||
|
||||
/** @type {readonly NativeDepDescriptor[]} */
|
||||
|
|
@ -70,18 +75,14 @@ export const nativeDeps = Object.freeze([
|
|||
},
|
||||
{
|
||||
id: 'pi-tui',
|
||||
name: () => '@earendil-works/pi-tui',
|
||||
// pi-tui is bundled into main.cjs at build time — we don't collect it as
|
||||
// a native dep, only register it so koffi can declare it as parent.
|
||||
collect: 'virtual',
|
||||
name: () => '@moonshot-ai/pi-tui',
|
||||
// pi-tui's JS is bundled into main.cjs, so only the platform-specific
|
||||
// native helper (.node under native/) ships alongside the binary — its
|
||||
// dist/ JS is intentionally NOT collected (it stays in the bundle). This
|
||||
// keeps the SEA native-asset payload small. Linux has no native helper.
|
||||
collect: 'native-file-only',
|
||||
parent: null,
|
||||
},
|
||||
{
|
||||
id: 'koffi',
|
||||
name: () => 'koffi',
|
||||
collect: 'js-and-native-file',
|
||||
parent: 'pi-tui',
|
||||
nativeFileRelatives: (target) => [`build/koffi/${koffiTripletByTarget[target]}/koffi.node`],
|
||||
nativeFileRelatives: (target) => piTuiNativeFileByTarget[target] ?? [],
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
|
|||
96
apps/kimi-code/src/cli/headless-exit.ts
Normal file
96
apps/kimi-code/src/cli/headless-exit.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import type { Writable } from 'node:stream';
|
||||
|
||||
import { HEADLESS_FORCE_EXIT_GRACE_MS, HEADLESS_STDIO_DRAIN_TIMEOUT_MS } from '#/constant/app';
|
||||
|
||||
/** Minimal process surface needed to force a headless run to terminate. */
|
||||
export interface ExitableProcess {
|
||||
exit(code?: number): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a best-effort force-exit for a completed headless (`kimi -p`) run.
|
||||
*
|
||||
* Print mode does not call `process.exit()`; it relies on the Node event loop
|
||||
* draining once the run is done. If a stray ref'd handle survives shutdown — a
|
||||
* lingering socket (e.g. a connection blackholed by a restrictive firewall, or
|
||||
* an HTTP/2 session kept alive by PING), an un-cleared timer, or a child whose
|
||||
* pipes stay open — the loop never empties and the process hangs until an
|
||||
* external timeout kills it.
|
||||
*
|
||||
* This arms an **unref'd** fallback timer: a healthy run drains and exits
|
||||
* naturally before it fires (so behaviour is unchanged), and the timer itself
|
||||
* never keeps the loop alive. It only force-exits a run whose loop is already
|
||||
* wedged. The exit code is read lazily at fire time so callers may set
|
||||
* `process.exitCode` after scheduling (e.g. a goal turn mapping its terminal
|
||||
* status to a non-zero code).
|
||||
*
|
||||
* Returns the timer handle so callers/tests can `clearTimeout` it.
|
||||
*/
|
||||
export function scheduleHeadlessForceExit(
|
||||
proc: ExitableProcess,
|
||||
getExitCode: () => number,
|
||||
graceMs: number = HEADLESS_FORCE_EXIT_GRACE_MS,
|
||||
): NodeJS.Timeout {
|
||||
const timer = setTimeout(() => {
|
||||
proc.exit(getExitCode());
|
||||
}, graceMs);
|
||||
timer.unref?.();
|
||||
return timer;
|
||||
}
|
||||
|
||||
/** Resolve once a stream's currently-buffered writes have flushed to its sink. */
|
||||
function flushStream(stream: Writable): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
try {
|
||||
// An empty write's callback fires after all previously-queued writes have
|
||||
// been flushed (writes are ordered), which is the documented way to know a
|
||||
// stream's buffer has drained.
|
||||
stream.write('', () => resolve());
|
||||
} catch {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for buffered output on the given streams to flush, bounded by `timeoutMs`.
|
||||
*
|
||||
* A slow or piped consumer that hasn't read all of stdout/stderr yet leaves the
|
||||
* pipe as a legitimate ref'd handle keeping the loop alive. Flushing before any
|
||||
* force-exit prevents truncating output from an otherwise-successful run. The
|
||||
* wait is bounded so a permanently-stuck consumer can't re-introduce the hang.
|
||||
*/
|
||||
export async function drainStdio(
|
||||
streams: readonly Writable[],
|
||||
timeoutMs: number = HEADLESS_STDIO_DRAIN_TIMEOUT_MS,
|
||||
): Promise<void> {
|
||||
let timer: NodeJS.Timeout | undefined;
|
||||
const timeout = new Promise<void>((resolve) => {
|
||||
timer = setTimeout(resolve, timeoutMs);
|
||||
timer.unref?.();
|
||||
});
|
||||
try {
|
||||
await Promise.race([Promise.all(streams.map(flushStream)).then(() => undefined), timeout]);
|
||||
} finally {
|
||||
if (timer !== undefined) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize a completed headless run: flush stdio, then arm the force-exit
|
||||
* backstop.
|
||||
*
|
||||
* Draining first means in-flight legitimate output is fully written before the
|
||||
* backstop can fire, and — since drained stdio no longer holds the loop — only a
|
||||
* genuinely leaked handle can keep it alive afterwards, which is exactly what
|
||||
* the backstop is for.
|
||||
*/
|
||||
export async function finalizeHeadlessRun(
|
||||
proc: ExitableProcess,
|
||||
streams: readonly Writable[],
|
||||
getExitCode: () => number,
|
||||
options: { drainTimeoutMs?: number; graceMs?: number } = {},
|
||||
): Promise<void> {
|
||||
await drainStdio(streams, options.drainTimeoutMs ?? HEADLESS_STDIO_DRAIN_TIMEOUT_MS);
|
||||
scheduleHeadlessForceExit(proc, getExitCode, options.graceMs);
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ import {
|
|||
} from '@moonshot-ai/kimi-code-sdk';
|
||||
import { resolve } from 'pathe';
|
||||
|
||||
import { CLI_SHUTDOWN_TIMEOUT_MS } from '#/constant/app';
|
||||
import { CLI_SHUTDOWN_TIMEOUT_MS, PROMPT_CLEANUP_TIMEOUT_MS } from '#/constant/app';
|
||||
|
||||
import type { CLIOptions, PromptOutputFormat } from './options';
|
||||
import {
|
||||
|
|
@ -32,6 +32,44 @@ import {
|
|||
import { createCliTelemetryBootstrap, initializeCliTelemetry } from './telemetry';
|
||||
import { createKimiCodeHostIdentity } from './version';
|
||||
|
||||
/**
|
||||
* Await `promise`, but stop waiting after `timeoutMs`.
|
||||
*
|
||||
* The timeout only bounds how long we WAIT — it does not change the outcome:
|
||||
* - if `promise` settles first, its result is propagated (a rejection throws),
|
||||
* so a cleanup step that actually fails in time still surfaces;
|
||||
* - if the timeout wins, we resolve (give up waiting) and swallow the abandoned
|
||||
* promise's eventual late rejection so it can't surface as an unhandled
|
||||
* rejection.
|
||||
*
|
||||
* Used to bound shutdown so a wedged cleanup step can't keep a completed
|
||||
* headless run alive, without silently swallowing a cleanup that fails fast. The
|
||||
* timer is unref'd so it never keeps the loop alive on its own.
|
||||
*/
|
||||
async function raceWithTimeout(promise: Promise<void>, timeoutMs: number): Promise<void> {
|
||||
let timedOut = false;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
// Attach the catch eagerly (synchronously) so `promise` is always consumed and
|
||||
// a late rejection can never become an unhandled rejection. Before the timeout
|
||||
// wins, the handler rethrows so a real cleanup failure still propagates.
|
||||
const guarded = promise.catch((error: unknown) => {
|
||||
if (timedOut) return;
|
||||
throw error;
|
||||
});
|
||||
const timedOutSignal = new Promise<void>((resolve) => {
|
||||
timer = setTimeout(() => {
|
||||
timedOut = true;
|
||||
resolve();
|
||||
}, timeoutMs);
|
||||
timer.unref?.();
|
||||
});
|
||||
try {
|
||||
await Promise.race([guarded, timedOutSignal]);
|
||||
} finally {
|
||||
if (timer !== undefined) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
interface PromptOutput {
|
||||
readonly columns?: number | undefined;
|
||||
write(chunk: string): boolean;
|
||||
|
|
@ -96,7 +134,7 @@ export async function runPrompt(
|
|||
let removeTerminationCleanup: (() => void) | undefined;
|
||||
let cleanupPromise: Promise<void> | undefined;
|
||||
const cleanupPromptRun = async (): Promise<void> => {
|
||||
cleanupPromise ??= (async () => {
|
||||
const pending = (cleanupPromise ??= (async () => {
|
||||
removeTerminationCleanup?.();
|
||||
setCrashPhase('shutdown');
|
||||
try {
|
||||
|
|
@ -105,8 +143,13 @@ export async function runPrompt(
|
|||
await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS });
|
||||
await harness.close();
|
||||
}
|
||||
})();
|
||||
await cleanupPromise;
|
||||
})());
|
||||
// Bound cleanup so a wedged shutdown step (e.g. a SessionEnd hook, MCP
|
||||
// shutdown, or a connection blackholed by a restrictive firewall) cannot
|
||||
// keep a completed headless run alive forever. The cleanup keeps running in
|
||||
// the background if it overruns; the caller (`kimi -p`) force-exits shortly
|
||||
// after, so any straggling work is torn down with the process.
|
||||
await raceWithTimeout(pending, PROMPT_CLEANUP_TIMEOUT_MS);
|
||||
};
|
||||
removeTerminationCleanup = installPromptTerminationCleanup(promptProcess, cleanupPromptRun);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { execSync } from 'node:child_process';
|
||||
import { execSync, spawnSync } from 'node:child_process';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
|
|
@ -25,6 +25,7 @@ import { KimiTUI } from '#/tui/index';
|
|||
import { currentTheme, getColorPalette } from '#/tui/theme';
|
||||
import { combineStartupNotice } from '#/tui/utils/startup';
|
||||
import { toTerminalHyperlink } from '#/utils/terminal-hyperlink';
|
||||
import { restoreTerminalModes } from '#/utils/terminal-restore';
|
||||
|
||||
import type { CLIOptions } from './options';
|
||||
import { createCliTelemetryBootstrap, initializeCliTelemetry } from './telemetry';
|
||||
|
|
@ -133,6 +134,59 @@ export async function runShell(
|
|||
trackLifecycleForSession(tui.getCurrentSessionId(), event, properties);
|
||||
};
|
||||
|
||||
let savedStty: string | undefined;
|
||||
try {
|
||||
// stty operates on the terminal behind stdin, so stdin must be the TTY —
|
||||
// piping /dev/null (ignore) makes stty fail with "not a tty".
|
||||
const saved = execSync('stty -g', {
|
||||
encoding: 'utf8',
|
||||
stdio: ['inherit', 'pipe', 'ignore'],
|
||||
});
|
||||
savedStty = typeof saved === 'string' ? saved.trim() : undefined;
|
||||
execSync('stty -ixon', { stdio: ['inherit', 'ignore', 'ignore'] });
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const restoreStty = (): void => {
|
||||
if (savedStty === undefined) return;
|
||||
const args = savedStty.split(/\s+/).filter((arg) => arg.length > 0);
|
||||
if (args.length === 0) return;
|
||||
spawnSync('stty', args, { stdio: ['inherit', 'ignore', 'ignore'] });
|
||||
};
|
||||
|
||||
// If we crash without going through KimiTUI.stop(), the terminal is left in
|
||||
// raw mode with a hidden cursor and XON/XOFF flow control disabled. Restore
|
||||
// both before exiting so the user's shell is usable afterwards.
|
||||
const emergencyExit = (exitCode: number): void => {
|
||||
restoreTerminalModes();
|
||||
restoreStty();
|
||||
process.exit(exitCode);
|
||||
};
|
||||
const onUncaughtException = (error: unknown): void => {
|
||||
try {
|
||||
log.error('uncaughtException, restoring terminal and exiting', { error: String(error) });
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
emergencyExit(1);
|
||||
};
|
||||
const onUnhandledRejection = (reason: unknown): void => {
|
||||
try {
|
||||
log.error('unhandledRejection, restoring terminal and exiting', { reason: String(reason) });
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
emergencyExit(1);
|
||||
};
|
||||
process.on('uncaughtException', onUncaughtException);
|
||||
process.on('unhandledRejection', onUnhandledRejection);
|
||||
// Remove the crash handlers once the TUI exits cleanly so repeated runShell()
|
||||
// calls in the same process (e.g. tests) don't accumulate process listeners.
|
||||
const removeCrashHandlers = (): void => {
|
||||
process.off('uncaughtException', onUncaughtException);
|
||||
process.off('unhandledRejection', onUnhandledRejection);
|
||||
};
|
||||
|
||||
tui.onExit = async (exitCode = 0) => {
|
||||
const sessionId = tui.getCurrentSessionId();
|
||||
const hasContent = tui.hasSessionContent();
|
||||
|
|
@ -151,13 +205,10 @@ export async function runShell(
|
|||
if (hints.length > 0) {
|
||||
process.stderr.write(`\n${hints.join('\n')}\n`);
|
||||
}
|
||||
removeCrashHandlers();
|
||||
restoreStty();
|
||||
process.exit(exitCode);
|
||||
};
|
||||
try {
|
||||
execSync('stty -ixon', { stdio: 'ignore' });
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
const initStartedAt = Date.now();
|
||||
await tui.start();
|
||||
|
|
@ -171,6 +222,7 @@ export async function runShell(
|
|||
mcp_ms: mcpMs,
|
||||
});
|
||||
} catch (error) {
|
||||
removeCrashHandlers();
|
||||
setCrashPhase('shutdown');
|
||||
trackLifecycle('exit', { duration_ms: Date.now() - startedAt });
|
||||
await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS });
|
||||
|
|
|
|||
|
|
@ -340,7 +340,7 @@ export async function handleCatalogAdd(
|
|||
// already-configured provider would lose the user's previously-set default
|
||||
// even when `--default-model` is not supplied.
|
||||
const previousDefaultModel = config.defaultModel;
|
||||
const previousDefaultThinking = config.defaultThinking;
|
||||
const previousThinking = config.thinking;
|
||||
|
||||
if (config.providers[providerId] !== undefined) {
|
||||
config = await harness.removeProvider(providerId);
|
||||
|
|
@ -348,7 +348,7 @@ export async function handleCatalogAdd(
|
|||
|
||||
const baseUrl = catalogBaseUrl(entry, wire);
|
||||
// `applyCatalogProvider` always overwrites both `defaultModel` and
|
||||
// `defaultThinking`. The values we pass here are temporary; we restore
|
||||
// `[thinking]`. The values we pass here are temporary; we restore
|
||||
// a consistent state in the post-apply block below.
|
||||
applyCatalogProvider(config, {
|
||||
providerId,
|
||||
|
|
@ -373,18 +373,18 @@ export async function handleCatalogAdd(
|
|||
config.defaultModel = stillResolves ? previousDefaultModel : undefined;
|
||||
}
|
||||
|
||||
// Always restore `defaultThinking` from what was there before — including
|
||||
// `undefined`. Persisting `false` when the user never set it would make
|
||||
// `resolveThinkingLevel` (agent-core/src/agent/config/thinking.ts) treat
|
||||
// it as an explicit "off" request and silently disable thinking, even
|
||||
// for thinking-capable models.
|
||||
config.defaultThinking = previousDefaultThinking;
|
||||
// Always restore `[thinking]` from what was there before — including
|
||||
// `undefined`. Persisting `enabled: false` when the user never set it would
|
||||
// make `resolveThinkingEffort` (agent-core/src/agent/config/thinking.ts) treat
|
||||
// it as an explicit "off" request and silently disable thinking, even for
|
||||
// thinking-capable models.
|
||||
config.thinking = previousThinking;
|
||||
|
||||
await harness.setConfig({
|
||||
providers: config.providers,
|
||||
models: config.models,
|
||||
defaultModel: config.defaultModel,
|
||||
defaultThinking: config.defaultThinking,
|
||||
thinking: config.thinking,
|
||||
});
|
||||
|
||||
const displayName = entry.name ?? providerId;
|
||||
|
|
|
|||
|
|
@ -488,7 +488,14 @@ export async function installUpdate(
|
|||
): Promise<void> {
|
||||
const { cmd, args } = spawnForSource(source, version, platform);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const child = spawn(cmd, [...args], { stdio: 'inherit' });
|
||||
// Windows package managers (npm/pnpm/yarn) are .cmd shims. Since the
|
||||
// CVE-2024-27980 fix, Node throws EINVAL when spawning a .cmd/.bat without
|
||||
// a shell, so run through the shell on win32. The version is a validated
|
||||
// semver and the package name is a constant, so args are shell-safe.
|
||||
const child = spawn(cmd, [...args], {
|
||||
stdio: 'inherit',
|
||||
shell: platform === 'win32' ? true : undefined,
|
||||
});
|
||||
child.once('error', reject);
|
||||
child.once('exit', (code, signal) => {
|
||||
if (code === 0) {
|
||||
|
|
@ -596,7 +603,15 @@ async function startBackgroundInstall(
|
|||
});
|
||||
};
|
||||
|
||||
const child = spawn(cmd, [...args], { detached: true, stdio: 'ignore' });
|
||||
const child = spawn(cmd, [...args], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
shell: platform === 'win32' ? true : undefined,
|
||||
// On Windows a detached child gets its own console window; with shell:true
|
||||
// that window would flash during a passive background update. Hide it so
|
||||
// the silent updater stays silent.
|
||||
windowsHide: platform === 'win32' ? true : undefined,
|
||||
});
|
||||
child.once('error', () => { finish(false); });
|
||||
child.once('exit', (code) => { finish(code === 0); });
|
||||
child.unref();
|
||||
|
|
|
|||
|
|
@ -14,6 +14,26 @@ export const WEB_UI_MODE = 'web';
|
|||
// Give telemetry a short flush window without making CLI exit feel stuck.
|
||||
export const CLI_SHUTDOWN_TIMEOUT_MS = 3000;
|
||||
|
||||
// Upper bound on headless (`kimi -p`) shutdown. A wedged cleanup step (e.g. a
|
||||
// SessionEnd hook, an MCP shutdown, or a connection blackholed by a restrictive
|
||||
// firewall) must not keep a completed run alive indefinitely — once this elapses
|
||||
// we stop waiting on cleanup and let the run return.
|
||||
export const PROMPT_CLEANUP_TIMEOUT_MS = 8000;
|
||||
|
||||
// Grace after a headless run has fully completed (turn done, cleanup attempted)
|
||||
// before force-exiting. `kimi -p` otherwise relies on the event loop draining to
|
||||
// exit; a stray ref'd handle (socket/timer/child) left over from the run would
|
||||
// wedge it. The guard timer is unref'd, so a healthy run still exits naturally
|
||||
// well before this fires.
|
||||
export const HEADLESS_FORCE_EXIT_GRACE_MS = 2000;
|
||||
|
||||
// Max time to wait for buffered stdout/stderr to flush before arming the
|
||||
// force-exit fallback. A slow/piped consumer's still-draining stdio is a
|
||||
// legitimate ref'd handle — flushing first prevents the fallback from
|
||||
// truncating completed output. Bounded so a permanently-stuck consumer can't
|
||||
// re-introduce the hang.
|
||||
export const HEADLESS_STDIO_DRAIN_TIMEOUT_MS = 10000;
|
||||
|
||||
// Published npm package name; this can differ from the executable command.
|
||||
export const NPM_PACKAGE_NAME = '@moonshot-ai/kimi-code';
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
} from '@moonshot-ai/kimi-telemetry';
|
||||
|
||||
import { createProgram } from './cli/commands';
|
||||
import { finalizeHeadlessRun } from './cli/headless-exit';
|
||||
import type { CLIOptions } from './cli/options';
|
||||
import { OptionConflictError, validateOptions } from './cli/options';
|
||||
import { runPrompt } from './cli/run-prompt';
|
||||
|
|
@ -38,7 +39,22 @@ import { cleanupStaleNativeCacheForCurrent } from './native/native-assets';
|
|||
import { installNativeModuleHook } from './native/module-hook';
|
||||
import { runNativeAssetSmokeIfRequested } from './native/smoke';
|
||||
|
||||
export async function handleMainCommand(opts: CLIOptions, version: string): Promise<void> {
|
||||
/**
|
||||
* Outcome of a CLI command run, reported back to the process entrypoint.
|
||||
*
|
||||
* `handleMainCommand` is a reusable, unit-tested handler — it must not terminate
|
||||
* the process itself. It reports here whether a headless (`kimi -p`) run
|
||||
* completed so the entrypoint (the only place that owns the process) can arm the
|
||||
* force-exit fallback.
|
||||
*/
|
||||
export interface MainCommandOutcome {
|
||||
readonly headlessCompleted: boolean;
|
||||
}
|
||||
|
||||
export async function handleMainCommand(
|
||||
opts: CLIOptions,
|
||||
version: string,
|
||||
): Promise<MainCommandOutcome> {
|
||||
let validated: ReturnType<typeof validateOptions>;
|
||||
try {
|
||||
validated = validateOptions(opts);
|
||||
|
|
@ -60,10 +76,11 @@ export async function handleMainCommand(opts: CLIOptions, version: string): Prom
|
|||
|
||||
if (validated.uiMode === 'print') {
|
||||
await runPrompt(validated.options, version);
|
||||
return;
|
||||
return { headlessCompleted: true };
|
||||
}
|
||||
|
||||
await runShell(validated.options, version);
|
||||
return { headlessCompleted: false };
|
||||
}
|
||||
|
||||
/** `kimi migrate`: launch the migration screen only, then exit. */
|
||||
|
|
@ -139,17 +156,32 @@ export function main(): void {
|
|||
const program = createProgram(
|
||||
version,
|
||||
(opts) => {
|
||||
void handleMainCommand(opts, version).catch(async (error: unknown) => {
|
||||
const operation = opts.prompt !== undefined ? 'run prompt' : 'start shell';
|
||||
await logStartupFailure(operation, error);
|
||||
process.stderr.write(
|
||||
formatStartupError(error, {
|
||||
operation,
|
||||
}),
|
||||
);
|
||||
process.stderr.write(`See log: ${resolveGlobalLogPath(resolveKimiHome())}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
void handleMainCommand(opts, version)
|
||||
.then(async (outcome) => {
|
||||
// Only the process entrypoint disposes of the process. Print mode
|
||||
// relies on the event loop draining to exit; flush any buffered output
|
||||
// and then arm an unref'd fallback so a stray ref'd handle left over
|
||||
// from the run can't wedge a completed `kimi -p` until an external
|
||||
// timeout. A healthy run drains and exits before the fallback fires.
|
||||
if (outcome.headlessCompleted) {
|
||||
await finalizeHeadlessRun(
|
||||
process,
|
||||
[process.stdout, process.stderr],
|
||||
() => Number(process.exitCode) || 0,
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(async (error: unknown) => {
|
||||
const operation = opts.prompt !== undefined ? 'run prompt' : 'start shell';
|
||||
await logStartupFailure(operation, error);
|
||||
process.stderr.write(
|
||||
formatStartupError(error, {
|
||||
operation,
|
||||
}),
|
||||
);
|
||||
process.stderr.write(`See log: ${resolveGlobalLogPath(resolveKimiHome())}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
},
|
||||
() => {
|
||||
void handleMigrateCommand(version).catch(async (error: unknown) => {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
* This file implements the ask, progress, and result phases. `beginMigration`
|
||||
* drives the real runMigration flow (injectable for tests).
|
||||
*/
|
||||
import { Container, matchesKey, Key, truncateToWidth, type Focusable } from '@earendil-works/pi-tui';
|
||||
import { Container, matchesKey, Key, truncateToWidth, type Focusable } from '@moonshot-ai/pi-tui';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import type { ColorPalette } from '#/tui/theme/colors';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { existsSync } from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { loadNativePackage } from './native-require';
|
||||
import { getNativePackageRoot } from './native-assets';
|
||||
|
||||
type ModuleLoad = (request: string, parent: unknown, isMain: boolean) => unknown;
|
||||
|
||||
|
|
@ -10,7 +12,16 @@ interface ModuleWithLoad {
|
|||
|
||||
const nodeRequire = createRequire(import.meta.url);
|
||||
let installed = false;
|
||||
let loadingNativePackage = false;
|
||||
|
||||
// pi-tui loads its platform-specific native helpers via an absolute-path
|
||||
// require() computed from import.meta.url / process.execPath
|
||||
// (see pi-tui dist/terminal.js and dist/native-modifiers.js). In a SEA binary
|
||||
// those .node files live in the native-asset cache, so redirect any absolute
|
||||
// require of a pi-tui native helper to the cached copy.
|
||||
//
|
||||
// Path shape: native/<darwin|win32>/prebuilds/<arch>/<file>.node — note the
|
||||
// two path segments after "prebuilds", so ".+" (not "[^/]+") is required.
|
||||
const PI_TUI_NATIVE_PATTERN = /native[\\/](?:win32|darwin)[\\/]prebuilds[\\/].+\.node$/;
|
||||
|
||||
export function installNativeModuleHook(): void {
|
||||
if (installed) return;
|
||||
|
|
@ -26,13 +37,18 @@ export function installNativeModuleHook(): void {
|
|||
parent: unknown,
|
||||
isMain: boolean,
|
||||
): unknown {
|
||||
if (request === 'koffi' && !loadingNativePackage) {
|
||||
loadingNativePackage = true;
|
||||
try {
|
||||
const pkg = loadNativePackage<unknown>('koffi');
|
||||
if (pkg !== null) return pkg;
|
||||
} finally {
|
||||
loadingNativePackage = false;
|
||||
if (
|
||||
typeof request === 'string' &&
|
||||
PI_TUI_NATIVE_PATTERN.test(request) &&
|
||||
!existsSync(request)
|
||||
) {
|
||||
const pkgRoot = getNativePackageRoot('@moonshot-ai/pi-tui');
|
||||
if (pkgRoot !== null) {
|
||||
const match = request.match(PI_TUI_NATIVE_PATTERN);
|
||||
if (match !== null) {
|
||||
const redirected = join(pkgRoot, match[0]);
|
||||
return originalLoad.call(this, redirected, parent, isMain);
|
||||
}
|
||||
}
|
||||
}
|
||||
return originalLoad.call(this, request, parent, isMain);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,38 @@
|
|||
import { createRequire } from 'node:module';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
import { getEmbeddedNativeAssetManifest, getNativePackageRoot } from './native-assets';
|
||||
|
||||
const smokePackages = ['@mariozechner/clipboard', 'koffi'];
|
||||
const smokePackages = ['@mariozechner/clipboard', '@moonshot-ai/pi-tui'];
|
||||
|
||||
// Verify pi-tui's native helper can actually be loaded through the module hook.
|
||||
// pi-tui computes native helper paths from process.execPath and require()s them;
|
||||
// those paths do not exist next to the SEA binary, so this only succeeds when
|
||||
// installNativeModuleHook() redirects the require into the native-asset cache.
|
||||
function smokePiTuiNativeLoad(): void {
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
let rel: string | undefined;
|
||||
if (platform === 'darwin' && (arch === 'x64' || arch === 'arm64')) {
|
||||
rel = join('native', 'darwin', 'prebuilds', `darwin-${arch}`, 'darwin-modifiers.node');
|
||||
} else if (platform === 'win32' && (arch === 'x64' || arch === 'arm64')) {
|
||||
rel = join('native', 'win32', 'prebuilds', `win32-${arch}`, 'win32-console-mode.node');
|
||||
}
|
||||
if (rel === undefined) return; // Linux: no native helper, nothing to load.
|
||||
|
||||
const req = createRequire(import.meta.url);
|
||||
const bogusPath = join(dirname(process.execPath), rel);
|
||||
const helper = req(bogusPath) as {
|
||||
isModifierPressed?: unknown;
|
||||
enableVirtualTerminalInput?: unknown;
|
||||
};
|
||||
const ok =
|
||||
typeof helper.isModifierPressed === 'function' ||
|
||||
typeof helper.enableVirtualTerminalInput === 'function';
|
||||
if (!ok) {
|
||||
throw new Error(`pi-tui native helper loaded but exports are unexpected: ${rel}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function runNativeAssetSmokeIfRequested(): boolean {
|
||||
if (process.env['KIMI_CODE_NATIVE_ASSET_SMOKE'] !== '1') return false;
|
||||
|
|
@ -16,6 +48,7 @@ export function runNativeAssetSmokeIfRequested(): boolean {
|
|||
throw new Error(`Native package is not available: ${packageName}`);
|
||||
}
|
||||
}
|
||||
smokePiTuiNativeLoad();
|
||||
process.stdout.write(`Native asset smoke passed: ${manifest.target}\n`);
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -159,7 +159,11 @@ async function handleOpenPlatformLogin(
|
|||
platform,
|
||||
models,
|
||||
selectedModel: selection.model,
|
||||
thinking: selection.thinking,
|
||||
thinking: selection.thinking !== 'off',
|
||||
effort:
|
||||
selection.thinking !== 'off' && selection.thinking !== 'on'
|
||||
? selection.thinking
|
||||
: undefined,
|
||||
apiKey,
|
||||
});
|
||||
|
||||
|
|
@ -167,7 +171,7 @@ async function handleOpenPlatformLogin(
|
|||
providers: config.providers,
|
||||
models: config.models,
|
||||
defaultModel: config.defaultModel,
|
||||
defaultThinking: config.defaultThinking,
|
||||
thinking: config.thinking,
|
||||
});
|
||||
|
||||
await host.authFlow.refreshConfigAfterLogin();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { AutocompleteItem } from '@earendil-works/pi-tui';
|
||||
import type { AutocompleteItem } from '@moonshot-ai/pi-tui';
|
||||
|
||||
/**
|
||||
* A completable token (subcommand or flag) for a slash command's argument
|
||||
|
|
|
|||
|
|
@ -1,25 +1,30 @@
|
|||
import type {
|
||||
ExperimentalFeatureState,
|
||||
FlagId,
|
||||
PermissionMode,
|
||||
Session,
|
||||
import {
|
||||
effectiveModelAlias,
|
||||
type ExperimentalFeatureState,
|
||||
type ModelAlias,
|
||||
type PermissionMode,
|
||||
type Session,
|
||||
type ThinkingEffort,
|
||||
} from '@moonshot-ai/kimi-code-sdk';
|
||||
|
||||
import { EditorSelectorComponent } from '../components/dialogs/editor-selector';
|
||||
import { EffortSelectorComponent } from '../components/dialogs/effort-selector';
|
||||
import {
|
||||
ExperimentsSelectorComponent,
|
||||
type ExperimentalFeatureDraftChange,
|
||||
} from '../components/dialogs/experiments-selector';
|
||||
import { modelDisplayName, segmentsFor } from '../components/dialogs/model-selector';
|
||||
import { TabbedModelSelectorComponent } from '../components/dialogs/tabbed-model-selector';
|
||||
import { PermissionSelectorComponent } from '../components/dialogs/permission-selector';
|
||||
import { SettingsSelectorComponent, type SettingsSelection } from '../components/dialogs/settings-selector';
|
||||
import { ThemeSelectorComponent } from '../components/dialogs/theme-selector';
|
||||
import { UpdatePreferenceSelectorComponent } from '../components/dialogs/update-preference-selector';
|
||||
import { saveTuiConfig } from '../config';
|
||||
import { DEFAULT_TUI_CONFIG, saveTuiConfig, type TuiConfig } from '../config';
|
||||
import type { ThemeName } from '#/tui/theme';
|
||||
import { currentTheme, isBuiltInTheme, lightColors, loadCustomThemeMerged } from '#/tui/theme';
|
||||
import { NO_ACTIVE_SESSION_MESSAGE } from '../constant/kimi-tui';
|
||||
import { formatErrorMessage } from '../utils/event-payload';
|
||||
import { thinkingEffortToConfig } from '../utils/thinking-config';
|
||||
import { showUsage } from './info';
|
||||
import { setExperimentalFeatures } from './experimental-flags';
|
||||
import type { SlashCommandHost } from './dispatch';
|
||||
|
|
@ -30,6 +35,16 @@ import type { SlashCommandHost } from './dispatch';
|
|||
|
||||
const MODEL_PICKER_REFRESH_TIMEOUT_MS = 2_000;
|
||||
|
||||
function currentTuiConfig(host: SlashCommandHost): TuiConfig {
|
||||
return {
|
||||
theme: host.state.appState.theme,
|
||||
editorCommand: host.state.appState.editorCommand,
|
||||
disablePasteBurst: host.state.appState.disablePasteBurst ?? DEFAULT_TUI_CONFIG.disablePasteBurst,
|
||||
notifications: host.state.appState.notifications,
|
||||
upgrade: host.state.appState.upgrade,
|
||||
};
|
||||
}
|
||||
|
||||
export async function handlePlanCommand(host: SlashCommandHost, args: string): Promise<void> {
|
||||
const session = host.session;
|
||||
if (session === undefined) {
|
||||
|
|
@ -212,6 +227,56 @@ export async function handleModelCommand(host: SlashCommandHost, args: string):
|
|||
showModelPicker(host, alias);
|
||||
}
|
||||
|
||||
export async function handleEffortCommand(host: SlashCommandHost, args: string): Promise<void> {
|
||||
const alias = host.state.appState.model;
|
||||
const model = host.state.appState.availableModels[alias];
|
||||
if (model === undefined) {
|
||||
host.showError('No model selected. Run /model to select one first.');
|
||||
return;
|
||||
}
|
||||
const effective = effectiveModelAlias(model);
|
||||
const segments = segmentsFor(effective);
|
||||
const arg = args.trim().toLowerCase();
|
||||
if (arg.length === 0) {
|
||||
showEffortPicker(host, effective, segments);
|
||||
return;
|
||||
}
|
||||
if (!segments.includes(arg)) {
|
||||
host.showError(
|
||||
`Unsupported thinking effort "${arg}" for ${alias}. Available: ${segments.join(', ')}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await performModelSwitch(host, alias, arg, true);
|
||||
}
|
||||
|
||||
function showEffortPicker(
|
||||
host: SlashCommandHost,
|
||||
model: ModelAlias,
|
||||
segments: readonly string[],
|
||||
): void {
|
||||
const liveEffort = host.state.appState.thinkingEffort;
|
||||
const currentValue = segments.includes(liveEffort) ? liveEffort : (segments[0] ?? 'off');
|
||||
const alias = host.state.appState.model;
|
||||
host.mountEditorReplacement(
|
||||
new EffortSelectorComponent({
|
||||
efforts: segments,
|
||||
currentValue,
|
||||
onSelect: (effort) => {
|
||||
host.restoreEditor();
|
||||
void performModelSwitch(host, alias, effort, true);
|
||||
},
|
||||
onSessionOnlySelect: (effort) => {
|
||||
host.restoreEditor();
|
||||
void performModelSwitch(host, alias, effort, false);
|
||||
},
|
||||
onCancel: () => {
|
||||
host.restoreEditor();
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pickers & config apply
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -273,10 +338,8 @@ async function applyEditorChoice(host: SlashCommandHost, value: string): Promise
|
|||
const editorCommand = value.length > 0 ? value : null;
|
||||
try {
|
||||
await saveTuiConfig({
|
||||
theme: host.state.appState.theme,
|
||||
...currentTuiConfig(host),
|
||||
editorCommand,
|
||||
notifications: host.state.appState.notifications,
|
||||
upgrade: host.state.appState.upgrade,
|
||||
});
|
||||
} catch (error) {
|
||||
host.showStatus(
|
||||
|
|
@ -308,7 +371,7 @@ export function showModelPicker(host: SlashCommandHost, selectedValue: string =
|
|||
models: host.state.appState.availableModels,
|
||||
currentValue: host.state.appState.model,
|
||||
selectedValue,
|
||||
currentThinking: host.state.appState.thinking,
|
||||
currentThinkingEffort: host.state.appState.thinkingEffort,
|
||||
onSelect: ({ alias, thinking }) => {
|
||||
host.restoreEditor();
|
||||
void performModelSwitch(host, alias, thinking, true);
|
||||
|
|
@ -327,7 +390,7 @@ export function showModelPicker(host: SlashCommandHost, selectedValue: string =
|
|||
async function performModelSwitch(
|
||||
host: SlashCommandHost,
|
||||
alias: string,
|
||||
thinking: boolean,
|
||||
effort: ThinkingEffort,
|
||||
persist: boolean,
|
||||
): Promise<void> {
|
||||
if (host.state.appState.streamingPhase !== 'idle') {
|
||||
|
|
@ -335,21 +398,23 @@ async function performModelSwitch(
|
|||
return;
|
||||
}
|
||||
|
||||
const level = thinking ? 'on' : 'off';
|
||||
const prevModel = host.state.appState.model;
|
||||
const prevThinking = host.state.appState.thinking;
|
||||
const runtimeChanged = alias !== prevModel || thinking !== prevThinking;
|
||||
const prevEffort = host.state.appState.thinkingEffort;
|
||||
const modelChanged = alias !== prevModel;
|
||||
const effortChanged = effort !== prevEffort;
|
||||
const runtimeChanged = modelChanged || effortChanged;
|
||||
const displayName = modelDisplayName(alias, host.state.appState.availableModels[alias]);
|
||||
|
||||
const session = host.session;
|
||||
try {
|
||||
if (session === undefined && runtimeChanged) {
|
||||
await host.authFlow.activateModelAfterLogin(alias, thinking);
|
||||
await host.authFlow.activateModelAfterLogin(alias, effort);
|
||||
} else if (session !== undefined) {
|
||||
if (alias !== prevModel) {
|
||||
await session.setModel(alias);
|
||||
}
|
||||
if (thinking !== prevThinking) {
|
||||
await session.setThinking(level);
|
||||
if (effort !== prevEffort) {
|
||||
await session.setThinking(effort);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -358,48 +423,65 @@ async function performModelSwitch(
|
|||
return;
|
||||
}
|
||||
|
||||
host.setAppState({ model: alias, thinking });
|
||||
host.setAppState({ model: alias, thinkingEffort: effort });
|
||||
if (session === undefined && runtimeChanged) {
|
||||
if (alias !== prevModel) {
|
||||
host.track('model_switch', { model: alias });
|
||||
}
|
||||
if (thinking !== prevThinking) {
|
||||
host.track('thinking_toggle', { enabled: thinking });
|
||||
if (effort !== prevEffort) {
|
||||
host.track('thinking_toggle', {
|
||||
enabled: effort !== 'off',
|
||||
effort,
|
||||
from: prevEffort,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let persisted = false;
|
||||
if (persist) {
|
||||
try {
|
||||
persisted = await persistModelSelection(host, alias, thinking);
|
||||
persisted = await persistModelSelection(host, alias, effort);
|
||||
} catch (error) {
|
||||
const msg = formatErrorMessage(error);
|
||||
host.showError(`Switched to ${alias}, but failed to save default: ${msg}`);
|
||||
host.showError(`Switched to ${displayName}, but failed to save default: ${msg}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let status: string;
|
||||
if (runtimeChanged) {
|
||||
if (modelChanged) {
|
||||
status = persist
|
||||
? `Switched to ${alias} with thinking ${level}.`
|
||||
: `Switched to ${alias} with thinking ${level} for this session only.`;
|
||||
? `Switched to ${displayName} with thinking ${effort}.`
|
||||
: `Switched to ${displayName} with thinking ${effort} for this session only.`;
|
||||
} else if (effortChanged) {
|
||||
status = persist
|
||||
? `Thinking set to ${effort}.`
|
||||
: `Thinking set to ${effort} for this session only.`;
|
||||
} else if (persist && persisted) {
|
||||
status = `Saved ${alias} with thinking ${level} as default.`;
|
||||
status = `Saved ${displayName} with thinking ${effort} as default.`;
|
||||
} else {
|
||||
status = `Already using ${alias} with thinking ${level}.`;
|
||||
status = `Already using ${displayName} with thinking ${effort}.`;
|
||||
}
|
||||
host.showStatus(status, 'success');
|
||||
}
|
||||
|
||||
async function persistModelSelection(host: SlashCommandHost, alias: string, thinking: boolean): Promise<boolean> {
|
||||
async function persistModelSelection(
|
||||
host: SlashCommandHost,
|
||||
alias: string,
|
||||
effort: ThinkingEffort,
|
||||
): Promise<boolean> {
|
||||
const config = await host.harness.getConfig({ reload: true });
|
||||
if (config.defaultModel === alias && config.defaultThinking === thinking) {
|
||||
const patch = thinkingEffortToConfig(effort);
|
||||
if (
|
||||
config.defaultModel === alias &&
|
||||
config.thinking?.enabled === patch.enabled &&
|
||||
config.thinking?.effort === patch.effort
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
await host.harness.setConfig({
|
||||
defaultModel: alias,
|
||||
defaultThinking: thinking,
|
||||
thinking: patch,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
|
@ -439,10 +521,8 @@ async function applyThemeChoice(host: SlashCommandHost, theme: ThemeName): Promi
|
|||
|
||||
try {
|
||||
await saveTuiConfig({
|
||||
...currentTuiConfig(host),
|
||||
theme,
|
||||
editorCommand: host.state.appState.editorCommand,
|
||||
notifications: host.state.appState.notifications,
|
||||
upgrade: host.state.appState.upgrade,
|
||||
});
|
||||
} catch (error) {
|
||||
host.showStatus(
|
||||
|
|
@ -515,7 +595,7 @@ export async function applyExperimentalFeatureChanges(
|
|||
return;
|
||||
}
|
||||
|
||||
const experimental: Partial<Record<FlagId, boolean>> = {};
|
||||
const experimental: Record<string, boolean> = {};
|
||||
for (const change of changes) {
|
||||
experimental[change.id] = change.enabled;
|
||||
}
|
||||
|
|
@ -582,9 +662,7 @@ export async function applyUpdatePreferenceChoice(
|
|||
const upgrade = { autoInstall };
|
||||
try {
|
||||
await saveTuiConfig({
|
||||
theme: host.state.appState.theme,
|
||||
editorCommand: host.state.appState.editorCommand,
|
||||
notifications: host.state.appState.notifications,
|
||||
...currentTuiConfig(host as unknown as SlashCommandHost),
|
||||
upgrade,
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Component, Focusable } from '@earendil-works/pi-tui';
|
||||
import type { Component, Focusable } from '@moonshot-ai/pi-tui';
|
||||
import type { DeviceAuthorization } from '@moonshot-ai/kimi-code-oauth';
|
||||
import type { KimiHarness, Session } from '@moonshot-ai/kimi-code-sdk';
|
||||
|
||||
|
|
@ -25,6 +25,7 @@ import {
|
|||
handleAutoCommand,
|
||||
handleCompactCommand,
|
||||
handleEditorCommand,
|
||||
handleEffortCommand,
|
||||
handleModelCommand,
|
||||
handlePlanCommand,
|
||||
handleThemeCommand,
|
||||
|
|
@ -65,6 +66,7 @@ export {
|
|||
handleAutoCommand,
|
||||
handleCompactCommand,
|
||||
handleEditorCommand,
|
||||
handleEffortCommand,
|
||||
handleModelCommand,
|
||||
handlePlanCommand,
|
||||
handleThemeCommand,
|
||||
|
|
@ -292,6 +294,9 @@ async function handleBuiltInSlashCommand(
|
|||
case 'model':
|
||||
await handleModelCommand(host, args);
|
||||
return;
|
||||
case 'effort':
|
||||
await handleEffortCommand(host, args);
|
||||
return;
|
||||
case 'provider':
|
||||
await handleProviderCommand(host);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
FEEDBACK_ISSUE_URL,
|
||||
FEEDBACK_STATUS_CANCELLED,
|
||||
FEEDBACK_STATUS_FALLBACK,
|
||||
FEEDBACK_STATUS_NETWORK_ERROR,
|
||||
FEEDBACK_STATUS_NOT_SIGNED_IN,
|
||||
FEEDBACK_STATUS_SUBMITTING,
|
||||
FEEDBACK_STATUS_SUCCESS,
|
||||
|
|
@ -58,29 +59,43 @@ export async function handleFeedbackCommand(host: SlashCommandHost): Promise<voi
|
|||
|
||||
const version = withFeedbackVersionPrefix(host.state.appState.version);
|
||||
const spinner = host.showLoginProgressSpinner(FEEDBACK_STATUS_SUBMITTING);
|
||||
const res = await host.harness.auth.submitFeedback({
|
||||
content: input.value,
|
||||
sessionId: host.state.appState.sessionId,
|
||||
version,
|
||||
os: `${osType()} ${osRelease()}`,
|
||||
model: host.state.appState.model.length > 0 ? host.state.appState.model : null,
|
||||
});
|
||||
// Guarantee the spinner's underlying setInterval is always cleared, even when
|
||||
// submitFeedback or submitFeedbackWithAttachments throws — otherwise the
|
||||
// interval (and its per-frame requestRender) leaks for the rest of the session.
|
||||
let stopped = false;
|
||||
const stopSpinner = (opts: { ok: boolean; label: string }): void => {
|
||||
if (stopped) return;
|
||||
stopped = true;
|
||||
spinner.stop(opts);
|
||||
};
|
||||
try {
|
||||
const res = await host.harness.auth.submitFeedback({
|
||||
content: input.value,
|
||||
sessionId: host.state.appState.sessionId,
|
||||
version,
|
||||
os: `${osType()} ${osRelease()}`,
|
||||
model: host.state.appState.model.length > 0 ? host.state.appState.model : null,
|
||||
});
|
||||
|
||||
if (res.kind !== 'ok') {
|
||||
spinner.stop({ ok: false, label: res.message });
|
||||
fallback(FEEDBACK_STATUS_FALLBACK);
|
||||
return;
|
||||
}
|
||||
if (res.kind !== 'ok') {
|
||||
stopSpinner({ ok: false, label: res.message });
|
||||
fallback(FEEDBACK_STATUS_FALLBACK);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stage 3: prepare and upload each requested attachment independently.
|
||||
const attachmentFailed = await submitFeedbackWithAttachments(host, res.feedbackId, level);
|
||||
// Stage 3: prepare and upload each requested attachment independently.
|
||||
const attachmentFailed = await submitFeedbackWithAttachments(host, res.feedbackId, level);
|
||||
|
||||
spinner.stop({ ok: true, label: FEEDBACK_STATUS_SUCCESS });
|
||||
host.showStatus(feedbackSessionLine(host.state.appState.sessionId));
|
||||
host.showStatus(feedbackIdLine(res.feedbackId));
|
||||
host.track(FEEDBACK_TELEMETRY_EVENT);
|
||||
if (attachmentFailed) {
|
||||
host.showStatus(FEEDBACK_STATUS_UPLOAD_FAILED);
|
||||
stopSpinner({ ok: true, label: FEEDBACK_STATUS_SUCCESS });
|
||||
host.showStatus(feedbackSessionLine(host.state.appState.sessionId));
|
||||
host.showStatus(feedbackIdLine(res.feedbackId));
|
||||
host.track(FEEDBACK_TELEMETRY_EVENT);
|
||||
if (attachmentFailed) {
|
||||
host.showStatus(FEEDBACK_STATUS_UPLOAD_FAILED);
|
||||
}
|
||||
} catch (error) {
|
||||
stopSpinner({ ok: false, label: FEEDBACK_STATUS_NETWORK_ERROR });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +147,7 @@ export async function showStatusReport(host: SlashCommandHost): Promise<void> {
|
|||
workDir: appState.workDir,
|
||||
sessionId: appState.sessionId,
|
||||
sessionTitle: appState.sessionTitle,
|
||||
thinking: appState.thinking,
|
||||
thinkingEffort: appState.thinkingEffort,
|
||||
permissionMode: appState.permissionMode,
|
||||
planMode: appState.planMode,
|
||||
contextUsage: appState.contextUsage,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
type Catalog,
|
||||
type CatalogModel,
|
||||
type ModelAlias,
|
||||
type ThinkingEffort,
|
||||
} from '@moonshot-ai/kimi-code-sdk';
|
||||
import { capabilitiesForModel } from '@moonshot-ai/kimi-code-oauth';
|
||||
import type {
|
||||
|
|
@ -166,7 +167,7 @@ export async function promptModelSelectionForOpenPlatform(
|
|||
host: SlashCommandHost,
|
||||
models: ManagedKimiCodeModelInfo[],
|
||||
platform: OpenPlatformDefinition,
|
||||
): Promise<{ model: ManagedKimiCodeModelInfo; thinking: boolean } | undefined> {
|
||||
): Promise<{ model: ManagedKimiCodeModelInfo; thinking: ThinkingEffort } | undefined> {
|
||||
const modelDict: Record<string, ModelAlias> = {};
|
||||
for (const m of models) {
|
||||
modelDict[`${platform.id}/${m.id}`] = {
|
||||
|
|
@ -187,7 +188,7 @@ export async function promptModelSelectionForCatalog(
|
|||
host: SlashCommandHost,
|
||||
providerId: string,
|
||||
models: CatalogModel[],
|
||||
): Promise<{ model: CatalogModel; thinking: boolean } | undefined> {
|
||||
): Promise<{ model: CatalogModel; thinking: ThinkingEffort } | undefined> {
|
||||
const modelDict: Record<string, ModelAlias> = {};
|
||||
for (const m of models) {
|
||||
modelDict[`${providerId}/${m.id}`] = catalogModelToAlias(providerId, m);
|
||||
|
|
@ -201,7 +202,7 @@ export async function promptModelSelectionForCatalog(
|
|||
export function runModelSelector(
|
||||
host: SlashCommandHost,
|
||||
modelDict: Record<string, ModelAlias>,
|
||||
): Promise<{ alias: string; thinking: boolean } | undefined> {
|
||||
): Promise<{ alias: string; thinking: ThinkingEffort } | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
const firstAlias = Object.keys(modelDict)[0] ?? '';
|
||||
const caps = modelDict[firstAlias]?.capabilities ?? [];
|
||||
|
|
@ -209,7 +210,7 @@ export function runModelSelector(
|
|||
const selector = new ModelSelectorComponent({
|
||||
models: modelDict,
|
||||
currentValue: firstAlias,
|
||||
currentThinking: initialThinking,
|
||||
currentThinkingEffort: initialThinking ? 'on' : 'off',
|
||||
searchable: true,
|
||||
onSelect: ({ alias, thinking }) => {
|
||||
host.restoreEditor();
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
fetchCatalog,
|
||||
inferWireType,
|
||||
type Catalog,
|
||||
type ThinkingEffort,
|
||||
} from '@moonshot-ai/kimi-code-sdk';
|
||||
|
||||
import { ChoicePickerComponent } from '../components/dialogs/choice-picker';
|
||||
|
|
@ -27,6 +28,7 @@ import {
|
|||
import { TabbedModelSelectorComponent } from '../components/dialogs/tabbed-model-selector';
|
||||
import { DEFAULT_OAUTH_PROVIDER_NAME } from '../constant/kimi-tui';
|
||||
import { formatErrorMessage } from '../utils/event-payload';
|
||||
import { thinkingEffortToConfig } from '../utils/thinking-config';
|
||||
import {
|
||||
promptApiKey,
|
||||
promptCatalogProviderSelection,
|
||||
|
|
@ -233,7 +235,7 @@ async function handleCatalogProviderAdd(host: SlashCommandHost): Promise<void> {
|
|||
models: mergedModels,
|
||||
currentValue: host.state.appState.model,
|
||||
selectedValue: Object.keys(mergedModels).find((a) => a.startsWith(`${providerId}/`)),
|
||||
currentThinking: host.state.appState.thinking,
|
||||
currentThinkingEffort: host.state.appState.thinkingEffort,
|
||||
initialTabId: providerId,
|
||||
onSelect: ({ alias, thinking }) => {
|
||||
host.restoreEditor();
|
||||
|
|
@ -251,15 +253,15 @@ async function handleCatalogProviderAdd(host: SlashCommandHost): Promise<void> {
|
|||
async function setDefaultModel(
|
||||
host: SlashCommandHost,
|
||||
alias: string,
|
||||
thinking: boolean,
|
||||
effort: ThinkingEffort,
|
||||
): Promise<void> {
|
||||
await host.harness.setConfig({
|
||||
defaultModel: alias,
|
||||
defaultThinking: thinking,
|
||||
thinking: thinkingEffortToConfig(effort),
|
||||
});
|
||||
await host.authFlow.refreshConfigAfterLogin();
|
||||
host.track('model_switch', { model: alias });
|
||||
host.showStatus(`Default model set to ${alias} with thinking ${thinking ? 'on' : 'off'}.`);
|
||||
host.showStatus(`Default model set to ${alias} with thinking ${effort}.`);
|
||||
}
|
||||
|
||||
async function handleCustomRegistryAddViaDialog(host: SlashCommandHost): Promise<boolean> {
|
||||
|
|
@ -323,7 +325,7 @@ async function handleCustomRegistryAddViaDialog(host: SlashCommandHost): Promise
|
|||
models: stateModels,
|
||||
currentValue: host.state.appState.model,
|
||||
selectedValue: firstNewAlias,
|
||||
currentThinking: host.state.appState.thinking,
|
||||
currentThinkingEffort: host.state.appState.thinkingEffort,
|
||||
initialTabId: firstNewProvider,
|
||||
onSelect: ({ alias, thinking }) => {
|
||||
host.restoreEditor();
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { readdirSync, statSync } from 'node:fs';
|
|||
import { homedir } from 'node:os';
|
||||
import { basename, dirname, join, relative, resolve } from 'pathe';
|
||||
|
||||
import type { AutocompleteItem } from '@earendil-works/pi-tui';
|
||||
import type { AutocompleteItem } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { completeLeadingArg, type ArgCompletionSpec } from './complete-args';
|
||||
import type { KimiSlashCommand, SlashCommandAvailability } from './types';
|
||||
|
|
@ -184,6 +184,13 @@ export const BUILTIN_SLASH_COMMANDS = [
|
|||
priority: 100,
|
||||
availability: 'always',
|
||||
},
|
||||
{
|
||||
name: 'effort',
|
||||
aliases: ['thinking'],
|
||||
description: 'Switch thinking effort',
|
||||
priority: 95,
|
||||
availability: 'always',
|
||||
},
|
||||
{
|
||||
name: 'provider',
|
||||
aliases: ['providers'],
|
||||
|
|
|
|||
|
|
@ -45,9 +45,11 @@ export async function applyReloadedTuiConfig(
|
|||
host.refreshTerminalThemeTracking();
|
||||
host.setAppState({
|
||||
editorCommand: config.editorCommand,
|
||||
disablePasteBurst: config.disablePasteBurst,
|
||||
notifications: config.notifications,
|
||||
upgrade: config.upgrade,
|
||||
});
|
||||
host.state.editor.setDisablePasteBurst(config.disablePasteBurst);
|
||||
}
|
||||
|
||||
function applyRuntimeConfig(host: SlashCommandHost, config: KimiConfig): void {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { AutocompleteItem, SlashCommand } from '@earendil-works/pi-tui';
|
||||
import type { AutocompleteItem, SlashCommand } from '@moonshot-ai/pi-tui';
|
||||
import type { FlagId } from '@moonshot-ai/kimi-code-sdk';
|
||||
|
||||
export type SlashCommandAvailability = 'always' | 'idle-only';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Component } from '@earendil-works/pi-tui';
|
||||
import type { Component } from '@moonshot-ai/pi-tui';
|
||||
import type { ContextMessage } from '@moonshot-ai/kimi-code-sdk';
|
||||
import { isKimiError } from '@moonshot-ai/kimi-code-sdk';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Component } from '@earendil-works/pi-tui';
|
||||
import { visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';
|
||||
import type { Component } from '@moonshot-ai/pi-tui';
|
||||
import { visibleWidth, wrapTextWithAnsi } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
import type { BannerState } from '#/tui/types';
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
* active palette so theme switches take effect on the next render.
|
||||
*/
|
||||
|
||||
import type { Component } from '@earendil-works/pi-tui';
|
||||
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
||||
import type { Component } from '@moonshot-ai/pi-tui';
|
||||
import { truncateToWidth, visibleWidth } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@
|
|||
* Line 2: context: XX.X% (tokens/max)
|
||||
*/
|
||||
|
||||
import type { Component } from '@earendil-works/pi-tui';
|
||||
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
||||
import type { Component } from '@moonshot-ai/pi-tui';
|
||||
import { truncateToWidth, visibleWidth } from '@moonshot-ai/pi-tui';
|
||||
import chalk from 'chalk';
|
||||
import { effectiveModelAlias } from '@moonshot-ai/kimi-code-sdk';
|
||||
|
||||
import { ALL_TIPS, type ToolbarTip } from '#/tui/constant/tips';
|
||||
import { isRainbowDancing, renderDanceFooterModel } from '#/tui/easter-eggs/dance';
|
||||
|
|
@ -132,7 +133,8 @@ function formatBadgeElapsed(ms: number): string {
|
|||
|
||||
function modelDisplayName(state: AppState): string {
|
||||
const model = state.availableModels[state.model];
|
||||
return model?.displayName ?? model?.model ?? state.model;
|
||||
const effective = model === undefined ? undefined : effectiveModelAlias(model);
|
||||
return effective?.displayName ?? effective?.model ?? state.model;
|
||||
}
|
||||
|
||||
function shortenCwd(path: string): string {
|
||||
|
|
@ -262,7 +264,18 @@ export class FooterComponent implements Component {
|
|||
|
||||
const model = modelDisplayName(state);
|
||||
if (model) {
|
||||
const thinkingLabel = state.thinking ? ' thinking' : '';
|
||||
const effort = state.thinkingEffort;
|
||||
const rawCurrentModel = state.availableModels[state.model];
|
||||
const currentModel = rawCurrentModel === undefined ? undefined : effectiveModelAlias(rawCurrentModel);
|
||||
// Only effort-capable models (those declaring support_efforts) show the
|
||||
// concrete effort; legacy boolean models keep the plain "thinking" suffix.
|
||||
const hasEfforts = (currentModel?.supportEfforts?.length ?? 0) > 0;
|
||||
const thinkingLabel =
|
||||
effort !== 'off'
|
||||
? hasEfforts && effort !== 'on'
|
||||
? ` thinking: ${effort}`
|
||||
: ' thinking'
|
||||
: '';
|
||||
const modelLabel = `${model}${thinkingLabel}`;
|
||||
let renderedModelLabel = chalk.hex(colors.text)(modelLabel);
|
||||
if (isRainbowDancing()) {
|
||||
|
|
@ -370,6 +383,13 @@ export class FooterComponent implements Component {
|
|||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.goalTimer !== null) {
|
||||
clearInterval(this.goalTimer);
|
||||
this.goalTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private goalWallClockMs(goal: AppState['goal']): number | undefined {
|
||||
if (goal === null || goal === undefined) return undefined;
|
||||
if (goal.status !== 'active') return goal.wallClockMs;
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
* the edge and adding them would just churn the diff renderer.
|
||||
*/
|
||||
|
||||
import { Container } from '@earendil-works/pi-tui';
|
||||
import type { Component } from '@earendil-works/pi-tui';
|
||||
import { Container } from '@moonshot-ai/pi-tui';
|
||||
import type { Component } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { isRenderCacheEnabled } from '#/tui/utils/render-cache';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Text, visibleWidth } from '@earendil-works/pi-tui';
|
||||
import type { TUI } from '@earendil-works/pi-tui';
|
||||
import { Text, visibleWidth } from '@moonshot-ai/pi-tui';
|
||||
import type { TUI } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import {
|
||||
BRAILLE_SPINNER_FRAMES,
|
||||
|
|
@ -59,6 +59,10 @@ export class MoonLoader extends Text {
|
|||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
setLabel(label: string): void {
|
||||
this.label = label;
|
||||
this.updateDisplay();
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@
|
|||
* is issued.
|
||||
*/
|
||||
|
||||
import type { Component } from '@earendil-works/pi-tui';
|
||||
import { truncateToWidth } from '@earendil-works/pi-tui';
|
||||
import type { Component } from '@moonshot-ai/pi-tui';
|
||||
import { truncateToWidth } from '@moonshot-ai/pi-tui';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
* Renders a round-bordered box with the logo, session, model, and version.
|
||||
*/
|
||||
|
||||
import type { Component } from '@earendil-works/pi-tui';
|
||||
import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
||||
import type { Component } from '@moonshot-ai/pi-tui';
|
||||
import { truncateToWidth, visibleWidth } from '@moonshot-ai/pi-tui';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { effectiveModelAlias } from '@moonshot-ai/kimi-code-sdk';
|
||||
|
||||
import { isRainbowDancing, renderDanceWelcomeHeader } from '#/tui/easter-eggs/dance';
|
||||
import type { AppState } from '#/tui/types';
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
|
@ -25,6 +27,7 @@ export class WelcomeComponent implements Component {
|
|||
const primary = (s: string): string => chalk.hex(currentTheme.palette.primary)(s);
|
||||
const isLoggedOut = !this.state.model;
|
||||
const activeModel = this.state.availableModels[this.state.model];
|
||||
const effectiveActiveModel = activeModel === undefined ? undefined : effectiveModelAlias(activeModel);
|
||||
|
||||
if (safeWidth < 24) {
|
||||
const title = chalk.bold.hex(currentTheme.palette.primary)('Welcome to Kimi Code!');
|
||||
|
|
@ -33,7 +36,7 @@ export class WelcomeComponent implements Component {
|
|||
: chalk.hex(currentTheme.palette.textDim)('Send /help for help information.');
|
||||
const model = isLoggedOut
|
||||
? chalk.hex(currentTheme.palette.warning)('not set, run /login or /provider')
|
||||
: (activeModel?.displayName ?? activeModel?.model ?? this.state.model);
|
||||
: (effectiveActiveModel?.displayName ?? effectiveActiveModel?.model ?? this.state.model);
|
||||
return ['', title, prompt, `Model: ${model}`].map((line) =>
|
||||
truncateToWidth(line, safeWidth, '…'),
|
||||
);
|
||||
|
|
@ -71,7 +74,7 @@ export class WelcomeComponent implements Component {
|
|||
|
||||
const modelValue = isLoggedOut
|
||||
? chalk.hex(currentTheme.palette.warning)('not set, run /login or /provider')
|
||||
: (activeModel?.displayName ?? activeModel?.model ?? this.state.model);
|
||||
: (effectiveActiveModel?.displayName ?? effectiveActiveModel?.model ?? this.state.model);
|
||||
|
||||
const infoLines = [
|
||||
labelStyle('Directory: ') + this.state.workDir,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
truncateToWidth,
|
||||
visibleWidth,
|
||||
type Focusable,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
truncateToWidth,
|
||||
visibleWidth,
|
||||
wrapTextWithAnsi,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
import { highlightLines, langFromPath } from '#/tui/components/media/code-highlight';
|
||||
import { renderDiffLinesClustered } from '#/tui/components/media/diff-preview';
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import {
|
|||
truncateToWidth,
|
||||
visibleWidth,
|
||||
type Focusable,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { highlightLines, langFromPath } from '#/tui/components/media/code-highlight';
|
||||
import { renderDiffLines } from '#/tui/components/media/diff-preview';
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
truncateToWidth,
|
||||
visibleWidth,
|
||||
type Focusable,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
import { CURRENT_MARK, SELECT_POINTER } from '#/tui/constant/symbols';
|
||||
import { currentTheme, type ColorToken } from '#/tui/theme';
|
||||
import { printableChar } from '#/tui/utils/printable-key';
|
||||
|
|
@ -49,6 +49,9 @@ export interface ChoicePickerOptions {
|
|||
/** Items per page. Lists longer than this paginate. */
|
||||
readonly pageSize?: number;
|
||||
readonly onSelect: (value: string) => void;
|
||||
/** When provided, Alt+S invokes this with the selected value instead of
|
||||
* onSelect — used to apply the choice to the current session only. */
|
||||
readonly onSessionOnlySelect?: (value: string) => void;
|
||||
readonly onCancel: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -99,6 +102,11 @@ export class ChoicePickerComponent extends Container implements Focusable {
|
|||
this.opts.onCancel();
|
||||
return;
|
||||
}
|
||||
if (matchesKey(data, Key.alt('s')) && this.opts.onSessionOnlySelect !== undefined) {
|
||||
const chosen = this.list.selected();
|
||||
if (chosen !== undefined) this.opts.onSessionOnlySelect(chosen.value);
|
||||
return;
|
||||
}
|
||||
// Left/Right page through the list (this picker has no horizontal control).
|
||||
if (matchesKey(data, Key.left)) {
|
||||
this.list.pageUp();
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@
|
|||
* reads the same "work in progress" signal across the UI.
|
||||
*/
|
||||
|
||||
import { Container, Text, Spacer } from '@earendil-works/pi-tui';
|
||||
import type { TUI } from '@earendil-works/pi-tui';
|
||||
import { Container, Text, Spacer } from '@moonshot-ai/pi-tui';
|
||||
import type { TUI } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { STATUS_BULLET } from '#/tui/constant/symbols';
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
truncateToWidth,
|
||||
visibleWidth,
|
||||
type Focusable,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
||||
|
|
|
|||
94
apps/kimi-code/src/tui/components/dialogs/effort-selector.ts
Normal file
94
apps/kimi-code/src/tui/components/dialogs/effort-selector.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import {
|
||||
Container,
|
||||
Key,
|
||||
matchesKey,
|
||||
truncateToWidth,
|
||||
type Focusable,
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
|
||||
import type { ThinkingEffort } from '@moonshot-ai/kimi-code-sdk';
|
||||
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
||||
import { effortLabel } from './model-selector';
|
||||
|
||||
export interface EffortSelectorOptions {
|
||||
readonly title?: string;
|
||||
/** Selectable thinking efforts for the current model (e.g. ["off","low","high","max"]). */
|
||||
readonly efforts: readonly ThinkingEffort[];
|
||||
/** Currently active effort (highlighted). */
|
||||
readonly currentValue: ThinkingEffort;
|
||||
readonly onSelect: (effort: ThinkingEffort) => void;
|
||||
/** When provided, Alt+S applies the choice to the current session only. */
|
||||
readonly onSessionOnlySelect?: (effort: ThinkingEffort) => void;
|
||||
readonly onCancel: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Horizontal segmented picker for the `/effort` command.
|
||||
*
|
||||
* Mirrors the thinking control rendered under `/model` (see
|
||||
* `renderThinkingControl` in model-selector.ts): a single row of segments,
|
||||
* the active one wrapped in `[ ]`. ←/→ step the active segment, Enter
|
||||
* commits, and Alt+S (when provided) applies session-only.
|
||||
*/
|
||||
export class EffortSelectorComponent extends Container implements Focusable {
|
||||
focused = false;
|
||||
private readonly opts: EffortSelectorOptions;
|
||||
private activeIndex: number;
|
||||
|
||||
constructor(opts: EffortSelectorOptions) {
|
||||
super();
|
||||
this.opts = opts;
|
||||
const idx = opts.efforts.indexOf(opts.currentValue);
|
||||
this.activeIndex = Math.max(idx, 0);
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
if (matchesKey(data, Key.escape)) {
|
||||
this.opts.onCancel();
|
||||
return;
|
||||
}
|
||||
if (matchesKey(data, Key.left)) {
|
||||
this.activeIndex = Math.max(0, this.activeIndex - 1);
|
||||
return;
|
||||
}
|
||||
if (matchesKey(data, Key.right)) {
|
||||
this.activeIndex = Math.min(this.opts.efforts.length - 1, this.activeIndex + 1);
|
||||
return;
|
||||
}
|
||||
if (matchesKey(data, Key.alt('s')) && this.opts.onSessionOnlySelect !== undefined) {
|
||||
this.opts.onSessionOnlySelect(this.opts.efforts[this.activeIndex]!);
|
||||
return;
|
||||
}
|
||||
if (matchesKey(data, Key.enter)) {
|
||||
this.opts.onSelect(this.opts.efforts[this.activeIndex]!);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
override render(width: number): string[] {
|
||||
const hintParts = ['←→ switch', 'Enter select'];
|
||||
if (this.opts.onSessionOnlySelect !== undefined) hintParts.push('Alt+S session-only');
|
||||
hintParts.push('Esc cancel');
|
||||
|
||||
const lines: string[] = [
|
||||
currentTheme.fg('primary', '─'.repeat(width)),
|
||||
currentTheme.boldFg('primary', ` ${this.opts.title ?? 'Select thinking effort'}`),
|
||||
currentTheme.fg('textMuted', ` ${hintParts.join(' · ')}`),
|
||||
'',
|
||||
];
|
||||
|
||||
const segments = this.opts.efforts.map((effort, index) => {
|
||||
const label = effortLabel(effort);
|
||||
return index === this.activeIndex
|
||||
? currentTheme.boldFg('primary', `[ ${label} ]`)
|
||||
: currentTheme.fg('text', ` ${label} `);
|
||||
});
|
||||
lines.push(` ${segments.join(' ')}`);
|
||||
|
||||
lines.push('');
|
||||
lines.push(currentTheme.fg('primary', '─'.repeat(width)));
|
||||
return lines.map((line) => truncateToWidth(line, width));
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import {
|
|||
truncateToWidth,
|
||||
visibleWidth,
|
||||
type Focusable,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
import type { ExperimentalFeatureState } from '@moonshot-ai/kimi-code-sdk';
|
||||
|
||||
import { SELECT_POINTER } from '#/tui/constant/symbols';
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
truncateToWidth,
|
||||
visibleWidth,
|
||||
type Focusable,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
||||
export type FeedbackInputDialogResult =
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
truncateToWidth,
|
||||
visibleWidth,
|
||||
type Focusable,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { SELECT_POINTER } from '#/tui/constant/symbols';
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
decodeKittyPrintable,
|
||||
type Focusable,
|
||||
truncateToWidth,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
||||
export interface KeyboardShortcut {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ModelAlias } from '@moonshot-ai/kimi-code-sdk';
|
||||
import { effectiveModelAlias, type ModelAlias, type ThinkingEffort } from '@moonshot-ai/kimi-code-sdk';
|
||||
import {
|
||||
Container,
|
||||
Key,
|
||||
|
|
@ -6,7 +6,7 @@ import {
|
|||
truncateToWidth,
|
||||
visibleWidth,
|
||||
type Focusable,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { DEFAULT_OAUTH_PROVIDER_NAME, PRODUCT_NAME } from '#/constant/app';
|
||||
import { CURRENT_MARK, SELECT_POINTER } from '#/tui/constant/symbols';
|
||||
|
|
@ -30,11 +30,15 @@ interface ModelChoice {
|
|||
|
||||
export interface ModelSelection {
|
||||
readonly alias: string;
|
||||
readonly thinking: boolean;
|
||||
/** Chosen thinking effort: 'off', or a concrete effort such as 'low' /
|
||||
* 'high' / 'max'. Boolean 'on' is normalized to the model's default effort
|
||||
* before the selection is committed (see commitEffort). */
|
||||
readonly thinking: ThinkingEffort;
|
||||
}
|
||||
|
||||
export function modelDisplayName(alias: string, model: ModelAlias | undefined): string {
|
||||
return model?.displayName ?? model?.model ?? alias;
|
||||
const effective = model === undefined ? undefined : effectiveModelAlias(model);
|
||||
return effective?.displayName ?? effective?.model ?? alias;
|
||||
}
|
||||
|
||||
export function providerDisplayName(provider: string): string {
|
||||
|
|
@ -46,17 +50,22 @@ export function providerDisplayName(provider: string): string {
|
|||
export function createModelChoiceOptions(
|
||||
models: Record<string, ModelAlias>,
|
||||
): readonly ChoiceOption[] {
|
||||
return Object.entries(models).map(([alias, cfg]) => ({
|
||||
value: alias,
|
||||
label: `${modelDisplayName(alias, cfg)} (${providerDisplayName(cfg.provider)})`,
|
||||
}));
|
||||
return Object.entries(models).map(([alias, cfg]) => {
|
||||
const effective = effectiveModelAlias(cfg);
|
||||
return {
|
||||
value: alias,
|
||||
label: `${modelDisplayName(alias, effective)} (${providerDisplayName(effective.provider)})`,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export interface ModelSelectorOptions {
|
||||
readonly models: Record<string, ModelAlias>;
|
||||
readonly currentValue: string;
|
||||
readonly selectedValue?: string;
|
||||
readonly currentThinking: boolean;
|
||||
/** Live thinking effort of the currently active model (e.g. 'off', 'on',
|
||||
* 'high'). Used to highlight the active segment for the current model. */
|
||||
readonly currentThinkingEffort: ThinkingEffort;
|
||||
/** When true, typed characters filter the list (fuzzy) and a search line is shown. */
|
||||
readonly searchable?: boolean;
|
||||
/** Items per page. Lists longer than this paginate (PgUp/PgDn). */
|
||||
|
|
@ -73,24 +82,69 @@ export interface ModelSelectorOptions {
|
|||
|
||||
function createModelChoices(models: Record<string, ModelAlias>): readonly ModelChoice[] {
|
||||
return Object.entries(models).map(([alias, cfg]) => {
|
||||
const name = modelDisplayName(alias, cfg);
|
||||
const provider = providerDisplayName(cfg.provider);
|
||||
return { alias, model: cfg, name, provider, label: `${name} (${provider})` };
|
||||
const effective = effectiveModelAlias(cfg);
|
||||
const name = modelDisplayName(alias, effective);
|
||||
const provider = providerDisplayName(effective.provider);
|
||||
return { alias, model: effective, name, provider, label: `${name} (${provider})` };
|
||||
});
|
||||
}
|
||||
|
||||
function thinkingAvailability(model: ModelAlias): ThinkingAvailability {
|
||||
export function thinkingAvailability(model: ModelAlias): ThinkingAvailability {
|
||||
const caps = model.capabilities ?? [];
|
||||
if (caps.includes('always_thinking')) return 'always-on';
|
||||
if (caps.includes('thinking') || model.adaptiveThinking === true) return 'toggle';
|
||||
return 'unsupported';
|
||||
}
|
||||
|
||||
function effectiveThinking(model: ModelAlias, thinkingDraft: boolean): boolean {
|
||||
export function effortsOf(model: ModelAlias): readonly string[] {
|
||||
return model.supportEfforts ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordered list of selectable thinking efforts for a model. Effort-capable models
|
||||
* expose their declared efforts (with an 'off' entry when the model is not
|
||||
* always-on); legacy boolean models expose 'on'/'off'; single-segment lists
|
||||
* mean the control is effectively locked.
|
||||
*/
|
||||
export function segmentsFor(model: ModelAlias): readonly string[] {
|
||||
const efforts = effortsOf(model);
|
||||
const availability = thinkingAvailability(model);
|
||||
if (availability === 'always-on') return true;
|
||||
if (availability === 'unsupported') return false;
|
||||
return thinkingDraft;
|
||||
if (efforts.length > 0) {
|
||||
return availability === 'always-on' ? efforts : ['off', ...efforts];
|
||||
}
|
||||
if (availability === 'always-on') return ['on'];
|
||||
if (availability === 'unsupported') return ['off'];
|
||||
return ['on', 'off'];
|
||||
}
|
||||
|
||||
export function effortLabel(effort: string): string {
|
||||
if (effort.length === 0) return effort;
|
||||
return effort.charAt(0).toUpperCase() + effort.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default thinking effort for a model: declared `default_effort`, else the
|
||||
* middle `support_efforts` entry, else `'on'` for boolean models, `'off'` when
|
||||
* thinking is unsupported.
|
||||
*/
|
||||
function defaultThinkingEffortFor(model: ModelAlias): ThinkingEffort {
|
||||
if (thinkingAvailability(model) === 'unsupported') return 'off';
|
||||
const efforts = effortsOf(model);
|
||||
if (efforts.length > 0) {
|
||||
return model.defaultEffort ?? efforts[Math.floor(efforts.length / 2)]!;
|
||||
}
|
||||
return 'on';
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a draft effort before committing a selection. A boolean `'on'`
|
||||
* never leaks past the UI boundary — it becomes the model's default effort
|
||||
* (a concrete effort for effort-capable models, `'on'` only for genuine
|
||||
* boolean models).
|
||||
*/
|
||||
function commitEffort(choice: ModelChoice, draft: ThinkingEffort): ThinkingEffort {
|
||||
if (draft === 'on') return defaultThinkingEffortFor(choice.model);
|
||||
return draft;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -105,8 +159,8 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|||
focused = false;
|
||||
private readonly opts: ModelSelectorOptions;
|
||||
private readonly list: SearchableList<ModelChoice>;
|
||||
/** Per-model thinking override set by ←/→; absent → the capability default. */
|
||||
private readonly thinkingOverrides = new Map<string, boolean>();
|
||||
/** Per-model thinking-effort override set by ←/→; absent → the default. */
|
||||
private readonly thinkingOverrides = new Map<string, string>();
|
||||
|
||||
constructor(opts: ModelSelectorOptions) {
|
||||
super();
|
||||
|
|
@ -124,15 +178,31 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Thinking draft for a model: an explicit ←/→ override when set, otherwise
|
||||
* the live thinking state for the active model, otherwise On for any other
|
||||
* thinking-capable model (a capable model should default to thinking on).
|
||||
* Thinking effort for a model: an explicit ←/→ override when set, otherwise
|
||||
* the live effort for the active model, otherwise the model's default effort
|
||||
* (effort-capable) or 'on' (other thinking-capable models).
|
||||
*/
|
||||
private draftFor(choice: ModelChoice): boolean {
|
||||
private draftFor(choice: ModelChoice): string {
|
||||
const override = this.thinkingOverrides.get(choice.alias);
|
||||
if (override !== undefined) return override;
|
||||
if (choice.alias === this.opts.currentValue) return this.opts.currentThinking;
|
||||
return thinkingAvailability(choice.model) !== 'unsupported';
|
||||
if (choice.alias === this.opts.currentValue) return this.opts.currentThinkingEffort;
|
||||
const efforts = effortsOf(choice.model);
|
||||
if (efforts.length > 0) {
|
||||
// A model with support_efforts but no default_effort defaults to the
|
||||
// middle entry of its supported efforts.
|
||||
const def = choice.model.defaultEffort ?? efforts[Math.floor(efforts.length / 2)];
|
||||
if (def !== undefined && efforts.includes(def)) return def;
|
||||
return efforts[0]!;
|
||||
}
|
||||
return thinkingAvailability(choice.model) !== 'unsupported' ? 'on' : 'off';
|
||||
}
|
||||
|
||||
/** Draft coerced onto the model's segment list so rendering/selection never
|
||||
* reference a effort the model cannot actually select. */
|
||||
private effectiveEffort(choice: ModelChoice): string {
|
||||
const draft = this.draftFor(choice);
|
||||
const segments = segmentsFor(choice.model);
|
||||
return segments.includes(draft) ? draft : segments[0]!;
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
|
|
@ -147,11 +217,27 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|||
return;
|
||||
}
|
||||
|
||||
// Left/Right toggle the thinking draft for models that support it.
|
||||
// Left/Right move the active thinking effort within the model's segments.
|
||||
if (matchesKey(data, Key.left) || matchesKey(data, Key.right)) {
|
||||
const selected = this.selectedChoice();
|
||||
if (selected !== undefined && thinkingAvailability(selected.model) === 'toggle') {
|
||||
this.thinkingOverrides.set(selected.alias, !this.draftFor(selected));
|
||||
if (selected !== undefined) {
|
||||
const segments = segmentsFor(selected.model);
|
||||
if (segments.length > 1) {
|
||||
const current = this.effectiveEffort(selected);
|
||||
const idx = segments.indexOf(current);
|
||||
// The two-segment case is the legacy boolean On/Off control: both
|
||||
// arrows flip it. With more segments (efforts), ←/→ step.
|
||||
let next: number;
|
||||
if (segments.length === 2) {
|
||||
next = idx === 0 ? 1 : 0;
|
||||
} else {
|
||||
const delta = matchesKey(data, Key.left) ? -1 : 1;
|
||||
next = Math.max(0, Math.min(segments.length - 1, idx + delta));
|
||||
}
|
||||
if (next !== idx) {
|
||||
this.thinkingOverrides.set(selected.alias, segments[next]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -161,7 +247,7 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|||
if (selected === undefined) return;
|
||||
this.opts.onSelect({
|
||||
alias: selected.alias,
|
||||
thinking: effectiveThinking(selected.model, this.draftFor(selected)),
|
||||
thinking: commitEffort(selected, this.effectiveEffort(selected)),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
|
@ -171,7 +257,7 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|||
if (selected === undefined) return;
|
||||
this.opts.onSessionOnlySelect({
|
||||
alias: selected.alias,
|
||||
thinking: effectiveThinking(selected.model, this.draftFor(selected)),
|
||||
thinking: commitEffort(selected, this.effectiveEffort(selected)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -255,8 +341,8 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|||
lines.push('');
|
||||
const selected = this.selectedChoice();
|
||||
if (selected !== undefined) {
|
||||
const availability = thinkingAvailability(selected.model);
|
||||
const thinkingHeader = availability === 'toggle' ? ' Thinking (←→ to switch)' : ' Thinking';
|
||||
const canSwitch = segmentsFor(selected.model).length > 1;
|
||||
const thinkingHeader = canSwitch ? ' Thinking (←→ to switch)' : ' Thinking';
|
||||
lines.push(currentTheme.fg('textMuted', thinkingHeader));
|
||||
lines.push(this.renderThinkingControl(selected));
|
||||
}
|
||||
|
|
@ -279,16 +365,20 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|||
const unavailable = (label: string): string =>
|
||||
currentTheme.fg('textMuted', ` ${label} (Unsupported) `);
|
||||
|
||||
// On stays left and Off right in all three states so the control never
|
||||
// shifts while the cursor moves across models.
|
||||
// Non-effort always-on / unsupported models keep the original On/Off layout
|
||||
// so the control never shifts while moving across legacy models.
|
||||
const efforts = effortsOf(choice.model);
|
||||
const availability = thinkingAvailability(choice.model);
|
||||
if (availability === 'always-on') {
|
||||
if (efforts.length === 0 && availability === 'always-on') {
|
||||
return ` ${segment('On', true)} ${unavailable('Off')}`;
|
||||
}
|
||||
if (availability === 'unsupported') {
|
||||
if (efforts.length === 0 && availability === 'unsupported') {
|
||||
return ` ${unavailable('On')} ${segment('Off', true)}`;
|
||||
}
|
||||
const draft = this.draftFor(choice);
|
||||
return ` ${segment('On', draft)} ${segment('Off', !draft)}`;
|
||||
|
||||
const segments = segmentsFor(choice.model);
|
||||
const active = this.effectiveEffort(choice);
|
||||
const rendered = segments.map((effort) => segment(effortLabel(effort), effort === active));
|
||||
return ` ${rendered.join(' ')}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
truncateToWidth,
|
||||
visibleWidth,
|
||||
type Focusable,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
import type { PluginInfo, PluginMcpServerInfo, PluginSummary } from '@moonshot-ai/kimi-code-sdk';
|
||||
import chalk from 'chalk';
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import {
|
|||
truncateToWidth,
|
||||
visibleWidth,
|
||||
type Focusable,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { DEFAULT_OAUTH_PROVIDER_NAME } from '#/constant/app';
|
||||
import { CURRENT_MARK, SELECT_POINTER } from '#/tui/constant/symbols';
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
truncateToWidth,
|
||||
visibleWidth,
|
||||
wrapTextWithAnsi,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
import type {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
truncateToWidth,
|
||||
visibleWidth,
|
||||
type Focusable,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
import { formatSessionLabel } from '#/migration/index';
|
||||
import { CURRENT_MARK, SELECT_POINTER } from '#/tui/constant/symbols';
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
visibleWidth,
|
||||
type Component,
|
||||
type Focusable,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { SELECT_POINTER } from '#/tui/constant/symbols';
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import {
|
|||
matchesKey,
|
||||
truncateToWidth,
|
||||
type Focusable,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
import { renderTabStrip } from '#/tui/utils/tab-strip';
|
||||
|
|
@ -39,7 +39,7 @@ export interface TabbedModelSelectorOptions {
|
|||
readonly models: Record<string, ModelAlias>;
|
||||
readonly currentValue: string;
|
||||
readonly selectedValue?: string;
|
||||
readonly currentThinking: boolean;
|
||||
readonly currentThinkingEffort: string;
|
||||
/** When set, the tab for this provider id is initially active instead of the
|
||||
* tab derived from `currentValue`. */
|
||||
readonly initialTabId?: string;
|
||||
|
|
@ -179,7 +179,7 @@ function makeSelector(
|
|||
models: subset,
|
||||
currentValue: opts.currentValue,
|
||||
...(selectedValue !== undefined ? { selectedValue } : {}),
|
||||
currentThinking: opts.currentThinking,
|
||||
currentThinkingEffort: opts.currentThinkingEffort,
|
||||
searchable: true,
|
||||
providerSwitchHint: true,
|
||||
onSelect: opts.onSelect,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
truncateToWidth,
|
||||
visibleWidth,
|
||||
type Focusable,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
import type { BackgroundTaskInfo, BackgroundTaskStatus } from '@moonshot-ai/kimi-code-sdk';
|
||||
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
truncateToWidth,
|
||||
visibleWidth,
|
||||
type Focusable,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
import type { BackgroundTaskInfo, BackgroundTaskStatus } from '@moonshot-ai/kimi-code-sdk';
|
||||
|
||||
import { SELECT_POINTER } from '@/tui/constant/symbols';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
truncateToWidth,
|
||||
visibleWidth,
|
||||
type Focusable,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { SELECT_POINTER } from '#/tui/constant/symbols';
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
visibleWidth,
|
||||
type SelectItem,
|
||||
type TUI,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
import { createEditorTheme } from '#/tui/theme/pi-tui-theme';
|
||||
|
|
@ -114,10 +114,8 @@ function stripSgr(s: string): string {
|
|||
return s.replace(ANSI_SGR, '');
|
||||
}
|
||||
|
||||
function getNewlineInput(data: string): string | undefined {
|
||||
if (data === '\n' || data === '\u001B\r' || data === '\u001B[13;2~') return data;
|
||||
if (matchesKey(data, Key.ctrl('j'))) return '\n';
|
||||
return undefined;
|
||||
interface CustomEditorOptions {
|
||||
disablePasteBurst?: boolean;
|
||||
}
|
||||
|
||||
export class CustomEditor extends Editor {
|
||||
|
|
@ -137,7 +135,6 @@ export class CustomEditor extends Editor {
|
|||
/** Return `true` to consume Ctrl+T (the todo list had overflow to toggle); return `false`/`undefined` to fall through to the editor default. */
|
||||
public onToggleTodoExpand?: () => boolean;
|
||||
public onUndo?: () => void;
|
||||
public onInsertNewline?: () => void;
|
||||
public onTextPaste?: () => void;
|
||||
/**
|
||||
* Called when ↑ is pressed in an empty editor. Return `true` to consume
|
||||
|
|
@ -171,14 +168,14 @@ export class CustomEditor extends Editor {
|
|||
this.argumentHints = hints;
|
||||
}
|
||||
|
||||
constructor(tui: TUI) {
|
||||
constructor(tui: TUI, options: CustomEditorOptions = {}) {
|
||||
// paddingX: 4 reserves column 0 for the left vertical border (│),
|
||||
// column 1 as a single space between border and prompt, column 2 for
|
||||
// the `>` prompt token, and column 3 as the space between prompt and
|
||||
// content. The right side mirrors with 3 padding columns and the right
|
||||
// border at the last column.
|
||||
const theme = createEditorTheme();
|
||||
super(tui, theme, { paddingX: 4 });
|
||||
super(tui, theme, { paddingX: 4, disablePasteBurst: options.disablePasteBurst });
|
||||
|
||||
// pi-tui keeps `createAutocompleteList` private; shadow it with an
|
||||
// instance property so slash command menus render descriptions wrapped
|
||||
|
|
@ -211,6 +208,16 @@ export class CustomEditor extends Editor {
|
|||
};
|
||||
}
|
||||
|
||||
override setDisablePasteBurst(disabled: boolean): void {
|
||||
super.setDisablePasteBurst(disabled);
|
||||
}
|
||||
|
||||
public setInputMode(mode: 'prompt' | 'bash'): void {
|
||||
if (this.inputMode === mode) return;
|
||||
this.inputMode = mode;
|
||||
this.onInputModeChange?.(mode);
|
||||
}
|
||||
|
||||
private expandPasteMarkerAtCursor(): boolean {
|
||||
const { line, col } = this.getCursor();
|
||||
const lines = this.getLines();
|
||||
|
|
@ -430,13 +437,6 @@ export class CustomEditor extends Editor {
|
|||
return;
|
||||
}
|
||||
|
||||
const newlineInput = getNewlineInput(normalized);
|
||||
if (newlineInput !== undefined) {
|
||||
this.onInsertNewline?.();
|
||||
super.handleInput(newlineInput);
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchesKey(normalized, Key.up)) {
|
||||
if (this.getText().length === 0 && this.onUpArrowEmpty) {
|
||||
if (this.onUpArrowEmpty()) return;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
type AutocompleteProvider,
|
||||
type AutocompleteSuggestions,
|
||||
type SlashCommand,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
|
||||
const PATH_DELIMITERS = new Set([' ', '\t', '"', "'", '=']);
|
||||
const MAX_FALLBACK_SCAN = 2000;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
type SelectItem,
|
||||
type SelectListLayoutOptions,
|
||||
type SelectListTheme,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
|
||||
// Mirror pi-tui's private select-list layout constants
|
||||
// (dist/components/select-list.js); keep in sync when bumping pi-tui.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
* the viewport; pi-tui handles proportional scaling internally.
|
||||
*/
|
||||
|
||||
import { Container, Image, Text, type ImageTheme, getCapabilities } from '@earendil-works/pi-tui';
|
||||
import { Container, Image, Text, type ImageTheme, getCapabilities } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
import type { ImageAttachment } from '#/tui/utils/image-attachment-store';
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
* - Ungrouping is not implemented. Once formed, a group stays grouped.
|
||||
*/
|
||||
|
||||
import type { TUI } from '@earendil-works/pi-tui';
|
||||
import { Container, Spacer, Text } from '@earendil-works/pi-tui';
|
||||
import type { TUI } from '@moonshot-ai/pi-tui';
|
||||
import { Container, Spacer, Text } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { STATUS_BULLET } from '#/tui/constant/symbols';
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { truncateToWidth, visibleWidth, type Component } from '@earendil-works/pi-tui';
|
||||
import { truncateToWidth, visibleWidth, type Component } from '@moonshot-ai/pi-tui';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* to align after the bullet.
|
||||
*/
|
||||
|
||||
import { Container, Markdown, truncateToWidth, visibleWidth, type Component } from '@earendil-works/pi-tui';
|
||||
import { Container, Markdown, truncateToWidth, visibleWidth, type Component } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { MESSAGE_INDENT } from '#/tui/constant/rendering';
|
||||
import { STATUS_BULLET } from '#/tui/constant/symbols';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Text, truncateToWidth, type Component } from '@earendil-works/pi-tui';
|
||||
import { Text, truncateToWidth, type Component } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { MESSAGE_INDENT } from '#/tui/constant/rendering';
|
||||
import { FAILURE_MARK, STATUS_BULLET } from '#/tui/constant/symbols';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Component } from '@earendil-works/pi-tui';
|
||||
import { Spacer, Text, visibleWidth } from '@earendil-works/pi-tui';
|
||||
import type { Component } from '@moonshot-ai/pi-tui';
|
||||
import { Spacer, Text, visibleWidth } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { STATUS_BULLET } from '#/tui/constant/symbols';
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
* the richer completion card (the `/goal` box), not this marker.
|
||||
*/
|
||||
|
||||
import { truncateToWidth, type Component } from '@earendil-works/pi-tui';
|
||||
import { truncateToWidth, type Component } from '@moonshot-ai/pi-tui';
|
||||
import type { GoalChange } from '@moonshot-ai/kimi-code-sdk';
|
||||
|
||||
import { STATUS_BULLET } from '#/tui/constant/symbols';
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
visibleWidth,
|
||||
wrapTextWithAnsi,
|
||||
type Component,
|
||||
} from '@earendil-works/pi-tui';
|
||||
} from '@moonshot-ai/pi-tui';
|
||||
import type { GoalSnapshot, GoalStatus } from '@moonshot-ai/kimi-code-sdk';
|
||||
|
||||
import { MESSAGE_INDENT } from '#/tui/constant/rendering';
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import path from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
import { Markdown, truncateToWidth, visibleWidth, type Component, type MarkdownTheme } from '@earendil-works/pi-tui';
|
||||
import { Markdown, truncateToWidth, visibleWidth, type Component, type MarkdownTheme } from '@moonshot-ai/pi-tui';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { toTerminalHyperlink } from '#/utils/terminal-hyperlink';
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
* context; the TUI only consumes the `plugin_command.activated` event.
|
||||
*/
|
||||
|
||||
import { Container, Text, Spacer } from '@earendil-works/pi-tui';
|
||||
import { Container, Text, Spacer } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@
|
|||
* src/missing.ts · failed
|
||||
*/
|
||||
|
||||
import type { TUI } from '@earendil-works/pi-tui';
|
||||
import { Container, Spacer, Text } from '@earendil-works/pi-tui';
|
||||
import type { TUI } from '@moonshot-ai/pi-tui';
|
||||
import { Container, Spacer, Text } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { STATUS_BULLET } from '#/tui/constant/symbols';
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Component } from '@earendil-works/pi-tui';
|
||||
import { Container, Text } from '@earendil-works/pi-tui';
|
||||
import type { Component } from '@moonshot-ai/pi-tui';
|
||||
import { Container, Text } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
import type { ToolCallBlockData, ToolResultBlockData } from '#/tui/types';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Container, Text } from '@earendil-works/pi-tui';
|
||||
import { Container, Text } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
* metadata.
|
||||
*/
|
||||
|
||||
import { Container, Text, Spacer } from '@earendil-works/pi-tui';
|
||||
import { Container, Text, Spacer } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
import type { SkillActivationTrigger } from '#/tui/types';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Container, Spacer, Text } from '@earendil-works/pi-tui';
|
||||
import { Container, Spacer, Text } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
import type { ColorToken } from '#/tui/theme';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,13 @@
|
|||
* separate from the TUI orchestration layer.
|
||||
*/
|
||||
|
||||
import type { ModelAlias, PermissionMode, SessionStatus } from '@moonshot-ai/kimi-code-sdk';
|
||||
import {
|
||||
effectiveModelAlias,
|
||||
type ModelAlias,
|
||||
type PermissionMode,
|
||||
type SessionStatus,
|
||||
type ThinkingEffort,
|
||||
} from '@moonshot-ai/kimi-code-sdk';
|
||||
|
||||
import { PRODUCT_NAME } from '#/constant/app';
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
|
@ -30,7 +36,7 @@ export interface StatusReportOptions {
|
|||
readonly workDir: string;
|
||||
readonly sessionId: string;
|
||||
readonly sessionTitle: string | null;
|
||||
readonly thinking: boolean;
|
||||
readonly thinkingEffort: ThinkingEffort;
|
||||
readonly permissionMode: PermissionMode;
|
||||
readonly planMode: boolean;
|
||||
readonly contextUsage: number;
|
||||
|
|
@ -47,17 +53,16 @@ type Colorize = (text: string) => string;
|
|||
|
||||
function displayModelName(alias: string, models: Record<string, ModelAlias>): string {
|
||||
const model = models[alias];
|
||||
return model?.displayName ?? model?.model ?? alias;
|
||||
const effective = model === undefined ? undefined : effectiveModelAlias(model);
|
||||
return effective?.displayName ?? effective?.model ?? alias;
|
||||
}
|
||||
|
||||
function formatModelStatus(options: StatusReportOptions): string {
|
||||
const model = options.status?.model ?? options.model;
|
||||
if (model.trim().length === 0) return 'not set';
|
||||
|
||||
const thinking = (options.status?.thinkingLevel ?? (options.thinking ? 'on' : 'off')) === 'off'
|
||||
? 'off'
|
||||
: 'on';
|
||||
return `${displayModelName(model, options.availableModels)} (thinking ${thinking})`;
|
||||
const effort = options.status?.thinkingEffort ?? options.thinkingEffort;
|
||||
return `${displayModelName(model, options.availableModels)} (thinking ${effort})`;
|
||||
}
|
||||
|
||||
function addFieldRows(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Component } from '@earendil-works/pi-tui';
|
||||
import type { Component } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { truncateToWidth, type Component } from '@earendil-works/pi-tui';
|
||||
import { truncateToWidth, type Component } from '@moonshot-ai/pi-tui';
|
||||
|
||||
import { STATUS_BULLET } from '#/tui/constant/symbols';
|
||||
import { currentTheme } from '#/tui/theme';
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue