From 3cfc27f0bb21b8d2bdc1d8c767f0ce4dc34f7087 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Thu, 16 Jul 2026 00:02:00 +0000 Subject: [PATCH 01/13] feat(kilocode): configure and launch from cli --- cli.js | 373 +++++++++++++++++- tests/e2e/run.js | 13 + tests/e2e/test-kilocode-config.js | 57 +++ tests/e2e/test-web-ui-assets.js | 3 + tests/unit/config-tabs-ui.test.mjs | 21 +- tests/unit/i18n-locales.test.mjs | 18 + tests/unit/kilocode-config-cli.test.mjs | 105 +++++ tests/unit/kilocode-config-ui.test.mjs | 74 ++++ tests/unit/opencode-config-ui.test.mjs | 2 +- tests/unit/run.mjs | 2 + tests/unit/web-ui-behavior-parity.test.mjs | 23 +- tests/unit/web-ui-preferences.test.mjs | 19 +- web-ui/app.js | 18 +- web-ui/index.html | 1 + web-ui/modules/app.methods.index.mjs | 2 + .../modules/app.methods.kilocode-config.mjs | 140 +++++++ web-ui/modules/app.methods.navigation.mjs | 3 + web-ui/modules/app.methods.startup-claude.mjs | 3 +- .../app.methods.tool-config-permissions.mjs | 5 +- .../app.methods.web-ui-preferences.mjs | 3 +- web-ui/modules/config-mode.computed.mjs | 12 +- web-ui/modules/i18n/locales/en.mjs | 27 +- web-ui/modules/i18n/locales/ja.mjs | 34 +- web-ui/modules/i18n/locales/vi.mjs | 36 +- web-ui/modules/i18n/locales/zh-tw.mjs | 37 +- web-ui/modules/i18n/locales/zh.mjs | 37 +- web-ui/partials/index/layout-header.html | 53 ++- .../partials/index/panel-config-kilocode.html | 101 +++++ .../partials/index/panel-config-opencode.html | 1 + web-ui/res/web-ui-render.precompiled.js | 309 +++++++++++++-- web-ui/styles/layout-shell.css | 11 + 31 files changed, 1448 insertions(+), 95 deletions(-) create mode 100644 tests/e2e/test-kilocode-config.js create mode 100644 tests/unit/kilocode-config-cli.test.mjs create mode 100644 tests/unit/kilocode-config-ui.test.mjs create mode 100644 web-ui/modules/app.methods.kilocode-config.mjs create mode 100644 web-ui/partials/index/panel-config-kilocode.html diff --git a/cli.js b/cli.js index a2ae2ec2..304418e7 100644 --- a/cli.js +++ b/cli.js @@ -193,6 +193,9 @@ const CONFIG_DIR = path.join(os.homedir(), '.codex'); const CONFIG_FILE = path.join(CONFIG_DIR, 'config.toml'); const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json'); const OPENCODE_CONFIG_DIR = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'opencode'); +const KILOCODE_CONFIG_DIR = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'kilo'); +const KILOCODE_GLOBAL_JSONC_CONFIG_FILE = path.join(KILOCODE_CONFIG_DIR, 'kilo.jsonc'); +const KILOCODE_GLOBAL_JSON_CONFIG_FILE = path.join(KILOCODE_CONFIG_DIR, 'kilo.json'); const OPENCODE_CONFIG_ENV_FILE = process.env.OPENCODE_CONFIG ? path.resolve(process.env.OPENCODE_CONFIG) : ''; const OPENCODE_GLOBAL_JSONC_CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.jsonc'); const OPENCODE_GLOBAL_JSON_CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.json'); @@ -900,8 +903,8 @@ function isPlainObject(value) { return !!value && typeof value === 'object' && !Array.isArray(value); } -const TOOL_CONFIG_PERMISSION_TARGETS = new Set(['codex', 'claude', 'opencode']); -const TOOL_CONFIG_PERMISSION_DEFAULTS = Object.freeze({ codex: false, claude: false, opencode: false }); +const TOOL_CONFIG_PERMISSION_TARGETS = new Set(['codex', 'claude', 'opencode', 'kilocode']); +const TOOL_CONFIG_PERMISSION_DEFAULTS = Object.freeze({ codex: false, claude: false, opencode: false, kilocode: false }); let toolConfigWriteGuardDepth = 0; function enterToolConfigWriteGuard() { @@ -928,7 +931,8 @@ function normalizeToolConfigPermissions(value) { return { codex: source.codex === true, claude: source.claude === true, - opencode: source.opencode === true + opencode: source.opencode === true, + kilocode: source.kilocode === true }; } @@ -986,7 +990,7 @@ function normalizeMainTabPreference(value) { function normalizeConfigModePreference(value) { const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''; - return ['codex', 'claude', 'openclaw', 'opencode'].includes(normalized) ? normalized : 'codex'; + return ['codex', 'claude', 'openclaw', 'opencode', 'kilocode'].includes(normalized) ? normalized : 'codex'; } function normalizeUsageTimeRangePreference(value) { @@ -1183,9 +1187,14 @@ function getApiToolConfigWriteTarget(action) { 'apply-opencode-config', 'update-opencode-selection' ]); + const kilocodeWriteActions = new Set([ + 'apply-kilocode-config', + 'start-kilocode' + ]); if (codexWriteActions.has(name)) return 'codex'; if (claudeWriteActions.has(name)) return 'claude'; if (opencodeWriteActions.has(name)) return 'opencode'; + if (kilocodeWriteActions.has(name)) return 'kilocode'; return ''; } @@ -10169,6 +10178,347 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) { } } +function stripJsoncComments(input) { + const text = String(input || ''); + let output = ''; + let inString = false; + let quote = ''; + let escaped = false; + for (let i = 0; i < text.length; i++) { + const ch = text[i]; + const next = text[i + 1]; + if (inString) { + output += ch; + if (escaped) { + escaped = false; + } else if (ch === '\\') { + escaped = true; + } else if (ch === quote) { + inString = false; + quote = ''; + } + continue; + } + if (ch === '"' || ch === "'") { + inString = true; + quote = ch; + output += ch; + continue; + } + if (ch === '/' && next === '/') { + while (i < text.length && text[i] !== '\n') i++; + output += '\n'; + continue; + } + if (ch === '/' && next === '*') { + i += 2; + while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i++; + i += 1; + continue; + } + output += ch; + } + return output.replace(/,\s*([}\]])/g, '$1'); +} + +function readKilocodeGlobalConfig() { + const candidates = [KILOCODE_GLOBAL_JSONC_CONFIG_FILE, KILOCODE_GLOBAL_JSON_CONFIG_FILE]; + const filePath = candidates.find((candidate) => fs.existsSync(candidate)) || KILOCODE_GLOBAL_JSONC_CONFIG_FILE; + if (!fs.existsSync(filePath)) { + return { filePath, config: {} }; + } + const raw = fs.readFileSync(filePath, 'utf-8'); + if (!raw.trim()) { + return { filePath, config: {} }; + } + try { + const parsed = JSON.parse(stripJsoncComments(raw)); + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + throw new Error('配置根节点必须是对象'); + } + return { filePath, config: parsed }; + } catch (e) { + throw new Error(`读取 KiloCode 配置失败: ${e.message}`); + } +} + +function normalizeKilocodeProviderName(value) { + const name = typeof value === 'string' && value.trim() ? value.trim() : 'codexmate'; + if (!/^[A-Za-z0-9._-]+$/.test(name)) { + throw new Error('provider 仅支持字母/数字/._-'); + } + return name; +} + +function writeKilocodeProviderConfig(params = {}) { + const provider = normalizeKilocodeProviderName(params.provider); + const url = normalizeBaseUrl(params.url || ''); + const incomingKey = typeof params.key === 'string' ? params.key.trim() : ''; + const model = typeof params.model === 'string' ? params.model.trim() : ''; + if (!url) throw new Error('URL 必填'); + if (!model) throw new Error('模型名称必填'); + if (!isValidHttpUrl(url)) throw new Error('URL 仅支持 http/https'); + + const { filePath, config } = readKilocodeGlobalConfig(); + const existingProvider = config.provider + && typeof config.provider === 'object' + && !Array.isArray(config.provider) + && config.provider[provider] + && typeof config.provider[provider] === 'object' + && !Array.isArray(config.provider[provider]) + ? config.provider[provider] + : {}; + const existingOptions = existingProvider.options && typeof existingProvider.options === 'object' && !Array.isArray(existingProvider.options) + ? existingProvider.options + : {}; + const existingKey = typeof existingOptions.apiKey === 'string' ? existingOptions.apiKey.trim() : ''; + const key = incomingKey || existingKey; + if (!key) throw new Error('API Key 必填'); + + const next = { ...config }; + if (!next.$schema) { + next.$schema = 'https://app.kilo.ai/config.json'; + } + next.provider = next.provider && typeof next.provider === 'object' && !Array.isArray(next.provider) + ? { ...next.provider } + : {}; + next.provider[provider] = { + ...(next.provider[provider] && typeof next.provider[provider] === 'object' && !Array.isArray(next.provider[provider]) + ? next.provider[provider] + : {}), + name: provider, + npm: '@ai-sdk/openai-compatible', + api: url, + env: [], + models: { + [model]: { + name: model, + tool_call: true + } + }, + options: { + ...(next.provider[provider] + && typeof next.provider[provider] === 'object' + && next.provider[provider].options + && typeof next.provider[provider].options === 'object' + && !Array.isArray(next.provider[provider].options) + ? next.provider[provider].options + : {}), + apiKey: key, + baseURL: url + } + }; + next.model = `${provider}/${model}`; + const enabled = Array.isArray(next.enabled_providers) ? next.enabled_providers.filter((item) => typeof item === 'string' && item.trim()) : []; + next.enabled_providers = enabled.includes(provider) ? enabled : [...enabled, provider]; + + ensureDir(path.dirname(filePath)); + fs.writeFileSync(filePath, JSON.stringify(next, null, 2) + '\n', 'utf-8'); + return { filePath, provider, model, url }; +} + +function parseKilocodeCommandArgs(argv = []) { + const options = { provider: 'codexmate', command: '' }; + const positional = []; + let passthrough = []; + for (let i = 0; i < argv.length; i++) { + const token = String(argv[i] || ''); + if (token === '--') { + passthrough = argv.slice(i + 1).map(item => String(item)); + break; + } + if (token === '--provider') { + const value = String(argv[i + 1] || ''); + if (!value || value.startsWith('--')) throw new Error('错误: --provider 需要一个值'); + options.provider = value; + i += 1; + continue; + } + if (token.startsWith('--provider=')) { + options.provider = token.slice('--provider='.length); + continue; + } + if (token === '--help' || token === '-h') { + options.help = true; + continue; + } + positional.push(token); + } + if (positional[0] === 'config' || positional[0] === 'setup') { + options.command = positional.shift(); + } + return { + command: options.command, + url: positional[0], + key: positional[1], + model: positional[2], + provider: options.provider, + help: !!options.help, + passthrough: passthrough.length ? passthrough : positional.slice(options.command ? 3 : 0) + }; +} + +function printKilocodeUsage() { + console.log('\n用法:'); + console.log(' codexmate kilo [KiloCode参数...]'); + console.log(' codexmate kilo <模型> [--provider ] [-- KiloCode参数...]'); + console.log(' codexmate kilo config <模型> [--provider ]'); + console.log('\n说明:'); + console.log(' codexmate kilo 默认启动 KiloCode;带 URL/API密钥/模型时会先写入 KiloCode provider 配置再启动。'); + console.log(' 纯配置请使用 codexmate kilo config ...'); + console.log(' 配置写入 ~/.config/kilo/kilo.jsonc(或已存在的 kilo.json)。'); +} + +function resolveKilocodeBinary() { + for (const bin of ['kilo', 'kilocode']) { + if (commandExists(bin, '--version')) return bin; + } + return ''; +} + +function runKilocodeCommand(args = [], options = {}) { + const bin = resolveKilocodeBinary(); + if (!bin) { + throw new Error('无法启动 KiloCode,请确认已安装 KiloCode CLI,并且 kilo 或 kilocode 在 PATH 中。'); + } + const finalArgs = Array.isArray(args) ? args.filter(item => item !== undefined).map(item => String(item)) : []; + if (options.detached === true) { + const child = spawn(bin, finalArgs, { + detached: true, + stdio: 'ignore', + windowsHide: true + }); + child.unref(); + return { success: true, bin, args: finalArgs, pid: child.pid }; + } + return new Promise((resolve, reject) => { + const child = spawn(bin, finalArgs, { + stdio: 'inherit', + windowsHide: false + }); + child.on('error', reject); + child.on('exit', (code, signal) => { + if (signal) { + reject(new Error(`KiloCode 已终止: ${signal}`)); + return; + } + resolve(code || 0); + }); + }); +} + +async function cmdKilocode(argv = []) { + const parsed = parseKilocodeCommandArgs(argv); + if (parsed.help) { + printKilocodeUsage(); + return 0; + } + if (parsed.command === 'config') { + if (!parsed.url || !parsed.key || !parsed.model) { + printKilocodeUsage(); + throw new Error('URL、API密钥和模型名称必填'); + } + const result = writeKilocodeProviderConfig(parsed); + console.log('✓ 已写入 KiloCode 配置'); + console.log(' 文件:', result.filePath); + console.log(' provider:', result.provider); + console.log(' URL:', result.url); + console.log(' 模型:', `${result.provider}/${result.model}`); + console.log(); + return 0; + } + let passthrough = parsed.passthrough; + if (parsed.url && parsed.key && parsed.model) { + const result = writeKilocodeProviderConfig(parsed); + console.log('✓ 已写入 KiloCode 配置,正在启动 KiloCode'); + console.log(' 文件:', result.filePath); + console.log(' 模型:', `${result.provider}/${result.model}`); + console.log(); + passthrough = parsed.passthrough; + } else if (parsed.url || parsed.key || parsed.model) { + passthrough = [parsed.url, parsed.key, parsed.model, ...parsed.passthrough].filter(item => item !== undefined && item !== ''); + } + return runKilocodeCommand(passthrough); +} + +function summarizeKilocodeConfig(config = {}, targetPath = KILOCODE_GLOBAL_JSONC_CONFIG_FILE, exists = false) { + const providers = getRecord(config.provider); + const modelRef = typeof config.model === 'string' ? config.model.trim() : ''; + const slash = modelRef.indexOf('/'); + const currentProvider = slash > 0 ? modelRef.slice(0, slash) : ''; + const currentModel = slash > 0 ? modelRef.slice(slash + 1) : modelRef; + const providerNames = [...new Set([...Object.keys(providers), currentProvider].filter(Boolean))]; + const redactedConfig = JSON.parse(JSON.stringify(config && typeof config === 'object' ? config : {})); + const redactedProviders = getRecord(redactedConfig.provider); + for (const provider of Object.values(redactedProviders)) { + const options = getRecord(provider && provider.options); + if (typeof options.apiKey === 'string' && options.apiKey) { + options.apiKey = maskKey(options.apiKey); + } + } + return { + exists: !!exists, + targetPath, + currentProvider, + currentModel, + providers: providerNames.map((name) => { + const provider = getRecord(providers[name]); + const options = getRecord(provider.options); + const apiKey = typeof options.apiKey === 'string' ? options.apiKey : ''; + const modelNames = Object.keys(getRecord(provider.models)); + return { + name, + api: typeof provider.api === 'string' ? provider.api : '', + baseURL: typeof options.baseURL === 'string' ? options.baseURL : '', + hasKey: apiKey.trim().length > 0, + apiKey: maskKey(apiKey), + models: modelNames + }; + }), + content: JSON.stringify(redactedConfig, null, 2) + '\n', + redacted: true + }; +} + +function readKilocodeConfigInfo() { + try { + const { filePath, config } = readKilocodeGlobalConfig(); + return summarizeKilocodeConfig(config, filePath, fs.existsSync(filePath)); + } catch (e) { + return { error: e.message || '读取 KiloCode 配置失败', targetPath: KILOCODE_GLOBAL_JSONC_CONFIG_FILE }; + } +} + +function applyKilocodeConfig(params = {}) { + assertToolConfigWriteAllowed('kilocode'); + try { + const result = writeKilocodeProviderConfig({ + provider: params.provider, + url: params.url, + key: params.apiKey, + model: params.model + }); + const info = readKilocodeConfigInfo(); + return { success: true, ...result, ...info }; + } catch (e) { + return { error: e.message || '写入 KiloCode 配置失败' }; + } +} + +function startKilocodeFromWeb(params = {}) { + assertToolConfigWriteAllowed('kilocode'); + try { + if (params && params.configure === true) { + const saved = applyKilocodeConfig(params); + if (saved && saved.error) return saved; + } + const args = Array.isArray(params.args) ? params.args.map(item => String(item)) : []; + return runKilocodeCommand(args, { detached: true }); + } catch (e) { + return { error: e.message || '启动 KiloCode 失败' }; + } +} + // 添加模型 function cmdAddModel(modelName, silent = false) { if (!modelName) { @@ -12776,12 +13126,21 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser case 'get-opencode-config': result = readOpencodeConfigInfo(); break; + case 'get-kilocode-config': + result = readKilocodeConfigInfo(); + break; case 'apply-opencode-config': result = applyOpencodeConfigRaw(params || {}); break; case 'update-opencode-selection': result = updateOpencodeSelection(params || {}); break; + case 'apply-kilocode-config': + result = applyKilocodeConfig(params || {}); + break; + case 'start-kilocode': + result = startKilocodeFromWeb(params || {}); + break; case 'apply-claude-config': result = await applyToClaudeSettings(params.config); if (result && !result.error) { @@ -18322,6 +18681,7 @@ function printMainHelp() { console.log(' codexmate delete <名称> 删除提供商'); console.log(' codexmate claude 等同于 claude --dangerously-skip-permissions'); console.log(' codexmate claude [模型] [--target-api responses|chat_completions|ollama] 写入 Claude Code 配置'); + console.log(' codexmate kilo [URL API密钥 模型] [--provider ] 配置后启动 KiloCode'); console.log(' codexmate auth 认证管理'); console.log(' codexmate add-model <模型> 添加模型'); console.log(' codexmate delete-model <模型> 删除模型'); @@ -18417,6 +18777,11 @@ async function main() { const exitCode = await cmdClaude(args.slice(1)); process.exit(exitCode); } + case 'kilo': + case 'kilocode': { + const exitCode = await cmdKilocode(args.slice(1)); + process.exit(exitCode || 0); + } case 'add-model': cmdAddModel(args[1]); break; case 'delete-model': cmdDeleteModel(args[1]); break; case 'auth': cmdAuth(args.slice(1)); break; diff --git a/tests/e2e/run.js b/tests/e2e/run.js index 49e712c7..0605588e 100644 --- a/tests/e2e/run.js +++ b/tests/e2e/run.js @@ -32,6 +32,7 @@ const testWebUiSessionBrowser = require('./test-web-ui-session-browser'); const testWebUiUsageInteractions = require('./test-web-ui-usage-interactions'); const testInstallStatus = require('./test-install-status'); const testWebhook = require('./test-webhook'); +const testKilocodeConfig = require('./test-kilocode-config'); async function main() { const realHome = os.homedir(); @@ -58,6 +59,17 @@ async function main() { try { fs.writeFileSync(binPath, '#!/usr/bin/env sh\necho \"claude 1.2.3\"\\n', 'utf-8'); fs.chmodSync(binPath, 0o755); + const kiloPath = path.join(tmpBin, 'kilo'); + fs.writeFileSync(kiloPath, `#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); +if (process.argv[2] === '--version') { + console.log('kilo 0.0.0-e2e'); + process.exit(0); +} +fs.writeFileSync(path.join(process.env.HOME, 'kilocode-launch.json'), JSON.stringify({ args: process.argv.slice(2) }), 'utf8'); +`, 'utf-8'); + fs.chmodSync(kiloPath, 0o755); } catch (e) {} } const env = { @@ -166,6 +178,7 @@ async function main() { await testWebUiAssets(ctx); await testWebUiSessionBrowser(ctx); await testWebUiUsageInteractions(ctx); + await testKilocodeConfig(ctx); } finally { const waitForExit = new Promise((resolve) => { diff --git a/tests/e2e/test-kilocode-config.js b/tests/e2e/test-kilocode-config.js new file mode 100644 index 00000000..c9735943 --- /dev/null +++ b/tests/e2e/test-kilocode-config.js @@ -0,0 +1,57 @@ +const path = require('path'); +const { assert, fs } = require('./helpers'); + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +module.exports = async function testKilocodeConfig(ctx) { + const { api, tmpHome, mockProviderUrl } = ctx; + const configPath = path.join(tmpHome, '.config', 'kilo', 'kilo.jsonc'); + const launchRecord = path.join(tmpHome, 'kilocode-launch.json'); + + const denied = await api('apply-kilocode-config', { + provider: 'codexmate', + url: `${mockProviderUrl}/v1`, + apiKey: 'sk-denied', + model: 'denied-model' + }); + assert(denied && denied.error, 'KiloCode config write should be denied before permission is enabled'); + assert(!fs.existsSync(configPath), 'KiloCode config should not be created while write permission is disabled'); + + const permission = await api('set-tool-config-permission', { target: 'kilocode', allowWrite: true }); + assert(permission && permission.success === true, 'KiloCode write permission should be enabled'); + assert(permission.permissions && permission.permissions.kilocode === true, 'KiloCode permission should persist in preferences'); + + const models = await api('models-by-url', { baseUrl: `${mockProviderUrl}/v1`, apiKey: 'sk-mock' }, 5000); + assert(models && Array.isArray(models.models), 'mock provider model lookup should return model list'); + assert(models.models.includes('e2e2-model'), 'mock provider model list should include fixture model'); + + const applied = await api('apply-kilocode-config', { + provider: 'codexmate-e2e', + url: `${mockProviderUrl}/v1`, + apiKey: 'sk-kilocode-e2e', + model: 'e2e2-model' + }); + assert(applied && applied.success === true, 'KiloCode config should be applied after permission is enabled'); + assert(fs.existsSync(configPath), 'KiloCode config file should be created'); + const rawConfig = fs.readFileSync(configPath, 'utf8'); + assert(rawConfig.includes('codexmate-e2e/e2e2-model'), 'KiloCode config should select provider/model reference'); + assert(rawConfig.includes(`${mockProviderUrl}/v1`), 'KiloCode config should store provider baseURL'); + assert(rawConfig.includes('sk-kilocode-e2e'), 'KiloCode config should store API key in target file'); + + const info = await api('get-kilocode-config'); + assert(info && info.currentProvider === 'codexmate-e2e', 'KiloCode config summary should expose current provider'); + assert(info.currentModel === 'e2e2-model', 'KiloCode config summary should expose current model'); + assert(info.content && !info.content.includes('sk-kilocode-e2e'), 'KiloCode config preview should redact API key'); + assert(info.providers.some(provider => provider.name === 'codexmate-e2e' && provider.hasKey === true), 'KiloCode summary should include configured provider'); + + const started = await api('start-kilocode', { args: ['--scenario', 'e2e'] }, 5000); + assert(started && started.success === true, 'KiloCode launch should return success with fake binary'); + for (let i = 0; i < 20 && !fs.existsSync(launchRecord); i++) { + await sleep(50); + } + assert(fs.existsSync(launchRecord), 'fake KiloCode binary should record launch invocation'); + const launch = JSON.parse(fs.readFileSync(launchRecord, 'utf8')); + assert(Array.isArray(launch.args) && launch.args.includes('--scenario') && launch.args.includes('e2e'), 'KiloCode launch should pass through web args'); +}; diff --git a/tests/e2e/test-web-ui-assets.js b/tests/e2e/test-web-ui-assets.js index 46e04330..8fdc3bf4 100644 --- a/tests/e2e/test-web-ui-assets.js +++ b/tests/e2e/test-web-ui-assets.js @@ -39,6 +39,8 @@ module.exports = async function testWebUiAssets(ctx) { 'root web ui page should return html content type' ); assert(rootPage.body.includes('id="panel-market"'), 'root web ui page should inline market panel'); + assert(rootPage.body.includes('id="panel-config-kilocode"'), 'root web ui page should inline KiloCode config panel'); + assert(rootPage.body.includes('id="side-tab-config-kilocode"'), 'root web ui page should inline KiloCode side tab'); assert(rootPage.body.includes('class="modal modal-wide skills-modal"'), 'root web ui page should inline skills modal'); assert(rootPage.body.includes('src="/web-ui/app.js"'), 'root web ui page should point to the absolute app entry'); assert(!rootPage.body.includes('src="web-ui/app.js"'), 'root web ui page should not use a relative app entry'); @@ -76,6 +78,7 @@ module.exports = async function testWebUiAssets(ctx) { 'app entry should not leak split re-export directives' ); assert(appEntry.body.includes('onClaudeModelChange'), 'app entry should expose Claude model handler'); + assert(appEntry.body.includes('createKilocodeConfigMethods'), 'app entry should include KiloCode config methods'); const logicEntry = await getText(port, '/web-ui/logic.mjs'); assert(logicEntry.statusCode === 200, 'logic entry should return 200'); diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index a312d9b7..af89f49b 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -1,4 +1,4 @@ -import assert from 'assert'; +import assert from 'assert'; import { readBundledWebUiCss, readBundledWebUiHtml, @@ -24,7 +24,7 @@ test('config template keeps expected config tabs in top and side navigation', () const sideTabModes = [...html.matchAll(/id="side-tab-config-([a-z]+)"/g)] .map((match) => match[1]); - assert.deepStrictEqual(sideTabModes, ['codex', 'claude', 'openclaw', 'opencode']); + assert.deepStrictEqual(sideTabModes, ['codex', 'claude', 'openclaw', 'opencode', 'kilocode']); assert.match(html, /id="tab-dashboard"/); assert.match(html, /v-if="healthCheckResult && healthCheckResult\.report" class="doctor-action-list"/); assert.match(html, /v-if="healthCheckResult\.report\.issues && healthCheckResult\.report\.issues\.length"/); @@ -92,6 +92,13 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(html, /@click="onMainTabClick\('prompts', \$event\)"/); assert.match(html, /t\('side\.prompts'\)/); assert.match(html, /t\('side\.prompts\.meta'\)/); + const configSectionIndex = sideRail.indexOf(':aria-label="t(\'side.config\')"'); + const workspaceSectionIndex = sideRail.indexOf(':aria-label="t(\'side.workspace\')"'); + const promptsSideTabIndex = sideRail.indexOf('id="side-tab-prompts"'); + assert.ok(configSectionIndex >= 0, 'config side section should exist'); + assert.ok(workspaceSectionIndex > configSectionIndex, 'workspace side section should follow config section'); + assert.ok(promptsSideTabIndex > workspaceSectionIndex, 'Prompts should live inside the workspace section, not config'); + assert.match(sideRail, /:aria-label="t\('side\.workspace'\)"[\s\S]*id="side-tab-sessions"[\s\S]*id="side-tab-prompts"[\s\S]*id="side-tab-usage"/); assert.doesNotMatch(html, /promptsSubTab === 'presets'/); assert.doesNotMatch(html, /switchPromptsSubTab\('presets'\)/); assert.match(html, /
/); @@ -368,7 +375,7 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(html, / @@ -196,14 +196,13 @@ data-main-tab="config" data-config-mode="codex" :aria-current="mainTab === 'config' && configMode === 'codex' ? 'page' : null" - :class="['side-item', { active: isConfigModeNavActive('codex') }]" + :class="['side-item', 'side-item-compact', { active: isConfigModeNavActive('codex') }]" @pointerdown="onConfigTabPointerDown('codex', $event)" @click="onConfigTabClick('codex', $event)">
{{ t('side.config.codex') }}
{{ t('side.config.codex.meta') }} - {{ t('common.current', { value: currentProvider }) }}
+ - - + +
+
+
{{ t('toolConfig.kilocode.title') }}
+

{{ t('toolConfig.kilocode.desc') }}

+
+ +
+ +
+
+
+
+ {{ t('kilocode.providerModel.title') }} +
+ +
+
+
{{ t('kilocode.targetFile', { path: kilocodeConfigPath || '~/.config/kilo/kilo.jsonc', status: kilocodeConfigExists ? t('common.exists') : t('common.notExistsWillCreateOnSave') }) }}
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
{{ kilocodeError }}
+
+ +
+
{{ t('kilocode.summary.title') }}
+
+
+
+
{{ provider.name.charAt(0).toUpperCase() }}
+
+
{{ provider.name }}
+
{{ provider.baseURL || provider.api || t('config.url.unset') }}
+
{{ (provider.models || []).join(', ') || t('opencode.summary.noModel') }}
+
+
+
+ {{ provider.hasKey ? t('common.configured') : t('common.notConfigured') }} +
+
+
+
+ +
+
{{ t('kilocode.configPreview.title') }}
+ +
{{ t('kilocode.configPreview.hint') }}
+
+ +
+
+
{{ t('toolConfig.kilocode.lockedTitle') }}
+

{{ t('toolConfig.kilocode.lockedDesc') }}

+ +
+
+
+
+ diff --git a/web-ui/partials/index/panel-config-opencode.html b/web-ui/partials/index/panel-config-opencode.html index 340126d2..9026aacc 100644 --- a/web-ui/partials/index/panel-config-opencode.html +++ b/web-ui/partials/index/panel-config-opencode.html @@ -10,6 +10,7 @@ +
diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index 2db6ce5f..e8d946f2 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -71,7 +71,7 @@ return function render(_ctx, _cache) { "data-config-mode": _ctx.configMode, tabindex: _ctx.mainTab === 'config' ? 0 : -1, "aria-selected": _ctx.mainTab === 'config', - "aria-controls": _ctx.configMode === 'claude' ? 'panel-config-claude' : (_ctx.configMode === 'openclaw' ? 'panel-config-openclaw' : (_ctx.configMode === 'opencode' ? 'panel-config-opencode' : 'panel-config-provider')), + "aria-controls": _ctx.configMode === 'claude' ? 'panel-config-claude' : (_ctx.configMode === 'openclaw' ? 'panel-config-openclaw' : (_ctx.configMode === 'opencode' ? 'panel-config-opencode' : (_ctx.configMode === 'kilocode' ? 'panel-config-kilocode' : 'panel-config-provider'))), onPointerdown: $event => (_ctx.onMainTabPointerDown('config', $event)), onClick: $event => (_ctx.onMainTabClick('config', $event)) }, _toDisplayString(_ctx.t('tab.config')), 43 /* TEXT, CLASS, PROPS, NEED_HYDRATION */, ["data-config-mode", "tabindex", "aria-selected", "aria-controls", "onPointerdown", "onClick"]), @@ -284,7 +284,7 @@ return function render(_ctx, _cache) { "data-main-tab": "config", "data-config-mode": "codex", "aria-current": _ctx.mainTab === 'config' && _ctx.configMode === 'codex' ? 'page' : null, - class: _normalizeClass(['side-item', { active: _ctx.isConfigModeNavActive('codex') }]), + class: _normalizeClass(['side-item', 'side-item-compact', { active: _ctx.isConfigModeNavActive('codex') }]), onPointerdown: $event => (_ctx.onConfigTabPointerDown('codex', $event)), onClick: $event => (_ctx.onConfigTabClick('codex', $event)) }, [ @@ -294,10 +294,7 @@ return function render(_ctx, _cache) { }, "C"), _createElementVNode("div", { class: "side-item-title" }, _toDisplayString(_ctx.t('side.config.codex')), 1 /* TEXT */), _createElementVNode("div", { class: "side-item-meta" }, [ - _createElementVNode("span", null, _toDisplayString(_ctx.t('side.config.codex.meta')), 1 /* TEXT */), - (_ctx.currentProvider) - ? (_openBlock(), _createElementBlock("span", { key: 0 }, _toDisplayString(_ctx.t('common.current', { value: _ctx.currentProvider })), 1 /* TEXT */)) - : _createCommentVNode("v-if", true) + _createElementVNode("span", null, _toDisplayString(_ctx.t('side.config.codex.meta')), 1 /* TEXT */) ]) ], 42 /* CLASS, PROPS, NEED_HYDRATION */, ["aria-current", "onPointerdown", "onClick"]), _createElementVNode("button", { @@ -305,7 +302,7 @@ return function render(_ctx, _cache) { "data-main-tab": "config", "data-config-mode": "claude", "aria-current": _ctx.mainTab === 'config' && _ctx.configMode === 'claude' ? 'page' : null, - class: _normalizeClass(['side-item', { active: _ctx.isConfigModeNavActive('claude') }]), + class: _normalizeClass(['side-item', 'side-item-compact', { active: _ctx.isConfigModeNavActive('claude') }]), onPointerdown: $event => (_ctx.onConfigTabPointerDown('claude', $event)), onClick: $event => (_ctx.onConfigTabClick('claude', $event)) }, [ @@ -315,10 +312,7 @@ return function render(_ctx, _cache) { }, "A"), _createElementVNode("div", { class: "side-item-title" }, _toDisplayString(_ctx.t('side.config.claude')), 1 /* TEXT */), _createElementVNode("div", { class: "side-item-meta" }, [ - _createElementVNode("span", null, _toDisplayString(_ctx.t('side.config.claude.meta')), 1 /* TEXT */), - (_ctx.currentClaudeConfig) - ? (_openBlock(), _createElementBlock("span", { key: 0 }, _toDisplayString(_ctx.t('common.current', { value: _ctx.currentClaudeConfig })), 1 /* TEXT */)) - : _createCommentVNode("v-if", true) + _createElementVNode("span", null, _toDisplayString(_ctx.t('side.config.claude.meta')), 1 /* TEXT */) ]) ], 42 /* CLASS, PROPS, NEED_HYDRATION */, ["aria-current", "onPointerdown", "onClick"]), _createElementVNode("button", { @@ -326,7 +320,7 @@ return function render(_ctx, _cache) { "data-main-tab": "config", "data-config-mode": "openclaw", "aria-current": _ctx.mainTab === 'config' && _ctx.configMode === 'openclaw' ? 'page' : null, - class: _normalizeClass(['side-item', { active: _ctx.isConfigModeNavActive('openclaw') }]), + class: _normalizeClass(['side-item', 'side-item-compact', { active: _ctx.isConfigModeNavActive('openclaw') }]), onPointerdown: $event => (_ctx.onConfigTabPointerDown('openclaw', $event)), onClick: $event => (_ctx.onConfigTabClick('openclaw', $event)) }, [ @@ -336,10 +330,7 @@ return function render(_ctx, _cache) { }, "O"), _createElementVNode("div", { class: "side-item-title" }, _toDisplayString(_ctx.t('side.config.openclaw')), 1 /* TEXT */), _createElementVNode("div", { class: "side-item-meta" }, [ - _createElementVNode("span", null, _toDisplayString(_ctx.t('side.config.openclaw.meta')), 1 /* TEXT */), - (_ctx.currentOpenclawConfig) - ? (_openBlock(), _createElementBlock("span", { key: 0 }, _toDisplayString(_ctx.t('common.current', { value: _ctx.currentOpenclawConfig })), 1 /* TEXT */)) - : _createCommentVNode("v-if", true) + _createElementVNode("span", null, _toDisplayString(_ctx.t('side.config.openclaw.meta')), 1 /* TEXT */) ]) ], 42 /* CLASS, PROPS, NEED_HYDRATION */, ["aria-current", "onPointerdown", "onClick"]), _createElementVNode("button", { @@ -347,7 +338,7 @@ return function render(_ctx, _cache) { "data-main-tab": "config", "data-config-mode": "opencode", "aria-current": _ctx.mainTab === 'config' && _ctx.configMode === 'opencode' ? 'page' : null, - class: _normalizeClass(['side-item', { active: _ctx.isConfigModeNavActive('opencode') }]), + class: _normalizeClass(['side-item', 'side-item-compact', { active: _ctx.isConfigModeNavActive('opencode') }]), onPointerdown: $event => (_ctx.onConfigTabPointerDown('opencode', $event)), onClick: $event => (_ctx.onConfigTabClick('opencode', $event)) }, [ @@ -357,36 +348,34 @@ return function render(_ctx, _cache) { }, "N"), _createElementVNode("div", { class: "side-item-title" }, _toDisplayString(_ctx.t('side.config.opencode')), 1 /* TEXT */), _createElementVNode("div", { class: "side-item-meta" }, [ - _createElementVNode("span", null, _toDisplayString(_ctx.t('side.config.opencode.meta')), 1 /* TEXT */), - (_ctx.opencodeModel) - ? (_openBlock(), _createElementBlock("span", { key: 0 }, _toDisplayString(_ctx.t('common.current', { value: _ctx.opencodeModel })), 1 /* TEXT */)) - : _createCommentVNode("v-if", true) + _createElementVNode("span", null, _toDisplayString(_ctx.t('side.config.opencode.meta')), 1 /* TEXT */) ]) ], 42 /* CLASS, PROPS, NEED_HYDRATION */, ["aria-current", "onPointerdown", "onClick"]), _createElementVNode("button", { - id: "side-tab-prompts", - "data-main-tab": "prompts", - "aria-current": _ctx.mainTab === 'prompts' ? 'page' : null, - class: _normalizeClass(['side-item', { active: _ctx.isMainTabNavActive('prompts') }]), - onPointerdown: $event => (_ctx.onMainTabPointerDown('prompts', $event)), - onClick: $event => (_ctx.onMainTabClick('prompts', $event)) + id: "side-tab-config-kilocode", + "data-main-tab": "config", + "data-config-mode": "kilocode", + "aria-current": _ctx.mainTab === 'config' && _ctx.configMode === 'kilocode' ? 'page' : null, + class: _normalizeClass(['side-item', 'side-item-compact', { active: _ctx.isConfigModeNavActive('kilocode') }]), + onPointerdown: $event => (_ctx.onConfigTabPointerDown('kilocode', $event)), + onClick: $event => (_ctx.onConfigTabClick('kilocode', $event)) }, [ _createElementVNode("span", { class: "side-item-icon", "aria-hidden": "true" - }, "P"), - _createElementVNode("div", { class: "side-item-title" }, _toDisplayString(_ctx.t('side.prompts')), 1 /* TEXT */), + }, "K"), + _createElementVNode("div", { class: "side-item-title" }, _toDisplayString(_ctx.t('side.config.kilocode')), 1 /* TEXT */), _createElementVNode("div", { class: "side-item-meta" }, [ - _createElementVNode("span", null, _toDisplayString(_ctx.t('side.prompts.meta')), 1 /* TEXT */) + _createElementVNode("span", null, _toDisplayString(_ctx.t('side.config.kilocode.meta')), 1 /* TEXT */) ]) ], 42 /* CLASS, PROPS, NEED_HYDRATION */, ["aria-current", "onPointerdown", "onClick"]) ], 8 /* PROPS */, ["aria-label"]), _createElementVNode("div", { class: "side-section", role: "navigation", - "aria-label": _ctx.t('side.sessions') + "aria-label": _ctx.t('side.workspace') }, [ - _createElementVNode("div", { class: "side-section-title" }, _toDisplayString(_ctx.t('side.sessions')), 1 /* TEXT */), + _createElementVNode("div", { class: "side-section-title" }, _toDisplayString(_ctx.t('side.workspace')), 1 /* TEXT */), _createElementVNode("button", { id: "side-tab-sessions", "data-main-tab": "sessions", @@ -405,6 +394,23 @@ return function render(_ctx, _cache) { _createElementVNode("span", null, _toDisplayString(_ctx.t('sessions.sourceLabel', { value: (_ctx.sessionFilterSource === 'all' ? _ctx.t('sessions.source.all') : (_ctx.sessionFilterSource === 'claude' ? 'Claude Code' : (_ctx.sessionFilterSource === 'gemini' ? 'Gemini CLI' : (_ctx.sessionFilterSource === 'codebuddy' ? 'CodeBuddy Code' : 'Codex')))) })), 1 /* TEXT */) ]) ], 42 /* CLASS, PROPS, NEED_HYDRATION */, ["aria-current", "onPointerdown", "onClick"]), + _createElementVNode("button", { + id: "side-tab-prompts", + "data-main-tab": "prompts", + "aria-current": _ctx.mainTab === 'prompts' ? 'page' : null, + class: _normalizeClass(['side-item', { active: _ctx.isMainTabNavActive('prompts') }]), + onPointerdown: $event => (_ctx.onMainTabPointerDown('prompts', $event)), + onClick: $event => (_ctx.onMainTabClick('prompts', $event)) + }, [ + _createElementVNode("span", { + class: "side-item-icon", + "aria-hidden": "true" + }, "P"), + _createElementVNode("div", { class: "side-item-title" }, _toDisplayString(_ctx.t('side.prompts')), 1 /* TEXT */), + _createElementVNode("div", { class: "side-item-meta" }, [ + _createElementVNode("span", null, _toDisplayString(_ctx.t('side.prompts.meta')), 1 /* TEXT */) + ]) + ], 42 /* CLASS, PROPS, NEED_HYDRATION */, ["aria-current", "onPointerdown", "onClick"]), _createElementVNode("button", { id: "side-tab-usage", "data-main-tab": "usage", @@ -2443,7 +2449,12 @@ return function render(_ctx, _cache) { type: "button", class: _normalizeClass(['segment', { active: _ctx.configMode === 'opencode' }]), onClick: $event => (_ctx.switchConfigMode('opencode')) - }, _toDisplayString(_ctx.t('tab.config.opencode')), 11 /* TEXT, CLASS, PROPS */, ["onClick"]) + }, _toDisplayString(_ctx.t('tab.config.opencode')), 11 /* TEXT, CLASS, PROPS */, ["onClick"]), + _createElementVNode("button", { + type: "button", + class: _normalizeClass(['segment', { active: _ctx.configMode === 'kilocode' }]), + onClick: $event => (_ctx.switchConfigMode('kilocode')) + }, _toDisplayString(_ctx.t('tab.config.kilocode')), 11 /* TEXT, CLASS, PROPS */, ["onClick"]) ])) : _createCommentVNode("v-if", true), _createElementVNode("section", { @@ -2800,6 +2811,238 @@ return function render(_ctx, _cache) { ], 8 /* PROPS */, ["aria-labelledby"]), [ [_vShow, _ctx.mainTab === 'config' && _ctx.configMode === 'opencode'] ]), + _createCommentVNode(" KiloCode 配置 "), + _withDirectives(_createElementVNode("div", { + class: "mode-content mode-cards", + id: "panel-config-kilocode", + role: "tabpanel", + "aria-labelledby": _ctx.forceCompactLayout ? 'tab-config' : 'side-tab-config-kilocode' + }, [ + (_ctx.forceCompactLayout && !_ctx.sessionStandalone) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "segmented-control" + }, [ + _createElementVNode("button", { + type: "button", + class: _normalizeClass(['segment', { active: _ctx.configMode === 'codex' }]), + onClick: $event => (_ctx.switchConfigMode('codex')) + }, _toDisplayString(_ctx.t('tab.config.codex')), 11 /* TEXT, CLASS, PROPS */, ["onClick"]), + _createElementVNode("button", { + type: "button", + class: _normalizeClass(['segment', { active: _ctx.configMode === 'claude' }]), + onClick: $event => (_ctx.switchConfigMode('claude')) + }, _toDisplayString(_ctx.t('tab.config.claude')), 11 /* TEXT, CLASS, PROPS */, ["onClick"]), + _createElementVNode("button", { + type: "button", + class: _normalizeClass(['segment', { active: _ctx.configMode === 'openclaw' }]), + onClick: $event => (_ctx.switchConfigMode('openclaw')) + }, _toDisplayString(_ctx.t('tab.config.openclaw')), 11 /* TEXT, CLASS, PROPS */, ["onClick"]), + _createElementVNode("button", { + type: "button", + class: _normalizeClass(['segment', { active: _ctx.configMode === 'opencode' }]), + onClick: $event => (_ctx.switchConfigMode('opencode')) + }, _toDisplayString(_ctx.t('tab.config.opencode')), 11 /* TEXT, CLASS, PROPS */, ["onClick"]), + _createElementVNode("button", { + type: "button", + class: _normalizeClass(['segment', { active: _ctx.configMode === 'kilocode' }]), + onClick: $event => (_ctx.switchConfigMode('kilocode')) + }, _toDisplayString(_ctx.t('tab.config.kilocode')), 11 /* TEXT, CLASS, PROPS */, ["onClick"]) + ])) + : _createCommentVNode("v-if", true), + _createElementVNode("section", { + class: "tool-config-write-card", + "aria-label": _ctx.t('toolConfig.kilocode.title') + }, [ + _createElementVNode("div", { class: "tool-config-write-copy" }, [ + _createElementVNode("div", { class: "tool-config-write-title" }, _toDisplayString(_ctx.t('toolConfig.kilocode.title')), 1 /* TEXT */), + _createElementVNode("p", { class: "tool-config-write-desc" }, _toDisplayString(_ctx.t('toolConfig.kilocode.desc')), 1 /* TEXT */) + ]), + _createElementVNode("label", { class: "settings-toggle-row tool-config-write-toggle" }, [ + _createElementVNode("input", { + type: "checkbox", + autocomplete: "off", + checked: _ctx.isToolConfigWriteAllowed('kilocode'), + disabled: _ctx.toolConfigPermissionSaving.kilocode, + onChange: $event => (_ctx.setToolConfigPermission('kilocode', $event.target.checked)) + }, null, 40 /* PROPS, NEED_HYDRATION */, ["checked", "disabled", "onChange"]), + _createElementVNode("span", { class: "toggle-track" }, [ + _createElementVNode("span", { class: "toggle-thumb" }) + ]), + _createElementVNode("span", null, _toDisplayString(_ctx.toolConfigPermissionStatusLabel('kilocode')), 1 /* TEXT */) + ]) + ], 8 /* PROPS */, ["aria-label"]), + _createElementVNode("div", { + class: _normalizeClass(["tool-config-write-scope", { locked: !_ctx.isToolConfigWriteAllowed('kilocode') }]) + }, [ + _createElementVNode("div", { class: "tool-config-write-body" }, [ + _createElementVNode("section", { class: "selector-section" }, [ + _createElementVNode("div", { class: "selector-header" }, [ + _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('kilocode.providerModel.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "selector-actions opencode-provider-actions" }, [ + _createElementVNode("button", { + type: "button", + class: "btn-tool btn-tool-compact", + onClick: $event => (_ctx.startKilocode(false)), + disabled: _ctx.kilocodeStarting || !_ctx.isToolConfigWriteAllowed('kilocode') + }, _toDisplayString(_ctx.kilocodeStarting ? _ctx.t('kilocode.starting') : _ctx.t('kilocode.start')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]) + ]) + ]), + _createElementVNode("div", { class: "config-template-hint" }, _toDisplayString(_ctx.t('kilocode.targetFile', { path: _ctx.kilocodeConfigPath || '~/.config/kilo/kilo.jsonc', status: _ctx.kilocodeConfigExists ? _ctx.t('common.exists') : _ctx.t('common.notExistsWillCreateOnSave') })), 1 /* TEXT */), + _createElementVNode("div", { class: "codex-config-grid" }, [ + _createElementVNode("div", { class: "form-group codex-config-field" }, [ + _createElementVNode("label", { + class: "form-label", + for: "kilocode-provider" + }, _toDisplayString(_ctx.t('field.provider')), 1 /* TEXT */), + _withDirectives(_createElementVNode("input", { + id: "kilocode-provider", + class: "form-input", + "onUpdate:modelValue": $event => ((_ctx.kilocodeProvider) = $event), + autocomplete: "off", + spellcheck: "false", + placeholder: "codexmate", + onBlur: _ctx.autoSaveKilocodeConfig + }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "onBlur"]), [ + [_vModelText, _ctx.kilocodeProvider] + ]) + ]), + _createElementVNode("div", { class: "form-group codex-config-field" }, [ + _createElementVNode("label", { + class: "form-label", + for: "kilocode-base-url" + }, _toDisplayString(_ctx.t('field.url')), 1 /* TEXT */), + _withDirectives(_createElementVNode("input", { + id: "kilocode-base-url", + class: "form-input", + "onUpdate:modelValue": $event => ((_ctx.kilocodeBaseUrl) = $event), + autocomplete: "off", + spellcheck: "false", + placeholder: "https://api.example.com/v1", + onBlur: _ctx.autoSaveKilocodeConfig + }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "onBlur"]), [ + [_vModelText, _ctx.kilocodeBaseUrl] + ]) + ]), + _createElementVNode("div", { class: "form-group codex-config-field" }, [ + _createElementVNode("label", { + class: "form-label", + for: "kilocode-model" + }, _toDisplayString(_ctx.t('field.model')), 1 /* TEXT */), + _withDirectives(_createElementVNode("input", { + id: "kilocode-model", + class: "form-input", + "onUpdate:modelValue": $event => ((_ctx.kilocodeModel) = $event), + autocomplete: "off", + spellcheck: "false", + placeholder: "gpt-5.3", + onBlur: _ctx.autoSaveKilocodeConfig + }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "onBlur"]), [ + [_vModelText, _ctx.kilocodeModel] + ]) + ]), + _createElementVNode("div", { class: "form-group codex-config-field" }, [ + _createElementVNode("label", { + class: "form-label", + for: "kilocode-api-key" + }, _toDisplayString(_ctx.t('opencode.field.apiKeyKeep')), 1 /* TEXT */), + _createElementVNode("div", { class: "input-with-toggle" }, [ + _withDirectives(_createElementVNode("input", { + id: "kilocode-api-key", + class: "form-input", + "onUpdate:modelValue": $event => ((_ctx.kilocodeApiKey) = $event), + type: _ctx.kilocodeShowKey ? 'text' : 'password', + autocomplete: "off", + spellcheck: "false", + placeholder: "sk-...", + onBlur: _ctx.autoSaveKilocodeConfig + }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "type", "onBlur"]), [ + [_vModelDynamic, _ctx.kilocodeApiKey] + ]), + _createElementVNode("button", { + type: "button", + class: "input-toggle-btn", + onClick: $event => (_ctx.kilocodeShowKey = !_ctx.kilocodeShowKey), + title: _ctx.kilocodeShowKey ? _ctx.t('common.hide') : _ctx.t('common.show') + }, _toDisplayString(_ctx.kilocodeShowKey ? _ctx.t('common.hide') : _ctx.t('common.show')), 9 /* TEXT, PROPS */, ["onClick", "title"]) + ]) + ]) + ]), + (_ctx.kilocodeError) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "config-template-hint error-text" + }, _toDisplayString(_ctx.kilocodeError), 1 /* TEXT */)) + : _createCommentVNode("v-if", true) + ]), + (_ctx.kilocodeProviders.length) + ? (_openBlock(), _createElementBlock("section", { + key: 0, + class: "selector-section" + }, [ + _createElementVNode("div", { class: "selector-header" }, [ + _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('kilocode.summary.title')), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "card-list" }, [ + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.kilocodeProviders, (provider) => { + return (_openBlock(), _createElementBlock("div", { + key: provider.name, + class: "card" + }, [ + _createElementVNode("div", { class: "card-leading" }, [ + _createElementVNode("div", { class: "card-icon" }, _toDisplayString(provider.name.charAt(0).toUpperCase()), 1 /* TEXT */), + _createElementVNode("div", { class: "card-content" }, [ + _createElementVNode("div", { class: "card-title" }, _toDisplayString(provider.name), 1 /* TEXT */), + _createElementVNode("div", { class: "card-subtitle" }, _toDisplayString(provider.baseURL || provider.api || _ctx.t('config.url.unset')), 1 /* TEXT */), + _createElementVNode("div", { class: "card-subtitle" }, _toDisplayString((provider.models || []).join(', ') || _ctx.t('opencode.summary.noModel')), 1 /* TEXT */) + ]) + ]), + _createElementVNode("div", { class: "card-trailing" }, [ + _createElementVNode("span", { + class: _normalizeClass(['pill', provider.hasKey ? 'configured' : 'empty']) + }, _toDisplayString(provider.hasKey ? _ctx.t('common.configured') : _ctx.t('common.notConfigured')), 3 /* TEXT, CLASS */) + ]) + ])) + }), 128 /* KEYED_FRAGMENT */)) + ]) + ])) + : _createCommentVNode("v-if", true), + _createElementVNode("section", { class: "selector-section" }, [ + _createElementVNode("div", { class: "selector-header" }, [ + _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('kilocode.configPreview.title')), 1 /* TEXT */) + ]), + _withDirectives(_createElementVNode("textarea", { + class: "template-textarea", + "onUpdate:modelValue": $event => ((_ctx.kilocodeContent) = $event), + spellcheck: "false", + readonly: "" + }, null, 8 /* PROPS */, ["onUpdate:modelValue"]), [ + [_vModelText, _ctx.kilocodeContent] + ]), + _createElementVNode("div", { class: "config-template-hint" }, _toDisplayString(_ctx.t('kilocode.configPreview.hint')), 1 /* TEXT */) + ]), + (!_ctx.isToolConfigWriteAllowed('kilocode')) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "tool-config-write-overlay" + }, [ + _createElementVNode("div", { class: "tool-config-write-overlay-card" }, [ + _createElementVNode("div", { class: "tool-config-write-overlay-title" }, _toDisplayString(_ctx.t('toolConfig.kilocode.lockedTitle')), 1 /* TEXT */), + _createElementVNode("p", null, _toDisplayString(_ctx.t('toolConfig.kilocode.lockedDesc')), 1 /* TEXT */), + _createElementVNode("button", { + type: "button", + class: "btn-tool", + onClick: $event => (_ctx.setToolConfigPermission('kilocode', true)), + disabled: _ctx.toolConfigPermissionSaving.kilocode + }, _toDisplayString(_ctx.t('toolConfig.enableWrite')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]) + ]) + ])) + : _createCommentVNode("v-if", true) + ]) + ], 2 /* CLASS */) + ], 8 /* PROPS */, ["aria-labelledby"]), [ + [_vShow, _ctx.mainTab === 'config' && _ctx.configMode === 'kilocode'] + ]), _createCommentVNode(" 会话浏览模式 "), _withDirectives(_createElementVNode("div", { class: "mode-content", diff --git a/web-ui/styles/layout-shell.css b/web-ui/styles/layout-shell.css index 99b8b623..6126a5f7 100644 --- a/web-ui/styles/layout-shell.css +++ b/web-ui/styles/layout-shell.css @@ -397,6 +397,17 @@ body::after { position: relative; } +.side-item-compact { + padding-top: 7px; + padding-bottom: 7px; + gap: 2px; +} + +.side-item-compact::before { + height: 14px; +} + + .side-item-icon { display: none; width: 28px; From 472d3c047e966b261324595e1cbc39a167106d29 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Thu, 16 Jul 2026 04:31:12 +0000 Subject: [PATCH 02/13] fix(kilocode): remove web launch button --- tests/unit/install-target-cards.test.mjs | 34 +++++++++++++ tests/unit/kilocode-config-ui.test.mjs | 4 +- tests/unit/web-ui-behavior-parity.test.mjs | 3 +- web-ui/modules/app.computed.dashboard.mjs | 4 ++ web-ui/modules/i18n/locales/en.mjs | 3 ++ web-ui/modules/i18n/locales/ja.mjs | 3 ++ web-ui/modules/i18n/locales/vi.mjs | 3 ++ web-ui/modules/i18n/locales/zh-tw.mjs | 3 ++ web-ui/modules/i18n/locales/zh.mjs | 3 ++ .../partials/index/panel-config-kilocode.html | 3 -- web-ui/partials/index/panel-docs.html | 18 ++++++- web-ui/res/web-ui-render.precompiled.js | 35 +++++++++---- web-ui/styles/docs-panel.css | 51 +++++++++++++++++++ 13 files changed, 151 insertions(+), 16 deletions(-) diff --git a/tests/unit/install-target-cards.test.mjs b/tests/unit/install-target-cards.test.mjs index e16c8d57..956fb71e 100644 --- a/tests/unit/install-target-cards.test.mjs +++ b/tests/unit/install-target-cards.test.mjs @@ -42,6 +42,40 @@ test('installTargetCards falls back when install-status is missing', () => { assert(codex.termuxCommand.includes('@mmmbuto/codex-cli-termux')); }); +test('currentInstalledCommandCards exposes detected installed commands only', () => { + const ctx = createContext({ + installStatusTargets: [ + { + id: 'codex', + name: 'Codex CLI', + packageName: '@openai/codex', + installed: true, + bin: 'codex', + version: '0.1.0', + commandPath: '/usr/local/bin/codex', + error: '' + }, + { + id: 'gemini', + name: 'Gemini CLI', + packageName: '@google/gemini-cli', + installed: false, + bin: 'gemini', + version: '', + commandPath: '', + error: 'not found' + } + ] + }); + ctx.installTargetCards = computed.installTargetCards.call(ctx); + + const cards = computed.currentInstalledCommandCards.call(ctx); + + assert.deepStrictEqual(cards.map((item) => item.id), ['codex']); + assert.strictEqual(cards[0].commandPath, '/usr/local/bin/codex'); + assert.strictEqual(cards[0].version, '0.1.0'); +}); + test('app update notice only appears when latest package version is newer', () => { const ctx = createContext({ appVersion: '0.0.40', diff --git a/tests/unit/kilocode-config-ui.test.mjs b/tests/unit/kilocode-config-ui.test.mjs index 309b0044..1eb5f246 100644 --- a/tests/unit/kilocode-config-ui.test.mjs +++ b/tests/unit/kilocode-config-ui.test.mjs @@ -26,7 +26,7 @@ function createVm(apiImpl = async () => ({})) { }; } -test('KiloCode panel auto-syncs on blur and keeps launch explicit', () => { +test('KiloCode panel auto-syncs on blur without Web UI launch buttons', () => { const html = readBundledWebUiHtml(); assert.match(html, /id="kilocode-provider"[^>]*@blur="autoSaveKilocodeConfig"/); assert.match(html, /id="kilocode-base-url"[^>]*@blur="autoSaveKilocodeConfig"/); @@ -35,7 +35,7 @@ test('KiloCode panel auto-syncs on blur and keeps launch explicit', () => { assert.doesNotMatch(html, /@click="loadKilocodeConfig\(\{ toast: true \}\)"/); assert.doesNotMatch(html, /@click="saveKilocodeConfig"/); assert.doesNotMatch(html, /@click="startKilocode\(true\)"/); - assert.match(html, /@click="startKilocode\(false\)"/); + assert.doesNotMatch(html, /@click="startKilocode\(false\)"/); }); test('KiloCode auto-save reuses stored API key when the key field is blank', async () => { diff --git a/tests/unit/web-ui-behavior-parity.test.mjs b/tests/unit/web-ui-behavior-parity.test.mjs index 5b59c4bd..b110a652 100644 --- a/tests/unit/web-ui-behavior-parity.test.mjs +++ b/tests/unit/web-ui-behavior-parity.test.mjs @@ -978,7 +978,8 @@ test('captured bundled app skeleton only exposes expected data key drift versus 'sessionContextUtilization', 'isLocalProviderDisabled', 'sessionTimelineProgressPercent', - 'activeSessionWorkspaceSummary' + 'activeSessionWorkspaceSummary', + 'currentInstalledCommandCards' ]; const allowedMissingCurrentComputedKeys = [ 'hasLocalAndProxy', diff --git a/web-ui/modules/app.computed.dashboard.mjs b/web-ui/modules/app.computed.dashboard.mjs index 0fadbdbe..ac090880 100644 --- a/web-ui/modules/app.computed.dashboard.mjs +++ b/web-ui/modules/app.computed.dashboard.mjs @@ -164,6 +164,10 @@ export function createDashboardComputed() { }; }); }, + currentInstalledCommandCards() { + const targets = Array.isArray(this.installTargetCards) ? this.installTargetCards : []; + return targets.filter((target) => target && target.installed === true); + }, installRegistryPreview() { return this.resolveInstallRegistryUrl(this.installRegistryPreset, this.installRegistryCustom); }, diff --git a/web-ui/modules/i18n/locales/en.mjs b/web-ui/modules/i18n/locales/en.mjs index 099012b6..e2195ee3 100644 --- a/web-ui/modules/i18n/locales/en.mjs +++ b/web-ui/modules/i18n/locales/en.mjs @@ -622,6 +622,9 @@ const en = Object.freeze({ 'docs.subtitle': 'Install commands for Claude Code / Gemini CLI / CodeBuddy Code / Codex CLI.', 'docs.section.commands': 'Commands', 'docs.section.commandsNote': 'Copy and run directly.', + 'docs.installed.title': 'Currently installed commands', + 'docs.installed.count': '{count} installed', + 'docs.installed.empty': 'No installed commands detected', 'docs.section.faq': 'FAQ', 'docs.section.faqNote': 'Common issues and tips.', 'docs.command.aria': '{name} command', diff --git a/web-ui/modules/i18n/locales/ja.mjs b/web-ui/modules/i18n/locales/ja.mjs index d9ed4ec0..d1144f39 100644 --- a/web-ui/modules/i18n/locales/ja.mjs +++ b/web-ui/modules/i18n/locales/ja.mjs @@ -624,6 +624,9 @@ const ja = Object.freeze({ 'docs.subtitle': 'Claude Code / Gemini CLI / CodeBuddy Code / Codex CLI コマンドを表示。', 'docs.section.commands': 'インストールコマンド', 'docs.section.commandsNote': 'コマンドは直接コピーできます。', + 'docs.installed.title': '現在インストール済みのコマンド', + 'docs.installed.count': '{count} 個インストール済み', + 'docs.installed.empty': 'インストール済みコマンドは検出されていません', 'docs.section.faq': 'よくある質問', 'docs.section.faqNote': 'よくある質問は以下をご覧ください。', 'docs.command.aria': '{name} コマンド', diff --git a/web-ui/modules/i18n/locales/vi.mjs b/web-ui/modules/i18n/locales/vi.mjs index dde1cbdc..cb08c705 100644 --- a/web-ui/modules/i18n/locales/vi.mjs +++ b/web-ui/modules/i18n/locales/vi.mjs @@ -829,6 +829,9 @@ const vi = Object.freeze({ 'docs.subtitle': 'Lệnh cài đặt cho Claude Code / Gemini CLI / CodeBuddy Code / Codex CLI.', 'docs.section.commands': 'Lệnh', 'docs.section.commandsNote': 'Sao chép và chạy trực tiếp.', + 'docs.installed.title': 'Lệnh hiện đã cài đặt', + 'docs.installed.count': 'Đã cài {count}', + 'docs.installed.empty': 'Chưa phát hiện lệnh đã cài', 'docs.section.faq': 'FAQ', 'docs.section.faqNote': 'Vấn đề phổ biến và mẹo.', 'docs.command.aria': 'Lệnh {name}', diff --git a/web-ui/modules/i18n/locales/zh-tw.mjs b/web-ui/modules/i18n/locales/zh-tw.mjs index c3b816c1..b6325bee 100644 --- a/web-ui/modules/i18n/locales/zh-tw.mjs +++ b/web-ui/modules/i18n/locales/zh-tw.mjs @@ -622,6 +622,9 @@ const zhTw = Object.freeze({ 'docs.subtitle': '查看 Claude Code / Gemini CLI / CodeBuddy Code / Codex CLI 命令。', 'docs.section.commands': '安裝命令', 'docs.section.commandsNote': '命令可直接複製。', + 'docs.installed.title': '目前已安裝命令', + 'docs.installed.count': '已安裝 {count} 個', + 'docs.installed.empty': '未偵測到已安裝命令', 'docs.section.faq': '常見問題', 'docs.section.faqNote': '常見問題見下。', 'docs.command.aria': '{name} 命令', diff --git a/web-ui/modules/i18n/locales/zh.mjs b/web-ui/modules/i18n/locales/zh.mjs index 02f71dbc..d4aacccf 100644 --- a/web-ui/modules/i18n/locales/zh.mjs +++ b/web-ui/modules/i18n/locales/zh.mjs @@ -622,6 +622,9 @@ const zh = Object.freeze({ 'docs.subtitle': '查看 Claude Code / Gemini CLI / CodeBuddy Code / Codex CLI 命令。', 'docs.section.commands': '安装命令', 'docs.section.commandsNote': '命令可直接复制。', + 'docs.installed.title': '当前已安装命令', + 'docs.installed.count': '已安装 {count} 个', + 'docs.installed.empty': '未检测到已安装命令', 'docs.section.faq': '常见问题', 'docs.section.faqNote': '常见问题见下。', 'docs.command.aria': '{name} 命令', diff --git a/web-ui/partials/index/panel-config-kilocode.html b/web-ui/partials/index/panel-config-kilocode.html index 11feeb46..08de689e 100644 --- a/web-ui/partials/index/panel-config-kilocode.html +++ b/web-ui/partials/index/panel-config-kilocode.html @@ -35,9 +35,6 @@
{{ t('kilocode.providerModel.title') }} -
- -
{{ t('kilocode.targetFile', { path: kilocodeConfigPath || '~/.config/kilo/kilo.jsonc', status: kilocodeConfigExists ? t('common.exists') : t('common.notExistsWillCreateOnSave') }) }}
diff --git a/web-ui/partials/index/panel-docs.html b/web-ui/partials/index/panel-docs.html index 875e10c1..45a7a9dc 100644 --- a/web-ui/partials/index/panel-docs.html +++ b/web-ui/partials/index/panel-docs.html @@ -54,6 +54,22 @@ {{ installRegistryPreview || 'npmmirror' }}
+ +
+
+ {{ t('docs.installed.title') }} + {{ currentInstalledCommandCards.length ? t('docs.installed.count', { count: currentInstalledCommandCards.length }) : t('docs.installed.empty') }} +
+
+
+
+ {{ target.name }} + {{ target.version || '—' }} +
+ {{ target.commandPath || target.bin }} +
+
+
@@ -111,4 +127,4 @@
- \ No newline at end of file + diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index e8d946f2..9669ff44 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -2878,15 +2878,7 @@ return function render(_ctx, _cache) { _createElementVNode("div", { class: "tool-config-write-body" }, [ _createElementVNode("section", { class: "selector-section" }, [ _createElementVNode("div", { class: "selector-header" }, [ - _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('kilocode.providerModel.title')), 1 /* TEXT */), - _createElementVNode("div", { class: "selector-actions opencode-provider-actions" }, [ - _createElementVNode("button", { - type: "button", - class: "btn-tool btn-tool-compact", - onClick: $event => (_ctx.startKilocode(false)), - disabled: _ctx.kilocodeStarting || !_ctx.isToolConfigWriteAllowed('kilocode') - }, _toDisplayString(_ctx.kilocodeStarting ? _ctx.t('kilocode.starting') : _ctx.t('kilocode.start')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]) - ]) + _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('kilocode.providerModel.title')), 1 /* TEXT */) ]), _createElementVNode("div", { class: "config-template-hint" }, _toDisplayString(_ctx.t('kilocode.targetFile', { path: _ctx.kilocodeConfigPath || '~/.config/kilo/kilo.jsonc', status: _ctx.kilocodeConfigExists ? _ctx.t('common.exists') : _ctx.t('common.notExistsWillCreateOnSave') })), 1 /* TEXT */), _createElementVNode("div", { class: "codex-config-grid" }, [ @@ -5487,6 +5479,31 @@ return function render(_ctx, _cache) { _createElementVNode("span", { class: "docs-summary-label" }, _toDisplayString(_ctx.t('common.registry')), 1 /* TEXT */), _createElementVNode("strong", { class: "docs-summary-value" }, _toDisplayString(_ctx.installRegistryPreview || 'npmmirror'), 1 /* TEXT */) ]) + ]), + _createElementVNode("div", { class: "docs-installed-card" }, [ + _createElementVNode("div", { class: "docs-installed-head" }, [ + _createElementVNode("span", { class: "docs-installed-title" }, _toDisplayString(_ctx.t('docs.installed.title')), 1 /* TEXT */), + _createElementVNode("span", { class: "docs-installed-count" }, _toDisplayString(_ctx.currentInstalledCommandCards.length ? _ctx.t('docs.installed.count', { count: _ctx.currentInstalledCommandCards.length }) : _ctx.t('docs.installed.empty')), 1 /* TEXT */) + ]), + (_ctx.currentInstalledCommandCards.length) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "docs-installed-list" + }, [ + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.currentInstalledCommandCards, (target) => { + return (_openBlock(), _createElementBlock("div", { + key: 'docs-installed-' + target.id, + class: "docs-installed-item" + }, [ + _createElementVNode("div", { class: "docs-installed-main" }, [ + _createElementVNode("span", { class: "docs-installed-name" }, _toDisplayString(target.name), 1 /* TEXT */), + _createElementVNode("span", { class: "docs-installed-version" }, _toDisplayString(target.version || '—'), 1 /* TEXT */) + ]), + _createElementVNode("code", { class: "docs-installed-command" }, _toDisplayString(target.commandPath || target.bin), 1 /* TEXT */) + ])) + }), 128 /* KEYED_FRAGMENT */)) + ])) + : _createCommentVNode("v-if", true) ]) ]), _createElementVNode("div", { class: "selector-section" }, [ diff --git a/web-ui/styles/docs-panel.css b/web-ui/styles/docs-panel.css index 806d469f..a1f4d3cc 100644 --- a/web-ui/styles/docs-panel.css +++ b/web-ui/styles/docs-panel.css @@ -69,6 +69,57 @@ word-break: break-word; } +.docs-installed-card { + margin-top: 12px; + padding: 12px; + border: 1px solid var(--color-border-soft); + border-radius: var(--radius-md); + background: var(--color-bg-soft); +} + +.docs-installed-head, +.docs-installed-main { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.docs-installed-title, +.docs-installed-name { + font-weight: 700; + color: var(--color-text-primary); +} + +.docs-installed-count, +.docs-installed-version { + font-size: 12px; + color: var(--color-text-muted); +} + +.docs-installed-list { + display: grid; + gap: 8px; + margin-top: 10px; +} + +.docs-installed-item { + min-width: 0; + padding: 10px; + border-radius: var(--radius-sm); + background: var(--color-bg-card); +} + +.docs-installed-command { + display: block; + margin-top: 6px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 12px; + color: var(--color-text-secondary); +} + /* ---- Install rows ---- */ .docs-install-list { display: flex; From 0e7383d80a8efcdf7565320a9ecb43d437dc4a6f Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Thu, 16 Jul 2026 06:30:07 +0000 Subject: [PATCH 03/13] fix(kilocode): persist url key without provider field --- tests/unit/kilocode-config-ui.test.mjs | 34 +++++++++++- web-ui/app.js | 2 +- .../modules/app.methods.kilocode-config.mjs | 24 +++++---- web-ui/modules/i18n/locales/en.mjs | 3 +- web-ui/modules/i18n/locales/ja.mjs | 3 +- web-ui/modules/i18n/locales/vi.mjs | 3 +- web-ui/modules/i18n/locales/zh-tw.mjs | 3 +- web-ui/modules/i18n/locales/zh.mjs | 3 +- .../partials/index/panel-config-kilocode.html | 25 +-------- web-ui/res/web-ui-render.precompiled.js | 53 +------------------ 10 files changed, 61 insertions(+), 92 deletions(-) diff --git a/tests/unit/kilocode-config-ui.test.mjs b/tests/unit/kilocode-config-ui.test.mjs index 1eb5f246..114ab89b 100644 --- a/tests/unit/kilocode-config-ui.test.mjs +++ b/tests/unit/kilocode-config-ui.test.mjs @@ -28,7 +28,8 @@ function createVm(apiImpl = async () => ({})) { test('KiloCode panel auto-syncs on blur without Web UI launch buttons', () => { const html = readBundledWebUiHtml(); - assert.match(html, /id="kilocode-provider"[^>]*@blur="autoSaveKilocodeConfig"/); + assert.doesNotMatch(html, /id="kilocode-provider"/); + assert.doesNotMatch(html, /kilocode\.summary\.title/); assert.match(html, /id="kilocode-base-url"[^>]*@blur="autoSaveKilocodeConfig"/); assert.match(html, /id="kilocode-model"[^>]*@blur="autoSaveKilocodeConfig"/); assert.match(html, /id="kilocode-api-key"[^>]*@blur="autoSaveKilocodeConfig"/); @@ -67,6 +68,37 @@ test('KiloCode auto-save reuses stored API key when the key field is blank', asy assert.deepStrictEqual(vm.messages.at(-1), { message: 'kilocode.autoSaved', type: 'success' }); }); +test('KiloCode auto-save defaults provider and model when only URL and key are filled', async () => { + const calls = []; + const vm = createVm(async (action, params) => { + calls.push({ action, params }); + assert.strictEqual(action, 'apply-kilocode-config'); + return { + targetPath: '/tmp/kilo.jsonc', + exists: true, + content: '{}\n', + currentProvider: params.provider, + currentModel: params.model, + providers: [{ name: params.provider, hasKey: true, api: params.url, models: [params.model] }] + }; + }); + vm.kilocodeProvider = ''; + vm.kilocodeModel = ''; + vm.kilocodeApiKey = 'sk-test'; + + const saved = await vm.autoSaveKilocodeConfig(); + + assert.strictEqual(saved, true); + assert.deepStrictEqual(calls[0].params, { + provider: 'codexmate', + url: 'https://new.example.com/v1', + model: 'gpt-5.3', + apiKey: 'sk-test' + }); + assert.strictEqual(vm.kilocodeProvider, 'codexmate'); + assert.strictEqual(vm.kilocodeModel, 'gpt-5.3'); +}); + test('KiloCode backend source preserves existing API key for blank Web auto-sync', () => { const cli = readProjectFile('cli.js'); assert.match(cli, /const existingKey = typeof existingOptions\.apiKey === 'string'/); diff --git a/web-ui/app.js b/web-ui/app.js index bc45ea00..3cc152c7 100644 --- a/web-ui/app.js +++ b/web-ui/app.js @@ -450,7 +450,7 @@ document.addEventListener('DOMContentLoaded', () => { kilocodeProviders: [], kilocodeProvider: 'codexmate', kilocodeBaseUrl: '', - kilocodeModel: '', + kilocodeModel: 'gpt-5.3', kilocodeApiKey: '', kilocodeShowKey: false, kilocodeAutoSaveSignature: '', diff --git a/web-ui/modules/app.methods.kilocode-config.mjs b/web-ui/modules/app.methods.kilocode-config.mjs index 3e0b95e7..301839fd 100644 --- a/web-ui/modules/app.methods.kilocode-config.mjs +++ b/web-ui/modules/app.methods.kilocode-config.mjs @@ -3,6 +3,9 @@ function normalizeKilocodeProviderName(value) { return /^[a-zA-Z0-9_.-]+$/.test(name) ? name : ''; } +const DEFAULT_KILOCODE_PROVIDER = 'codexmate'; +const DEFAULT_KILOCODE_MODEL = 'gpt-5.3'; + export function createKilocodeConfigMethods(options = {}) { const { api } = options; return { @@ -15,14 +18,13 @@ export function createKilocodeConfigMethods(options = {}) { const firstProvider = providers.find(item => item && item.name); this.kilocodeProvider = normalizeKilocodeProviderName(res.currentProvider) || normalizeKilocodeProviderName(firstProvider && firstProvider.name) - || normalizeKilocodeProviderName(this.kilocodeProvider) - || 'codexmate'; + || DEFAULT_KILOCODE_PROVIDER; const selected = providers.find(item => normalizeKilocodeProviderName(item && item.name) === this.kilocodeProvider); this.kilocodeBaseUrl = typeof (selected && (selected.baseURL || selected.api)) === 'string' ? (selected.baseURL || selected.api) : this.kilocodeBaseUrl; const model = typeof res.currentModel === 'string' ? res.currentModel.trim() : ''; - this.kilocodeModel = model || (selected && Array.isArray(selected.models) ? (selected.models[0] || '') : this.kilocodeModel); + this.kilocodeModel = model || (selected && Array.isArray(selected.models) ? (selected.models[0] || '') : this.kilocodeModel) || DEFAULT_KILOCODE_MODEL; }, async loadKilocodeConfig(options = {}) { @@ -56,11 +58,11 @@ export function createKilocodeConfigMethods(options = {}) { async autoSaveKilocodeConfig() { if (!this.isToolConfigWriteAllowed('kilocode') || this.kilocodeSaving || this.kilocodeLoading) return false; - const provider = normalizeKilocodeProviderName(this.kilocodeProvider); + const provider = DEFAULT_KILOCODE_PROVIDER; const url = typeof this.kilocodeBaseUrl === 'string' ? this.kilocodeBaseUrl.trim() : ''; - const model = typeof this.kilocodeModel === 'string' ? this.kilocodeModel.trim() : ''; + const model = (typeof this.kilocodeModel === 'string' ? this.kilocodeModel.trim() : '') || DEFAULT_KILOCODE_MODEL; const apiKey = typeof this.kilocodeApiKey === 'string' ? this.kilocodeApiKey.trim() : ''; - if (!provider || !url || !model || (!apiKey && !this.hasKilocodeStoredKey(provider))) return false; + if (!url || (!apiKey && !this.hasKilocodeStoredKey(provider))) return false; const signature = this.kilocodeConfigSignature(provider, url, model, apiKey); if (signature === this.kilocodeAutoSaveSignature) return false; return this.saveKilocodeConfig({ auto: true, signature }); @@ -73,14 +75,16 @@ export function createKilocodeConfigMethods(options = {}) { if (!auto) this.showMessage(this.t ? this.t('kilocode.writeRequired') : '请先打开 KiloCode 写入开关', 'error'); return false; } - const provider = normalizeKilocodeProviderName(this.kilocodeProvider); + const provider = DEFAULT_KILOCODE_PROVIDER; const url = typeof this.kilocodeBaseUrl === 'string' ? this.kilocodeBaseUrl.trim() : ''; - const model = typeof this.kilocodeModel === 'string' ? this.kilocodeModel.trim() : ''; + const model = (typeof this.kilocodeModel === 'string' ? this.kilocodeModel.trim() : '') || DEFAULT_KILOCODE_MODEL; const apiKey = typeof this.kilocodeApiKey === 'string' ? this.kilocodeApiKey.trim() : ''; - if (!provider || !url || !model || (!apiKey && !this.hasKilocodeStoredKey(provider))) { - if (!auto) this.showMessage(this.t ? this.t('kilocode.fillRequired') : '请填写 KiloCode provider、URL、API Key 和模型', 'error'); + if (!url || (!apiKey && !this.hasKilocodeStoredKey(provider))) { + if (!auto) this.showMessage(this.t ? this.t('kilocode.fillRequired') : '请填写 KiloCode URL 和 API Key', 'error'); return false; } + this.kilocodeProvider = provider; + this.kilocodeModel = model; this.kilocodeSaving = true; this.kilocodeError = ''; try { diff --git a/web-ui/modules/i18n/locales/en.mjs b/web-ui/modules/i18n/locales/en.mjs index e2195ee3..acb6f6a3 100644 --- a/web-ui/modules/i18n/locales/en.mjs +++ b/web-ui/modules/i18n/locales/en.mjs @@ -1545,6 +1545,7 @@ const en = Object.freeze({ 'toolConfig.kilocode.lockedDesc': 'Enable write access to auto-sync config or launch KiloCode.', 'toolConfig.kilocode.confirmMessage': 'When enabled, actions in the KiloCode tab can write ~/.config/kilo/kilo.jsonc or an existing kilo.json, and can launch local KiloCode.', 'kilocode.providerModel.title': 'KiloCode provider / model', + 'kilocode.connection.title': 'KiloCode connection', 'kilocode.targetFile': 'Active KiloCode config: {path} · {status}', 'kilocode.saveConfig': 'Save config', 'kilocode.start': 'Launch KiloCode', @@ -1554,7 +1555,7 @@ const en = Object.freeze({ 'kilocode.configPreview.title': 'KiloCode config preview', 'kilocode.configPreview.hint': 'API Key is redacted; auto-sync writes the real key.', 'kilocode.saveFailed': 'Failed to save KiloCode config', - 'kilocode.fillRequired': 'Fill KiloCode provider, URL, API Key, and model', + 'kilocode.fillRequired': 'Fill KiloCode URL and API Key', 'kilocode.writeRequired': 'Enable KiloCode write access first', 'kilocode.saved': 'KiloCode config saved', 'kilocode.autoSaved': 'KiloCode config synced', diff --git a/web-ui/modules/i18n/locales/ja.mjs b/web-ui/modules/i18n/locales/ja.mjs index d1144f39..563a2c3b 100644 --- a/web-ui/modules/i18n/locales/ja.mjs +++ b/web-ui/modules/i18n/locales/ja.mjs @@ -1532,6 +1532,7 @@ const ja = Object.freeze({ 'toolConfig.kilocode.lockedDesc': '書き込み権限を有効化すると、設定の自動同期または KiloCode の起動ができます。', 'toolConfig.kilocode.confirmMessage': '有効化すると、KiloCode タブ内の操作が ~/.config/kilo/kilo.jsonc または既存の kilo.json に書き込み、ローカル KiloCode を起動できます。', 'kilocode.providerModel.title': 'KiloCode プロバイダー / モデル', + 'kilocode.connection.title': 'KiloCode 接続', 'kilocode.targetFile': '有効な KiloCode 設定:{path} · {status}', 'kilocode.saveConfig': '設定を保存', 'kilocode.start': 'KiloCode を起動', @@ -1541,7 +1542,7 @@ const ja = Object.freeze({ 'kilocode.configPreview.title': 'KiloCode 設定プレビュー', 'kilocode.configPreview.hint': 'API Key はマスク済みです。自動同期では実際の key が書き込まれます。', 'kilocode.saveFailed': 'KiloCode 設定の保存に失敗しました', - 'kilocode.fillRequired': 'KiloCode provider、URL、API Key、モデルを入力してください', + 'kilocode.fillRequired': 'KiloCode URL と API Key を入力してください', 'kilocode.writeRequired': '先に KiloCode の書き込みを有効化してください', 'kilocode.saved': 'KiloCode 設定を保存しました', 'kilocode.autoSaved': 'KiloCode 設定を同期しました', diff --git a/web-ui/modules/i18n/locales/vi.mjs b/web-ui/modules/i18n/locales/vi.mjs index cb08c705..bd756647 100644 --- a/web-ui/modules/i18n/locales/vi.mjs +++ b/web-ui/modules/i18n/locales/vi.mjs @@ -1517,6 +1517,7 @@ const vi = Object.freeze({ 'toolConfig.kilocode.lockedDesc': 'Bật quyền ghi để tự động đồng bộ cấu hình hoặc khởi chạy KiloCode.', 'toolConfig.kilocode.confirmMessage': 'Khi bật, các thao tác trong tab KiloCode có thể ghi ~/.config/kilo/kilo.jsonc hoặc kilo.json hiện có, và có thể khởi chạy KiloCode cục bộ.', 'kilocode.providerModel.title': 'Nhà cung cấp / mô hình KiloCode', + 'kilocode.connection.title': 'Kết nối KiloCode', 'kilocode.targetFile': 'Cấu hình KiloCode đang dùng: {path} · {status}', 'kilocode.saveConfig': 'Lưu cấu hình', 'kilocode.start': 'Khởi chạy KiloCode', @@ -1526,7 +1527,7 @@ const vi = Object.freeze({ 'kilocode.configPreview.title': 'Xem trước cấu hình KiloCode', 'kilocode.configPreview.hint': 'API Key đã được che; tự động đồng bộ sẽ ghi key thật.', 'kilocode.saveFailed': 'Không thể lưu cấu hình KiloCode', - 'kilocode.fillRequired': 'Điền provider, URL, API Key và mô hình KiloCode', + 'kilocode.fillRequired': 'Điền URL và API Key KiloCode', 'kilocode.writeRequired': 'Hãy bật quyền ghi KiloCode trước', 'kilocode.saved': 'Đã lưu cấu hình KiloCode', 'kilocode.autoSaved': 'Đã đồng bộ cấu hình KiloCode', diff --git a/web-ui/modules/i18n/locales/zh-tw.mjs b/web-ui/modules/i18n/locales/zh-tw.mjs index b6325bee..48ced4ad 100644 --- a/web-ui/modules/i18n/locales/zh-tw.mjs +++ b/web-ui/modules/i18n/locales/zh-tw.mjs @@ -1545,6 +1545,7 @@ const zhTw = Object.freeze({ 'toolConfig.kilocode.lockedDesc': '啟用寫入權限後可自動同步設定或啟動 KiloCode。', 'toolConfig.kilocode.confirmMessage': '開啟後,KiloCode tab 內的操作會寫入 ~/.config/kilo/kilo.jsonc 或既有的 kilo.json,並可啟動本機 KiloCode。', 'kilocode.providerModel.title': 'KiloCode provider / 模型', + 'kilocode.connection.title': 'KiloCode 連線', 'kilocode.targetFile': 'KiloCode 生效設定:{path} · {status}', 'kilocode.saveConfig': '儲存設定', 'kilocode.start': '啟動 KiloCode', @@ -1554,7 +1555,7 @@ const zhTw = Object.freeze({ 'kilocode.configPreview.title': 'KiloCode 設定預覽', 'kilocode.configPreview.hint': 'API Key 已遮罩;自動同步會寫入真實 key。', 'kilocode.saveFailed': '儲存 KiloCode 設定失敗', - 'kilocode.fillRequired': '請填寫 KiloCode provider、URL、API Key 與模型', + 'kilocode.fillRequired': '請填寫 KiloCode URL 與 API Key', 'kilocode.writeRequired': '請先開啟 KiloCode 寫入開關', 'kilocode.saved': 'KiloCode 設定已儲存', 'kilocode.autoSaved': 'KiloCode 設定已同步', diff --git a/web-ui/modules/i18n/locales/zh.mjs b/web-ui/modules/i18n/locales/zh.mjs index d4aacccf..b3fa013b 100644 --- a/web-ui/modules/i18n/locales/zh.mjs +++ b/web-ui/modules/i18n/locales/zh.mjs @@ -1545,6 +1545,7 @@ const zh = Object.freeze({ 'toolConfig.kilocode.lockedDesc': '开启写入权限后可保存配置或启动 KiloCode。', 'toolConfig.kilocode.confirmMessage': '打开后,KiloCode tab 内的操作会写入 ~/.config/kilo/kilo.jsonc 或已存在的 kilo.json,并可启动本机 KiloCode。', 'kilocode.providerModel.title': 'KiloCode provider / model', + 'kilocode.connection.title': 'KiloCode 连接', 'kilocode.targetFile': 'KiloCode 生效配置:{path} · {status}', 'kilocode.saveConfig': '保存配置', 'kilocode.start': '启动 KiloCode', @@ -1554,7 +1555,7 @@ const zh = Object.freeze({ 'kilocode.configPreview.title': 'KiloCode 配置预览', 'kilocode.configPreview.hint': 'API Key 已脱敏;保存会写入真实 key。', 'kilocode.saveFailed': '保存 KiloCode 配置失败', - 'kilocode.fillRequired': '请填写 KiloCode provider、URL、API Key 和模型', + 'kilocode.fillRequired': '请填写 KiloCode URL 和 API Key', 'kilocode.writeRequired': '请先打开 KiloCode 写入开关', 'kilocode.saved': 'KiloCode 配置已保存', 'kilocode.autoSaved': 'KiloCode 配置已同步', diff --git a/web-ui/partials/index/panel-config-kilocode.html b/web-ui/partials/index/panel-config-kilocode.html index 08de689e..ff9f405b 100644 --- a/web-ui/partials/index/panel-config-kilocode.html +++ b/web-ui/partials/index/panel-config-kilocode.html @@ -34,14 +34,10 @@
- {{ t('kilocode.providerModel.title') }} + {{ t('kilocode.connection.title') }}
{{ t('kilocode.targetFile', { path: kilocodeConfigPath || '~/.config/kilo/kilo.jsonc', status: kilocodeConfigExists ? t('common.exists') : t('common.notExistsWillCreateOnSave') }) }}
-
- - -
@@ -61,25 +57,6 @@
{{ kilocodeError }}
-
-
{{ t('kilocode.summary.title') }}
-
-
-
-
{{ provider.name.charAt(0).toUpperCase() }}
-
-
{{ provider.name }}
-
{{ provider.baseURL || provider.api || t('config.url.unset') }}
-
{{ (provider.models || []).join(', ') || t('opencode.summary.noModel') }}
-
-
-
- {{ provider.hasKey ? t('common.configured') : t('common.notConfigured') }} -
-
-
-
-
{{ t('kilocode.configPreview.title') }}
diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index 9669ff44..894a555c 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -2878,27 +2878,10 @@ return function render(_ctx, _cache) { _createElementVNode("div", { class: "tool-config-write-body" }, [ _createElementVNode("section", { class: "selector-section" }, [ _createElementVNode("div", { class: "selector-header" }, [ - _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('kilocode.providerModel.title')), 1 /* TEXT */) + _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('kilocode.connection.title')), 1 /* TEXT */) ]), _createElementVNode("div", { class: "config-template-hint" }, _toDisplayString(_ctx.t('kilocode.targetFile', { path: _ctx.kilocodeConfigPath || '~/.config/kilo/kilo.jsonc', status: _ctx.kilocodeConfigExists ? _ctx.t('common.exists') : _ctx.t('common.notExistsWillCreateOnSave') })), 1 /* TEXT */), _createElementVNode("div", { class: "codex-config-grid" }, [ - _createElementVNode("div", { class: "form-group codex-config-field" }, [ - _createElementVNode("label", { - class: "form-label", - for: "kilocode-provider" - }, _toDisplayString(_ctx.t('field.provider')), 1 /* TEXT */), - _withDirectives(_createElementVNode("input", { - id: "kilocode-provider", - class: "form-input", - "onUpdate:modelValue": $event => ((_ctx.kilocodeProvider) = $event), - autocomplete: "off", - spellcheck: "false", - placeholder: "codexmate", - onBlur: _ctx.autoSaveKilocodeConfig - }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "onBlur"]), [ - [_vModelText, _ctx.kilocodeProvider] - ]) - ]), _createElementVNode("div", { class: "form-group codex-config-field" }, [ _createElementVNode("label", { class: "form-label", @@ -2967,38 +2950,6 @@ return function render(_ctx, _cache) { }, _toDisplayString(_ctx.kilocodeError), 1 /* TEXT */)) : _createCommentVNode("v-if", true) ]), - (_ctx.kilocodeProviders.length) - ? (_openBlock(), _createElementBlock("section", { - key: 0, - class: "selector-section" - }, [ - _createElementVNode("div", { class: "selector-header" }, [ - _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('kilocode.summary.title')), 1 /* TEXT */) - ]), - _createElementVNode("div", { class: "card-list" }, [ - (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.kilocodeProviders, (provider) => { - return (_openBlock(), _createElementBlock("div", { - key: provider.name, - class: "card" - }, [ - _createElementVNode("div", { class: "card-leading" }, [ - _createElementVNode("div", { class: "card-icon" }, _toDisplayString(provider.name.charAt(0).toUpperCase()), 1 /* TEXT */), - _createElementVNode("div", { class: "card-content" }, [ - _createElementVNode("div", { class: "card-title" }, _toDisplayString(provider.name), 1 /* TEXT */), - _createElementVNode("div", { class: "card-subtitle" }, _toDisplayString(provider.baseURL || provider.api || _ctx.t('config.url.unset')), 1 /* TEXT */), - _createElementVNode("div", { class: "card-subtitle" }, _toDisplayString((provider.models || []).join(', ') || _ctx.t('opencode.summary.noModel')), 1 /* TEXT */) - ]) - ]), - _createElementVNode("div", { class: "card-trailing" }, [ - _createElementVNode("span", { - class: _normalizeClass(['pill', provider.hasKey ? 'configured' : 'empty']) - }, _toDisplayString(provider.hasKey ? _ctx.t('common.configured') : _ctx.t('common.notConfigured')), 3 /* TEXT, CLASS */) - ]) - ])) - }), 128 /* KEYED_FRAGMENT */)) - ]) - ])) - : _createCommentVNode("v-if", true), _createElementVNode("section", { class: "selector-section" }, [ _createElementVNode("div", { class: "selector-header" }, [ _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('kilocode.configPreview.title')), 1 /* TEXT */) @@ -3015,7 +2966,7 @@ return function render(_ctx, _cache) { ]), (!_ctx.isToolConfigWriteAllowed('kilocode')) ? (_openBlock(), _createElementBlock("div", { - key: 1, + key: 0, class: "tool-config-write-overlay" }, [ _createElementVNode("div", { class: "tool-config-write-overlay-card" }, [ From 798959a853e19e755f94cf9779fd699c3022f99d Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Thu, 16 Jul 2026 06:41:01 +0000 Subject: [PATCH 04/13] fix(kilocode): add cli install docs target --- cli.js | 6 +++ tests/unit/install-target-cards.test.mjs | 8 ++- tests/unit/kilocode-config-ui.test.mjs | 4 +- web-ui/modules/app.computed.dashboard.mjs | 10 ++++ web-ui/modules/app.methods.install.mjs | 14 +++++ .../modules/app.methods.kilocode-config.mjs | 4 +- web-ui/modules/i18n/locales/en.mjs | 2 +- web-ui/modules/i18n/locales/ja.mjs | 2 +- web-ui/modules/i18n/locales/vi.mjs | 2 +- web-ui/modules/i18n/locales/zh-tw.mjs | 2 +- web-ui/modules/i18n/locales/zh.mjs | 2 +- .../partials/index/panel-config-kilocode.html | 25 ++++++++- web-ui/res/web-ui-render.precompiled.js | 53 ++++++++++++++++++- 13 files changed, 120 insertions(+), 14 deletions(-) diff --git a/cli.js b/cli.js index 304418e7..ab9858f7 100644 --- a/cli.js +++ b/cli.js @@ -365,6 +365,12 @@ const CLI_INSTALL_TARGETS = Object.freeze([ name: 'Codex CLI', packageName: '@openai/codex', bins: ['codex'] + }, + { + id: 'kilocode', + name: 'KiloCode CLI', + packageName: '@kilocode/cli', + bins: ['kilo', 'kilocode'] } ]); diff --git a/tests/unit/install-target-cards.test.mjs b/tests/unit/install-target-cards.test.mjs index 956fb71e..251c29d3 100644 --- a/tests/unit/install-target-cards.test.mjs +++ b/tests/unit/install-target-cards.test.mjs @@ -29,9 +29,9 @@ function createContext(overrides = {}) { test('installTargetCards falls back when install-status is missing', () => { const ctx = createContext(); const cards = computed.installTargetCards.call(ctx); - assert.strictEqual(cards.length, 4); + assert.strictEqual(cards.length, 5); const ids = cards.map((item) => item.id).sort(); - assert.deepStrictEqual(ids, ['claude', 'codebuddy', 'codex', 'gemini']); + assert.deepStrictEqual(ids, ['claude', 'codebuddy', 'codex', 'gemini', 'kilocode']); for (const card of cards) { assert.strictEqual(typeof card.command, 'string'); assert(card.command.length > 0); @@ -40,6 +40,10 @@ test('installTargetCards falls back when install-status is missing', () => { assert(codex); assert.strictEqual(typeof codex.termuxCommand, 'string'); assert(codex.termuxCommand.includes('@mmmbuto/codex-cli-termux')); + const kilocode = cards.find((item) => item.id === 'kilocode'); + assert(kilocode); + assert(kilocode.command.includes('@kilocode/cli')); + assert.strictEqual(kilocode.termuxCommand, ''); }); test('currentInstalledCommandCards exposes detected installed commands only', () => { diff --git a/tests/unit/kilocode-config-ui.test.mjs b/tests/unit/kilocode-config-ui.test.mjs index 114ab89b..342da6b6 100644 --- a/tests/unit/kilocode-config-ui.test.mjs +++ b/tests/unit/kilocode-config-ui.test.mjs @@ -28,8 +28,8 @@ function createVm(apiImpl = async () => ({})) { test('KiloCode panel auto-syncs on blur without Web UI launch buttons', () => { const html = readBundledWebUiHtml(); - assert.doesNotMatch(html, /id="kilocode-provider"/); - assert.doesNotMatch(html, /kilocode\.summary\.title/); + assert.match(html, /id="kilocode-provider"[^>]*@blur="autoSaveKilocodeConfig"/); + assert.match(html, /kilocode\.summary\.title/); assert.match(html, /id="kilocode-base-url"[^>]*@blur="autoSaveKilocodeConfig"/); assert.match(html, /id="kilocode-model"[^>]*@blur="autoSaveKilocodeConfig"/); assert.match(html, /id="kilocode-api-key"[^>]*@blur="autoSaveKilocodeConfig"/); diff --git a/web-ui/modules/app.computed.dashboard.mjs b/web-ui/modules/app.computed.dashboard.mjs index ac090880..f531f525 100644 --- a/web-ui/modules/app.computed.dashboard.mjs +++ b/web-ui/modules/app.computed.dashboard.mjs @@ -149,6 +149,16 @@ export function createDashboardComputed() { version: '', commandPath: '', error: '' + }, + { + id: 'kilocode', + name: 'KiloCode CLI', + packageName: '@kilocode/cli', + installed: false, + bin: 'kilo', + version: '', + commandPath: '', + error: '' } ]; const action = this.normalizeInstallAction(this.installCommandAction); diff --git a/web-ui/modules/app.methods.install.mjs b/web-ui/modules/app.methods.install.mjs index 604c44ee..c1f18c23 100644 --- a/web-ui/modules/app.methods.install.mjs +++ b/web-ui/modules/app.methods.install.mjs @@ -246,6 +246,11 @@ export function createInstallMethods(options = {}) { install: '', update: '', uninstall: '' + }, + kilocode: { + install: '', + update: '', + uninstall: '' } }; if (manager === 'pnpm') { @@ -261,6 +266,9 @@ export function createInstallMethods(options = {}) { matrix.codex.install = `pnpm add -g ${codexInstallPackage}`; matrix.codex.update = `pnpm up -g ${codexPackage}`; matrix.codex.uninstall = `pnpm remove -g ${codexPackage}`; + matrix.kilocode.install = 'pnpm add -g @kilocode/cli'; + matrix.kilocode.update = 'pnpm up -g @kilocode/cli'; + matrix.kilocode.uninstall = 'pnpm remove -g @kilocode/cli'; return matrix; } if (manager === 'bun') { @@ -276,6 +284,9 @@ export function createInstallMethods(options = {}) { matrix.codex.install = `bun add -g ${codexInstallPackage}`; matrix.codex.update = `bun update -g ${codexPackage}`; matrix.codex.uninstall = `bun remove -g ${codexPackage}`; + matrix.kilocode.install = 'bun add -g @kilocode/cli'; + matrix.kilocode.update = 'bun update -g @kilocode/cli'; + matrix.kilocode.uninstall = 'bun remove -g @kilocode/cli'; return matrix; } matrix.claude.install = 'npm install -g @anthropic-ai/claude-code'; @@ -292,6 +303,9 @@ export function createInstallMethods(options = {}) { ? `npm install -g ${codexInstallPackage}` : `npm update -g ${codexPackage}`; matrix.codex.uninstall = `npm uninstall -g ${codexPackage}`; + matrix.kilocode.install = 'npm install -g @kilocode/cli'; + matrix.kilocode.update = 'npm update -g @kilocode/cli'; + matrix.kilocode.uninstall = 'npm uninstall -g @kilocode/cli'; return matrix; }, diff --git a/web-ui/modules/app.methods.kilocode-config.mjs b/web-ui/modules/app.methods.kilocode-config.mjs index 301839fd..16b29f4d 100644 --- a/web-ui/modules/app.methods.kilocode-config.mjs +++ b/web-ui/modules/app.methods.kilocode-config.mjs @@ -58,7 +58,7 @@ export function createKilocodeConfigMethods(options = {}) { async autoSaveKilocodeConfig() { if (!this.isToolConfigWriteAllowed('kilocode') || this.kilocodeSaving || this.kilocodeLoading) return false; - const provider = DEFAULT_KILOCODE_PROVIDER; + const provider = normalizeKilocodeProviderName(this.kilocodeProvider) || DEFAULT_KILOCODE_PROVIDER; const url = typeof this.kilocodeBaseUrl === 'string' ? this.kilocodeBaseUrl.trim() : ''; const model = (typeof this.kilocodeModel === 'string' ? this.kilocodeModel.trim() : '') || DEFAULT_KILOCODE_MODEL; const apiKey = typeof this.kilocodeApiKey === 'string' ? this.kilocodeApiKey.trim() : ''; @@ -75,7 +75,7 @@ export function createKilocodeConfigMethods(options = {}) { if (!auto) this.showMessage(this.t ? this.t('kilocode.writeRequired') : '请先打开 KiloCode 写入开关', 'error'); return false; } - const provider = DEFAULT_KILOCODE_PROVIDER; + const provider = normalizeKilocodeProviderName(this.kilocodeProvider) || DEFAULT_KILOCODE_PROVIDER; const url = typeof this.kilocodeBaseUrl === 'string' ? this.kilocodeBaseUrl.trim() : ''; const model = (typeof this.kilocodeModel === 'string' ? this.kilocodeModel.trim() : '') || DEFAULT_KILOCODE_MODEL; const apiKey = typeof this.kilocodeApiKey === 'string' ? this.kilocodeApiKey.trim() : ''; diff --git a/web-ui/modules/i18n/locales/en.mjs b/web-ui/modules/i18n/locales/en.mjs index acb6f6a3..262b2da3 100644 --- a/web-ui/modules/i18n/locales/en.mjs +++ b/web-ui/modules/i18n/locales/en.mjs @@ -619,7 +619,7 @@ const en = Object.freeze({ // Docs panel 'docs.title': 'CLI Install', - 'docs.subtitle': 'Install commands for Claude Code / Gemini CLI / CodeBuddy Code / Codex CLI.', + 'docs.subtitle': 'Install commands for Claude Code / Gemini CLI / CodeBuddy Code / Codex CLI / KiloCode CLI.', 'docs.section.commands': 'Commands', 'docs.section.commandsNote': 'Copy and run directly.', 'docs.installed.title': 'Currently installed commands', diff --git a/web-ui/modules/i18n/locales/ja.mjs b/web-ui/modules/i18n/locales/ja.mjs index 563a2c3b..fba2ab2a 100644 --- a/web-ui/modules/i18n/locales/ja.mjs +++ b/web-ui/modules/i18n/locales/ja.mjs @@ -621,7 +621,7 @@ const ja = Object.freeze({ // Docs panel 'docs.title': 'CLI インストールドキュメント', - 'docs.subtitle': 'Claude Code / Gemini CLI / CodeBuddy Code / Codex CLI コマンドを表示。', + 'docs.subtitle': 'Claude Code / Gemini CLI / CodeBuddy Code / Codex CLI / KiloCode CLI コマンドを表示。', 'docs.section.commands': 'インストールコマンド', 'docs.section.commandsNote': 'コマンドは直接コピーできます。', 'docs.installed.title': '現在インストール済みのコマンド', diff --git a/web-ui/modules/i18n/locales/vi.mjs b/web-ui/modules/i18n/locales/vi.mjs index bd756647..a4c882a4 100644 --- a/web-ui/modules/i18n/locales/vi.mjs +++ b/web-ui/modules/i18n/locales/vi.mjs @@ -826,7 +826,7 @@ const vi = Object.freeze({ 'modal.openclaw.quick.optionsHint': 'Khi tắt ghi đè, chỉ điền các trường còn thiếu.', 'modal.openclaw.quick.writeToEditor': 'Ghi vào editor', 'docs.title': 'Cài CLI', - 'docs.subtitle': 'Lệnh cài đặt cho Claude Code / Gemini CLI / CodeBuddy Code / Codex CLI.', + 'docs.subtitle': 'Lệnh cài đặt cho Claude Code / Gemini CLI / CodeBuddy Code / Codex CLI / KiloCode CLI.', 'docs.section.commands': 'Lệnh', 'docs.section.commandsNote': 'Sao chép và chạy trực tiếp.', 'docs.installed.title': 'Lệnh hiện đã cài đặt', diff --git a/web-ui/modules/i18n/locales/zh-tw.mjs b/web-ui/modules/i18n/locales/zh-tw.mjs index 48ced4ad..9f05bae0 100644 --- a/web-ui/modules/i18n/locales/zh-tw.mjs +++ b/web-ui/modules/i18n/locales/zh-tw.mjs @@ -619,7 +619,7 @@ const zhTw = Object.freeze({ // Docs panel 'docs.title': 'CLI 安裝文檔', - 'docs.subtitle': '查看 Claude Code / Gemini CLI / CodeBuddy Code / Codex CLI 命令。', + 'docs.subtitle': '查看 Claude Code / Gemini CLI / CodeBuddy Code / Codex CLI / KiloCode CLI 命令。', 'docs.section.commands': '安裝命令', 'docs.section.commandsNote': '命令可直接複製。', 'docs.installed.title': '目前已安裝命令', diff --git a/web-ui/modules/i18n/locales/zh.mjs b/web-ui/modules/i18n/locales/zh.mjs index b3fa013b..fad8ce5d 100644 --- a/web-ui/modules/i18n/locales/zh.mjs +++ b/web-ui/modules/i18n/locales/zh.mjs @@ -619,7 +619,7 @@ const zh = Object.freeze({ // Docs panel 'docs.title': 'CLI 安装文档', - 'docs.subtitle': '查看 Claude Code / Gemini CLI / CodeBuddy Code / Codex CLI 命令。', + 'docs.subtitle': '查看 Claude Code / Gemini CLI / CodeBuddy Code / Codex CLI / KiloCode CLI 命令。', 'docs.section.commands': '安装命令', 'docs.section.commandsNote': '命令可直接复制。', 'docs.installed.title': '当前已安装命令', diff --git a/web-ui/partials/index/panel-config-kilocode.html b/web-ui/partials/index/panel-config-kilocode.html index ff9f405b..08de689e 100644 --- a/web-ui/partials/index/panel-config-kilocode.html +++ b/web-ui/partials/index/panel-config-kilocode.html @@ -34,10 +34,14 @@
- {{ t('kilocode.connection.title') }} + {{ t('kilocode.providerModel.title') }}
{{ t('kilocode.targetFile', { path: kilocodeConfigPath || '~/.config/kilo/kilo.jsonc', status: kilocodeConfigExists ? t('common.exists') : t('common.notExistsWillCreateOnSave') }) }}
+
+ + +
@@ -57,6 +61,25 @@
{{ kilocodeError }}
+
+
{{ t('kilocode.summary.title') }}
+
+
+
+
{{ provider.name.charAt(0).toUpperCase() }}
+
+
{{ provider.name }}
+
{{ provider.baseURL || provider.api || t('config.url.unset') }}
+
{{ (provider.models || []).join(', ') || t('opencode.summary.noModel') }}
+
+
+
+ {{ provider.hasKey ? t('common.configured') : t('common.notConfigured') }} +
+
+
+
+
{{ t('kilocode.configPreview.title') }}
diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index 894a555c..9669ff44 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -2878,10 +2878,27 @@ return function render(_ctx, _cache) { _createElementVNode("div", { class: "tool-config-write-body" }, [ _createElementVNode("section", { class: "selector-section" }, [ _createElementVNode("div", { class: "selector-header" }, [ - _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('kilocode.connection.title')), 1 /* TEXT */) + _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('kilocode.providerModel.title')), 1 /* TEXT */) ]), _createElementVNode("div", { class: "config-template-hint" }, _toDisplayString(_ctx.t('kilocode.targetFile', { path: _ctx.kilocodeConfigPath || '~/.config/kilo/kilo.jsonc', status: _ctx.kilocodeConfigExists ? _ctx.t('common.exists') : _ctx.t('common.notExistsWillCreateOnSave') })), 1 /* TEXT */), _createElementVNode("div", { class: "codex-config-grid" }, [ + _createElementVNode("div", { class: "form-group codex-config-field" }, [ + _createElementVNode("label", { + class: "form-label", + for: "kilocode-provider" + }, _toDisplayString(_ctx.t('field.provider')), 1 /* TEXT */), + _withDirectives(_createElementVNode("input", { + id: "kilocode-provider", + class: "form-input", + "onUpdate:modelValue": $event => ((_ctx.kilocodeProvider) = $event), + autocomplete: "off", + spellcheck: "false", + placeholder: "codexmate", + onBlur: _ctx.autoSaveKilocodeConfig + }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "onBlur"]), [ + [_vModelText, _ctx.kilocodeProvider] + ]) + ]), _createElementVNode("div", { class: "form-group codex-config-field" }, [ _createElementVNode("label", { class: "form-label", @@ -2950,6 +2967,38 @@ return function render(_ctx, _cache) { }, _toDisplayString(_ctx.kilocodeError), 1 /* TEXT */)) : _createCommentVNode("v-if", true) ]), + (_ctx.kilocodeProviders.length) + ? (_openBlock(), _createElementBlock("section", { + key: 0, + class: "selector-section" + }, [ + _createElementVNode("div", { class: "selector-header" }, [ + _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('kilocode.summary.title')), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "card-list" }, [ + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.kilocodeProviders, (provider) => { + return (_openBlock(), _createElementBlock("div", { + key: provider.name, + class: "card" + }, [ + _createElementVNode("div", { class: "card-leading" }, [ + _createElementVNode("div", { class: "card-icon" }, _toDisplayString(provider.name.charAt(0).toUpperCase()), 1 /* TEXT */), + _createElementVNode("div", { class: "card-content" }, [ + _createElementVNode("div", { class: "card-title" }, _toDisplayString(provider.name), 1 /* TEXT */), + _createElementVNode("div", { class: "card-subtitle" }, _toDisplayString(provider.baseURL || provider.api || _ctx.t('config.url.unset')), 1 /* TEXT */), + _createElementVNode("div", { class: "card-subtitle" }, _toDisplayString((provider.models || []).join(', ') || _ctx.t('opencode.summary.noModel')), 1 /* TEXT */) + ]) + ]), + _createElementVNode("div", { class: "card-trailing" }, [ + _createElementVNode("span", { + class: _normalizeClass(['pill', provider.hasKey ? 'configured' : 'empty']) + }, _toDisplayString(provider.hasKey ? _ctx.t('common.configured') : _ctx.t('common.notConfigured')), 3 /* TEXT, CLASS */) + ]) + ])) + }), 128 /* KEYED_FRAGMENT */)) + ]) + ])) + : _createCommentVNode("v-if", true), _createElementVNode("section", { class: "selector-section" }, [ _createElementVNode("div", { class: "selector-header" }, [ _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('kilocode.configPreview.title')), 1 /* TEXT */) @@ -2966,7 +3015,7 @@ return function render(_ctx, _cache) { ]), (!_ctx.isToolConfigWriteAllowed('kilocode')) ? (_openBlock(), _createElementBlock("div", { - key: 0, + key: 1, class: "tool-config-write-overlay" }, [ _createElementVNode("div", { class: "tool-config-write-overlay-card" }, [ From 4afaf9e5864c11e2ec1e0c4d78d434263966c75c Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Thu, 16 Jul 2026 06:49:48 +0000 Subject: [PATCH 05/13] fix(kilocode): load config when restoring tab --- tests/unit/kilocode-config-ui.test.mjs | 8 ++++++++ web-ui/modules/app.methods.navigation.mjs | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/tests/unit/kilocode-config-ui.test.mjs b/tests/unit/kilocode-config-ui.test.mjs index 342da6b6..70914ad1 100644 --- a/tests/unit/kilocode-config-ui.test.mjs +++ b/tests/unit/kilocode-config-ui.test.mjs @@ -39,6 +39,14 @@ test('KiloCode panel auto-syncs on blur without Web UI launch buttons', () => { assert.doesNotMatch(html, /@click="startKilocode\(false\)"/); }); +test('KiloCode config tab reloads stored config when the tab is restored or re-entered', () => { + const navigation = readProjectFile('web-ui/modules/app.methods.navigation.mjs'); + const loadCalls = navigation.match(/loadKilocodeConfig/g) || []; + assert(loadCalls.length >= 4); + assert.match(navigation, /targetTab === 'config' && this\.configMode === 'kilocode'/); + assert.match(navigation, /pendingTarget === 'config' && this\.configMode === 'kilocode'/); +}); + test('KiloCode auto-save reuses stored API key when the key field is blank', async () => { const calls = []; const vm = createVm(async (action, params) => { diff --git a/web-ui/modules/app.methods.navigation.mjs b/web-ui/modules/app.methods.navigation.mjs index 800909ff..dd2509ad 100644 --- a/web-ui/modules/app.methods.navigation.mjs +++ b/web-ui/modules/app.methods.navigation.mjs @@ -461,6 +461,9 @@ if (targetTab === previousTab) { switchState.ticket += 1; switchState.pendingTarget = ''; + if (targetTab === 'config' && this.configMode === 'kilocode' && typeof this.loadKilocodeConfig === 'function') { + void this.loadKilocodeConfig(); + } if (targetTab === 'dashboard' && !this.__doctorLoadedOnce) { void loadDoctorOverview(this); } @@ -493,6 +496,9 @@ switchState.pendingTarget = ''; const result = switchMainTabHelper.call(this, targetTab); persistNavState(this); + if (targetTab === 'config' && this.configMode === 'kilocode' && typeof this.loadKilocodeConfig === 'function') { + void this.loadKilocodeConfig(); + } if (targetTab === 'dashboard') { void loadDoctorOverview(this); } @@ -511,6 +517,9 @@ liveState.pendingTarget = ''; switchMainTabHelper.call(this, pendingTarget); persistNavState(this); + if (pendingTarget === 'config' && this.configMode === 'kilocode' && typeof this.loadKilocodeConfig === 'function') { + void this.loadKilocodeConfig(); + } if (pendingTarget === 'dashboard') { void loadDoctorOverview(this); } From db575f33af0b3b765287be538385e04590d81d0d Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Thu, 16 Jul 2026 07:03:17 +0000 Subject: [PATCH 06/13] fix(kilocode): use eye icon for api key toggle --- tests/unit/kilocode-config-ui.test.mjs | 5 +++ .../partials/index/panel-config-kilocode.html | 5 ++- web-ui/res/web-ui-render.precompiled.js | 34 +++++++++++++++++-- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/tests/unit/kilocode-config-ui.test.mjs b/tests/unit/kilocode-config-ui.test.mjs index 70914ad1..e7c09378 100644 --- a/tests/unit/kilocode-config-ui.test.mjs +++ b/tests/unit/kilocode-config-ui.test.mjs @@ -33,6 +33,11 @@ test('KiloCode panel auto-syncs on blur without Web UI launch buttons', () => { assert.match(html, /id="kilocode-base-url"[^>]*@blur="autoSaveKilocodeConfig"/); assert.match(html, /id="kilocode-model"[^>]*@blur="autoSaveKilocodeConfig"/); assert.match(html, /id="kilocode-api-key"[^>]*@blur="autoSaveKilocodeConfig"/); + assert.match(html, /@click="kilocodeShowKey = !kilocodeShowKey"/); + assert.match(html, /:aria-label="kilocodeShowKey \? t\('common\.hide'\) : t\('common\.show'\)"/); + assert.match(html, //); assert.doesNotMatch(html, /@click="loadKilocodeConfig\(\{ toast: true \}\)"/); assert.doesNotMatch(html, /@click="saveKilocodeConfig"/); assert.doesNotMatch(html, /@click="startKilocode\(true\)"/); diff --git a/web-ui/partials/index/panel-config-kilocode.html b/web-ui/partials/index/panel-config-kilocode.html index 08de689e..21a8fd3d 100644 --- a/web-ui/partials/index/panel-config-kilocode.html +++ b/web-ui/partials/index/panel-config-kilocode.html @@ -54,7 +54,10 @@
- +
diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index 9669ff44..dd6d29b1 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -2955,8 +2955,38 @@ return function render(_ctx, _cache) { type: "button", class: "input-toggle-btn", onClick: $event => (_ctx.kilocodeShowKey = !_ctx.kilocodeShowKey), - title: _ctx.kilocodeShowKey ? _ctx.t('common.hide') : _ctx.t('common.show') - }, _toDisplayString(_ctx.kilocodeShowKey ? _ctx.t('common.hide') : _ctx.t('common.show')), 9 /* TEXT, PROPS */, ["onClick", "title"]) + title: _ctx.kilocodeShowKey ? _ctx.t('common.hide') : _ctx.t('common.show'), + "aria-label": _ctx.kilocodeShowKey ? _ctx.t('common.hide') : _ctx.t('common.show') + }, [ + (!_ctx.kilocodeShowKey) + ? (_openBlock(), _createElementBlock("svg", { + key: 0, + viewBox: "0 0 20 20", + fill: "none", + stroke: "currentColor", + "stroke-width": "1.5", + width: "16", + height: "16" + }, [ + _createElementVNode("path", { d: "M10 4C5 4 1.73 8.11 1 10c.73 1.89 4 6 9 6s8.27-4.11 9-6c-.73-1.89-4-6-9-6z" }), + _createElementVNode("circle", { + cx: "10", + cy: "10", + r: "3" + }) + ])) + : (_openBlock(), _createElementBlock("svg", { + key: 1, + viewBox: "0 0 20 20", + fill: "none", + stroke: "currentColor", + "stroke-width": "1.5", + width: "16", + height: "16" + }, [ + _createElementVNode("path", { d: "M2 2l16 16M8.2 4.2A9.9 9.9 0 0 1 10 4c5 0 8.27 4.11 9 6-.44.94-1.5 2.7-3.2 4.2M14.5 14.5A5.9 5.9 0 0 1 10 16c-5 0-8.27-4.11-9-6 .76-1.66 2.2-3.6 4.3-5" }) + ])) + ], 8 /* PROPS */, ["onClick", "title", "aria-label"]) ]) ]) ]), From 46d04804b0f6318be5188f0d7f8bafc2b813435a Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Thu, 16 Jul 2026 07:38:50 +0000 Subject: [PATCH 07/13] fix(kilocode): make provider cards selectable --- tests/unit/kilocode-config-ui.test.mjs | 36 +++++++++++++++++++ tests/unit/web-ui-behavior-parity.test.mjs | 2 ++ .../modules/app.methods.kilocode-config.mjs | 20 +++++++++++ .../partials/index/panel-config-kilocode.html | 4 +-- web-ui/res/web-ui-render.precompiled.js | 20 ++++++----- 5 files changed, 72 insertions(+), 10 deletions(-) diff --git a/tests/unit/kilocode-config-ui.test.mjs b/tests/unit/kilocode-config-ui.test.mjs index e7c09378..8d386fa6 100644 --- a/tests/unit/kilocode-config-ui.test.mjs +++ b/tests/unit/kilocode-config-ui.test.mjs @@ -38,12 +38,48 @@ test('KiloCode panel auto-syncs on blur without Web UI launch buttons', () => { assert.match(html, //); + assert.match(html, /@dblclick="selectKilocodeProvider\(provider\)"/); + assert.match(html, /role="button"/); + assert.match(html, /
{{ t('kilocode.summary.title') }}
-
+
{{ provider.name.charAt(0).toUpperCase() }}
@@ -85,7 +85,7 @@
{{ t('kilocode.configPreview.title') }}
- +
{{ t('kilocode.configPreview.hint') }}
diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index dd6d29b1..67aaf1c4 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -3009,7 +3009,12 @@ return function render(_ctx, _cache) { (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.kilocodeProviders, (provider) => { return (_openBlock(), _createElementBlock("div", { key: provider.name, - class: "card" + class: _normalizeClass(['card', { active: _ctx.normalizeKilocodeProviderNameForUi(provider.name) === _ctx.normalizeKilocodeProviderNameForUi(_ctx.kilocodeProvider) }]), + onDblclick: $event => (_ctx.selectKilocodeProvider(provider)), + onKeydown: _withKeys(_withModifiers($event => (_ctx.selectKilocodeProvider(provider)), ["self","prevent"]), ["enter"]), + tabindex: "0", + role: "button", + "aria-current": _ctx.normalizeKilocodeProviderNameForUi(provider.name) === _ctx.normalizeKilocodeProviderNameForUi(_ctx.kilocodeProvider) ? 'true' : null }, [ _createElementVNode("div", { class: "card-leading" }, [ _createElementVNode("div", { class: "card-icon" }, _toDisplayString(provider.name.charAt(0).toUpperCase()), 1 /* TEXT */), @@ -3024,7 +3029,7 @@ return function render(_ctx, _cache) { class: _normalizeClass(['pill', provider.hasKey ? 'configured' : 'empty']) }, _toDisplayString(provider.hasKey ? _ctx.t('common.configured') : _ctx.t('common.notConfigured')), 3 /* TEXT, CLASS */) ]) - ])) + ], 42 /* CLASS, PROPS, NEED_HYDRATION */, ["onDblclick", "onKeydown", "aria-current"])) }), 128 /* KEYED_FRAGMENT */)) ]) ])) @@ -3033,14 +3038,13 @@ return function render(_ctx, _cache) { _createElementVNode("div", { class: "selector-header" }, [ _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('kilocode.configPreview.title')), 1 /* TEXT */) ]), - _withDirectives(_createElementVNode("textarea", { + _createElementVNode("textarea", { class: "template-textarea", - "onUpdate:modelValue": $event => ((_ctx.kilocodeContent) = $event), + value: _ctx.kilocodeContent, spellcheck: "false", - readonly: "" - }, null, 8 /* PROPS */, ["onUpdate:modelValue"]), [ - [_vModelText, _ctx.kilocodeContent] - ]), + readonly: "", + "aria-readonly": "true" + }, null, 8 /* PROPS */, ["value"]), _createElementVNode("div", { class: "config-template-hint" }, _toDisplayString(_ctx.t('kilocode.configPreview.hint')), 1 /* TEXT */) ]), (!_ctx.isToolConfigWriteAllowed('kilocode')) From 46dbca457eb1b3468bb79de970e813d312f8f567 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Thu, 16 Jul 2026 07:43:01 +0000 Subject: [PATCH 08/13] docs(readme): document kilocode support --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d9a1ce92..90b584c0 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # Codex Mate -**One dashboard for all your local AI coding agents. Switch providers, manage sessions, and orchestrate tasks across Codex, Claude Code, OpenCode, and OpenClaw. Zero cloud, local-first control plane.** +**One dashboard for all your local AI coding agents. Switch providers, manage sessions, and orchestrate tasks across Codex, Claude Code, OpenCode, KiloCode, and OpenClaw. Zero cloud, local-first control plane.**

[Documentation] @@ -49,7 +49,7 @@ Have you ever felt overwhelmed by managing multiple local AI agents? Each has its own config format, session storage, and skills directory. -**Codex Mate** offers a unified control plane to bring order to the chaos. It's a local-first CLI + Web UI designed to manage [Codex](https://github.com/openai/codex), [Claude Code](https://github.com/anthropic-ai/claude-code), [OpenCode](https://opencode.ai/), and [OpenClaw](https://github.com/moeru-ai/openclaw) seamlessly. +**Codex Mate** offers a unified control plane to bring order to the chaos. It's a local-first CLI + Web UI designed to manage [Codex](https://github.com/openai/codex), [Claude Code](https://github.com/anthropic-ai/claude-code), [OpenCode](https://opencode.ai/), KiloCode, and [OpenClaw](https://github.com/moeru-ai/openclaw) seamlessly. ### What's So Special? @@ -58,6 +58,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**: - **OpenAI-Compatible Bridge**: Use Codex with any OpenAI-compatible UI by normalizing the Responses API; the built-in Codex conversion also fills and normalizes Codex fingerprint headers such as `User-Agent`, `Version`, `OpenAI-Beta`, and `Originator` so upstream providers see an official Codex CLI-shaped request. - **Claude Provider Bridge**: Connect Claude Code to OpenAI Chat Completions-compatible providers and Ollama through the built-in local Claude-compatible proxy. - **OpenCode Provider Control**: Manage OpenCode provider/model selection with a CodexMate-owned provider store under `~/.codexmate`, projecting only the active provider into native OpenCode config to avoid polluting or deleting user-owned settings. +- **KiloCode Config Bridge**: Configure KiloCode providers from the CLI or Web UI, writing to `~/.config/kilo/kilo.jsonc` while preserving stored API keys. - **Provider Health Cleanup**: Probe local Codex and Claude provider routes, surface failed configs in one modal, and bulk-clean selected broken providers without touching healthy or protected entries. - **Skills Marketplace**: A local-first market to share and import skills between different agent apps. - **Prompt File Editor**: Unified editor for global and project-level `CLAUDE.md` and `AGENTS.md` with auto-detection of project paths, plus a shared preset pool for reusable prompts. @@ -69,7 +70,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**: | Feature | Status | Description | | --- | --- | --- | -| **Provider Management** | ✅ | Switch providers/models for Codex, Claude, OpenCode, and OpenClaw | +| **Provider Management** | ✅ | Switch providers/models for Codex, Claude, OpenCode, KiloCode, and OpenClaw | | **Live Agent Sync** | ✅ | Real-time monitoring of Codex/Claude config & status | | **Session Browser** | ✅ | Search, preview, filter, and export sessions across Codex, Claude Code, Gemini CLI, and CodeBuddy Code | | **Usage Analytics** | ✅ | Visualize message trends and top projects | @@ -78,6 +79,7 @@ Unlike simple wrappers, Codex Mate acts as a **Local Agent Bridge**: | **OpenAI Bridge** | ✅ | Convert Codex Responses API to standard OpenAI format and attach/normalize Codex fingerprints in the built-in conversion | | **Claude Provider Bridge** | ✅ | Connect Claude Code to OpenAI Chat Completions-compatible providers and Ollama via the built-in Claude-compatible proxy | | **OpenCode Provider Store** | ✅ | Keep multiple OpenCode providers in `~/.codexmate` while projecting only the selected provider to native OpenCode config | +| **KiloCode Config Bridge** | ✅ | Configure KiloCode through `codexmate kilo config` or the Web UI, writing provider URL/model/API key data to `~/.config/kilo/kilo.jsonc` | | **Provider Health Check** | ✅ | Probe local Codex/Claude provider routes, highlight failed configs, and bulk-remove selected broken providers safely | | **Prompt Templates** | ✅ | Reusable prompt plugins with variables | | **Prompt File Editor** | ✅ | Edit global and project-level CLAUDE.md / AGENTS.md with auto-detect, path switching, and a shared preset pool. Applying a preset only updates the editor; save manually to write the file. | @@ -128,6 +130,7 @@ curl -fsSL https://raw.githubusercontent.com/SakuraByteCore/codexmate/main/scrip - **Claude Code**: `npm install -g @anthropic-ai/claude-code` - **Gemini CLI**: `npm install -g @google/gemini-cli` - **CodeBuddy**: `npm install -g @tencent-ai/codebuddy-code` +- **KiloCode**: `npm install -g @kilocode/cli` (`kilo` / `kilocode`) - **OpenCode**: install from the [official OpenCode docs](https://opencode.ai/) --- From ce6f844cee3968083c31595d7235de69d66c9469 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Thu, 16 Jul 2026 08:38:58 +0000 Subject: [PATCH 09/13] fix(kilocode): use default cursor for readonly preview --- tests/unit/kilocode-config-ui.test.mjs | 3 +++ web-ui/styles/controls-forms.css | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/tests/unit/kilocode-config-ui.test.mjs b/tests/unit/kilocode-config-ui.test.mjs index 8d386fa6..081f61a5 100644 --- a/tests/unit/kilocode-config-ui.test.mjs +++ b/tests/unit/kilocode-config-ui.test.mjs @@ -42,6 +42,9 @@ test('KiloCode panel auto-syncs on blur without Web UI launch buttons', () => { assert.match(html, /role="button"/); assert.match(html, /