From eee236f4660e842b3d757933b60f5b9b81d489dd Mon Sep 17 00:00:00 2001 From: Zhicong Lian Date: Wed, 3 Jun 2026 14:27:57 +0800 Subject: [PATCH 01/13] feat(cowork): scaffold kimi_cli engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the Kimi CLI engine to the Cowork agent engine registry as a non-functional scaffold, so the next PRs can fill in the runtime, event normalization, and config-source behavior incrementally. Tracking issue: https://github.com/freestylefly/wesight/issues/34 What this PR introduces: * CoworkAgentEngine.KimiCli constant + KimiCliPermissionMode enum and isKimiCliPermissionMode guard, mirrored on the Qwen Code pattern. * New engine added to CoworkAgentEngineValues and CliCoworkAgentEngines. * src/main/libs/kimiCliConfig.ts: buildKimiCliRuntimeEnv env builder and KIMI_CLI_BINARY / DEFAULT_KIMI_CLI_MODEL constants. Respects issue #33 (WeSight does not write API keys into ~/.kimi/config.toml). * src/main/libs/kimiCliCliEvent.ts: stream-json event normalizer stub. Only assistant_text / result error events are recognized today; the full event schema lands in a follow-up commit. * src/main/libs/agentEngine/kimiCliRuntimeAdapter.ts: dedicated runtime adapter that implements CoworkRuntime but emits a 'not implemented' error for every start/continue. Chosen as a separate class instead of extending ExternalCliRuntimeAdapter to keep Kimi CLI's CLI flag shape (--print --output-format stream-json --work-dir --yolo / --plan) isolated from the other nine engines' command builders. * coworkEngineRouter + main.ts: register kimiCliRuntime in RouterDeps, runtimeByEngine, bindRuntimeEvents, and instantiate a singleton KimiCliRuntimeAdapter in getCoworkEngineRouter. * i18n: zh + en keys for engine label, hint, permission modes, and a 'not implemented' notice (mirrors coworkAgentEngineQwenCode* layout). Out of scope (follow-up commits): * UI integration in AgentEngineSelect / CoworkEngineSelector / model selector / AgentEnvironmentSetup. * Real spawn of 'kimi --print --output-format stream-json --work-dir --yolo --model --prompt '. * Reading ~/.kimi/config.toml and syncing back to it (issue #32/#33). * 'kimi --login' status detection. * Vitest coverage equivalent to qwenCodeConfig.test.ts / qwenCodeCliEvent.test.ts. Design notes: * Reused the existing QwenCode permission mode shape (auto / conservative) rather than introducing yolo / plan literals yet — the mapping kimi --yolo <-> Auto and kimi --plan <-> Conservative is captured in the i18n hints, and the actual flag translation can land with the runtime work. * Adapter deliberately does not touch the shared ExternalCliRuntimeAdapter to avoid widening its engine switch fan-out before the Kimi CLI flag shape is finalized. Co-Authored-By: Claude Opus 4.8 --- .../libs/agentEngine/coworkEngineRouter.ts | 3 + src/main/libs/agentEngine/index.ts | 1 + .../libs/agentEngine/kimiCliRuntimeAdapter.ts | 80 +++++++++++++++++++ src/main/libs/kimiCliCliEvent.ts | 68 ++++++++++++++++ src/main/libs/kimiCliConfig.ts | 36 +++++++++ src/main/main.ts | 6 ++ src/renderer/services/i18n.ts | 28 +++++++ src/shared/cowork/constants.ts | 20 +++++ 8 files changed, 242 insertions(+) create mode 100644 src/main/libs/agentEngine/kimiCliRuntimeAdapter.ts create mode 100644 src/main/libs/kimiCliCliEvent.ts create mode 100644 src/main/libs/kimiCliConfig.ts diff --git a/src/main/libs/agentEngine/coworkEngineRouter.ts b/src/main/libs/agentEngine/coworkEngineRouter.ts index 8073f78c..aebe511c 100644 --- a/src/main/libs/agentEngine/coworkEngineRouter.ts +++ b/src/main/libs/agentEngine/coworkEngineRouter.ts @@ -28,6 +28,7 @@ type RouterDeps = { grokBuildRuntime: CoworkRuntime; qwenCodeRuntime: CoworkRuntime; deepSeekTuiRuntime: CoworkRuntime; + kimiCliRuntime: CoworkRuntime; telemetryTracker?: RuntimeTelemetryTracker; }; @@ -54,6 +55,7 @@ export class CoworkEngineRouter extends EventEmitter implements CoworkRuntime { [CoworkAgentEngineValue.GrokBuild]: deps.grokBuildRuntime, [CoworkAgentEngineValue.QwenCode]: deps.qwenCodeRuntime, [CoworkAgentEngineValue.DeepSeekTui]: deps.deepSeekTuiRuntime, + [CoworkAgentEngineValue.KimiCli]: deps.kimiCliRuntime, }; this.currentEngine = this.safeResolveEngine(); this.telemetryTracker = deps.telemetryTracker; @@ -68,6 +70,7 @@ export class CoworkEngineRouter extends EventEmitter implements CoworkRuntime { this.bindRuntimeEvents(CoworkAgentEngineValue.GrokBuild, deps.grokBuildRuntime); this.bindRuntimeEvents(CoworkAgentEngineValue.QwenCode, deps.qwenCodeRuntime); this.bindRuntimeEvents(CoworkAgentEngineValue.DeepSeekTui, deps.deepSeekTuiRuntime); + this.bindRuntimeEvents(CoworkAgentEngineValue.KimiCli, deps.kimiCliRuntime); } override on( diff --git a/src/main/libs/agentEngine/index.ts b/src/main/libs/agentEngine/index.ts index b5d59f2e..4dedbf44 100644 --- a/src/main/libs/agentEngine/index.ts +++ b/src/main/libs/agentEngine/index.ts @@ -4,5 +4,6 @@ export { CoworkEngineRouter } from './coworkEngineRouter'; export { DeepSeekTuiRuntimeAdapter } from './deepSeekTuiRuntimeAdapter'; export { ExternalCliRuntimeAdapter } from './externalCliRuntimeAdapter'; export { HermesRuntimeAdapter } from './hermesRuntimeAdapter'; +export { KimiCliRuntimeAdapter } from './kimiCliRuntimeAdapter'; export { OpenClawRuntimeAdapter } from './openclawRuntimeAdapter'; export * from './types'; diff --git a/src/main/libs/agentEngine/kimiCliRuntimeAdapter.ts b/src/main/libs/agentEngine/kimiCliRuntimeAdapter.ts new file mode 100644 index 00000000..525b18be --- /dev/null +++ b/src/main/libs/agentEngine/kimiCliRuntimeAdapter.ts @@ -0,0 +1,80 @@ +/** + * Kimi CLI Cowork 运行时适配器(占位 / scaffold)。 + * + * 状态:仅实现 CoworkRuntime 接口外壳,所有会话调用都立即返回「尚未实现」错误。 + * 完整功能(spawn `kimi --print --output-format stream-json`、事件归一化、 + * 权限模式 `--yolo` / `--plan` 路由、本机 `~/.kimi/config.toml` 复用等) + * 将在后续 commit 中补全,跟踪于 + * https://github.com/freestylefly/wesight/issues/34 + * + * 设计选择:单独建一个 adapter 而不是复用 `ExternalCliRuntimeAdapter`, + * 因为后者与现有 9 个引擎的命令拼装、env 注入、provider store 紧密耦合, + * 引入 Kimi CLI 一次性会污染 ~15 处分支;先以独立类落位,等行为稳定后再决定 + * 是否合并到 `ExternalCliRuntimeAdapter`。 + */ + +import type { PermissionResult } from '@anthropic-ai/claude-agent-sdk'; +import { EventEmitter } from 'events'; + +import type { + CoworkContinueOptions, + CoworkRuntime, + CoworkRuntimeEvents, + CoworkStartOptions, +} from './types'; + +const NOT_IMPLEMENTED_MESSAGE = '[KimiCli] engine is not yet implemented; tracked in issue #34.'; + +export class KimiCliRuntimeAdapter extends EventEmitter implements CoworkRuntime { + constructor() { + super(); + } + + override on( + event: U, + listener: CoworkRuntimeEvents[U], + ): this { + return super.on(event, listener); + } + + override off( + event: U, + listener: CoworkRuntimeEvents[U], + ): this { + return super.off(event, listener); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async startSession(sessionId: string, _prompt: string, _options: CoworkStartOptions = {}): Promise { + this.emit('error', sessionId, NOT_IMPLEMENTED_MESSAGE); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async continueSession(sessionId: string, _prompt: string, _options: CoworkContinueOptions = {}): Promise { + this.emit('error', sessionId, NOT_IMPLEMENTED_MESSAGE); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + stopSession(_sessionId: string): void { + // No-op: no sessions are ever started by the scaffold. + } + + stopAllSessions(): void { + // No-op. + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + respondToPermission(_requestId: string, _result: PermissionResult): void { + // No-op: no permission requests are ever emitted by the scaffold. + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isSessionActive(_sessionId: string): boolean { + return false; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getSessionConfirmationMode(_sessionId: string): 'modal' | 'text' | null { + return null; + } +} diff --git a/src/main/libs/kimiCliCliEvent.ts b/src/main/libs/kimiCliCliEvent.ts new file mode 100644 index 00000000..5464cefc --- /dev/null +++ b/src/main/libs/kimiCliCliEvent.ts @@ -0,0 +1,68 @@ +/** + * Kimi CLI stream-json 事件归一化。 + * + * 状态:占位(scaffold)。Kimi CLI 的 `--output-format stream-json` 事件 schema + * 与 Claude Code `stream-json` 同源(type: system | assistant | tool_use | + * tool_result | result),但本仓库需要实测后才能确定每种 event 的字段。 + * 当前实现把所有未识别事件归为 `none`,等真实事件可复现后补全。 + * + * 完整实现见 https://github.com/freestylefly/wesight/issues/34 + */ + +const isRecord = (value: unknown): value is Record => { + return Boolean(value && typeof value === 'object' && !Array.isArray(value)); +}; + +export type KimiCliNormalizedEvent = + | { kind: 'none'; sessionId: string | null } + | { kind: 'assistant_text'; sessionId: string | null; text: string; replace: boolean } + | { kind: 'tool_use'; sessionId: string | null; toolName: string; input: Record } + | { kind: 'tool_result'; sessionId: string | null; toolName: string; output: string; isError: boolean } + | { kind: 'error'; sessionId: string | null; message: string }; + +const firstString = (...values: unknown[]): string | null => { + for (const value of values) { + if (typeof value === 'string' && value.trim()) return value; + } + return null; +}; + +export const parseKimiCliJsonLine = (line: string): KimiCliNormalizedEvent | null => { + try { + return normalizeKimiCliCliEvent(JSON.parse(line)); + } catch { + return null; + } +}; + +/** + * 占位实现:只识别 `text` 字段,输出 assistant_text;其他一律视为 none。 + * TODO: 完整识别 system / assistant / tool_use / tool_result / result。 + */ +export const normalizeKimiCliCliEvent = (event: unknown): KimiCliNormalizedEvent => { + if (!isRecord(event)) { + return { kind: 'none', sessionId: null }; + } + const sessionId = firstString(event.session_id, event.sessionId, event.sessionID); + const type = String(event.type ?? ''); + if (type === 'result') { + const isError = Boolean(event.is_error) || String(event.subtype ?? '') !== 'success'; + if (isError) { + const errorRecord = isRecord(event.error) ? event.error : {}; + return { + kind: 'error', + sessionId, + message: firstString(errorRecord.message, event.error, event.result) ?? 'Kimi CLI run failed.', + }; + } + const result = firstString(event.result); + return result + ? { kind: 'assistant_text', sessionId, text: result, replace: true } + : { kind: 'none', sessionId }; + } + const text = firstString(event.text, event.content, event.message, event.delta); + if (text) { + return { kind: 'assistant_text', sessionId, text, replace: false }; + } + return { kind: 'none', sessionId }; +}; diff --git a/src/main/libs/kimiCliConfig.ts b/src/main/libs/kimiCliConfig.ts new file mode 100644 index 00000000..9fc79e75 --- /dev/null +++ b/src/main/libs/kimiCliConfig.ts @@ -0,0 +1,36 @@ +/** + * Kimi CLI 配置与环境变量工具。 + * + * 状态:占位(scaffold)。仅暴露最小 API 以让路由与 UI 编译通过。 + * 完整实现见 https://github.com/freestylefly/wesight/issues/34 + */ + +import type { CoworkApiConfig } from './coworkConfigStore'; + +/** Kimi CLI 在「WeSight 模型」配置源下注入的 env var 集合。 */ +export interface KimiCliRuntimeEnv { + KIMI_API_KEY: string; + KIMI_BASE_URL: string; + KIMI_MODEL_NAME: string; +} + +/** + * 把 WeSight 的 CoworkApiConfig 翻译为 Kimi CLI 进程 env。 + * + * 真实实现需要参考 Kimi CLI 官方文档读取 `~/.kimi/config.toml`, + * 并尊重「WeSight 不写回本机配置文件」的凭据隔离原则(issue #33)。 + * 当前为占位:仅透传三组核心 env 变量。 + */ +export const buildKimiCliRuntimeEnv = (config: CoworkApiConfig): KimiCliRuntimeEnv => { + return { + KIMI_API_KEY: config.apiKey, + KIMI_BASE_URL: config.baseURL, + KIMI_MODEL_NAME: config.model, + }; +}; + +/** Kimi CLI 二进制名。 */ +export const KIMI_CLI_BINARY = 'kimi'; + +/** Kimi CLI 默认模型(与 src/shared/providers/constants.ts 中 Moonshot 默认保持一致)。 */ +export const DEFAULT_KIMI_CLI_MODEL = 'kimi-k2.5'; diff --git a/src/main/main.ts b/src/main/main.ts index d0459f96..9c9fecee 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -84,6 +84,7 @@ import { DeepSeekTuiRuntimeAdapter, ExternalCliRuntimeAdapter, HermesRuntimeAdapter, + KimiCliRuntimeAdapter, OpenClawRuntimeAdapter, } from './libs/agentEngine'; import { cancelActiveDownload,downloadUpdate, installUpdate } from './libs/appUpdateInstaller'; @@ -735,6 +736,7 @@ let openCodeRuntimeAdapter: ExternalCliRuntimeAdapter | null = null; let grokBuildRuntimeAdapter: ExternalCliRuntimeAdapter | null = null; let qwenCodeRuntimeAdapter: ExternalCliRuntimeAdapter | null = null; let deepSeekTuiRuntimeManager: DeepSeekTuiRuntimeManager | null = null; +let kimiCliRuntimeAdapter: KimiCliRuntimeAdapter | null = null; let deepSeekTuiRuntimeAdapter: DeepSeekTuiRuntimeAdapter | null = null; let coworkEngineRouter: CoworkEngineRouter | null = null; let skillManager: SkillManager | null = null; @@ -2097,6 +2099,9 @@ const getCoworkEngineRouter = () => { ensureRunning: ensureHermesRunningForCowork, }); } + if (!kimiCliRuntimeAdapter) { + kimiCliRuntimeAdapter = new KimiCliRuntimeAdapter(); + } coworkEngineRouter = new CoworkEngineRouter({ getCurrentEngine: resolveCoworkAgentEngine, openclawRuntime: openClawRuntimeAdapter, @@ -2109,6 +2114,7 @@ const getCoworkEngineRouter = () => { grokBuildRuntime: grokBuildRuntimeAdapter, qwenCodeRuntime: qwenCodeRuntimeAdapter, deepSeekTuiRuntime: deepSeekTuiRuntimeAdapter, + kimiCliRuntime: kimiCliRuntimeAdapter, telemetryTracker: getRuntimeTelemetryTracker(), }); } diff --git a/src/renderer/services/i18n.ts b/src/renderer/services/i18n.ts index d2a8ec9b..6cf2f4c5 100644 --- a/src/renderer/services/i18n.ts +++ b/src/renderer/services/i18n.ts @@ -458,6 +458,9 @@ const translations: Record> = { coworkAgentEngineQwenCodeHint: '读取或安装 Qwen Code CLI,适合把 Qwen 的终端编码 Agent 工作流接入图形化对话。', coworkAgentEngineDeepSeekTui: 'DeepSeek-TUI', coworkAgentEngineDeepSeekTuiHint: '读取或安装 DeepSeek-TUI CLI,通过本地 HTTP/SSE runtime 接入图形化对话。', + coworkAgentEngineKimiCli: 'Kimi CLI', + coworkAgentEngineKimiCliHint: '读取或安装 Moonshot AI 官方的 Kimi CLI 终端编码 Agent,把长上下文(kimi-k2.5 等)与 MCP 工具流接入图形化对话。', + coworkAgentEngineKimiCliNotImplemented: 'Kimi CLI 引擎正在开发中,跟踪于 GitHub issue #34。', coworkAgentEngineCliInstalled: '已检测到 CLI', coworkAgentEngineCliMissing: '未检测到 CLI', coworkAgentEngineInstallCli: '安装 CLI', @@ -652,6 +655,17 @@ const translations: Record> = { coworkAgentQwenCodeSyncGlobalHint: '默认只在 WeSight 运行时生效;需要终端 qwen 也使用当前模型时,可同步到全局配置。', coworkAgentQwenCodeSyncGlobalSuccess: '已同步到 Qwen Code 全局配置。', coworkAgentQwenCodeSyncGlobalFailed: 'Qwen Code 配置同步失败', + coworkAgentKimiCliPermissionTitle: 'Kimi CLI 权限模式', + coworkAgentKimiCliPermissionHint: '控制 Kimi CLI 非交互任务遇到工具权限时的行为。', + coworkAgentKimiCliPermissionAuto: '自动执行', + coworkAgentKimiCliPermissionAutoHint: '适合常规 Agent 任务,会通过 --yolo 自动批准所有工具调用。', + coworkAgentKimiCliPermissionConservative: '保守模式', + coworkAgentKimiCliPermissionConservativeHint: '使用 --plan 模式先输出计划再执行,适合先观察任务方案。', + coworkAgentKimiCliSyncGlobal: '同步到 Kimi CLI', + coworkAgentKimiCliSyncGlobalSyncing: '正在同步...', + coworkAgentKimiCliSyncGlobalHint: '默认只在 WeSight 运行时通过 KIMI_API_KEY env 注入;需要终端 kimi 也使用当前模型时,可同步到 ~/.kimi/config.toml。', + coworkAgentKimiCliSyncGlobalSuccess: '已同步到 Kimi CLI 全局配置。', + coworkAgentKimiCliSyncGlobalFailed: 'Kimi CLI 配置同步失败', coworkAgentDeepSeekTuiPermissionTitle: 'DeepSeek-TUI 权限模式', coworkAgentDeepSeekTuiPermissionHint: '控制 DeepSeek-TUI runtime 遇到工具权限时的行为。', coworkAgentDeepSeekTuiPermissionAuto: '自动执行', @@ -2287,6 +2301,9 @@ const translations: Record> = { coworkAgentEngineQwenCodeHint: 'Reads or installs the Qwen Code CLI and brings Qwen terminal coding agent workflows into graphical chat.', coworkAgentEngineDeepSeekTui: 'DeepSeek-TUI', coworkAgentEngineDeepSeekTuiHint: 'Reads or installs the DeepSeek-TUI CLI and connects its local HTTP/SSE runtime to graphical chat.', + coworkAgentEngineKimiCli: 'Kimi CLI', + coworkAgentEngineKimiCliHint: 'Reads or installs the Moonshot AI Kimi CLI terminal coding agent and brings its long-context (kimi-k2.5 etc.) and MCP tool flow into graphical chat.', + coworkAgentEngineKimiCliNotImplemented: 'Kimi CLI engine is under development; tracked in GitHub issue #34.', coworkAgentEngineCliInstalled: 'CLI detected', coworkAgentEngineCliMissing: 'CLI not detected', coworkAgentEngineInstallCli: 'Install CLI', @@ -2481,6 +2498,17 @@ const translations: Record> = { coworkAgentQwenCodeSyncGlobalHint: 'By default this only affects WeSight runtime. Sync when terminal qwen should use the current model too.', coworkAgentQwenCodeSyncGlobalSuccess: 'Synced to Qwen Code global config.', coworkAgentQwenCodeSyncGlobalFailed: 'Failed to sync Qwen Code config', + coworkAgentKimiCliPermissionTitle: 'Kimi CLI Permission Mode', + coworkAgentKimiCliPermissionHint: 'Controls how non-interactive Kimi CLI tasks handle tool permissions.', + coworkAgentKimiCliPermissionAuto: 'Auto Execute', + coworkAgentKimiCliPermissionAutoHint: 'Best for agent tasks that should run required tools through --yolo (auto-approve all tool calls).', + coworkAgentKimiCliPermissionConservative: 'Conservative', + coworkAgentKimiCliPermissionConservativeHint: 'Use --plan mode first, useful for reviewing the task plan before execution.', + coworkAgentKimiCliSyncGlobal: 'Sync to Kimi CLI', + coworkAgentKimiCliSyncGlobalSyncing: 'Syncing...', + coworkAgentKimiCliSyncGlobalHint: 'By default this only affects WeSight runtime via the KIMI_API_KEY env. Sync when terminal kimi should also use the current model.', + coworkAgentKimiCliSyncGlobalSuccess: 'Synced to Kimi CLI global config.', + coworkAgentKimiCliSyncGlobalFailed: 'Failed to sync Kimi CLI config', coworkAgentDeepSeekTuiPermissionTitle: 'DeepSeek-TUI Permission Mode', coworkAgentDeepSeekTuiPermissionHint: 'Controls how the DeepSeek-TUI runtime handles tool permissions.', coworkAgentDeepSeekTuiPermissionAuto: 'Auto Execute', diff --git a/src/shared/cowork/constants.ts b/src/shared/cowork/constants.ts index 07e286bd..620775c9 100644 --- a/src/shared/cowork/constants.ts +++ b/src/shared/cowork/constants.ts @@ -15,6 +15,7 @@ export const CoworkAgentEngine = { GrokBuild: 'grok_build', QwenCode: 'qwen_code', DeepSeekTui: 'deepseek_tui', + KimiCli: 'kimi_cli', } as const; export type CoworkAgentEngine = typeof CoworkAgentEngine[keyof typeof CoworkAgentEngine]; @@ -32,6 +33,7 @@ export const CoworkAgentEngineValues = [ CoworkAgentEngine.GrokBuild, CoworkAgentEngine.QwenCode, CoworkAgentEngine.DeepSeekTui, + CoworkAgentEngine.KimiCli, ] as const; export const CliCoworkAgentEngines = [ @@ -43,6 +45,7 @@ export const CliCoworkAgentEngines = [ CoworkAgentEngine.GrokBuild, CoworkAgentEngine.QwenCode, CoworkAgentEngine.DeepSeekTui, + CoworkAgentEngine.KimiCli, ] as const; export type CliCoworkAgentEngine = typeof CliCoworkAgentEngines[number]; @@ -111,6 +114,18 @@ export const DeepSeekTuiPermissionModeValues = [ DeepSeekTuiPermissionMode.Conservative, ] as const; +export const KimiCliPermissionMode = { + Auto: 'auto', + Conservative: 'conservative', +} as const; + +export type KimiCliPermissionMode = typeof KimiCliPermissionMode[keyof typeof KimiCliPermissionMode]; + +export const KimiCliPermissionModeValues = [ + KimiCliPermissionMode.Auto, + KimiCliPermissionMode.Conservative, +] as const; + export function isCoworkAgentEngine(value: unknown): value is CoworkAgentEngine { return typeof value === 'string' && CoworkAgentEngineValues.includes(value as CoworkAgentEngine); @@ -141,6 +156,11 @@ export function isDeepSeekTuiPermissionMode(value: unknown): value is DeepSeekTu && DeepSeekTuiPermissionModeValues.includes(value as DeepSeekTuiPermissionMode); } +export function isKimiCliPermissionMode(value: unknown): value is KimiCliPermissionMode { + return typeof value === 'string' + && KimiCliPermissionModeValues.includes(value as KimiCliPermissionMode); +} + export function isCliCoworkAgentEngine(value: unknown): value is CliCoworkAgentEngine { return typeof value === 'string' && CliCoworkAgentEngines.includes(value as CliCoworkAgentEngine); From 16265fb33bf62d53224b41d47dc135c4079859e0 Mon Sep 17 00:00:00 2001 From: Zhicong Lian Date: Wed, 3 Jun 2026 14:42:58 +0800 Subject: [PATCH 02/13] fix(cowork): add KimiCli avatar entry to satisfy Record Adding CoworkAgentEngine.KimiCli broke the engineAvatarManifest Record type check. Register a purple-themed avatar with the existing 'terminal' prop and skip getConfigSource (returns null until config.kimiCliConfigSource lands). Co-Authored-By: Claude Opus 4.8 --- src/renderer/utils/coworkStudio.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/renderer/utils/coworkStudio.ts b/src/renderer/utils/coworkStudio.ts index 39bd96fa..6b7ecea5 100644 --- a/src/renderer/utils/coworkStudio.ts +++ b/src/renderer/utils/coworkStudio.ts @@ -118,6 +118,15 @@ const engineAvatarManifest: Record = { faceColor: 0xbfdbfe, prop: 'tui', }, + [CoworkAgentEngine.KimiCli]: { + id: 'kimi_cli', + nameTag: 'Kimi', + primaryColor: 0x7c3aed, + secondaryColor: 0x4c1d95, + accentColor: 0xc4b5fd, + faceColor: 0xede9fe, + prop: 'terminal', + }, [CoworkAgentEngine.YdCowork]: { id: 'yd_cowork', nameTag: 'WeSight', @@ -151,6 +160,11 @@ const getConfigSource = (config: CoworkConfig): ExternalAgentConfigSource | null if (config.agentEngine === CoworkAgentEngine.GrokBuild) return ExternalAgentConfigSource.LocalCli; if (config.agentEngine === CoworkAgentEngine.QwenCode) return config.qwenCodeConfigSource; if (config.agentEngine === CoworkAgentEngine.DeepSeekTui) return config.deepseekTuiConfigSource; + if (config.agentEngine === CoworkAgentEngine.KimiCli) { + // TODO(issue #34): introduce config.kimiCliConfigSource once Kimi CLI + // supports a WeSight-model / local-~/.kimi/config.toml source toggle. + return null; + } if (config.agentEngine === CoworkAgentEngine.OpenClaw) return config.openclawConfigSource; return null; }; From 5095db02ce1daf5dc738ce46355183d27aa95ad3 Mon Sep 17 00:00:00 2001 From: Zhicong Lian Date: Wed, 3 Jun 2026 15:10:36 +0800 Subject: [PATCH 03/13] feat(cowork): expose Kimi CLI in the UI engine selector Wire CoworkAgentEngine.KimiCli into every place the existing engines are enumerated so users can pick Kimi CLI from the engine dropdown and have it appear correctly in session headers, the runtime dashboard, slash-command hints, etc. Updated: * AgentEngineSelect: add KimiCli to ENGINE_OPTIONS and the label switch. * CoworkEngineSelector: add KimiCli to ENGINE_OPTIONS and isCliEngine so the row also shows the 'CLI detected / CLI not detected' status dot. * CoworkView: getEngineLabelKey and the getEngineLabel switch case now handle KimiCli. * CoworkSessionDetail: both engine-label switch cases handle KimiCli. * RuntimeDashboardView: getEngineLabel handles KimiCli. * CoworkPromptInput: slash-command hint ternary routes KimiCli to coworkSlashCommandsKimiCli. * i18n (zh + en): add coworkSlashCommandsKimiCli. Out of scope on purpose (mirrors what was committed for the scaffold): * No 'kimi' ExternalAgentProviderAppType yet, so the engine doesn't appear in AgentEnvironmentSetup, getCliAppTypeForEngine, or the configSource check in CoworkView/RuntimeDashboard. This means the 'local CLI' source toggle is not wired up; the engine only runs with the WeSight-model env-injection path. The install-detection surface in AgentEnvironmentSetup will be added together with the real spawn in a follow-up commit (issue #34). * KimiCliRuntimeAdapter is still a scaffold that emits a 'not implemented' error on startSession/continueSession, so picking the engine and sending a prompt surfaces a clear error rather than a crash. Real 'kimi --print --output-format stream-json --work-dir ... --yolo --prompt ...' wiring lands next. Also: include the 12 missing @types/* packages that the tsc strict auto-type-resolution required to compile electron-tsconfig.json (they are referenced via @types/* symlinks but the package.json didn't list them as devDependencies). Without these, `npm run compile:electron` fails with TS2688 'Cannot find type definition file for X' on a clean install. This is a pre-existing repo issue; bundling the fix here so the Kimi CLI branch compiles end-to-end. Co-Authored-By: Claude Opus 4.8 --- package.json | 12 ++++++++++++ src/renderer/components/agent/AgentEngineSelect.tsx | 3 +++ .../components/cowork/CoworkEngineSelector.tsx | 8 +++++++- src/renderer/components/cowork/CoworkPromptInput.tsx | 4 +++- .../components/cowork/CoworkSessionDetail.tsx | 4 ++++ src/renderer/components/cowork/CoworkView.tsx | 3 +++ .../components/runtime/RuntimeDashboardView.tsx | 1 + src/renderer/services/i18n.ts | 2 ++ 8 files changed, 35 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a9244b3f..6657973c 100644 --- a/package.json +++ b/package.json @@ -166,14 +166,26 @@ "@commitlint/cli": "^20.5.0", "@commitlint/config-conventional": "^20.5.0", "@tailwindcss/typography": "^0.5.16", + "@types/babel__generator": "^7.27.0", + "@types/babel__template": "^7.4.4", + "@types/babel__traverse": "^7.28.0", "@types/better-sqlite3": "^7.6.13", + "@types/d3-color": "^3.1.3", + "@types/d3-path": "^3.1.1", + "@types/d3-time": "^3.0.4", + "@types/deep-eql": "^4.0.2", "@types/dompurify": "^3.0.5", "@types/extract-zip": "^2.0.1", + "@types/http-cache-semantics": "^4.2.0", "@types/js-yaml": "^4.0.9", + "@types/ms": "^2.1.0", "@types/node": "^24.0.0", + "@types/prop-types": "^15.7.15", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@types/react-syntax-highlighter": "^15.5.13", + "@types/trusted-types": "^2.0.7", + "@types/unist": "^3.0.3", "@types/yazl": "^3.3.0", "@typescript-eslint/eslint-plugin": "^8.57.1", "@typescript-eslint/parser": "^8.57.1", diff --git a/src/renderer/components/agent/AgentEngineSelect.tsx b/src/renderer/components/agent/AgentEngineSelect.tsx index b4165284..980f124a 100644 --- a/src/renderer/components/agent/AgentEngineSelect.tsx +++ b/src/renderer/components/agent/AgentEngineSelect.tsx @@ -14,6 +14,7 @@ const ENGINE_OPTIONS: CoworkAgentEngineType[] = [ CoworkAgentEngine.GrokBuild, CoworkAgentEngine.QwenCode, CoworkAgentEngine.DeepSeekTui, + CoworkAgentEngine.KimiCli, ]; export const getAgentEngineLabel = (engine: CoworkAgentEngineType): string => { @@ -36,6 +37,8 @@ export const getAgentEngineLabel = (engine: CoworkAgentEngineType): string => { return i18nService.t('coworkAgentEngineQwenCode'); case CoworkAgentEngine.DeepSeekTui: return i18nService.t('coworkAgentEngineDeepSeekTui'); + case CoworkAgentEngine.KimiCli: + return i18nService.t('coworkAgentEngineKimiCli'); case CoworkAgentEngine.YdCowork: default: return i18nService.t('coworkAgentEngineClaudeLegacy'); diff --git a/src/renderer/components/cowork/CoworkEngineSelector.tsx b/src/renderer/components/cowork/CoworkEngineSelector.tsx index 0795c7e9..0e4e2602 100644 --- a/src/renderer/components/cowork/CoworkEngineSelector.tsx +++ b/src/renderer/components/cowork/CoworkEngineSelector.tsx @@ -77,6 +77,11 @@ const ENGINE_OPTIONS: Array<{ labelKey: 'coworkAgentEngineDeepSeekTui', hintKey: 'coworkAgentEngineDeepSeekTuiHint', }, + { + engine: CoworkAgentEngine.KimiCli, + labelKey: 'coworkAgentEngineKimiCli', + hintKey: 'coworkAgentEngineKimiCliHint', + }, ]; const isCliEngine = (engine: CoworkAgentEngineType): boolean => { @@ -85,7 +90,8 @@ const isCliEngine = (engine: CoworkAgentEngineType): boolean => { || engine === CoworkAgentEngine.OpenCode || engine === CoworkAgentEngine.GrokBuild || engine === CoworkAgentEngine.QwenCode - || engine === CoworkAgentEngine.DeepSeekTui; + || engine === CoworkAgentEngine.DeepSeekTui + || engine === CoworkAgentEngine.KimiCli; }; const CoworkEngineSelector: React.FC = ({ diff --git a/src/renderer/components/cowork/CoworkPromptInput.tsx b/src/renderer/components/cowork/CoworkPromptInput.tsx index a908a862..a0c408d5 100644 --- a/src/renderer/components/cowork/CoworkPromptInput.tsx +++ b/src/renderer/components/cowork/CoworkPromptInput.tsx @@ -938,7 +938,9 @@ const CoworkPromptInput = React.forwardRef
diff --git a/src/renderer/components/cowork/CoworkSessionDetail.tsx b/src/renderer/components/cowork/CoworkSessionDetail.tsx index 1643c3ee..37306b5b 100644 --- a/src/renderer/components/cowork/CoworkSessionDetail.tsx +++ b/src/renderer/components/cowork/CoworkSessionDetail.tsx @@ -2500,6 +2500,8 @@ const CoworkSessionDetail: React.FC = ({ return i18nService.t('coworkAgentEngineQwenCode'); case CoworkAgentEngine.DeepSeekTui: return i18nService.t('coworkAgentEngineDeepSeekTui'); + case CoworkAgentEngine.KimiCli: + return i18nService.t('coworkAgentEngineKimiCli'); case CoworkAgentEngine.OpenClaw: return i18nService.t('coworkAgentEngineOpenClaw'); case CoworkAgentEngine.Hermes: @@ -2524,6 +2526,8 @@ const CoworkSessionDetail: React.FC = ({ return i18nService.t('coworkAgentEngineQwenCode'); case CoworkAgentEngine.DeepSeekTui: return i18nService.t('coworkAgentEngineDeepSeekTui'); + case CoworkAgentEngine.KimiCli: + return i18nService.t('coworkAgentEngineKimiCli'); case CoworkAgentEngine.OpenClaw: return i18nService.t('coworkAgentEngineOpenClaw'); case CoworkAgentEngine.Hermes: diff --git a/src/renderer/components/cowork/CoworkView.tsx b/src/renderer/components/cowork/CoworkView.tsx index ba9c363d..a8993f12 100644 --- a/src/renderer/components/cowork/CoworkView.tsx +++ b/src/renderer/components/cowork/CoworkView.tsx @@ -115,6 +115,7 @@ const getEngineLabelKey = (engine: CoworkAgentEngine): string => { if (engine === CoworkAgentEngine.GrokBuild) return 'coworkAgentEngineGrokBuild'; if (engine === CoworkAgentEngine.QwenCode) return 'coworkAgentEngineQwenCode'; if (engine === CoworkAgentEngine.DeepSeekTui) return 'coworkAgentEngineDeepSeekTui'; + if (engine === CoworkAgentEngine.KimiCli) return 'coworkAgentEngineKimiCli'; if (engine === CoworkAgentEngine.CodexApp) return 'coworkAgentEngineCodexApp'; return 'coworkAgentEngineClaudeLegacy'; }; @@ -598,6 +599,8 @@ const CoworkView: React.FC = ({ onRequestAppSettings, onShowSki return i18nService.t('coworkAgentEngineQwenCode'); case CoworkAgentEngine.DeepSeekTui: return i18nService.t('coworkAgentEngineDeepSeekTui'); + case CoworkAgentEngine.KimiCli: + return i18nService.t('coworkAgentEngineKimiCli'); case CoworkAgentEngine.OpenClaw: return i18nService.t('coworkAgentEngineOpenClaw'); case CoworkAgentEngine.Hermes: diff --git a/src/renderer/components/runtime/RuntimeDashboardView.tsx b/src/renderer/components/runtime/RuntimeDashboardView.tsx index 6ef74c7d..2e6a429b 100644 --- a/src/renderer/components/runtime/RuntimeDashboardView.tsx +++ b/src/renderer/components/runtime/RuntimeDashboardView.tsx @@ -149,6 +149,7 @@ const getEngineLabel = (engine: string): string => { if (engine === CoworkAgentEngine.GrokBuild) return i18nService.t('coworkAgentEngineGrokBuild'); if (engine === CoworkAgentEngine.QwenCode) return i18nService.t('coworkAgentEngineQwenCode'); if (engine === CoworkAgentEngine.DeepSeekTui) return i18nService.t('coworkAgentEngineDeepSeekTui'); + if (engine === CoworkAgentEngine.KimiCli) return i18nService.t('coworkAgentEngineKimiCli'); return i18nService.t('coworkAgentEngineClaudeLegacy'); }; diff --git a/src/renderer/services/i18n.ts b/src/renderer/services/i18n.ts index 6cf2f4c5..f33398c4 100644 --- a/src/renderer/services/i18n.ts +++ b/src/renderer/services/i18n.ts @@ -519,6 +519,7 @@ const translations: Record> = { coworkSlashCommandsGrokBuild: 'Grok Build 指令', coworkSlashCommandsQwenCode: 'Qwen Code 指令', coworkSlashCommandsDeepSeekTui: 'DeepSeek-TUI 指令', + coworkSlashCommandsKimiCli: 'Kimi CLI 指令', coworkSlashCommandsWesight: 'WeSight 指令', coworkSlashPanelContextTitle: '上下文概览', coworkSlashPanelStatusTitle: '当前状态', @@ -2362,6 +2363,7 @@ const translations: Record> = { coworkSlashCommandsGrokBuild: 'Grok Build Commands', coworkSlashCommandsQwenCode: 'Qwen Code Commands', coworkSlashCommandsDeepSeekTui: 'DeepSeek-TUI Commands', + coworkSlashCommandsKimiCli: 'Kimi CLI Commands', coworkSlashCommandsWesight: 'WeSight Commands', coworkSlashPanelContextTitle: 'Context Overview', coworkSlashPanelStatusTitle: 'Current Status', From bfe0512b32f5c3055aa98083b8a72c62f48c0ec0 Mon Sep 17 00:00:00 2001 From: Zhicong Lian Date: Wed, 3 Jun 2026 17:00:28 +0800 Subject: [PATCH 04/13] feat(cowork): expose Kimi CLI in Settings Agent Engine + detection probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire Kimi CLI into the Settings → Agent Engine tab (not the runtime selector / model page) so users can: 1. Pick Kimi CLI as the default Cowork engine from a radio list with a one-line hint. 2. See live 'CLI detected / CLI not detected' status, the resolved binary path, and the installed version (CLI installed to $HOME/.local/bin/kimi is probed via the same resolveCommand() path that other engines use). 3. See a 'No extra config' panel under the radio when selected; the standard configSource widget is also rendered and gracefully no-ops because CoworkConfig has no kimiCliConfigSource field yet (TODO(issue #34) — local CLI / WeSight-model source toggle lands together with the real spawn). Detection plumbing: * externalAgentEnvironment.ts: extend CliAppType with 'kimi' and register buildCommandStatus(CoworkAgentEngine.KimiCli, 'kimi', 'kimi', ...) in the engines array. The snapshot.engines consumer (getCliEngineStatus in Settings.tsx) auto-picks it up. Install stub: * externalAgentCliInstaller.ts: add the matching 'kimi' entry in INSTALL_TARGETS to keep Record exhaustive. The 'pip' install method is registered but buildInstallScript returns a clear 'not yet implemented' shell script (exit 64) with a hint to run 'pip install kimi-cli' or 'uv tool install kimi-cli' manually. The Settings UI's Install button surfaces that error rather than silently misrunning npm. Co-Authored-By: Claude Opus 4.8 --- src/main/libs/externalAgentCliInstaller.ts | 26 ++++++++++++++++++++++ src/main/libs/externalAgentEnvironment.ts | 3 ++- src/renderer/components/Settings.tsx | 6 +++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main/libs/externalAgentCliInstaller.ts b/src/main/libs/externalAgentCliInstaller.ts index 513e2eca..8b9af02c 100644 --- a/src/main/libs/externalAgentCliInstaller.ts +++ b/src/main/libs/externalAgentCliInstaller.ts @@ -138,6 +138,20 @@ const INSTALL_TARGETS: Record = { }, ], }, + kimi: { + appType: 'kimi', + displayName: 'Kimi CLI', + command: 'kimi', + // TODO(issue #34): wire `pip install kimi-cli` (or `uv tool install + // kimi-cli`) once the install runner supports a pip-style method. For + // now users install Kimi CLI manually via `pip install kimi-cli`; the + // "Install CLI" button will surface a "not yet implemented" error. + methods: [ + { + id: 'pip', + }, + ], + }, }; const quoteForShell = (value: string): string => { @@ -157,6 +171,18 @@ const truncateProgressLine = (value: string): string => { const buildInstallScript = (target: InstallTarget): string => { const method = target.methods[0]; + if (method.id === 'pip') { + // TODO(issue #34): implement `pip install kimi-cli` (or `uv tool install + // kimi-cli`) with proper pip/uv bootstrap. Until then, surface a clear + // "not implemented" so the Install CLI button doesn't silently misrun + // an npm command. + return [ + 'set -e', + `echo "Auto-install for ${target.displayName} is not implemented yet." >&2`, + `echo "Install it manually with: pip install kimi-cli (or: uv tool install kimi-cli)" >&2`, + 'exit 64', + ].join('\n'); + } if (method.id === 'official-installer') { const scriptUrl = method.scriptUrl; if (!scriptUrl) { diff --git a/src/main/libs/externalAgentEnvironment.ts b/src/main/libs/externalAgentEnvironment.ts index e799117b..6e0567c3 100644 --- a/src/main/libs/externalAgentEnvironment.ts +++ b/src/main/libs/externalAgentEnvironment.ts @@ -24,7 +24,7 @@ import { import { readOpenClawGlobalConfig, summarizeOpenClawConfig } from './openclawSystemRuntime'; import { resolveUserShellPath } from './coworkUtil'; -export type CliAppType = 'claude' | 'codex' | 'hermes' | 'openclaw' | 'opencode' | 'grok' | 'qwen' | 'deepseek_tui'; +export type CliAppType = 'claude' | 'codex' | 'hermes' | 'openclaw' | 'opencode' | 'grok' | 'qwen' | 'deepseek_tui' | 'kimi'; export interface CliAppConfigSnapshot { appType: CliAppType; @@ -560,6 +560,7 @@ export function getExternalAgentEnvironmentSnapshot(): ExternalAgentEnvironmentS buildCommandStatus(CoworkAgentEngine.GrokBuild, 'grok', 'grok', settings, dbPath), buildCommandStatus(CoworkAgentEngine.QwenCode, 'qwen', 'qwen', settings, dbPath), buildCommandStatus(CoworkAgentEngine.DeepSeekTui, 'deepseek_tui', 'deepseek-tui', settings, dbPath), + buildCommandStatus(CoworkAgentEngine.KimiCli, 'kimi', 'kimi', settings, dbPath), ], }; } diff --git a/src/renderer/components/Settings.tsx b/src/renderer/components/Settings.tsx index 308f7609..af1bcae4 100644 --- a/src/renderer/components/Settings.tsx +++ b/src/renderer/components/Settings.tsx @@ -142,6 +142,11 @@ const COWORK_AGENT_ENGINE_OPTIONS: Array<{ labelKey: 'coworkAgentEngineDeepSeekTui', hintKey: 'coworkAgentEngineDeepSeekTuiHint', }, + { + value: CoworkAgentEngineValue.KimiCli, + labelKey: 'coworkAgentEngineKimiCli', + hintKey: 'coworkAgentEngineKimiCliHint', + }, ]; const PET_VARIANT_OPTIONS: Array<{ @@ -3178,6 +3183,7 @@ const Settings: React.FC = ({ onClose, initialTab, notice, notice || engine === CoworkAgentEngineValue.GrokBuild || engine === CoworkAgentEngineValue.QwenCode || engine === CoworkAgentEngineValue.DeepSeekTui + || engine === CoworkAgentEngineValue.KimiCli ) { return (
From 973ee9692a5a22245429f34c8d9f6fda859ef970 Mon Sep 17 00:00:00 2001 From: Zhicong Lian Date: Wed, 3 Jun 2026 17:12:19 +0800 Subject: [PATCH 05/13] feat(cowork): add Kimi CLI to the More Agent Engines card grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the Settings → Agent Engine radio list (bfe0512) into the 'More Agent Engines' card grid that renders in AgentEnvironmentSetup so users can see Kimi CLI alongside OpenCode / Grok Build / Qwen Code / DeepSeek-TUI, with the same Install / CLI-detected plumbing provided by externalAgentEnvironment.ts. The card uses `appType: 'kimi'`, so the three `CliAppType` union definitions in the repo had to be kept in lockstep: * src/renderer/types/cowork.ts L266 (renderer-side type alias) * src/renderer/types/electron.d.ts L129 (preload IPC contract) * src/main/libs/externalAgentEnvironment.ts L27 (main process, from bfe0512) Add 'kimi' to the renderer's Record install-progress map so the install-progress state is exhaustive. These three `CliAppType` declarations are a known pre-existing duplication; consolidating them into a single shared type is a separate cleanup that should be tracked under issue #32 rather than expanded in this branch. Co-Authored-By: Claude Opus 4.8 --- src/renderer/components/Settings.tsx | 1 + src/renderer/components/cowork/AgentEnvironmentSetup.tsx | 8 ++++++++ src/renderer/types/cowork.ts | 2 +- src/renderer/types/electron.d.ts | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/Settings.tsx b/src/renderer/components/Settings.tsx index af1bcae4..ff371450 100644 --- a/src/renderer/components/Settings.tsx +++ b/src/renderer/components/Settings.tsx @@ -892,6 +892,7 @@ const Settings: React.FC = ({ onClose, initialTab, notice, notice grok: '', qwen: '', deepseek_tui: '', + kimi: '', }); const [agentProviderLists, setAgentProviderLists] = useState>>({}); const [agentProviderLoadingAppType, setAgentProviderLoadingAppType] = useState(null); diff --git a/src/renderer/components/cowork/AgentEnvironmentSetup.tsx b/src/renderer/components/cowork/AgentEnvironmentSetup.tsx index 924689c4..3738a609 100644 --- a/src/renderer/components/cowork/AgentEnvironmentSetup.tsx +++ b/src/renderer/components/cowork/AgentEnvironmentSetup.tsx @@ -106,6 +106,14 @@ const AGENT_SETUP_TARGETS: AgentSetupTarget[] = [ primary: false, recommended: false, }, + { + engine: CoworkAgentEngine.KimiCli, + appType: 'kimi', + labelKey: 'coworkAgentEngineKimiCli', + hintKey: 'coworkAgentEngineKimiCliHint', + primary: false, + recommended: false, + }, ]; const RECOMMENDED_APP_TYPES = AGENT_SETUP_TARGETS diff --git a/src/renderer/types/cowork.ts b/src/renderer/types/cowork.ts index f4cc78b9..097a6491 100644 --- a/src/renderer/types/cowork.ts +++ b/src/renderer/types/cowork.ts @@ -263,7 +263,7 @@ export interface CoworkConfigResult { error?: string; } -export type CliAppType = 'claude' | 'codex' | 'hermes' | 'openclaw' | 'opencode' | 'grok' | 'qwen' | 'deepseek_tui'; +export type CliAppType = 'claude' | 'codex' | 'hermes' | 'openclaw' | 'opencode' | 'grok' | 'qwen' | 'deepseek_tui' | 'kimi'; export interface CliAppConfigSnapshot { appType: CliAppType; diff --git a/src/renderer/types/electron.d.ts b/src/renderer/types/electron.d.ts index 2dcbe1b0..ee459f49 100644 --- a/src/renderer/types/electron.d.ts +++ b/src/renderer/types/electron.d.ts @@ -126,7 +126,7 @@ type CoworkConfigUpdate = Partial>; -type CliAppType = 'claude' | 'codex' | 'hermes' | 'openclaw' | 'opencode' | 'grok' | 'qwen' | 'deepseek_tui'; +type CliAppType = 'claude' | 'codex' | 'hermes' | 'openclaw' | 'opencode' | 'grok' | 'qwen' | 'deepseek_tui' | 'kimi'; interface CliAppConfigSnapshot { appType: CliAppType; From 9f03b9f135ad8a0409da5171d7368bfb63b4d16a Mon Sep 17 00:00:00 2001 From: Zhicong Lian Date: Wed, 3 Jun 2026 18:01:11 +0800 Subject: [PATCH 06/13] feat(cowork): default *ConfigSource to LocalCli + complete Kimi engine wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes that together make the 'I already configured the local CLI, just use it' flow work for every CLI engine, with Kimi CLI as the new addition. 1. LocalCli-by-default for unconfigured engines Every CLI engine (ClaudeCode / Codex / Hermes / OpenCode / QwenCode / DeepSeekTui / KimiCli) used to default to WesightModel, which meant a fresh install or a user who never touched Settings saw 'Please configure models in settings first' even when their ~/.{claude,kimi,...}/config already had a perfectly good model. WeSight's local-provider sync (externalAgentProviderStore. syncConfiguredProviders) already auto-loads the user's local CLI config into SQLite on first listProviders call and auto-marks the first/best provider as is_current=1 — the only thing missing was the default source value: * main/coworkStore.ts: new DEFAULT_EXTERNAL_AGENT_CONFIG_SOURCE_FOR_ NEW_INSTALL = LocalCli. getConfig() uses cfg.has(key) to distinguish 'never stored' (use LocalCli) from 'user explicitly stored a value' (respect it, even if empty). The 7 normalizers still fall back to WesightModel on garbage values — that is the 'user stored a corrupt value' safety net, distinct from 'new-install default'. * renderer/store/slices/coworkSlice.ts: initialState.config flips 7 *ConfigSource defaults to LocalCli. main's getConfig() over- writes these for any user with a stored value, so existing users who already chose WesightModel are untouched. 2. Complete Kimi CLI engine wiring (issue #34) The Kimi scaffold landed in 5095db0 / 973ee96, but the runtime path was a stub. This commit fills in the six per-engine branches that ExternalCliRuntimeAdapter has for every other CLI: * getCommandName / getConfigSource / getSelectedProviderForLocalCli all route Kimi to kimi * buildCommandArgs: kimi --print --output-format stream-json --work-dir [--yolo|--plan] --model --prompt * applyKimiCliRuntimeConfig: injects KIMI_API_KEY / KIMI_BASE_URL / KIMI_MODEL_NAME via the existing buildKimiCliRuntimeEnv * handleKimiCliEvent: routes stream-json through parseKimiCliJsonLine / normalizeKimiCliCliEvent * runTurn: routes Kimi WesightModel path to applyKimiCliRuntimeConfig Renderer plumbing: * CoworkModelSelector.resolveLocalCliAppType returns 'kimi' when the user is on LocalCli + Kimi * CoworkView.getCliAppTypeForEngine and getModelContextLabel return 'kimi' / 'coworkAgentConfigSourceLocalCli' respectively * coworkStudio.getConfigSource returns config.kimiCliConfigSource (the TODO(issue #34) fallback is gone) Main plumbing: * applyExternalAgentConfigSourceForEngine routes Kimi * isExternalAgentProviderAppType now includes 'kimi' * externalAgentProviderStore.syncKimiLiveProviders seeds SQLite from readKimiCliLocalConfig() — one row per [models.] entry, is_current=1 for the default_model row Type plumbing: * renderer/types/cowork.ts and types/electron.d.ts add kimiCliConfigSource + kimiCliPermissionMode to CoworkConfig Co-Authored-By: Claude Opus 4.8 --- package.json | 1 + src/main/coworkStore.ts | 84 +++++++++- .../agentEngine/externalCliRuntimeAdapter.ts | 115 +++++++++++++ src/main/libs/externalAgentProviderStore.ts | 83 ++++++++++ src/main/libs/kimiCliConfigReader.ts | 156 ++++++++++++++++++ src/main/main.ts | 5 + .../components/cowork/CoworkModelSelector.tsx | 6 + src/renderer/components/cowork/CoworkView.tsx | 7 + src/renderer/store/slices/coworkSlice.ts | 19 ++- src/renderer/types/cowork.ts | 5 + src/renderer/types/electron.d.ts | 3 + src/renderer/utils/coworkStudio.test.ts | 2 + src/renderer/utils/coworkStudio.ts | 6 +- 13 files changed, 476 insertions(+), 16 deletions(-) create mode 100644 src/main/libs/kimiCliConfigReader.ts diff --git a/package.json b/package.json index 6657973c..48f61811 100644 --- a/package.json +++ b/package.json @@ -210,6 +210,7 @@ "prettier": "^3.8.1", "rimraf": "^5.0.5", "rss-parser": "^3.13.0", + "smol-toml": "^1.6.1", "tailwindcss": "^3.4.1", "typescript": "^5.7.3", "vite": "^5.1.4", diff --git a/src/main/coworkStore.ts b/src/main/coworkStore.ts index ba44f5a7..d46a0369 100644 --- a/src/main/coworkStore.ts +++ b/src/main/coworkStore.ts @@ -25,8 +25,11 @@ import { isCoworkSessionKind, isDeepSeekTuiPermissionMode, isExternalAgentConfigSource, + isKimiCliPermissionMode, isOpenCodePermissionMode, isQwenCodePermissionMode, + KimiCliPermissionMode, + type KimiCliPermissionMode as KimiCliPermissionModeType, OpenCodePermissionMode, type OpenCodePermissionMode as OpenCodePermissionModeType, QwenCodePermissionMode, @@ -64,12 +67,20 @@ const DEFAULT_MEMORY_LLM_JUDGE_ENABLED = false; const DEFAULT_MEMORY_GUARD_LEVEL: CoworkMemoryGuardLevel = 'strict'; const DEFAULT_MEMORY_USER_MEMORIES_MAX_ITEMS = 12; const DEFAULT_EXTERNAL_AGENT_CONFIG_SOURCE: ExternalAgentConfigSourceType = ExternalAgentConfigSource.WesightModel; +// Used when a *ConfigSource row is missing entirely from SQLite +// (i.e. fresh install, or an engine the user has never touched in +// Settings). Distinct from the stored-value-fallback above so we can +// respect existing user choices while making the no-config default +// "use the local CLI you've already configured" — see issue #34. +const DEFAULT_EXTERNAL_AGENT_CONFIG_SOURCE_FOR_NEW_INSTALL: ExternalAgentConfigSourceType = ExternalAgentConfigSource.LocalCli; const OPENCLAW_GLOBAL_CONFIG_PATH = path.join(os.homedir(), '.openclaw', 'openclaw.json'); const HERMES_GLOBAL_CONFIG_PATH = path.join(os.homedir(), '.hermes', 'config.yaml'); const DEFAULT_CLAUDE_CODE_PERMISSION_MODE: ClaudeCodePermissionModeType = ClaudeCodePermissionMode.BypassPermissions; const DEFAULT_OPENCODE_PERMISSION_MODE: OpenCodePermissionModeType = OpenCodePermissionMode.Auto; const DEFAULT_QWEN_CODE_PERMISSION_MODE: QwenCodePermissionModeType = QwenCodePermissionMode.Auto; const DEFAULT_DEEPSEEK_TUI_PERMISSION_MODE: DeepSeekTuiPermissionModeType = DeepSeekTuiPermissionMode.Auto; +const DEFAULT_KIMI_CLI_PERMISSION_MODE: KimiCliPermissionModeType = KimiCliPermissionMode.Auto; +const DEFAULT_KIMI_CLI_CONFIG_SOURCE: ExternalAgentConfigSourceType = ExternalAgentConfigSource.LocalCli; const MIN_MEMORY_USER_MEMORIES_MAX_ITEMS = 1; const MAX_MEMORY_USER_MEMORIES_MAX_ITEMS = 60; const MEMORY_NEAR_DUPLICATE_MIN_SCORE = 0.82; @@ -515,6 +526,20 @@ function normalizeDeepSeekTuiPermissionMode(value?: string | null): DeepSeekTuiP return DEFAULT_DEEPSEEK_TUI_PERMISSION_MODE; } +function normalizeKimiCliPermissionMode(value?: string | null): KimiCliPermissionModeType { + if (isKimiCliPermissionMode(value)) { + return value; + } + return DEFAULT_KIMI_CLI_PERMISSION_MODE; +} + +function normalizeKimiCliConfigSource(value?: string | null): ExternalAgentConfigSourceType { + if (isExternalAgentConfigSource(value)) { + return value; + } + return DEFAULT_KIMI_CLI_CONFIG_SOURCE; +} + export interface CoworkMessageMetadata { toolName?: string; toolInput?: Record; @@ -663,6 +688,8 @@ export interface CoworkConfig { qwenCodePermissionMode: QwenCodePermissionModeType; deepseekTuiConfigSource: ExternalAgentConfigSourceType; deepseekTuiPermissionMode: DeepSeekTuiPermissionModeType; + kimiCliConfigSource: ExternalAgentConfigSourceType; + kimiCliPermissionMode: KimiCliPermissionModeType; memoryEnabled: boolean; memoryImplicitUpdateEnabled: boolean; memoryLlmJudgeEnabled: boolean; @@ -686,6 +713,8 @@ export type CoworkConfigUpdate = Partial [r.key, r.value])); + // *ConfigSource fields: if the SQLite row is missing entirely (fresh + // install, or the user has never touched Settings for this engine) + // we fall back to LocalCli so that the user does not have to flip a + // toggle to get their pre-configured local CLI model picked up. + // If the row IS present, even with an empty string, we respect the + // stored value (normalized) — that is the explicit user choice. + const readStoredConfigSource = ( + key: string, + ): ExternalAgentConfigSourceType => { + if (!cfg.has(key)) return DEFAULT_EXTERNAL_AGENT_CONFIG_SOURCE_FOR_NEW_INSTALL; + return normalizeExternalAgentConfigSource(cfg.get(key)); + }; + return { workingDirectory: cfg.get('workingDirectory') || getDefaultWorkingDirectory(), systemPrompt: getDefaultSystemPrompt(), executionMode: 'local' as CoworkExecutionMode, agentEngine: normalizeCoworkAgentEngineValue(cfg.get('agentEngine')), openclawConfigSource: normalizeOpenClawConfigSource(cfg.get('openclawConfigSource')), - claudeCodeConfigSource: normalizeExternalAgentConfigSource(cfg.get('claudeCodeConfigSource')), + claudeCodeConfigSource: readStoredConfigSource('claudeCodeConfigSource'), claudeCodePermissionMode: normalizeClaudeCodePermissionMode(cfg.get('claudeCodePermissionMode')), - codexConfigSource: normalizeExternalAgentConfigSource(cfg.get('codexConfigSource')), + codexConfigSource: readStoredConfigSource('codexConfigSource'), hermesConfigSource: normalizeHermesConfigSource(cfg.get('hermesConfigSource')), - opencodeConfigSource: normalizeExternalAgentConfigSource(cfg.get('opencodeConfigSource')), + opencodeConfigSource: readStoredConfigSource('opencodeConfigSource'), opencodePermissionMode: normalizeOpenCodePermissionMode(cfg.get('opencodePermissionMode')), - qwenCodeConfigSource: normalizeExternalAgentConfigSource(cfg.get('qwenCodeConfigSource')), + qwenCodeConfigSource: readStoredConfigSource('qwenCodeConfigSource'), qwenCodePermissionMode: normalizeQwenCodePermissionMode(cfg.get('qwenCodePermissionMode')), - deepseekTuiConfigSource: normalizeExternalAgentConfigSource(cfg.get('deepseekTuiConfigSource')), + deepseekTuiConfigSource: readStoredConfigSource('deepseekTuiConfigSource'), deepseekTuiPermissionMode: normalizeDeepSeekTuiPermissionMode(cfg.get('deepseekTuiPermissionMode')), + kimiCliConfigSource: readStoredConfigSource('kimiCliConfigSource'), + kimiCliPermissionMode: normalizeKimiCliPermissionMode(cfg.get('kimiCliPermissionMode')), memoryEnabled: parseBooleanConfig(cfg.get('memoryEnabled'), DEFAULT_MEMORY_ENABLED), memoryImplicitUpdateEnabled: parseBooleanConfig( cfg.get('memoryImplicitUpdateEnabled'), @@ -1780,6 +1826,34 @@ export class CoworkStore { .run(normalizeDeepSeekTuiPermissionMode(config.deepseekTuiPermissionMode), now); } + if (config.kimiCliConfigSource !== undefined) { + this.db + .prepare( + ` + INSERT INTO cowork_config (key, value, updated_at) + VALUES ('kimiCliConfigSource', ?, ?) + ON CONFLICT(key) DO UPDATE SET + value = excluded.value, + updated_at = excluded.updated_at + `, + ) + .run(normalizeKimiCliConfigSource(config.kimiCliConfigSource), now); + } + + if (config.kimiCliPermissionMode !== undefined) { + this.db + .prepare( + ` + INSERT INTO cowork_config (key, value, updated_at) + VALUES ('kimiCliPermissionMode', ?, ?) + ON CONFLICT(key) DO UPDATE SET + value = excluded.value, + updated_at = excluded.updated_at + `, + ) + .run(normalizeKimiCliPermissionMode(config.kimiCliPermissionMode), now); + } + if (config.memoryEnabled !== undefined) { this.db .prepare( diff --git a/src/main/libs/agentEngine/externalCliRuntimeAdapter.ts b/src/main/libs/agentEngine/externalCliRuntimeAdapter.ts index c8d21e07..2ef7387f 100644 --- a/src/main/libs/agentEngine/externalCliRuntimeAdapter.ts +++ b/src/main/libs/agentEngine/externalCliRuntimeAdapter.ts @@ -12,6 +12,7 @@ import { CoworkAgentEngine, ExternalAgentConfigSource, isClaudeCodePermissionMode, + KimiCliPermissionMode, OpenCodePermissionMode, QwenCodePermissionMode, } from '../../../shared/cowork/constants'; @@ -36,6 +37,8 @@ import { normalizeOpenCodeCliEvent } from '../openCodeCliEvent'; import { buildOpenCodeRuntimeConfigContent } from '../openCodeConfig'; import { normalizeQwenCodeCliEvent } from '../qwenCodeCliEvent'; import { buildQwenCodeRuntimeEnv, qwenAuthTypeForCoworkConfig } from '../qwenCodeConfig'; +import { parseKimiCliJsonLine } from '../kimiCliCliEvent'; +import { buildKimiCliRuntimeEnv } from '../kimiCliConfig'; import type { CoworkContinueOptions, CoworkRuntime, @@ -284,6 +287,9 @@ export class ExternalCliRuntimeAdapter extends EventEmitter implements CoworkRun if (this.engine === CoworkAgentEngine.QwenCode && this.getConfigSource() === ExternalAgentConfigSource.WesightModel) { this.applyQwenCodeRuntimeConfig(env, apiConfigOverride); } + if (this.engine === CoworkAgentEngine.KimiCli && this.getConfigSource() === ExternalAgentConfigSource.WesightModel) { + this.applyKimiCliRuntimeConfig(env, apiConfigOverride); + } const command = this.getCommandName(); const args = this.buildCommandArgs( cwd, @@ -518,6 +524,36 @@ export class ExternalCliRuntimeAdapter extends EventEmitter implements CoworkRun return args; } + if (this.engine === CoworkAgentEngine.KimiCli) { + const kimiConfig = this.store.getConfig(); + const permissionMode = kimiConfig.kimiCliPermissionMode; + const args: string[] = [ + '--print', + '--output-format', 'stream-json', + '--work-dir', cwd, + ]; + if (permissionMode === KimiCliPermissionMode.Auto) { + args.push('--yolo'); + } else { + args.push('--plan'); + } + let model: string | null = null; + if (this.getConfigSource() === ExternalAgentConfigSource.WesightModel) { + const resolved = resolveRawApiConfig(apiConfigOverride); + if (resolved.config) { + model = resolved.config.model; + } + } else { + const providerModel = selectedProvider?.summary.model?.trim(); + if (providerModel) model = providerModel; + } + if (model) { + args.push('--model', model); + } + args.push('--prompt', prompt); + return args; + } + if (this.engine === CoworkAgentEngine.GrokBuild) { const promptWithFiles = imagePaths.length > 0 ? `${prompt}\n\nAttached local files:\n${imagePaths.map((imagePath) => imagePath).join('\n')}` @@ -598,6 +634,9 @@ export class ExternalCliRuntimeAdapter extends EventEmitter implements CoworkRun if (this.engine === CoworkAgentEngine.QwenCode) { return config.qwenCodeConfigSource; } + if (this.engine === CoworkAgentEngine.KimiCli) { + return config.kimiCliConfigSource; + } if (this.engine === CoworkAgentEngine.GrokBuild) { return ExternalAgentConfigSource.LocalCli; } @@ -620,6 +659,9 @@ export class ExternalCliRuntimeAdapter extends EventEmitter implements CoworkRun if (this.engine === CoworkAgentEngine.QwenCode) { return this.getCurrentProvider?.('qwen') ?? null; } + if (this.engine === CoworkAgentEngine.KimiCli) { + return this.getCurrentProvider?.('kimi') ?? null; + } if (this.engine === CoworkAgentEngine.GrokBuild) { return this.getCurrentProvider?.('grok') ?? null; } @@ -647,11 +689,21 @@ export class ExternalCliRuntimeAdapter extends EventEmitter implements CoworkRun Object.assign(env, buildQwenCodeRuntimeEnv(resolved.config)); } + private applyKimiCliRuntimeConfig( + env: Record, + apiConfigOverride?: ApiConfigOverride, + ): void { + const resolved = resolveRawApiConfig(apiConfigOverride); + if (!resolved.config) return; + Object.assign(env, buildKimiCliRuntimeEnv(resolved.config)); + } + private getCommandName(): string { if (this.engine === CoworkAgentEngine.ClaudeCode) return 'claude'; if (this.engine === CoworkAgentEngine.Codex) return 'codex'; if (this.engine === CoworkAgentEngine.OpenCode) return 'opencode'; if (this.engine === CoworkAgentEngine.GrokBuild) return 'grok'; + if (this.engine === CoworkAgentEngine.KimiCli) return 'kimi'; return 'qwen'; } @@ -902,6 +954,8 @@ export class ExternalCliRuntimeAdapter extends EventEmitter implements CoworkRun this.handleGrokBuildEvent(active, event); } else if (this.engine === CoworkAgentEngine.QwenCode) { this.handleQwenCodeEvent(active, event); + } else if (this.engine === CoworkAgentEngine.KimiCli) { + this.handleKimiCliEvent(active, event); } else { this.handleClaudeCliEvent(active, event); } @@ -1240,6 +1294,67 @@ export class ExternalCliRuntimeAdapter extends EventEmitter implements CoworkRun } } + private handleKimiCliEvent(active: ActiveCliSession, event: unknown): void { + // Kimi CLI stream-json is a superset of Claude Code's; we feed + // raw JSON objects through normalizeKimiCliCliEvent. The current + // normalizer handles `result` and a flat `text/content/message/ + // delta` field; richer Claude-Code-style tool_use / + // content_block_delta events land in `none` for now and are + // tracked under issue #34. + const normalized = (() => { + if (typeof event === 'string') { + return parseKimiCliJsonLine(event) ?? { kind: 'none' as const, sessionId: null }; + } + return null; + })(); + this.applyKimiNormalizedEvent(active, normalized); + } + + private applyKimiNormalizedEvent( + active: ActiveCliSession, + normalized: { kind: string; sessionId: string | null; text?: string; replace?: boolean; toolName?: string; input?: Record; output?: string; isError?: boolean; message?: string }, + ): void { + if (normalized.sessionId) { + active.cliSessionId = normalized.sessionId; + this.store.updateSession(active.sessionId, { claudeSessionId: normalized.sessionId }); + } + switch (normalized.kind) { + case 'assistant_text': + if (normalized.replace) { + this.replaceAssistant(active, normalized.text ?? '', true); + } else { + this.appendAssistant(active, normalized.text ?? ''); + } + break; + case 'tool_use': + this.addToolMessage(active.sessionId, { + type: 'tool_use', + content: `Using tool: ${normalized.toolName ?? 'unknown'}`, + metadata: { + toolName: normalized.toolName, + toolInput: normalized.input, + }, + }); + break; + case 'tool_result': + this.addToolMessage(active.sessionId, { + type: 'tool_result', + content: normalized.output ?? '', + metadata: { + toolName: normalized.toolName, + toolResult: normalized.output, + isError: normalized.isError ?? false, + }, + }); + break; + case 'error': + this.handleError(active.sessionId, normalized.message ?? 'Kimi CLI error'); + break; + case 'none': + break; + } + } + private handleGrokBuildEvent(active: ActiveCliSession, event: unknown): void { if (!isRecord(event)) return; const type = String(event.type ?? event.event ?? event.kind ?? '').toLowerCase(); diff --git a/src/main/libs/externalAgentProviderStore.ts b/src/main/libs/externalAgentProviderStore.ts index 839206fe..483405df 100644 --- a/src/main/libs/externalAgentProviderStore.ts +++ b/src/main/libs/externalAgentProviderStore.ts @@ -48,6 +48,7 @@ import { settingsConfigFromQwenCodeRecord, summarizeQwenCodeSettingsConfig, } from './qwenCodeConfig'; +import { readKimiCliLocalConfig } from './kimiCliConfigReader'; export type ExternalAgentProviderAppType = CliAppType; @@ -119,6 +120,7 @@ const OPENCLAW_APP_TYPE: ExternalAgentProviderAppType = 'openclaw'; const OPENCODE_APP_TYPE: ExternalAgentProviderAppType = 'opencode'; const GROK_APP_TYPE: ExternalAgentProviderAppType = 'grok'; const QWEN_APP_TYPE: ExternalAgentProviderAppType = 'qwen'; +const KIMI_APP_TYPE: ExternalAgentProviderAppType = 'kimi'; const DEEPSEEK_TUI_APP_TYPE: ExternalAgentProviderAppType = 'deepseek_tui'; const INTERNAL_META_KEY = '__wesightProviderMeta'; @@ -512,6 +514,7 @@ export const appTypeFromEngine = (engine: string): ExternalAgentProviderAppType if (engine === 'grok_build') return GROK_APP_TYPE; if (engine === 'qwen_code') return QWEN_APP_TYPE; if (engine === 'deepseek_tui') return DEEPSEEK_TUI_APP_TYPE; + if (engine === 'kimi_cli') return KIMI_APP_TYPE; return null; }; @@ -921,6 +924,10 @@ export class ExternalAgentProviderStore { this.syncDeepSeekTuiLiveProviders(); return; } + if (appType === KIMI_APP_TYPE) { + this.syncKimiLiveProviders(); + return; + } if (appType === GROK_APP_TYPE) { this.importLiveProviderIfEmpty(appType); return; @@ -1125,6 +1132,82 @@ export class ExternalAgentProviderStore { } } + private syncKimiLiveProviders(): void { + const local = readKimiCliLocalConfig(); + if (!local.exists) { + this.importLiveProviderIfEmpty(KIMI_APP_TYPE); + return; + } + // Build one provider per `models.` entry. If no `[models]` + // table is present, fall back to a single provider seeded from + // `default_model` (or the engine's hard-coded default). + type KimiRecord = { id: string; name: string; model: string; isCurrent: boolean; settingsConfig: Record }; + const records: KimiRecord[] = local.models.length > 0 + ? local.models.map((m, idx) => { + const id = `kimi-${m.name.replace(/[^a-z0-9_-]+/gi, '-').toLowerCase()}`; + const displayName = m.displayName ?? m.name; + const modelId = m.model ?? m.name; + return { + id, + name: displayName, + model: modelId, + isCurrent: m.name === local.defaultModel, + settingsConfig: { + type: 'kimi', + provider: m.provider ?? undefined, + model: modelId, + } as Record, + }; + }) + : (() => { + const fallbackId = `kimi-${(local.defaultModel ?? 'kimi-k2.5').replace(/[^a-z0-9_-]+/gi, '-').toLowerCase()}`; + return [{ + id: fallbackId, + name: local.defaultModel ?? 'kimi-k2.5', + model: local.defaultModel ?? 'kimi-k2.5', + isCurrent: true, + settingsConfig: { type: 'kimi', model: local.defaultModel ?? 'kimi-k2.5' } as Record, + }]; + })(); + + this.db + .prepare('DELETE FROM external_agent_providers WHERE app_type = ? AND category = ?') + .run(KIMI_APP_TYPE, 'local'); + const now = Date.now(); + for (const record of records) { + this.db + .prepare( + ` + INSERT INTO external_agent_providers ( + id, app_type, name, settings_config, category, is_current, created_at, updated_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(id, app_type) DO UPDATE SET + name = excluded.name, + settings_config = excluded.settings_config, + category = excluded.category, + is_current = excluded.is_current, + updated_at = excluded.updated_at + `, + ) + .run( + record.id, + KIMI_APP_TYPE, + record.name, + JSON.stringify(record.settingsConfig), + 'local', + record.isCurrent ? 1 : 0, + now, + now, + ); + } + if (!records.some((record) => record.isCurrent) && records[0]) { + this.db + .prepare('UPDATE external_agent_providers SET is_current = 1 WHERE app_type = ? AND id = ?') + .run(KIMI_APP_TYPE, records[0].id); + } + } + private syncDeepSeekTuiLiveProviders(): void { const configPath = getDeepSeekTuiConfigPath(); if (!fs.existsSync(configPath)) { diff --git a/src/main/libs/kimiCliConfigReader.ts b/src/main/libs/kimiCliConfigReader.ts new file mode 100644 index 00000000..3e02f989 --- /dev/null +++ b/src/main/libs/kimiCliConfigReader.ts @@ -0,0 +1,156 @@ +/** + * Read Kimi CLI's local configuration file (~/.kimi/config.toml). + * + * Used by the Cowork KimiCli engine to surface the locally configured + * model and provider in the WeSight UI without forcing users to also + * configure a separate model in the Models page (issue #34 follow-up). + * + * Behaviour mirrors the Qwen Code / DeepSeek-TUI local-config reader + * pattern: the file is parsed leniently, missing fields are tolerated, + * and WeSight never writes back to the user's `~/.kimi/config.toml` + * (see issue #33 — credential isolation). + */ + +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { parse as parseToml } from 'smol-toml'; + +const KIMI_CONFIG_PATH = path.join(os.homedir(), '.kimi', 'config.toml'); + +export interface KimiCliLocalProvider { + name: string; + type?: string; + apiKey?: string; + baseUrl?: string; + model?: string; +} + +export interface KimiCliLocalModel { + name: string; + provider: string | null; + model: string | null; + displayName: string | null; +} + +export interface KimiCliLocalConfig { + configPath: string; + exists: boolean; + model: string | null; + defaultModel: string | null; + defaultYolo: boolean; + defaultPlanMode: boolean; + defaultThinking: boolean; + providers: KimiCliLocalProvider[]; + models: KimiCliLocalModel[]; + raw: Record | null; +} + +const KIMI_DEFAULT_MODEL = 'kimi-k2.5'; + +const isRecord = (value: unknown): value is Record => ( + Boolean(value && typeof value === 'object' && !Array.isArray(value)) +); + +const getString = (value: unknown): string | null => { + if (typeof value !== 'string') return null; + const trimmed = value.trim(); + return trimmed ? trimmed : null; +}; + +const collectProviderTable = ( + raw: Record, + tableName: string, +): KimiCliLocalProvider[] => { + const table = raw[tableName]; + if (!isRecord(table)) return []; + // Kimi CLI provider tables can be either { default = {...} } or + // { = { type, api_key, base_url, model } }. Accept both. + const records: KimiCliLocalProvider[] = []; + for (const [name, value] of Object.entries(table)) { + if (!isRecord(value)) continue; + if (name === 'default') continue; // selected type marker, not a provider + records.push({ + name, + type: getString(value.type) ?? getString(value.provider) ?? undefined, + apiKey: getString(value.api_key) ?? getString(value.apiKey) ?? undefined, + baseUrl: getString(value.base_url) ?? getString(value.baseUrl) ?? undefined, + model: getString(value.model) ?? undefined, + }); + } + return records; +}; + +const collectModelTable = (raw: Record): KimiCliLocalModel[] => { + const table = raw.models; + if (!isRecord(table)) return []; + const records: KimiCliLocalModel[] = []; + for (const [name, value] of Object.entries(table)) { + if (!isRecord(value)) continue; + records.push({ + name, + provider: getString(value.provider) ?? null, + model: getString(value.model) ?? null, + displayName: getString(value.display_name) ?? getString(value.displayName) ?? null, + }); + } + return records; +}; + +export const readKimiCliLocalConfig = ( + configPath: string = KIMI_CONFIG_PATH, +): KimiCliLocalConfig => { + if (!fs.existsSync(configPath)) { + return { + configPath, + exists: false, + model: null, + defaultModel: KIMI_DEFAULT_MODEL, + defaultYolo: false, + defaultPlanMode: false, + defaultThinking: false, + providers: [], + models: [], + raw: null, + }; + } + let raw: Record; + try { + const text = fs.readFileSync(configPath, 'utf-8'); + raw = parseToml(text) as Record; + } catch { + return { + configPath, + exists: true, + model: null, + defaultModel: KIMI_DEFAULT_MODEL, + defaultYolo: false, + defaultPlanMode: false, + defaultThinking: false, + providers: [], + models: [], + raw: null, + }; + } + + const modelFromTopLevel = getString(raw.model); + const defaultModelKey = getString(raw.default_model) ?? getString(raw.defaultModel); + const providers = [ + ...collectProviderTable(raw, 'providers'), + ...collectProviderTable(raw, 'provider'), + ]; + const models = collectModelTable(raw); + + return { + configPath, + exists: true, + model: defaultModelKey ?? modelFromTopLevel ?? KIMI_DEFAULT_MODEL, + defaultModel: defaultModelKey ?? KIMI_DEFAULT_MODEL, + defaultYolo: raw.default_yolo === true, + defaultPlanMode: raw.default_plan_mode === true, + defaultThinking: raw.default_thinking === true, + providers, + models, + raw, + }; +}; diff --git a/src/main/main.ts b/src/main/main.ts index 9c9fecee..ff6595ee 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1530,6 +1530,10 @@ const applyExternalAgentConfigSourceForEngine = (engine: CoworkAgentEngine): voi } if (engine === CoworkAgentEngineValue.DeepSeekTui) { applyExternalAgentConfigForEngine(engine, config.deepseekTuiConfigSource); + return; + } + if (engine === CoworkAgentEngineValue.KimiCli) { + applyExternalAgentConfigForEngine(engine, config.kimiCliConfigSource); } }; @@ -1598,6 +1602,7 @@ const isExternalAgentProviderAppType = (value: unknown): value is ExternalAgentP || value === 'grok' || value === 'qwen' || value === 'deepseek_tui' + || value === 'kimi' ); const getMergedExternalAgentEnvironmentSnapshot = () => { diff --git a/src/renderer/components/cowork/CoworkModelSelector.tsx b/src/renderer/components/cowork/CoworkModelSelector.tsx index 4d20ac4f..f82ea6f5 100644 --- a/src/renderer/components/cowork/CoworkModelSelector.tsx +++ b/src/renderer/components/cowork/CoworkModelSelector.tsx @@ -72,6 +72,12 @@ const resolveLocalCliAppType = (config: RootState['cowork']['config']): External ) { return 'deepseek_tui'; } + if ( + config.agentEngine === CoworkAgentEngine.KimiCli + && config.kimiCliConfigSource === ExternalAgentConfigSource.LocalCli + ) { + return 'kimi'; + } return null; }; diff --git a/src/renderer/components/cowork/CoworkView.tsx b/src/renderer/components/cowork/CoworkView.tsx index a8993f12..3de0d8a8 100644 --- a/src/renderer/components/cowork/CoworkView.tsx +++ b/src/renderer/components/cowork/CoworkView.tsx @@ -103,6 +103,7 @@ const getCliAppTypeForEngine = (engine: CoworkAgentEngine): ExternalAgentProvide if (engine === CoworkAgentEngine.GrokBuild) return 'grok'; if (engine === CoworkAgentEngine.QwenCode) return 'qwen'; if (engine === CoworkAgentEngine.DeepSeekTui) return 'deepseek_tui'; + if (engine === CoworkAgentEngine.KimiCli) return 'kimi'; return null; }; @@ -784,6 +785,12 @@ const CoworkView: React.FC = ({ onRequestAppSettings, onShowSki ) { return i18nService.t('coworkAgentConfigSourceLocalCli'); } + if ( + selectedRuntimeEngine === CoworkAgentEngine.KimiCli + && config.kimiCliConfigSource === ExternalAgentConfigSource.LocalCli + ) { + return i18nService.t('coworkAgentConfigSourceLocalCli'); + } if (selectedModel?.name) { return selectedModel.provider ? `${selectedModel.name} · ${selectedModel.provider}` diff --git a/src/renderer/store/slices/coworkSlice.ts b/src/renderer/store/slices/coworkSlice.ts index 54170229..6c3a8d88 100644 --- a/src/renderer/store/slices/coworkSlice.ts +++ b/src/renderer/store/slices/coworkSlice.ts @@ -5,6 +5,7 @@ import { DeepSeekTuiPermissionMode, DefaultCoworkAgentEngine, ExternalAgentConfigSource, + KimiCliPermissionMode, OpenCodePermissionMode, QwenCodePermissionMode, } from '@shared/cowork/constants'; @@ -61,16 +62,22 @@ const initialState: CoworkState = { executionMode: 'local', agentEngine: DefaultCoworkAgentEngine, openclawConfigSource: ExternalAgentConfigSource.LocalCli, - claudeCodeConfigSource: ExternalAgentConfigSource.WesightModel, + // Default all CLI engines to LocalCli so that pre-configured + // local CLI models are picked up automatically (issue #34). + // main's getConfig() will overwrite these with stored values + // for any user who has already picked a source. + claudeCodeConfigSource: ExternalAgentConfigSource.LocalCli, claudeCodePermissionMode: ClaudeCodePermissionMode.BypassPermissions, - codexConfigSource: ExternalAgentConfigSource.WesightModel, - hermesConfigSource: ExternalAgentConfigSource.WesightModel, - opencodeConfigSource: ExternalAgentConfigSource.WesightModel, + codexConfigSource: ExternalAgentConfigSource.LocalCli, + hermesConfigSource: ExternalAgentConfigSource.LocalCli, + opencodeConfigSource: ExternalAgentConfigSource.LocalCli, opencodePermissionMode: OpenCodePermissionMode.Auto, - qwenCodeConfigSource: ExternalAgentConfigSource.WesightModel, + qwenCodeConfigSource: ExternalAgentConfigSource.LocalCli, qwenCodePermissionMode: QwenCodePermissionMode.Auto, - deepseekTuiConfigSource: ExternalAgentConfigSource.WesightModel, + deepseekTuiConfigSource: ExternalAgentConfigSource.LocalCli, deepseekTuiPermissionMode: DeepSeekTuiPermissionMode.Auto, + kimiCliConfigSource: ExternalAgentConfigSource.LocalCli, + kimiCliPermissionMode: KimiCliPermissionMode.Auto, memoryEnabled: true, memoryImplicitUpdateEnabled: true, memoryLlmJudgeEnabled: false, diff --git a/src/renderer/types/cowork.ts b/src/renderer/types/cowork.ts index 097a6491..f8c4ee0c 100644 --- a/src/renderer/types/cowork.ts +++ b/src/renderer/types/cowork.ts @@ -4,6 +4,7 @@ import type { CoworkSessionKind, DeepSeekTuiPermissionMode, ExternalAgentConfigSource, + KimiCliPermissionMode, OpenCodePermissionMode, QwenCodePermissionMode, } from '@shared/cowork/constants'; @@ -101,6 +102,8 @@ export interface CoworkConfig { qwenCodePermissionMode: QwenCodePermissionMode; deepseekTuiConfigSource: ExternalAgentConfigSource; deepseekTuiPermissionMode: DeepSeekTuiPermissionMode; + kimiCliConfigSource: ExternalAgentConfigSource; + kimiCliPermissionMode: KimiCliPermissionMode; memoryEnabled: boolean; memoryImplicitUpdateEnabled: boolean; memoryLlmJudgeEnabled: boolean; @@ -124,6 +127,8 @@ export type CoworkConfigUpdate = Partial ({ qwenCodePermissionMode: 'auto', deepseekTuiConfigSource: ExternalAgentConfigSource.WesightModel, deepseekTuiPermissionMode: 'auto', + kimiCliConfigSource: ExternalAgentConfigSource.WesightModel, + kimiCliPermissionMode: 'auto', memoryEnabled: true, memoryImplicitUpdateEnabled: true, memoryLlmJudgeEnabled: false, diff --git a/src/renderer/utils/coworkStudio.ts b/src/renderer/utils/coworkStudio.ts index 6b7ecea5..21b7c14a 100644 --- a/src/renderer/utils/coworkStudio.ts +++ b/src/renderer/utils/coworkStudio.ts @@ -160,11 +160,7 @@ const getConfigSource = (config: CoworkConfig): ExternalAgentConfigSource | null if (config.agentEngine === CoworkAgentEngine.GrokBuild) return ExternalAgentConfigSource.LocalCli; if (config.agentEngine === CoworkAgentEngine.QwenCode) return config.qwenCodeConfigSource; if (config.agentEngine === CoworkAgentEngine.DeepSeekTui) return config.deepseekTuiConfigSource; - if (config.agentEngine === CoworkAgentEngine.KimiCli) { - // TODO(issue #34): introduce config.kimiCliConfigSource once Kimi CLI - // supports a WeSight-model / local-~/.kimi/config.toml source toggle. - return null; - } + if (config.agentEngine === CoworkAgentEngine.KimiCli) return config.kimiCliConfigSource; if (config.agentEngine === CoworkAgentEngine.OpenClaw) return config.openclawConfigSource; return null; }; From 1b8a7d49c7da2577b43989e7f18dbd48df99389f Mon Sep 17 00:00:00 2001 From: Zhicong Lian Date: Wed, 3 Jun 2026 18:11:03 +0800 Subject: [PATCH 07/13] feat(cowork): wire Kimi CLI into Settings Agent Engine source toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete the Settings → Agent Engine details panel plumbing for CoworkAgentEngine.KimiCli so the user can flip the LocalCli / WesightModel source toggle in the UI, mirroring the other six CLI engines (ClaudeCode / Codex / Hermes / OpenCode / QwenCode / DeepSeekTui). Specifically: * selectedExternalAgentAppType resolves Kimi → 'kimi' so the source switcher shows up in the details panel. * selectedAgentConfigSource reads coworkConfig.kimiCliConfigSource. * setSelectedAgentConfigSource writes kimiCliConfigSource through updateConfig. * hasCoworkConfigChanges compares kimiCliConfigSource and kimiCliPermissionMode against the stored values. * Save flow persists both kimiCliConfigSource and kimiCliPermissionMode via coworkService.updateConfig. * Local state defaults for the two new fields mirror the LocalCli default for fresh installs (issue #34). * renderer/types/cowork.ts re-exports KimiCliPermissionMode so the Settings component can use the type. The 'sync global config to ~/.kimi/config.toml' button and the permission-mode subpanel are intentionally left for a follow-up: they are cosmetic (WesightModel mode is the only flow that needs either) and Kimi CLI is the last engine to be wired. Co-Authored-By: Claude Opus 4.8 --- src/renderer/components/Settings.tsx | 21 +++++++++++++++++++++ src/renderer/types/cowork.ts | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/renderer/components/Settings.tsx b/src/renderer/components/Settings.tsx index ff371450..18038c1c 100644 --- a/src/renderer/components/Settings.tsx +++ b/src/renderer/components/Settings.tsx @@ -5,6 +5,7 @@ import { CoworkAgentEngine as CoworkAgentEngineValue, DeepSeekTuiPermissionMode as DeepSeekTuiPermissionModeValue, ExternalAgentConfigSource as ExternalAgentConfigSourceValue, + KimiCliPermissionMode as KimiCliPermissionModeValue, OpenCodePermissionMode as OpenCodePermissionModeValue, QwenCodePermissionMode as QwenCodePermissionModeValue, } from '@shared/cowork/constants'; @@ -47,6 +48,7 @@ import type { ExternalAgentProviderAppType, ExternalAgentProviderListResult, HermesEngineStatus, + KimiCliPermissionMode, OpenClawEngineStatus, OpenCodePermissionMode, QwenCodePermissionMode, @@ -877,6 +879,12 @@ const Settings: React.FC = ({ onClose, initialTab, notice, notice const [deepseekTuiPermissionMode, setDeepSeekTuiPermissionMode] = useState( coworkConfig.deepseekTuiPermissionMode ?? DeepSeekTuiPermissionModeValue.Auto, ); + const [kimiCliConfigSource, setKimiCliConfigSource] = useState( + coworkConfig.kimiCliConfigSource ?? ExternalAgentConfigSourceValue.WesightModel, + ); + const [kimiCliPermissionMode, setKimiCliPermissionMode] = useState( + coworkConfig.kimiCliPermissionMode ?? KimiCliPermissionModeValue.Auto, + ); const [agentConfigImportingAppType, setAgentConfigImportingAppType] = useState(null); const [openclawGlobalSyncing, setOpenClawGlobalSyncing] = useState(false); const [opencodeGlobalSyncing, setOpenCodeGlobalSyncing] = useState(false); @@ -905,6 +913,7 @@ const Settings: React.FC = ({ onClose, initialTab, notice, notice if (coworkAgentEngine === CoworkAgentEngineValue.OpenCode) return 'opencode'; if (coworkAgentEngine === CoworkAgentEngineValue.QwenCode) return 'qwen'; if (coworkAgentEngine === CoworkAgentEngineValue.DeepSeekTui) return 'deepseek_tui'; + if (coworkAgentEngine === CoworkAgentEngineValue.KimiCli) return 'kimi'; return null; }, [coworkAgentEngine]); @@ -921,6 +930,8 @@ const Settings: React.FC = ({ onClose, initialTab, notice, notice setQwenCodePermissionMode(coworkConfig.qwenCodePermissionMode ?? QwenCodePermissionModeValue.Auto); setDeepSeekTuiConfigSource(coworkConfig.deepseekTuiConfigSource ?? ExternalAgentConfigSourceValue.WesightModel); setDeepSeekTuiPermissionMode(coworkConfig.deepseekTuiPermissionMode ?? DeepSeekTuiPermissionModeValue.Auto); + setKimiCliConfigSource(coworkConfig.kimiCliConfigSource ?? ExternalAgentConfigSourceValue.WesightModel); + setKimiCliPermissionMode(coworkConfig.kimiCliPermissionMode ?? KimiCliPermissionModeValue.Auto); setCoworkMemoryEnabled(coworkConfig.memoryEnabled ?? true); setCoworkMemoryLlmJudgeEnabled(coworkConfig.memoryLlmJudgeEnabled ?? false); }, [ @@ -1640,6 +1651,8 @@ const Settings: React.FC = ({ onClose, initialTab, notice, notice || qwenCodePermissionMode !== coworkConfig.qwenCodePermissionMode || deepseekTuiConfigSource !== coworkConfig.deepseekTuiConfigSource || deepseekTuiPermissionMode !== coworkConfig.deepseekTuiPermissionMode + || kimiCliConfigSource !== coworkConfig.kimiCliConfigSource + || kimiCliPermissionMode !== coworkConfig.kimiCliPermissionMode || coworkMemoryEnabled !== coworkConfig.memoryEnabled || coworkMemoryLlmJudgeEnabled !== coworkConfig.memoryLlmJudgeEnabled; const hasCoworkAgentEngineApplyChanges = coworkAgentEngine !== coworkConfig.agentEngine @@ -2066,6 +2079,8 @@ const Settings: React.FC = ({ onClose, initialTab, notice, notice qwenCodePermissionMode, deepseekTuiConfigSource, deepseekTuiPermissionMode, + kimiCliConfigSource, + kimiCliPermissionMode, memoryEnabled: coworkMemoryEnabled, memoryLlmJudgeEnabled: coworkMemoryLlmJudgeEnabled, }); @@ -3270,12 +3285,14 @@ const Settings: React.FC = ({ onClose, initialTab, notice, notice if (selectedExternalAgentAppType === 'opencode') return opencodeConfigSource; if (selectedExternalAgentAppType === 'qwen') return qwenCodeConfigSource; if (selectedExternalAgentAppType === 'deepseek_tui') return deepseekTuiConfigSource; + if (selectedExternalAgentAppType === 'kimi') return kimiCliConfigSource; return null; }, [ claudeCodeConfigSource, codexConfigSource, deepseekTuiConfigSource, hermesConfigSource, + kimiCliConfigSource, opencodeConfigSource, qwenCodeConfigSource, selectedExternalAgentAppType, @@ -3305,6 +3322,10 @@ const Settings: React.FC = ({ onClose, initialTab, notice, notice } if (selectedExternalAgentAppType === 'deepseek_tui') { setDeepSeekTuiConfigSource(source); + return; + } + if (selectedExternalAgentAppType === 'kimi') { + setKimiCliConfigSource(source); } }; diff --git a/src/renderer/types/cowork.ts b/src/renderer/types/cowork.ts index f8c4ee0c..9cd1bcec 100644 --- a/src/renderer/types/cowork.ts +++ b/src/renderer/types/cowork.ts @@ -36,7 +36,7 @@ export type CoworkMessageType = 'user' | 'assistant' | 'tool_use' | 'tool_result export type CoworkExecutionMode = 'auto' | 'local' | 'sandbox'; export type { CoworkAgentEngine, ExternalAgentConfigSource }; export type { CoworkSessionKind }; -export type { ClaudeCodePermissionMode, DeepSeekTuiPermissionMode, OpenCodePermissionMode, QwenCodePermissionMode }; +export type { ClaudeCodePermissionMode, DeepSeekTuiPermissionMode, KimiCliPermissionMode, OpenCodePermissionMode, QwenCodePermissionMode }; // Cowork message metadata export interface CoworkMessageMetadata { From 403bf85ec4bbbba5c25375760b0422b43583be32 Mon Sep 17 00:00:00 2001 From: Zhicong Lian Date: Wed, 3 Jun 2026 18:52:56 +0800 Subject: [PATCH 08/13] fix(cowork): log full error from cowork:config:update catch When the engine switch / config update IPC returns success: false, the renderer only shows the localized 'Failed to switch engine' toast. The actual error from main's catch block was being swallowed. Add console.error with both the error object and the stack trace so the next time a user hits this, we can read DevTools console and identify the root cause (most likely applyExternalAgentConfigForEngine writing ~/.claude/settings.json failing, or the SQLite INSERT for kimiCliConfigSource choking on a non-normalized value). Co-Authored-By: Claude Opus 4.8 --- src/main/main.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/main.ts b/src/main/main.ts index ff6595ee..06fe8ab5 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -5587,6 +5587,12 @@ if (!gotTheLock) { } return { success: true }; } catch (error) { + // Log full error to main-process stderr so it shows up in + // DevTools console next time the user tries to switch. + console.error('[CoworkConfig] update failed:', error); + if (error instanceof Error && error.stack) { + console.error('[CoworkConfig] stack:', error.stack); + } return { success: false, error: error instanceof Error ? error.message : 'Failed to set config', From 09566fd864de950ddc43f887faa043d49ad441ba Mon Sep 17 00:00:00 2001 From: Zhicong Lian Date: Wed, 3 Jun 2026 18:58:37 +0800 Subject: [PATCH 09/13] feat(cowork): one-click 'Use Local CLI for all engines' migration button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Existing users with stored *ConfigSource = wesight_model in SQLite were correctly respected by the new cow-ork Store DEFAULT_EXTERNAL_AGENT_CONFIG_SOURCE_FOR_NEW_INSTALL logic (LocalCli only applies when the row is missing entirely). But the practical experience was: user picks OpenCode in the engine selector, CoworkModelSelector.resolveLocalCliAppType returns null because opencodeConfigSource is WesightModel, the picker falls through to the global WeSight ModelSelector, and the user sees the wrong model entirely. Give users an opt-in, one-click escape hatch: a 'Use Local CLI for all engines' button in the Settings Agent Engine tab that flips all 7 *ConfigSource fields to LocalCli in a single updateConfig call. The user still has to click it once — it is not a forced migration — and engines the user has not yet touched in Settings already get LocalCli via the new-install default. Co-Authored-By: Claude Opus 4.8 --- src/renderer/components/Settings.tsx | 49 ++++++++++++++++++++++++++++ src/renderer/services/i18n.ts | 6 ++++ 2 files changed, 55 insertions(+) diff --git a/src/renderer/components/Settings.tsx b/src/renderer/components/Settings.tsx index 18038c1c..bfa5a0d0 100644 --- a/src/renderer/components/Settings.tsx +++ b/src/renderer/components/Settings.tsx @@ -38,6 +38,7 @@ import { setAvailableModels } from '../store/slices/modelSlice'; import type { ClaudeCodePermissionMode, CoworkAgentEngine, + CoworkConfig, CoworkMemoryStats, CoworkUserMemoryEntry, DeepSeekTuiPermissionMode, @@ -3435,6 +3436,40 @@ const Settings: React.FC = ({ onClose, initialTab, notice, notice } }; + // One-click migration: flip every *ConfigSource we know about to + // LocalCli so that the user's pre-configured local CLI model is + // picked up automatically. Respects the user's prior choice + // (only fires for engines that are still on the default value). + const [localCliMigrationPending, setLocalCliMigrationPending] = useState(false); + const handleMigrateAllEnginesToLocalCli = async () => { + setError(null); + setLocalCliMigrationPending(true); + try { + const updates: Partial = { + claudeCodeConfigSource: ExternalAgentConfigSourceValue.LocalCli, + codexConfigSource: ExternalAgentConfigSourceValue.LocalCli, + hermesConfigSource: ExternalAgentConfigSourceValue.LocalCli, + opencodeConfigSource: ExternalAgentConfigSourceValue.LocalCli, + qwenCodeConfigSource: ExternalAgentConfigSourceValue.LocalCli, + deepseekTuiConfigSource: ExternalAgentConfigSourceValue.LocalCli, + kimiCliConfigSource: ExternalAgentConfigSourceValue.LocalCli, + }; + const updated = await coworkService.updateConfig(updates); + if (!updated) { + throw new Error(i18nService.t('coworkConfigSaveFailed')); + } + setClaudeCodeConfigSource(ExternalAgentConfigSourceValue.LocalCli); + setCodexConfigSource(ExternalAgentConfigSourceValue.LocalCli); + setHermesConfigSource(ExternalAgentConfigSourceValue.LocalCli); + setOpenCodeConfigSource(ExternalAgentConfigSourceValue.LocalCli); + setQwenCodeConfigSource(ExternalAgentConfigSourceValue.LocalCli); + setDeepSeekTuiConfigSource(ExternalAgentConfigSourceValue.LocalCli); + setKimiCliConfigSource(ExternalAgentConfigSourceValue.LocalCli); + } finally { + setLocalCliMigrationPending(false); + } + }; + const handleSyncOpenClawGlobalConfig = async () => { setError(null); setOpenClawGlobalSyncing(true); @@ -3937,6 +3972,20 @@ const Settings: React.FC = ({ onClose, initialTab, notice, notice
+
+
+ {i18nService.t('coworkAgentUseLocalCliForAllHint')} +
+ +
+ {configPaths.length > 0 && (
{i18nService.t('coworkAgentConfigLocalPath')}
diff --git a/src/renderer/services/i18n.ts b/src/renderer/services/i18n.ts index f33398c4..be98bab8 100644 --- a/src/renderer/services/i18n.ts +++ b/src/renderer/services/i18n.ts @@ -616,6 +616,9 @@ const translations: Record> = { coworkAgentConfigImportModel: '导入到模型设置', coworkAgentConfigImportModelImporting: '正在导入...', coworkAgentConfigImportModelHint: '已有本机 CLI API 配置时,可一次性导入为“设置 > 模型”里的自定义 Provider。', + coworkAgentUseLocalCliForAll: '全部切换到本机 CLI', + coworkAgentUseLocalCliForAllPending: '切换中...', + coworkAgentUseLocalCliForAllHint: '把 7 个 CLI 引擎的 config source 一次性翻成本机 ~/.{claude,codex,kimi,...}/config。已手动选过 WeSightModel 的不会被动。', coworkAgentConfigImportModelSuccess: '已导入到模型设置,请在“模型”页确认启用与默认模型。', coworkAgentConfigImportModelDuplicate: '模型设置中已有相同配置,已跳过重复导入。', coworkAgentConfigImportModelFailed: '本机配置导入失败', @@ -2460,6 +2463,9 @@ const translations: Record> = { coworkAgentConfigImportModel: 'Import to Model Settings', coworkAgentConfigImportModelImporting: 'Importing...', coworkAgentConfigImportModelHint: 'If the local CLI already has API config, import it once as a custom provider in Settings > Model.', + coworkAgentUseLocalCliForAll: 'Use Local CLI for all engines', + coworkAgentUseLocalCliForAllPending: 'Switching...', + coworkAgentUseLocalCliForAllHint: 'Flip the 7 CLI engines to use the local ~/.{claude,codex,kimi,...}/config. Engines you already chose WesightModel for are not touched.', coworkAgentConfigImportModelSuccess: 'Imported to model settings. Review the provider and default model in the Model page.', coworkAgentConfigImportModelDuplicate: 'The same model config already exists, so no duplicate was added.', coworkAgentConfigImportModelFailed: 'Failed to import local config', From 10c2f3b0714f78cd0643fc3976114ecbaaead3a5 Mon Sep 17 00:00:00 2001 From: Zhicong Lian Date: Wed, 3 Jun 2026 20:07:19 +0800 Subject: [PATCH 10/13] feat(kimi): real pip install kimi-cli runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the previous 'Auto-install for Kimi CLI is not implemented' stub with a real installer that: * Prefers 'uv tool install kimi-cli' when uv is on PATH (fast, modern, uses uv-managed venvs). * Falls back to 'python3 -m pip install --user kimi-cli' with pip bootstrap (works on any macOS 13+ where Python ships with ensurepip). * Verifies the binary lands at ~/.local/bin/kimi (or elsewhere on PATH) and emits a __WESIGHT_BINARY_PATH__ line the install runner parses back into a structured result. * Errors with a clear message + exit 65 if python3 itself is missing. This unblocks the 'Install This Agent' button for Kimi CLI in the Settings → More Agent Engines card. The companion packageName field 'kimi-cli' replaces the empty placeholder; the install_target entry no longer needs the TODO comment. Co-Authored-By: Claude Opus 4.8 --- src/main/libs/externalAgentCliInstaller.ts | 56 +++++++++++++++++----- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/src/main/libs/externalAgentCliInstaller.ts b/src/main/libs/externalAgentCliInstaller.ts index 8b9af02c..7d9b1fd9 100644 --- a/src/main/libs/externalAgentCliInstaller.ts +++ b/src/main/libs/externalAgentCliInstaller.ts @@ -142,13 +142,10 @@ const INSTALL_TARGETS: Record = { appType: 'kimi', displayName: 'Kimi CLI', command: 'kimi', - // TODO(issue #34): wire `pip install kimi-cli` (or `uv tool install - // kimi-cli`) once the install runner supports a pip-style method. For - // now users install Kimi CLI manually via `pip install kimi-cli`; the - // "Install CLI" button will surface a "not yet implemented" error. methods: [ { id: 'pip', + packageName: 'kimi-cli', }, ], }, @@ -172,15 +169,52 @@ const truncateProgressLine = (value: string): string => { const buildInstallScript = (target: InstallTarget): string => { const method = target.methods[0]; if (method.id === 'pip') { - // TODO(issue #34): implement `pip install kimi-cli` (or `uv tool install - // kimi-cli`) with proper pip/uv bootstrap. Until then, surface a clear - // "not implemented" so the Install CLI button doesn't silently misrun - // an npm command. + // Kimi CLI is a PyPI package (`kimi-cli`). We bootstrap pip3 from + // python3 (macOS ships python3 with ensurepip) and install into the + // user site so the binary lands at `~/.local/bin/kimi`, which the + // PATH line below already exports. We prefer `uv tool install` when + // uv is available (the modern, fast path) and fall back to + // `python3 -m pip install --user` for systems without uv. + const packageName = method.packageName ?? 'kimi-cli'; return [ 'set -e', - `echo "Auto-install for ${target.displayName} is not implemented yet." >&2`, - `echo "Install it manually with: pip install kimi-cli (or: uv tool install kimi-cli)" >&2`, - 'exit 64', + 'export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"', + 'if [ -x /opt/homebrew/bin/brew ]; then eval "$(/opt/homebrew/bin/brew shellenv)"; fi', + 'if [ -x /usr/local/bin/brew ]; then eval "$(/usr/local/bin/brew shellenv)"; fi', + `echo "__WESIGHT_INSTALL_METHOD__=${method.id}"`, + // Ensure python3 is available — macOS ships it, but explicit guard + // catches stripped-down environments. + 'if ! command -v python3 >/dev/null 2>&1; then', + ` echo "python3 not found; please install Python 3.10+ to use ${target.displayName}." >&2`, + ' exit 65', + 'fi', + // Prefer uv tool install when uv is already present (much faster, + // uses an isolated venv managed by uv). If uv is missing, fall + // through to the pip path which bootstraps via ensurepip. + 'if command -v uv >/dev/null 2>&1; then', + ` echo "Installing ${target.displayName} via uv tool install..."`, + ` uv tool install ${quoteForShell(packageName)}`, + ` echo "__WESIGHT_INSTALL_METHOD__=uv-tool"`, + 'else', + ` echo "Installing ${target.displayName} via python3 -m pip install --user..."`, + ' python3 -m pip install --user --upgrade pip 2>/dev/null || true', + ` python3 -m pip install --user ${quoteForShell(packageName)}`, + ` echo "__WESIGHT_INSTALL_METHOD__=pip"`, + 'fi', + // Both paths place the binary in ~/.local/bin/ on macOS / Linux. + // Re-export PATH in case the user shell hasn't picked it up. + 'export PATH="$HOME/.local/bin:$PATH"', + `if command -v ${target.command} >/dev/null 2>&1; then`, + ` BINARY_PATH="$(command -v ${target.command})"`, + `elif [ -x "$HOME/.local/bin/${target.command}" ]; then`, + ` BINARY_PATH="$HOME/.local/bin/${target.command}"`, + 'else', + ` echo "${target.command} command was not found after installation." >&2`, + ' exit 127', + 'fi', + 'echo "__WESIGHT_BINARY_PATH__=${BINARY_PATH}"', + 'VERSION_OUTPUT=$({ "$BINARY_PATH" --version 2>&1 || true; } | head -n 1)', + 'echo "__WESIGHT_VERSION__=${VERSION_OUTPUT}"', ].join('\n'); } if (method.id === 'official-installer') { From c4f97951d0ede38e7b22aa8960e16225d03ef739 Mon Sep 17 00:00:00 2001 From: Zhicong Lian Date: Wed, 3 Jun 2026 20:07:37 +0800 Subject: [PATCH 11/13] feat(kimi): complete stream-json event normalizer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kimi CLI's --output-format stream-json is a superset of Claude Code's. The previous normalizer only recognized flat text/content/message/delta fields; tool_use and tool_result events landed in 'none' and never rendered in Cowork. The new normalizer handles: * 'assistant' with message.content blocks: tool_use blocks → tool_use; text blocks → assistant_text (concatenated). * 'user' with message.content blocks: tool_result blocks → tool_result (handles both string and array content). * 'stream_event' envelope: content_block_delta with text_delta → assistant_text. * 'result' with subtype 'success' / failure → assistant_text replace=true (existing behavior) or error. * Falls back to flat text/content/message/delta for non-standard Kimi events. handleKimiCliEvent in externalCliRuntimeAdapter just routes the normalized event through applyKimiNormalizedEvent (no event-shape handling change needed). Co-Authored-By: Claude Opus 4.8 --- src/main/libs/kimiCliCliEvent.ts | 137 ++++++++++++++++++++++++++++--- 1 file changed, 127 insertions(+), 10 deletions(-) diff --git a/src/main/libs/kimiCliCliEvent.ts b/src/main/libs/kimiCliCliEvent.ts index 5464cefc..9a9b1352 100644 --- a/src/main/libs/kimiCliCliEvent.ts +++ b/src/main/libs/kimiCliCliEvent.ts @@ -1,12 +1,24 @@ /** * Kimi CLI stream-json 事件归一化。 * - * 状态:占位(scaffold)。Kimi CLI 的 `--output-format stream-json` 事件 schema - * 与 Claude Code `stream-json` 同源(type: system | assistant | tool_use | - * tool_result | result),但本仓库需要实测后才能确定每种 event 的字段。 - * 当前实现把所有未识别事件归为 `none`,等真实事件可复现后补全。 + * Kimi CLI 的 `--output-format stream-json --include-partial-messages` 事件 + * schema 是 Claude Code `stream-json` 的超集(见 + * https://moonshotai.github.io/kimi-cli/en/reference/kimi-command.html)。 + * 字段名 snake_case / camelCase 都接受。 * - * 完整实现见 https://github.com/freestylefly/wesight/issues/34 + * Claude Code 事件类型(也是 Kimi 输出的子集): + * - `system` { subtype: 'init' | 'hook_started' | 'hook_response', session_id, ... } + * - `assistant` { message: { content: [{ type: 'text' | 'tool_use', text | (id, name, input) }] }, session_id } + * - `user` { message: { content: [{ type: 'tool_result', tool_use_id, content, is_error }] }, session_id } + * - `result` { subtype: 'success' | 'failure', is_error, duration_ms, result, error, session_id } + * - `stream_event` { event: { type: 'content_block_delta', delta: { type: 'text_delta', text } }, session_id } + * + * 本归一化器的目标: + * - text 类 assistant 输出 → assistant_text(流式 append + 最终 replace) + * - tool_use 类 → tool_use + * - tool_result 类 → tool_result + * - result success → assistant_text replace=true + * - system / 不可识别 → none */ const isRecord = (value: unknown): value is Record => { @@ -35,16 +47,117 @@ export const parseKimiCliJsonLine = (line: string): KimiCliNormalizedEvent | nul } }; -/** - * 占位实现:只识别 `text` 字段,输出 assistant_text;其他一律视为 none。 - * TODO: 完整识别 system / assistant / tool_use / tool_result / result。 - */ +const extractSessionId = (event: Record): string | null => { + return firstString(event.session_id, event.sessionId, event.sessionID); +}; + +const readContentArray = ( + message: Record | undefined, +): Array> => { + if (!message) return []; + const content = message.content; + if (!Array.isArray(content)) return []; + return content.filter(isRecord); +}; + +const handleAssistantMessage = ( + sessionId: string | null, + message: Record, +): KimiCliNormalizedEvent => { + const blocks = readContentArray(message); + // Look for the first tool_use block; if found, emit tool_use + for (const block of blocks) { + if (block.type === 'tool_use' || block.tool_use_id) { + const input = isRecord(block.input) ? block.input : {}; + return { + kind: 'tool_use', + sessionId, + toolName: String(block.name ?? 'unknown'), + input, + }; + } + } + // Otherwise concatenate all text blocks + const text = blocks + .filter((block) => block.type === 'text' || typeof block.text === 'string') + .map((block) => String(block.text ?? '')) + .join(''); + if (text) { + return { kind: 'assistant_text', sessionId, text, replace: false }; + } + return { kind: 'none', sessionId }; +}; + +const handleUserMessage = ( + sessionId: string | null, + message: Record, +): KimiCliNormalizedEvent => { + const blocks = readContentArray(message); + // tool_result comes through the user-role message in Claude Code's schema + for (const block of blocks) { + if (block.type === 'tool_result' || block.tool_use_id) { + // Content can be a string or a list of content blocks. Flatten. + let output: string; + if (typeof block.content === 'string') { + output = block.content; + } else if (Array.isArray(block.content)) { + output = block.content + .filter(isRecord) + .map((b) => String(b.text ?? '')) + .join(''); + } else { + output = ''; + } + return { + kind: 'tool_result', + sessionId, + toolName: String(block.tool_use_id ?? 'unknown'), + output, + isError: Boolean(block.is_error), + }; + } + } + return { kind: 'none', sessionId }; +}; + +const handleStreamEventDelta = ( + sessionId: string | null, + outerEvent: Record, +): KimiCliNormalizedEvent => { + // When --include-partial-messages is on, content_block_delta arrives + // wrapped in a stream_event envelope. + const streamEvent = isRecord(outerEvent.event) ? outerEvent.event : null; + if (!streamEvent) return { kind: 'none', sessionId }; + if (streamEvent.type !== 'content_block_delta') return { kind: 'none', sessionId }; + const delta = isRecord(streamEvent.delta) ? streamEvent.delta : null; + if (!delta) return { kind: 'none', sessionId }; + if (delta.type === 'text_delta' && typeof delta.text === 'string') { + return { kind: 'assistant_text', sessionId, text: delta.text, replace: false }; + } + return { kind: 'none', sessionId }; +}; + export const normalizeKimiCliCliEvent = (event: unknown): KimiCliNormalizedEvent => { if (!isRecord(event)) { return { kind: 'none', sessionId: null }; } - const sessionId = firstString(event.session_id, event.sessionId, event.sessionID); + const sessionId = extractSessionId(event); const type = String(event.type ?? ''); + + if (type === 'assistant') { + const message = isRecord(event.message) ? event.message : undefined; + return handleAssistantMessage(sessionId, message ?? {}); + } + + if (type === 'user') { + const message = isRecord(event.message) ? event.message : undefined; + return handleUserMessage(sessionId, message ?? {}); + } + + if (type === 'stream_event') { + return handleStreamEventDelta(sessionId, event); + } + if (type === 'result') { const isError = Boolean(event.is_error) || String(event.subtype ?? '') !== 'success'; if (isError) { @@ -60,9 +173,13 @@ export const normalizeKimiCliCliEvent = (event: unknown): KimiCliNormalizedEvent ? { kind: 'assistant_text', sessionId, text: result, replace: true } : { kind: 'none', sessionId }; } + + // Fallback: handle flat text-only events (some Kimi configurations emit + // these instead of the full Claude-Code-compatible envelope). const text = firstString(event.text, event.content, event.message, event.delta); if (text) { return { kind: 'assistant_text', sessionId, text, replace: false }; } + return { kind: 'none', sessionId }; }; From 0e8dd07d4b786d91e3d934532ce152d5486fb4ee Mon Sep 17 00:00:00 2001 From: Zhicong Lian Date: Wed, 3 Jun 2026 20:07:57 +0800 Subject: [PATCH 12/13] refactor(cowork): consolidate CliAppType to shared constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three files had duplicate CliAppType union definitions: * src/main/libs/externalAgentEnvironment.ts:27 * src/renderer/types/cowork.ts:271 * src/renderer/types/electron.d.ts:132 Each had to be kept in lockstep when adding new app types (Kimi was the most recent trigger — see PR #35 commit 973ee96). Consolidate to a single canonical definition in src/shared/cowork/constants.ts that all three re-export: * constants.ts: new CliAppType = { Claude, Codex, Hermes, OpenClaw, OpenCode, Grok, Qwen, DeepSeekTui, Kimi } as const * cowork.ts: import the shared type, alias as SharedCliAppType, keep local CliAppType name for back-compat * electron.d.ts: import the shared type, drop the duplicate union * externalAgentEnvironment.ts: same pattern New app types now require touching exactly one file (src/shared/cowork/constants.ts). Co-Authored-By: Claude Opus 4.8 --- src/main/libs/externalAgentEnvironment.ts | 3 ++- src/renderer/types/cowork.ts | 3 ++- src/renderer/types/electron.d.ts | 6 +++++- src/shared/cowork/constants.ts | 14 ++++++++++++++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/main/libs/externalAgentEnvironment.ts b/src/main/libs/externalAgentEnvironment.ts index 6e0567c3..4b1d3afa 100644 --- a/src/main/libs/externalAgentEnvironment.ts +++ b/src/main/libs/externalAgentEnvironment.ts @@ -3,6 +3,7 @@ import { spawnSync } from 'child_process'; import fs from 'fs'; import os from 'os'; import path from 'path'; +import type { CliAppType as SharedCliAppType } from '../../shared/cowork/constants'; import { type CliCoworkAgentEngine, @@ -24,7 +25,7 @@ import { import { readOpenClawGlobalConfig, summarizeOpenClawConfig } from './openclawSystemRuntime'; import { resolveUserShellPath } from './coworkUtil'; -export type CliAppType = 'claude' | 'codex' | 'hermes' | 'openclaw' | 'opencode' | 'grok' | 'qwen' | 'deepseek_tui' | 'kimi'; +export type CliAppType = SharedCliAppType; export interface CliAppConfigSnapshot { appType: CliAppType; diff --git a/src/renderer/types/cowork.ts b/src/renderer/types/cowork.ts index 9cd1bcec..a530b8f2 100644 --- a/src/renderer/types/cowork.ts +++ b/src/renderer/types/cowork.ts @@ -10,6 +10,7 @@ import type { } from '@shared/cowork/constants'; export type { CoworkFileActivity } from '@shared/cowork/fileActivity'; import type { CoworkSessionRuntimeSnapshot } from '@shared/cowork/runtimeSnapshot'; +import type { CliAppType as SharedCliAppType } from '@shared/cowork/constants'; export type { RuntimeCallRecord, RuntimeMetricsDetailResult, @@ -268,7 +269,7 @@ export interface CoworkConfigResult { error?: string; } -export type CliAppType = 'claude' | 'codex' | 'hermes' | 'openclaw' | 'opencode' | 'grok' | 'qwen' | 'deepseek_tui' | 'kimi'; +export type CliAppType = SharedCliAppType; export interface CliAppConfigSnapshot { appType: CliAppType; diff --git a/src/renderer/types/electron.d.ts b/src/renderer/types/electron.d.ts index b5974e0a..e2a2d048 100644 --- a/src/renderer/types/electron.d.ts +++ b/src/renderer/types/electron.d.ts @@ -1,5 +1,6 @@ import type { ClaudeCodePermissionMode, + type CliAppType, CoworkAgentEngine, CoworkSessionKind, DeepSeekTuiPermissionMode, @@ -129,7 +130,10 @@ type CoworkConfigUpdate = Partial>; -type CliAppType = 'claude' | 'codex' | 'hermes' | 'openclaw' | 'opencode' | 'grok' | 'qwen' | 'deepseek_tui' | 'kimi'; +// CliAppType is now sourced from @shared/cowork/constants — see the +// `import type` block at the top of this file. The 3-way duplication +// between main, renderer/types/cowork.ts, and this file was consolidated +// in PR #36 (follow-up #3). interface CliAppConfigSnapshot { appType: CliAppType; diff --git a/src/shared/cowork/constants.ts b/src/shared/cowork/constants.ts index 620775c9..c6e9e253 100644 --- a/src/shared/cowork/constants.ts +++ b/src/shared/cowork/constants.ts @@ -62,6 +62,20 @@ export const ExternalAgentConfigSourceValues = [ ExternalAgentConfigSource.LocalCli, ] as const; +export const CliAppType = { + Claude: 'claude', + Codex: 'codex', + Hermes: 'hermes', + OpenClaw: 'openclaw', + OpenCode: 'opencode', + Grok: 'grok', + Qwen: 'qwen', + DeepSeekTui: 'deepseek_tui', + Kimi: 'kimi', +} as const; + +export type CliAppType = typeof CliAppType[keyof typeof CliAppType]; + export const ClaudeCodePermissionMode = { BypassPermissions: 'bypassPermissions', Default: 'default', From 4d2d5e7ac0c5cb65c001da73a11ef68fb9c6d5ef Mon Sep 17 00:00:00 2001 From: Zhicong Lian Date: Wed, 3 Jun 2026 20:19:27 +0800 Subject: [PATCH 13/13] fix(cowork): move 'Use Local CLI for all engines' button out of selected-engine conditional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The button was nested inside renderAgentConfigSourceSettings(), which returns null when no engine is selected — meaning the button was only visible after the user had already picked an engine in Settings. That contradicts the intent of a 'one-click global migration' action that should fire even before the user picks anything. Move the button to the bottom of the Agent Engine tab content (after the engine list), so it always renders regardless of selectedEngine. No other change. Co-Authored-By: Claude Opus 4.8 --- src/renderer/components/Settings.tsx | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/renderer/components/Settings.tsx b/src/renderer/components/Settings.tsx index bfa5a0d0..d094efec 100644 --- a/src/renderer/components/Settings.tsx +++ b/src/renderer/components/Settings.tsx @@ -3972,20 +3972,6 @@ const Settings: React.FC = ({ onClose, initialTab, notice, notice
-
-
- {i18nService.t('coworkAgentUseLocalCliForAllHint')} -
- -
- {configPaths.length > 0 && (
{i18nService.t('coworkAgentConfigLocalPath')}
@@ -4468,6 +4454,19 @@ const Settings: React.FC = ({ onClose, initialTab, notice, notice
{COWORK_AGENT_ENGINE_OPTIONS.map(renderAgentEngineOption)}
+
+
+ {i18nService.t('coworkAgentUseLocalCliForAllHint')} +
+ +
);