Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f4b2f57
fix(orchestration): redact private task details
awsl233777 Jun 30, 2026
0a57a0e
fix(orchestration): keep workspace selector private
awsl233777 Jun 30, 2026
94c89bd
fix(orchestration): localize Japanese privacy labels
awsl233777 Jun 30, 2026
a076274
fix(orchestration): add private workspace split action
awsl233777 Jun 30, 2026
9425163
fix(orchestration): mask advanced errors and inputs
awsl233777 Jun 30, 2026
eb8696f
fix(orchestration): hide plan node identifiers
awsl233777 Jun 30, 2026
e253c0c
fix(orchestration): hide task toast details
awsl233777 Jun 30, 2026
115a8ee
feat(orchestration): add Kilo webview host shim
awsl233777 Jun 30, 2026
02440cd
refactor(orchestration): align tab with Kilo app shell
awsl233777 Jun 30, 2026
ef76a11
refactor(orchestration): replace tab with Kilo webview UI
awsl233777 Jun 30, 2026
c617770
refactor(orchestration): remove legacy tab settings surface
awsl233777 Jun 30, 2026
41ba76d
fix(orchestration): remove inert title button semantics
awsl233777 Jun 30, 2026
2edac28
fix(orchestration): align tab with chat plugin surface
awsl233777 Jun 30, 2026
950b68b
fix(orchestration): neutralize task chat surface copy
awsl233777 Jun 30, 2026
6f435db
test(orchestration): cover chat composer locale keys
awsl233777 Jun 30, 2026
3602196
fix(orchestration): harden kilo webview bridge privacy
awsl233777 Jun 30, 2026
64d442c
fix(orchestration): keep task composer visible
awsl233777 Jul 1, 2026
354ec45
style(orchestration): separate custom task sizing properties
awsl233777 Jul 1, 2026
e1ed927
style(orchestration): align task surface with reference app
awsl233777 Jul 1, 2026
0ca4836
fix(orchestration): align task UI with chat shell
awsl233777 Jul 1, 2026
c7d935f
fix(providers): keep endpoint validation unchanged
awsl233777 Jul 1, 2026
6e465ba
fix(orchestration): keep changes scoped to task tab
awsl233777 Jul 1, 2026
fb0d484
fix(orchestration): add OpenAI chat provider fallback
awsl233777 Jul 2, 2026
c42afdf
fix(orchestration): surface provider setup in task tab
awsl233777 Jul 2, 2026
1b21e8b
fix(orchestration): decouple task provider setup
awsl233777 Jul 2, 2026
8086cfa
fix(orchestration): prioritize provider setup state
awsl233777 Jul 2, 2026
e2daa56
fix(task-orchestration): add Kilo device settings modal
awsl233777 Jul 3, 2026
d31dc6c
fix(task-orchestration): surface model config in device settings
awsl233777 Jul 3, 2026
92c3f55
fix(task-orchestration): support task provider switching
awsl233777 Jul 3, 2026
0850d77
fix(task-orchestration): isolate task settings modal config
awsl233777 Jul 3, 2026
435ab7a
feat(local-ai): add local AI service provider tab
awsl233777 Jul 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
495 changes: 428 additions & 67 deletions cli.js

Large diffs are not rendered by default.

185 changes: 183 additions & 2 deletions tests/e2e/test-task-orchestration.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,76 @@ process.on('SIGINT', () => server.close(() => process.exit(0)));
throw new Error(`OpenAI Chat mock did not start: ${stderr}`);
}

async function startFailingOpenAiChatMock(tmpHome, status = 401, message = 'Invalid token') {
const scriptPath = path.join(tmpHome, `task-openai-chat-failing-${status}.cjs`);
const portFile = path.join(tmpHome, `task-openai-chat-failing-${status}.port`);
fs.writeFileSync(scriptPath, `
const http = require('http');
const fs = require('fs');
const portFile = process.argv[2];
const status = Number(process.argv[3] || 401);
const message = process.argv[4] || 'Invalid token';
const server = http.createServer((req, res) => {
req.resume();
req.on('end', () => {
const body = JSON.stringify({ error: { message } });
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(body, 'utf-8') });
res.end(body, 'utf-8');
});
});
server.listen(0, '127.0.0.1', () => {
fs.writeFileSync(portFile, String(server.address().port), 'utf-8');
});
process.on('SIGTERM', () => server.close(() => process.exit(0)));
process.on('SIGINT', () => server.close(() => process.exit(0)));
`, 'utf-8');

const child = spawn(process.execPath, [scriptPath, portFile, String(status), message], {
stdio: ['ignore', 'ignore', 'pipe']
});
let stderr = '';
child.stderr.on('data', chunk => { stderr += chunk.toString(); });
for (let i = 0; i < 80; i += 1) {
if (fs.existsSync(portFile)) {
const port = Number(fs.readFileSync(portFile, 'utf-8').trim());
if (Number.isFinite(port) && port > 0) {
return {
port,
close() {
return new Promise((resolve) => {
if (child.exitCode !== null || child.signalCode) return resolve();
const timer = setTimeout(() => {
try { child.kill('SIGKILL'); } catch (_) {}
resolve();
}, 2000);
child.once('exit', () => {
clearTimeout(timer);
resolve();
});
try { child.kill('SIGTERM'); } catch (_) { resolve(); }
});
}
};
}
}
if (child.exitCode !== null) {
throw new Error(`Failing OpenAI Chat mock exited early: ${stderr}`);
}
await sleep(100);
}
try { child.kill('SIGKILL'); } catch (_) {}
throw new Error(`Failing OpenAI Chat mock did not start: ${stderr}`);
}

function writeOpenAiChatConfig(tmpHome, baseUrl) {
const configDir = path.join(tmpHome, '.codex');
const taskConfigDir = path.join(tmpHome, '.codexmate');
fs.mkdirSync(configDir, { recursive: true });
fs.mkdirSync(taskConfigDir, { recursive: true });
fs.writeFileSync(path.join(configDir, 'codexmate-init.json'), JSON.stringify({ version: 1, mode: 'task-openai-chat-e2e' }), 'utf-8');
fs.writeFileSync(path.join(configDir, 'config.toml'), [
'model = "gpt-5.3-codex"',
'model_provider = "local"',
'task_openai_chat_provider = "new-api-chat"',
'',
'[model_providers.local]',
'name = "Local Codex"',
Expand All @@ -143,6 +205,79 @@ function writeOpenAiChatConfig(tmpHome, baseUrl) {
'models = ["glm-5.2", "glm-5.2-flash"]',
''
].join('\n'), 'utf-8');
fs.writeFileSync(path.join(taskConfigDir, 'task-openai-chat-provider.json'), JSON.stringify({
selectedProvider: 'new-api-chat',
selectedModels: { 'new-api-chat': 'glm-5.2' },
providers: {
'new-api-chat': {
name: 'New API Chat',
base_url: `${baseUrl}/v1`,
wire_api: 'chat_completions',
preferred_auth_method: 'sk-task-e2e-secret',
temperature: 0.7,
models: ['glm-5.2', 'glm-5.2-flash'],
selected_model: 'glm-5.2'
}
}
}, null, 2), 'utf-8');
}

function writeOpenAiChatFallbackConfig(tmpHome, badBaseUrl, fallbackBaseUrl) {
const configDir = path.join(tmpHome, '.codex');
const taskConfigDir = path.join(tmpHome, '.codexmate');
fs.mkdirSync(configDir, { recursive: true });
fs.mkdirSync(taskConfigDir, { recursive: true });
fs.writeFileSync(path.join(configDir, 'codexmate-init.json'), JSON.stringify({ version: 1, mode: 'task-openai-chat-fallback-e2e' }), 'utf-8');
fs.writeFileSync(path.join(configDir, 'config.toml'), [
'model = "gpt-5.3-codex"',
'model_provider = "local"',
'',
'[model_providers.local]',
'name = "Local Codex"',
'base_url = "http://127.0.0.1:9/v1"',
'wire_api = "responses"',
'preferred_auth_method = "sk-codex-tab-secret"',
'models = ["gpt-5.3-codex"]',
'',
'[model_providers.bad-chat]',
'name = "Bad Chat"',
`base_url = "${badBaseUrl}/v1"`,
'wire_api = "chat_completions"',
'preferred_auth_method = "sk-bad-token"',
'models = ["bad-model"]',
'',
'[model_providers.new-api-chat]',
'name = "New API Chat"',
`base_url = "${fallbackBaseUrl}/v1"`,
'wire_api = "chat_completions"',
'preferred_auth_method = "sk-task-e2e-secret"',
'temperature = 0.7',
'models = ["glm-5.2"]',
''
].join('\n'), 'utf-8');
fs.writeFileSync(path.join(taskConfigDir, 'task-openai-chat-provider.json'), JSON.stringify({
selectedProvider: 'bad-chat',
selectedModels: { 'bad-chat': 'bad-model', 'new-api-chat': 'glm-5.2' },
providers: {
'bad-chat': {
name: 'Bad Chat',
base_url: `${badBaseUrl}/v1`,
wire_api: 'chat_completions',
preferred_auth_method: 'sk-bad-token',
models: ['bad-model'],
selected_model: 'bad-model'
},
'new-api-chat': {
name: 'New API Chat',
base_url: `${fallbackBaseUrl}/v1`,
wire_api: 'chat_completions',
preferred_auth_method: 'sk-task-e2e-secret',
temperature: 0.7,
models: ['glm-5.2'],
selected_model: 'glm-5.2'
}
}
}, null, 2), 'utf-8');
}

function assertOpenAiRunPayload(payload, label) {
Expand Down Expand Up @@ -347,6 +482,24 @@ module.exports = async function testTaskOrchestration(ctx) {
try {
writeOpenAiChatConfig(tmpHome, `http://codex-user:codex-secret@127.0.0.1:${openAiMock.port}`);

const localAiOverview = await api('local-ai-service-overview');
assert(localAiOverview && localAiOverview.service === 'local-ai-service', 'Local AI service overview should identify service');
assert(Array.isArray(localAiOverview.providerOptions), 'Local AI service overview should list providers');
assert(localAiOverview.providerOptions.some((item) => item.name === 'New API Chat' && item.type === 'openai'), 'Local AI service overview should expose OpenAI provider');
assert(!JSON.stringify(localAiOverview).includes('sk-task-e2e-secret'), 'Local AI service overview must not leak OpenAI provider key');

const saveClaudeProvider = await api('save-local-ai-provider', {
name: 'claude-local',
type: 'claude',
url: 'https://claude.example.test/v1',
key: 'sk-claude-e2e-secret',
model: 'claude-3-5-sonnet-latest'
});
assert(saveClaudeProvider && saveClaudeProvider.success === true, 'save-local-ai-provider should save Claude provider');
const localAiAfterClaude = await api('local-ai-service-overview');
assert(localAiAfterClaude.providerOptions.some((item) => item.name === 'claude-local' && item.type === 'claude' && item.ready === true), 'Local AI service overview should expose ready Claude provider');
assert(!JSON.stringify(localAiAfterClaude).includes('sk-claude-e2e-secret'), 'Local AI service overview must not leak Claude provider key');

const openAiPlanResult = runSync(node, [
cliPath,
'task',
Expand Down Expand Up @@ -668,7 +821,35 @@ module.exports = async function testTaskOrchestration(ctx) {
assert(Array.isArray(apiOpenAiOverview.runs), 'OpenAI Chat task-overview API should return runs after execution');
assert(apiOpenAiOverview.runs.some((item) => item.runId === apiOpenAiRun.runId), 'OpenAI Chat task-overview API should include API run');

assertOpenAiRequests(openAiMock, 6, 'OpenAI Chat full chain');
const failingOpenAiMock = await startFailingOpenAiChatMock(tmpHome, 401, 'Invalid token');
try {
writeOpenAiChatFallbackConfig(tmpHome, `http://127.0.0.1:${failingOpenAiMock.port}`, `http://codex-user:codex-secret@127.0.0.1:${openAiMock.port}`);
const fallbackRunResult = runSync(node, [
cliPath,
'task',
'run',
'--target',
'OpenAI Chat provider fallback 链路',
'--engine',
'openai-chat',
'--cwd',
path.join(tmpHome, 'task-fallback-workspace'),
'--thread-id',
'thread-fallback-run',
'--json'
], { env });
assert(fallbackRunResult.status === 0, `OpenAI Chat fallback run failed: ${fallbackRunResult.stderr || fallbackRunResult.stdout}`);
const fallbackPayload = parseJsonOutput(fallbackRunResult.stdout);
assertOpenAiRunPayload(fallbackPayload, 'OpenAI Chat fallback run');
const fallbackOutputs = fallbackPayload.run.nodes.map(item => item && item.output ? item.output : {});
assert(fallbackOutputs.some(item => Array.isArray(item.attemptedProviders) && item.attemptedProviders.includes('bad-chat') && item.attemptedProviders.includes('new-api-chat')), 'OpenAI Chat fallback should record attempted providers');
const fallbackLogs = JSON.stringify(fallbackPayload.run.nodes.flatMap(item => Array.isArray(item && item.logs) ? item.logs : []));
assert(fallbackLogs.includes('provider fallback: bad-chat -> new-api-chat'), 'OpenAI Chat fallback should be visible in logs');
} finally {
await failingOpenAiMock.close();
}

assertOpenAiRequests(openAiMock, 7, 'OpenAI Chat full chain');
} finally {
await openAiMock.close();
}
Expand Down
Loading
Loading