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