Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
ea433d6
feat: add task orchestration tab and task CLI
awsl233777 Apr 12, 2026
e8329d6
fix: harden task orchestration flows
awsl233777 Apr 12, 2026
28673cc
fix(usage): recover session totals and estimate cost
awsl233777 Apr 12, 2026
67b74ab
feat(usage): show estimated cost and duration
awsl233777 Apr 12, 2026
8b58f5a
fix(ui): tighten task orchestration copy and pricing
awsl233777 Apr 12, 2026
c9ecfa0
fix(ui): add usage pricing fallback and align orchestration actions
awsl233777 Apr 12, 2026
af26a3e
fix(ui): keep usage cost in sync with selected range
awsl233777 Apr 12, 2026
55433da
fix(ui): show precise usage cost ranges
awsl233777 Apr 12, 2026
41ce3d7
fix(ui): show usage range and coverage on cost card
awsl233777 Apr 12, 2026
ef446ee
fix(ui): align orchestration action controls
awsl233777 Apr 12, 2026
8733e03
fix(ui): explain unavailable usage cost ranges
awsl233777 Apr 12, 2026
113d231
fix(ui): tighten orchestration action row layout
awsl233777 Apr 12, 2026
f8c926f
feat(ui): show all used models in usage
awsl233777 Apr 12, 2026
bea099f
feat(ui): polish task orchestration onboarding
awsl233777 Apr 12, 2026
b04ffbe
feat(ui): improve task orchestration guidance
awsl233777 Apr 12, 2026
980f74c
feat(ui): restructure orchestration workbench
awsl233777 Apr 12, 2026
6832323
fix(usage): recover session model metadata
awsl233777 Apr 12, 2026
431ec33
fix(ui): tighten orchestration state handling
awsl233777 Apr 12, 2026
43c8f14
fix(usage): preserve all session model names
awsl233777 Apr 12, 2026
a0fbf40
fix(usage): scan full sessions for model names
awsl233777 Apr 12, 2026
b3eba50
refactor(ui): simplify orchestration workbench flow
awsl233777 Apr 12, 2026
32421a3
fix(usage): surface model coverage gaps and simplify workbench
awsl233777 Apr 12, 2026
ba7697b
feat(claude): supplement bigmodel endpoint models
awsl233777 Apr 12, 2026
dbb0e95
feat(claude): supplement anthropic endpoint models
awsl233777 Apr 12, 2026
8a0e86c
feat(claude): add newer anthropic model aliases
awsl233777 Apr 12, 2026
0df1bc9
feat(claude): add local fallback model catalog
awsl233777 Apr 12, 2026
20cbb60
fix(usage): tighten copy and cost estimation
awsl233777 Apr 12, 2026
0266322
fix(ui): align orchestration hero actions
awsl233777 Apr 12, 2026
4a93d8c
feat(ui): tighten usage scope and orchestration copy
awsl233777 Apr 12, 2026
2bd785e
fix(task): harden orchestration follow-up flows
awsl233777 Apr 12, 2026
25e2469
fix(ui): extract orchestration log formatting
awsl233777 Apr 12, 2026
3a0c1a8
fix(ui): ignore synthetic usage records
awsl233777 Apr 13, 2026
636c537
fix(ui): polish usage chart scrollbars
awsl233777 Apr 13, 2026
f7fc444
fix(ui): tighten sidebar section spacing
awsl233777 Apr 13, 2026
6c7ca41
fix(ui): compact usage duration cards
awsl233777 Apr 13, 2026
ef38eaa
fix(ui): simplify usage summary cards
awsl233777 Apr 13, 2026
8b52997
refactor(repo): unify maintenance tools layout
awsl233777 Apr 13, 2026
d6d592a
fix(web): suppress html startup error noise
awsl233777 Apr 13, 2026
ddcc10d
fix(task): use spawn-safe codex command on windows
awsl233777 Apr 13, 2026
94f209e
fix(web): reduce startup api retries when backend is offline
awsl233777 Apr 13, 2026
6c8649a
fix(task): run codex through shell on windows
awsl233777 Apr 13, 2026
9df2f0c
fix(web): hide task orchestration tab for now
awsl233777 Apr 13, 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
1,851 changes: 1,813 additions & 38 deletions cli.js

Large diffs are not rendered by default.

851 changes: 851 additions & 0 deletions lib/task-orchestrator.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions tests/e2e/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const testHealthSpeed = require('./test-health-speed');
const testMessages = require('./test-messages');
const testMcp = require('./test-mcp');
const testWorkflow = require('./test-workflow');
const testTaskOrchestration = require('./test-task-orchestration');
const testInvalidConfig = require('./test-invalid-config');
const testWebUiAssets = require('./test-web-ui-assets');
const testWebUiSessionBrowser = require('./test-web-ui-session-browser');
Expand Down Expand Up @@ -134,6 +135,7 @@ async function main() {
await testMessages(ctx);
await testMcp(ctx);
await testWorkflow(ctx);
await testTaskOrchestration(ctx);
await testWebUiAssets(ctx);
await testWebUiSessionBrowser(ctx);

Expand Down
221 changes: 221 additions & 0 deletions tests/e2e/test-task-orchestration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
const { assert, runSync, fs, path } = require('./helpers');

function parseJsonOutput(rawText) {
const text = String(rawText || '').trim();
if (!text) {
return {};
}
try {
return JSON.parse(text);
} catch (_) {
const start = text.indexOf('{');
const end = text.lastIndexOf('}');
if (start >= 0 && end > start) {
return JSON.parse(text.slice(start, end + 1));
}
throw new Error(`invalid json output: ${text.slice(0, 200)}`);
}
}

module.exports = async function testTaskOrchestration(ctx) {
const { api, node, cliPath, env, tmpHome } = ctx;

const planResult = runSync(node, [
cliPath,
'task',
'plan',
'--target',
'检查当前配置并输出摘要',
'--follow-up',
'整理结论',
'--json'
], { env });
assert(planResult.status === 0, `task plan failed: ${planResult.stderr || planResult.stdout}`);
const planPayload = parseJsonOutput(planResult.stdout);
assert(planPayload.ok === true, 'task plan should validate');
assert(planPayload.plan && Array.isArray(planPayload.plan.nodes), 'task plan should include nodes');
assert(planPayload.plan.nodes.length >= 2, 'task plan should include multiple nodes');

const invalidWorkflowPlanResult = runSync(node, [
cliPath,
'task',
'plan',
'--target',
'plain target',
'--workflow-id',
'missing-workflow',
'--engine',
'workflow',
'--json'
], { env });
assert(invalidWorkflowPlanResult.status !== 0, 'task plan should fail for unknown workflow ids');
const invalidWorkflowPlanPayload = parseJsonOutput(invalidWorkflowPlanResult.stdout);
assert(invalidWorkflowPlanPayload.ok === false, 'invalid workflow plan should be rejected');
assert(Array.isArray(invalidWorkflowPlanPayload.issues), 'invalid workflow plan should include issues');
assert(invalidWorkflowPlanPayload.issues.some((item) => String(item.message || '').includes('unknown workflow')), 'invalid workflow plan should mention unknown workflow');

const runResult = runSync(node, [
cliPath,
'task',
'run',
'--target',
'诊断当前配置',
'--workflow-id',
'diagnose-config',
'--engine',
'workflow',
'--json'
], { env });
assert(runResult.status === 0, `task run failed: ${runResult.stderr || runResult.stdout}`);
const runPayload = parseJsonOutput(runResult.stdout);
assert(runPayload.run && runPayload.run.status === 'success', 'task run should succeed with diagnose-config workflow');
assert(typeof runPayload.runId === 'string' && runPayload.runId, 'task run should return runId');
assert(typeof runPayload.taskId === 'string' && runPayload.taskId, 'task run should return taskId');

const runsResult = runSync(node, [cliPath, 'task', 'runs', '--limit', '10', '--json'], { env });
assert(runsResult.status === 0, `task runs failed: ${runsResult.stderr || runsResult.stdout}`);
const runsPayload = parseJsonOutput(runsResult.stdout);
assert(Array.isArray(runsPayload.runs), 'task runs should return runs array');
assert(runsPayload.runs.some((item) => item.runId === runPayload.runId), 'task runs should include latest run');

const queueAddResult = runSync(node, [
cliPath,
'task',
'queue',
'add',
'--target',
'再次诊断当前配置',
'--workflow-id',
'diagnose-config',
'--engine',
'workflow',
'--json'
], { env });
assert(queueAddResult.status === 0, `task queue add failed: ${queueAddResult.stderr || queueAddResult.stdout}`);
const queueAddPayload = parseJsonOutput(queueAddResult.stdout);
assert(queueAddPayload.ok === true, 'task queue add should succeed');
assert(queueAddPayload.task && queueAddPayload.task.taskId, 'queue add should return task');

const queueShowResult = runSync(node, [
cliPath,
'task',
'queue',
'show',
queueAddPayload.task.taskId
], { env });
assert(queueShowResult.status === 0, `task queue show failed: ${queueShowResult.stderr || queueShowResult.stdout}`);
const queueShowPayload = parseJsonOutput(queueShowResult.stdout);
assert(queueShowPayload.taskId === queueAddPayload.task.taskId, 'task queue show should resolve task');

const queueStartResult = runSync(node, [
cliPath,
'task',
'queue',
'start',
queueAddPayload.task.taskId,
'--json'
], { env });
assert(queueStartResult.status === 0, `task queue start failed: ${queueStartResult.stderr || queueStartResult.stdout}`);
const queueStartPayload = parseJsonOutput(queueStartResult.stdout);
assert(queueStartPayload.ok === true, 'task queue start should succeed');
assert(queueStartPayload.detail && queueStartPayload.detail.run && queueStartPayload.detail.run.status === 'success', 'queued task should complete successfully');

const logsResult = runSync(node, [
cliPath,
'task',
'logs',
queueStartPayload.detail.runId,
'--json'
], { env });
assert(logsResult.status === 0, `task logs failed: ${logsResult.stderr || logsResult.stdout}`);
const logsPayload = parseJsonOutput(logsResult.stdout);
assert(typeof logsPayload.logs === 'string', 'task logs should return log text');
assert(logsPayload.logs.includes('# workflow-01') || logsPayload.logs.includes('# diagnose-config') || logsPayload.logs.includes('# workflow'), 'task logs should include node heading');

const queueListResult = runSync(node, [cliPath, 'task', 'queue', 'list', '--json'], { env });
assert(queueListResult.status === 0, `task queue list failed: ${queueListResult.stderr || queueListResult.stdout}`);
const queueListPayload = parseJsonOutput(queueListResult.stdout);
assert(Array.isArray(queueListPayload.tasks), 'task queue list should return tasks array');
assert(queueListPayload.tasks.some((item) => item.taskId === queueAddPayload.task.taskId), 'task queue list should include queued task record');

const taskRunsFile = path.join(tmpHome, '.codex', 'codexmate-task-runs.jsonl');
const taskQueueFile = path.join(tmpHome, '.codex', 'codexmate-task-queue.json');
assert(fs.existsSync(taskRunsFile), 'task runs file should be created');
assert(fs.existsSync(taskQueueFile), 'task queue file should be created');

const apiOverview = await api('task-overview');
assert(Array.isArray(apiOverview.queue), 'task-overview API should return queue');
assert(Array.isArray(apiOverview.runs), 'task-overview API should return runs');

const apiPlan = await api('task-plan', {
target: '检查配置后输出摘要',
followUps: ['整理结果']
});
assert(apiPlan.ok === true, 'task-plan API should validate');
assert(apiPlan.plan && Array.isArray(apiPlan.plan.waves), 'task-plan API should return waves');

const apiQueueAdd = await api('task-queue-add', {
target: '排队执行配置诊断',
workflowIds: ['diagnose-config'],
engine: 'workflow'
});
assert(apiQueueAdd.ok === true, 'task-queue-add API should succeed');

const apiQueueStart = await api('task-queue-start', {
taskId: apiQueueAdd.task.taskId,
detach: false
});
assert(apiQueueStart.ok === true, 'task-queue-start API should succeed');
assert(apiQueueStart.detail && apiQueueStart.detail.run && apiQueueStart.detail.run.status === 'success', 'API queue start should execute task');

const apiRunDetail = await api('task-run-detail', { runId: apiQueueStart.detail.runId });
assert(apiRunDetail && apiRunDetail.runId === apiQueueStart.detail.runId, 'task-run-detail API should return detail');

const missingQueueStartResult = runSync(node, [
cliPath,
'task',
'queue',
'start',
'missing-task',
'--json'
], { env });
assert(missingQueueStartResult.status !== 0, 'task queue start should fail for missing task');
const missingQueueStartPayload = parseJsonOutput(missingQueueStartResult.stdout);
assert(typeof missingQueueStartPayload.error === 'string' && missingQueueStartPayload.error.includes('task not found'), 'missing task queue start should report not found');

const invalidRunIdResult = runSync(node, [
cliPath,
'task',
'run',
'--target',
'诊断当前配置',
'--workflow-id',
'diagnose-config',
'--engine',
'workflow',
'--run-id',
'../escaped-run',
'--json'
], { env });
assert(invalidRunIdResult.status !== 0, 'task run should reject unsafe run ids');
const invalidRunIdPayload = parseJsonOutput(invalidRunIdResult.stdout);
assert(typeof invalidRunIdPayload.error === 'string' && invalidRunIdPayload.error.includes('unsupported characters'), 'unsafe run id should report validation error');

const apiRetry = await api('task-retry', {
runId: apiQueueStart.detail.runId,
detach: false
});
assert(apiRetry && apiRetry.run && apiRetry.run.status === 'success', 'task-retry API should rerun task');

const apiLogs = await api('task-logs', { runId: apiRetry.runId });
assert(typeof apiLogs.logs === 'string', 'task-logs API should return logs');

const apiCancelQueued = await api('task-queue-add', {
target: '待取消任务',
workflowIds: ['diagnose-config'],
engine: 'workflow'
});
assert(apiCancelQueued.ok === true, 'second task-queue-add API should succeed');
const apiCancel = await api('task-cancel', { taskId: apiCancelQueued.task.taskId });
assert(apiCancel.ok === true, 'task-cancel API should cancel queued task');
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};
18 changes: 18 additions & 0 deletions tests/unit/config-tabs-ui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ test('config template keeps expected config tabs in top and side navigation', ()
const openclawModal = readProjectFile('web-ui/partials/index/modal-openclaw-config.html');
const sessionsPanel = readProjectFile('web-ui/partials/index/panel-sessions.html');
const usagePanel = readProjectFile('web-ui/partials/index/panel-usage.html');
const orchestrationPanel = readProjectFile('web-ui/partials/index/panel-orchestration.html');
const baseTheme = readProjectFile('web-ui/styles/base-theme.css');
const controlsForms = readProjectFile('web-ui/styles/controls-forms.css');
const sideRail = html.match(/<aside class="side-rail"[\s\S]*?<\/aside>/)?.[0] || '';
Expand Down Expand Up @@ -57,6 +58,22 @@ test('config template keeps expected config tabs in top and side navigation', ()
assert.match(html, /settingsTab === 'trash'/);
assert.match(html, /settingsTab === 'device'/);
assert.match(html, /sessionTrashCount/);
assert.match(html, /id="side-tab-orchestration"/);
assert.match(html, /id="tab-orchestration"/);
assert.match(html, /data-main-tab="orchestration"/);
assert.match(html, /onMainTabPointerDown\('orchestration', \$event\)/);
assert.match(html, /onMainTabClick\('orchestration', \$event\)/);
assert.match(html, /aria-controls="panel-orchestration"/);
assert.match(html, /:aria-selected="mainTab === 'orchestration'"/);
assert.match(html, /id="panel-orchestration"/);
assert.match(html, /v-show="mainTab === 'orchestration'"/);
assert.match(orchestrationPanel, /<span class="selector-title">任务目标<\/span>/);
assert.match(orchestrationPanel, /@click="previewTaskPlan\(\)"/);
assert.match(orchestrationPanel, /@click="runTaskOrchestration\(\)"/);
assert.match(orchestrationPanel, /@click="addTaskOrchestrationToQueue\(\)"/);
assert.match(orchestrationPanel, /@click="startTaskQueueRunner\(\)"/);
assert.match(orchestrationPanel, /@click="retryTaskRunFromUi\(taskOrchestration.selectedRunId\)"/);
assert.match(orchestrationPanel, /taskOrchestrationSelectedRunNodes/);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
assert.match(html, /id="side-tab-market"/);
assert.match(html, /id="tab-market"/);
assert.match(html, /id="side-tab-docs"/);
Expand Down Expand Up @@ -119,6 +136,7 @@ test('config template keeps expected config tabs in top and side navigation', ()
assert.doesNotMatch(html, /class="market-target-switch market-target-switch-compact" role="tablist" aria-label="选择 Skills 管理目标"/);
assert.match(html, /class="side-section" role="navigation" aria-label="配置管理"/);
assert.match(html, /class="side-section" role="navigation" aria-label="会话管理"/);
assert.match(html, /class="side-section" role="navigation" aria-label="任务编排"/);
assert.match(html, /class="side-section" role="navigation" aria-label="技能市场"/);
assert.match(html, /class="side-section" role="navigation" aria-label="文档"/);
assert.match(html, /class="side-section" role="navigation" aria-label="设置"/);
Expand Down
1 change: 1 addition & 0 deletions tests/unit/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ await import(pathToFileURL(path.join(__dirname, 'reset-main.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'session-query.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'mcp-stdio.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'workflow-engine.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'task-orchestrator.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'skills-modal-ui.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'skills-market-runtime.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'readme-docs-consistency.test.mjs')));
Expand Down
Loading
Loading