-
Notifications
You must be signed in to change notification settings - Fork 35
feat: add task orchestration and usage insights #86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 14 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
awsldev e8329d6
fix: harden task orchestration flows
awsldev 28673cc
fix(usage): recover session totals and estimate cost
awsldev 67b74ab
feat(usage): show estimated cost and duration
awsldev 8b58f5a
fix(ui): tighten task orchestration copy and pricing
awsldev c9ecfa0
fix(ui): add usage pricing fallback and align orchestration actions
awsldev af26a3e
fix(ui): keep usage cost in sync with selected range
awsldev 55433da
fix(ui): show precise usage cost ranges
awsldev 41ce3d7
fix(ui): show usage range and coverage on cost card
awsldev ef446ee
fix(ui): align orchestration action controls
awsldev 8733e03
fix(ui): explain unavailable usage cost ranges
awsldev 113d231
fix(ui): tighten orchestration action row layout
awsldev f8c926f
feat(ui): show all used models in usage
awsldev bea099f
feat(ui): polish task orchestration onboarding
awsldev b04ffbe
feat(ui): improve task orchestration guidance
awsldev 980f74c
feat(ui): restructure orchestration workbench
awsldev 6832323
fix(usage): recover session model metadata
awsldev 431ec33
fix(ui): tighten orchestration state handling
awsldev 43c8f14
fix(usage): preserve all session model names
awsldev a0fbf40
fix(usage): scan full sessions for model names
awsldev b3eba50
refactor(ui): simplify orchestration workbench flow
awsldev 32421a3
fix(usage): surface model coverage gaps and simplify workbench
awsldev ba7697b
feat(claude): supplement bigmodel endpoint models
awsldev dbb0e95
feat(claude): supplement anthropic endpoint models
awsldev 8a0e86c
feat(claude): add newer anthropic model aliases
awsldev 0df1bc9
feat(claude): add local fallback model catalog
awsldev 20cbb60
fix(usage): tighten copy and cost estimation
awsldev 0266322
fix(ui): align orchestration hero actions
awsldev 4a93d8c
feat(ui): tighten usage scope and orchestration copy
awsldev 2bd785e
fix(task): harden orchestration follow-up flows
awsldev 25e2469
fix(ui): extract orchestration log formatting
awsldev 3a0c1a8
fix(ui): ignore synthetic usage records
awsldev 636c537
fix(ui): polish usage chart scrollbars
awsldev f7fc444
fix(ui): tighten sidebar section spacing
awsldev 6c7ca41
fix(ui): compact usage duration cards
awsldev ef38eaa
fix(ui): simplify usage summary cards
awsldev 8b52997
refactor(repo): unify maintenance tools layout
awsldev d6d592a
fix(web): suppress html startup error noise
awsldev ddcc10d
fix(task): use spawn-safe codex command on windows
awsldev 94f209e
fix(web): reduce startup api retries when backend is offline
awsldev 6c8649a
fix(task): run codex through shell on windows
awsldev 9df2f0c
fix(web): hide task orchestration tab for now
awsldev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'); | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.