diff --git a/package.json b/package.json index a9244b3f..48f61811 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", @@ -198,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/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/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/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/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/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/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/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 d0459f96..06fe8ab5 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; @@ -1528,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); } }; @@ -1596,6 +1602,7 @@ const isExternalAgentProviderAppType = (value: unknown): value is ExternalAgentP || value === 'grok' || value === 'qwen' || value === 'deepseek_tui' + || value === 'kimi' ); const getMergedExternalAgentEnvironmentSnapshot = () => { @@ -2097,6 +2104,9 @@ const getCoworkEngineRouter = () => { ensureRunning: ensureHermesRunningForCowork, }); } + if (!kimiCliRuntimeAdapter) { + kimiCliRuntimeAdapter = new KimiCliRuntimeAdapter(); + } coworkEngineRouter = new CoworkEngineRouter({ getCurrentEngine: resolveCoworkAgentEngine, openclawRuntime: openClawRuntimeAdapter, @@ -2109,6 +2119,7 @@ const getCoworkEngineRouter = () => { grokBuildRuntime: grokBuildRuntimeAdapter, qwenCodeRuntime: qwenCodeRuntimeAdapter, deepSeekTuiRuntime: deepSeekTuiRuntimeAdapter, + kimiCliRuntime: kimiCliRuntimeAdapter, telemetryTracker: getRuntimeTelemetryTracker(), }); } @@ -5576,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', diff --git a/src/renderer/components/Settings.tsx b/src/renderer/components/Settings.tsx index 308f7609..bfa5a0d0 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'; @@ -37,6 +38,7 @@ import { setAvailableModels } from '../store/slices/modelSlice'; import type { ClaudeCodePermissionMode, CoworkAgentEngine, + CoworkConfig, CoworkMemoryStats, CoworkUserMemoryEntry, DeepSeekTuiPermissionMode, @@ -47,6 +49,7 @@ import type { ExternalAgentProviderAppType, ExternalAgentProviderListResult, HermesEngineStatus, + KimiCliPermissionMode, OpenClawEngineStatus, OpenCodePermissionMode, QwenCodePermissionMode, @@ -142,6 +145,11 @@ const COWORK_AGENT_ENGINE_OPTIONS: Array<{ labelKey: 'coworkAgentEngineDeepSeekTui', hintKey: 'coworkAgentEngineDeepSeekTuiHint', }, + { + value: CoworkAgentEngineValue.KimiCli, + labelKey: 'coworkAgentEngineKimiCli', + hintKey: 'coworkAgentEngineKimiCliHint', + }, ]; const PET_VARIANT_OPTIONS: Array<{ @@ -872,6 +880,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); @@ -887,6 +901,7 @@ const Settings: React.FC = ({ onClose, initialTab, notice, notice grok: '', qwen: '', deepseek_tui: '', + kimi: '', }); const [agentProviderLists, setAgentProviderLists] = useState>>({}); const [agentProviderLoadingAppType, setAgentProviderLoadingAppType] = useState(null); @@ -899,6 +914,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]); @@ -915,6 +931,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); }, [ @@ -1634,6 +1652,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 @@ -2060,6 +2080,8 @@ const Settings: React.FC = ({ onClose, initialTab, notice, notice qwenCodePermissionMode, deepseekTuiConfigSource, deepseekTuiPermissionMode, + kimiCliConfigSource, + kimiCliPermissionMode, memoryEnabled: coworkMemoryEnabled, memoryLlmJudgeEnabled: coworkMemoryLlmJudgeEnabled, }); @@ -3178,6 +3200,7 @@ const Settings: React.FC = ({ onClose, initialTab, notice, notice || engine === CoworkAgentEngineValue.GrokBuild || engine === CoworkAgentEngineValue.QwenCode || engine === CoworkAgentEngineValue.DeepSeekTui + || engine === CoworkAgentEngineValue.KimiCli ) { return (
@@ -3263,12 +3286,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, @@ -3298,6 +3323,10 @@ const Settings: React.FC = ({ onClose, initialTab, notice, notice } if (selectedExternalAgentAppType === 'deepseek_tui') { setDeepSeekTuiConfigSource(source); + return; + } + if (selectedExternalAgentAppType === 'kimi') { + setKimiCliConfigSource(source); } }; @@ -3407,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); @@ -3909,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/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/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/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/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/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..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; }; @@ -115,6 +116,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 +600,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: @@ -781,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/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 d2a8ec9b..be98bab8 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', @@ -516,6 +519,7 @@ const translations: Record> = { coworkSlashCommandsGrokBuild: 'Grok Build 指令', coworkSlashCommandsQwenCode: 'Qwen Code 指令', coworkSlashCommandsDeepSeekTui: 'DeepSeek-TUI 指令', + coworkSlashCommandsKimiCli: 'Kimi CLI 指令', coworkSlashCommandsWesight: 'WeSight 指令', coworkSlashPanelContextTitle: '上下文概览', coworkSlashPanelStatusTitle: '当前状态', @@ -612,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: '本机配置导入失败', @@ -652,6 +659,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 +2305,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', @@ -2345,6 +2366,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', @@ -2441,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', @@ -2481,6 +2506,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/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 f4cc78b9..9cd1bcec 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'; @@ -35,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 { @@ -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>; -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; diff --git a/src/renderer/utils/coworkStudio.test.ts b/src/renderer/utils/coworkStudio.test.ts index 049c4913..169460d6 100644 --- a/src/renderer/utils/coworkStudio.test.ts +++ b/src/renderer/utils/coworkStudio.test.ts @@ -53,6 +53,8 @@ const makeConfig = (agentEngine: CoworkAgentEngine): CoworkConfig => ({ 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 39bd96fa..21b7c14a 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,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) return config.kimiCliConfigSource; if (config.agentEngine === CoworkAgentEngine.OpenClaw) return config.openclawConfigSource; return null; }; 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);