From 511101aa3e7a621dedffa511c4bbe2df878bd289 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sat, 27 Jun 2026 15:54:31 +0000 Subject: [PATCH 01/54] feat(task-orchestration): add OpenAI Chat console flow --- cli.js | 327 +++++++++++------- lib/task-orchestrator.js | 46 ++- pnpm-lock.yaml | 14 +- tests/e2e/test-task-orchestration.js | 273 +++++++++++++++ tests/unit/config-tabs-ui.test.mjs | 2 +- tests/unit/task-codex-exec-windows.test.mjs | 101 +++--- tests/unit/task-orchestrator.test.mjs | 28 +- tests/unit/web-ui-behavior-parity.test.mjs | 1 + tests/unit/web-ui-logic.test.mjs | 21 ++ web-ui/app.js | 2 +- web-ui/modules/app.computed.main-tabs.mjs | 13 +- .../app.methods.task-orchestration.mjs | 6 +- web-ui/modules/i18n/locales/en.mjs | 4 +- web-ui/modules/i18n/locales/ja.mjs | 4 +- web-ui/modules/i18n/locales/vi.mjs | 4 +- web-ui/modules/i18n/locales/zh-tw.mjs | 4 +- web-ui/modules/i18n/locales/zh.mjs | 4 +- .../partials/index/panel-orchestration.html | 30 +- web-ui/res/web-ui-render.precompiled.js | 64 ++-- 19 files changed, 670 insertions(+), 278 deletions(-) diff --git a/cli.js b/cli.js index 78808257..01ba4411 100644 --- a/cli.js +++ b/cli.js @@ -258,6 +258,8 @@ const TASK_RUNS_FILE = path.join(CONFIG_DIR, 'codexmate-task-runs.jsonl'); const TASK_RUN_DETAILS_DIR = path.join(CONFIG_DIR, 'codexmate-task-runs'); const TASK_QUEUE_WORKER_FILE = path.join(CONFIG_DIR, 'codexmate-task-queue-worker.json'); const TASK_ARTIFACTS_DIR = path.join(CONFIG_DIR, 'codexmate-task-artifacts'); +const TASK_OPENAI_CHAT_TIMEOUT_MS = 180000; +const TASK_OPENAI_CHAT_MAX_RESPONSE_BYTES = 4 * 1024 * 1024; const AUTOMATION_CONFIG_FILE = path.join(CONFIG_DIR, 'codexmate-automation.json'); const DEFAULT_CLAUDE_MODEL = 'glm-4.7'; const DEFAULT_MODEL_CONTEXT_WINDOW = 190000; @@ -13859,7 +13861,7 @@ function printTaskHelp() { console.log(' --allow-write 允许写入工作区'); console.log(' --dry-run 仅计划/预演,不执行写入'); console.log(' --plan-only 仅输出计划,不执行'); - console.log(' --engine 选择编排引擎'); + console.log(' --engine 选择编排引擎'); console.log(' --concurrency 并发度'); console.log(' --auto-fix-rounds 自动修复回合数'); console.log(' --limit runs/queue list 数量'); @@ -13882,7 +13884,7 @@ function parseTaskCliOptions(args = []) { allowWrite: false, dryRun: false, planOnly: false, - engine: 'codex', + engine: 'openai-chat', concurrency: 2, autoFixRounds: 1, limit: 20, @@ -14086,7 +14088,7 @@ function buildTaskCliPayload(options = {}, rest = []) { if (explicit.followUps && Array.isArray(options.followUps)) payload.followUps = options.followUps.slice(); if (explicit.allowWrite) payload.allowWrite = options.allowWrite === true; if (explicit.dryRun) payload.dryRun = options.dryRun === true; - if (explicit.engine) payload.engine = options.engine || 'codex'; + if (explicit.engine) payload.engine = options.engine || 'openai-chat'; if (explicit.concurrency) payload.concurrency = options.concurrency; if (explicit.autoFixRounds) payload.autoFixRounds = options.autoFixRounds; if (explicit.taskId && options.taskId) payload.taskId = options.taskId; @@ -14102,7 +14104,7 @@ function buildTaskCliPayload(options = {}, rest = []) { function printTaskPlanSummary(plan, warnings = []) { console.log(`\n任务计划: ${plan.title || '(untitled)'}`); - console.log(` engine: ${plan.engine || 'codex'}`); + console.log(` engine: ${plan.engine || 'openai-chat'}`); console.log(` allowWrite: ${plan.allowWrite === true ? 'yes' : 'no'}`); console.log(` dryRun: ${plan.dryRun === true ? 'yes' : 'no'}`); console.log(` concurrency: ${plan.concurrency || 1}`); @@ -15568,8 +15570,10 @@ function validateTaskRunId(value) { } function normalizeTaskEngine(value) { - const normalized = typeof value === 'string' ? value.trim().toLowerCase() : ''; - return normalized === 'workflow' ? 'workflow' : 'codex'; + const normalized = typeof value === 'string' ? value.trim().toLowerCase().replace(/[\s_/]+/g, '-') : ''; + if (normalized === 'workflow') return 'workflow'; + // Legacy task plans used `codex`; new task execution uses the configured OpenAI Chat-compatible provider. + return 'openai-chat'; } function normalizeTaskFollowUps(input = []) { @@ -16058,20 +16062,171 @@ function readCodexLastMessageFile(filePath) { } } -async function runCodexExecTaskNode(node, context = {}) { - const codexPath = resolveSpawnCommand('codex'); - const codexProbeCommand = process.platform === 'win32' ? 'codex' : codexPath; - if (!commandExists(codexProbeCommand, '--version')) { +function buildOpenAiChatEndpointUrl(baseUrl) { + const trimmed = normalizeBaseUrl(baseUrl); + if (!trimmed) return ''; + if (/\/v1\/chat\/completions$/i.test(trimmed) || /\/chat\/completions$/i.test(trimmed)) { + return trimmed; + } + return joinApiUrl(trimmed, 'chat/completions'); +} + +function pickTaskProviderModel(providerName, provider, config) { + const currentModels = readCurrentModels(); + const savedModel = currentModels && typeof currentModels[providerName] === 'string' + ? currentModels[providerName].trim() + : ''; + const activeProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : ''; + const activeModel = typeof config.model === 'string' ? config.model.trim() : ''; + const providerModels = Array.isArray(provider && provider.models) ? provider.models : []; + const firstProviderModel = providerModels + .map((item) => { + if (typeof item === 'string') return item.trim(); + if (item && typeof item === 'object') return String(item.id || item.name || item.model || '').trim(); + return ''; + }) + .find(Boolean) || ''; + return savedModel || (activeProvider === providerName ? activeModel : '') || firstProviderModel; +} + +function resolveTaskOpenAiChatConfig() { + const configResult = readConfigOrVirtualDefault(); + const config = configResult && configResult.config && typeof configResult.config === 'object' ? configResult.config : {}; + const providerName = typeof config.model_provider === 'string' ? config.model_provider.trim() : ''; + if (!providerName) { + return { error: '未设置当前 OpenAI Chat 提供商' }; + } + const providers = config.model_providers && typeof config.model_providers === 'object' ? config.model_providers : {}; + const provider = providers[providerName]; + if (!provider || typeof provider !== 'object') { + return { error: `OpenAI Chat 提供商不存在: ${providerName}` }; + } + + const bridgeType = typeof provider.codexmate_bridge === 'string' ? provider.codexmate_bridge.trim() : ''; + const isOpenaiBridgeProvider = bridgeType === 'openai' + || (typeof provider.base_url === 'string' && provider.base_url.includes('/bridge/openai/')); + let baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : ''; + let apiKey = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method.trim() : ''; + let extraHeaders = {}; + if (isOpenaiBridgeProvider) { + const upstream = resolveOpenaiBridgeUpstream(OPENAI_BRIDGE_SETTINGS_FILE, providerName); + if (upstream && !upstream.error) { + baseUrl = upstream.baseUrl || baseUrl; + apiKey = upstream.apiKey || apiKey; + extraHeaders = upstream.headers && typeof upstream.headers === 'object' && !Array.isArray(upstream.headers) + ? upstream.headers + : {}; + } + } + + const model = pickTaskProviderModel(providerName, provider, config); + if (!baseUrl) { + return { error: `OpenAI Chat 提供商 ${providerName} 缺少 base_url` }; + } + if (!isValidHttpUrl(baseUrl)) { + return { error: `OpenAI Chat 提供商 ${providerName} 的 base_url 无效` }; + } + if (!model) { + return { error: `OpenAI Chat 提供商 ${providerName} 未设置模型` }; + } + return { + providerName, + baseUrl, + endpointUrl: buildOpenAiChatEndpointUrl(baseUrl), + apiKey, + extraHeaders, + model + }; +} + +function postOpenAiChatCompletion(requestConfig, body, options = {}) { + const endpointUrl = requestConfig && requestConfig.endpointUrl ? requestConfig.endpointUrl : ''; + return new Promise((resolve) => { + let parsed; + try { + parsed = new URL(endpointUrl); + } catch (error) { + resolve({ ok: false, error: 'OpenAI Chat endpoint URL 无效', status: 0, payload: null, body: '' }); + return; + } + const transport = parsed.protocol === 'https:' ? https : http; + const agent = parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT; + const payloadText = JSON.stringify(body || {}); + const headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': 'codexmate-task-orchestration', + 'Content-Length': Buffer.byteLength(payloadText, 'utf-8'), + ...(requestConfig.extraHeaders && typeof requestConfig.extraHeaders === 'object' ? requestConfig.extraHeaders : {}) + }; + if (requestConfig.apiKey) { + headers.Authorization = `Bearer ${requestConfig.apiKey}`; + } + let settled = false; + let req = null; + const finish = (result) => { + if (settled) return; + settled = true; + resolve(result); + }; + req = transport.request(parsed, { method: 'POST', headers, agent }, (res) => { + const status = res.statusCode || 0; + let raw = ''; + let receivedBytes = 0; + res.on('data', (chunk) => { + receivedBytes += chunk.length || 0; + if (receivedBytes > TASK_OPENAI_CHAT_MAX_RESPONSE_BYTES) { + res.destroy(new Error('response too large')); + return; + } + raw += chunk; + }); + res.on('end', () => { + if (settled) return; + let parsedPayload = null; + try { + parsedPayload = raw ? JSON.parse(raw) : null; + } catch (_) {} + if (status < 200 || status >= 300) { + const message = parsedPayload && parsedPayload.error + ? (typeof parsedPayload.error === 'string' ? parsedPayload.error : (parsedPayload.error.message || JSON.stringify(parsedPayload.error))) + : (raw || `OpenAI Chat request failed: ${status}`); + finish({ ok: false, status, error: truncateTaskText(message, 1200), payload: parsedPayload, body: raw }); + return; + } + finish({ ok: true, status, error: '', payload: parsedPayload, body: raw }); + }); + }); + if (typeof options.registerAbort === 'function') { + options.registerAbort(() => { + try { + req.destroy(new Error('cancelled')); + } catch (_) {} + }); + } + req.setTimeout(TASK_OPENAI_CHAT_TIMEOUT_MS, () => { + req.destroy(new Error('OpenAI Chat request timeout')); + }); + req.on('error', (error) => { + finish({ ok: false, status: 0, error: error && error.message ? error.message : String(error || 'OpenAI Chat request failed'), payload: null, body: '' }); + }); + req.write(payloadText); + req.end(); + }); +} + +async function runOpenAiChatTaskNode(node, context = {}) { + const requestConfig = resolveTaskOpenAiChatConfig(); + if (requestConfig.error) { return { success: false, - error: '未找到 codex CLI,请先安装并确保 PATH 可用', - summary: 'codex CLI 不可用', + error: requestConfig.error, + summary: requestConfig.error, output: null, - logs: [{ at: toIsoTime(Date.now()), level: 'error', message: 'codex CLI 不可用' }] + logs: [{ at: toIsoTime(Date.now()), level: 'error', message: requestConfig.error }] }; } const allowWrite = context.allowWrite === true && node.write === true; - const cwd = typeof context.cwd === 'string' && context.cwd.trim() ? context.cwd.trim() : process.cwd(); const dependencyResults = Array.isArray(context.dependencyResults) ? context.dependencyResults : []; const dependencyLines = dependencyResults .map((item) => { @@ -16091,124 +16246,48 @@ async function runCodexExecTaskNode(node, context = {}) { promptParts.push('请在保持目标不变的前提下修复上一轮失败并继续完成当前节点。'); } const finalPrompt = promptParts.filter(Boolean).join('\n\n'); - const tempRoot = path.join(TASK_RUN_DETAILS_DIR, 'tmp'); - ensureDir(tempRoot); - const tempDir = fs.mkdtempSync(path.join(tempRoot, 'codex-')); - const outputFile = path.join(tempDir, 'last-message.txt'); - const args = [ - '-a', 'never', - '-s', allowWrite ? 'workspace-write' : 'read-only', - '-C', cwd, - 'exec', - '--json', - '--skip-git-repo-check', - '--output-last-message', outputFile, - finalPrompt - ]; - const stdoutLines = []; - const stderrLines = []; - const parsedEvents = []; - let sessionId = ''; - let stdoutPartial = ''; - let stderrPartial = ''; - const processCapturedLine = (bucket, line) => { - const normalizedLine = String(line || '').trim(); - if (!normalizedLine) { - return; - } - if (bucket.length < 120) { - bucket.push(truncateTaskText(normalizedLine, 1200)); - } - try { - const payload = JSON.parse(normalizedLine); - if (parsedEvents.length < 120) { - parsedEvents.push(payload); - } - if (!sessionId) { - sessionId = findCodexSessionId(payload); - } - } catch (_) { } - }; - const captureLines = (bucket, text, stream) => { - const currentPartial = stream === 'stderr' ? stderrPartial : stdoutPartial; - const merged = `${currentPartial}${String(text || '')}`; - const pieces = merged.split(/\r?\n/g); - const nextPartial = pieces.pop() || ''; - if (stream === 'stderr') { - stderrPartial = nextPartial; - } else { - stdoutPartial = nextPartial; - } - for (const line of pieces) { - processCapturedLine(bucket, line); - } - }; - const flushCapturedPartial = (bucket, stream) => { - const partial = stream === 'stderr' ? stderrPartial : stdoutPartial; - if (stream === 'stderr') { - stderrPartial = ''; - } else { - stdoutPartial = ''; - } - processCapturedLine(bucket, partial); + const systemPrompt = [ + '你是 codexmate 任务编排中的 OpenAI Chat-compatible 执行节点。', + '基于用户给出的目标、前置节点摘要和当前节点范围,输出可执行、可验证、事实谨慎的结果。', + allowWrite + ? '当前任务允许写入,但本 OpenAI Chat 节点没有直接本地工具能力;如果需要改文件,请输出精确改动方案、补丁建议、验证命令与风险说明,不要声称已经实际写入。' + : '当前任务是只读/验证节点,不要声称修改了文件。', + '回答使用中文,避免空泛总结。' + ].join('\n'); + const startedAt = Date.now(); + const body = { + model: requestConfig.model, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: finalPrompt } + ], + temperature: 0.2, + stream: false }; - const exit = await new Promise((resolve) => { - const child = spawn(codexPath, args, { - stdio: ['ignore', 'pipe', 'pipe'], - windowsHide: true, - shell: process.platform === 'win32' - }); - if (typeof context.registerAbort === 'function') { - context.registerAbort(() => { - try { - child.kill('SIGTERM'); - } catch (_) { } - }); - } - child.stdout.on('data', (chunk) => { - captureLines(stdoutLines, chunk, 'stdout'); - }); - child.stderr.on('data', (chunk) => { - captureLines(stderrLines, chunk, 'stderr'); - }); - child.on('error', (error) => { - resolve({ code: 1, signal: '', error: error && error.message ? error.message : String(error || 'spawn failed') }); - }); - child.on('close', (code, signal) => { - flushCapturedPartial(stdoutLines, 'stdout'); - flushCapturedPartial(stderrLines, 'stderr'); - resolve({ code: typeof code === 'number' ? code : 1, signal: signal || '', error: '' }); - }); + const result = await postOpenAiChatCompletion(requestConfig, body, { + registerAbort: context.registerAbort }); - const lastMessage = readCodexLastMessageFile(outputFile); - try { - if (fs.rmSync) { - fs.rmSync(tempDir, { recursive: true, force: true }); - } else { - fs.rmdirSync(tempDir, { recursive: true }); - } - } catch (_) { } - const success = exit.code === 0; - const errorMessage = success - ? '' - : (exit.error || stderrLines[stderrLines.length - 1] || stdoutLines[stdoutLines.length - 1] || `codex exec exited with code ${exit.code}`); - const summary = truncateTaskText(lastMessage || (success ? 'Codex 执行完成' : errorMessage), 400); + const text = result.ok ? extractModelResponseText(result.payload) : ''; + const success = result.ok && !!text; + const errorMessage = success ? '' : (result.error || 'OpenAI Chat response did not contain text'); + const summary = truncateTaskText(text || errorMessage, 400); + const safeEndpoint = requestConfig.endpointUrl.replace(/([?&](?:key|api_key|token)=)[^&]+/ig, '$1***'); return { success, error: errorMessage, summary, output: { - exitCode: exit.code, - signal: exit.signal || '', - sessionId, - lastMessage, - events: parsedEvents, - stdoutPreview: stdoutLines, - stderrPreview: stderrLines + provider: requestConfig.providerName, + model: requestConfig.model, + endpoint: safeEndpoint, + status: result.status || 0, + text, + response: result.payload || null, + durationMs: Date.now() - startedAt }, logs: [ - ...stdoutLines.map((line) => ({ at: toIsoTime(Date.now()), level: 'info', message: line })), - ...stderrLines.map((line) => ({ at: toIsoTime(Date.now()), level: 'warn', message: line })) + { at: toIsoTime(Date.now()), level: 'info', message: `OpenAI Chat request provider=${requestConfig.providerName} model=${requestConfig.model} status=${result.status || 0}` }, + ...(success ? [{ at: toIsoTime(Date.now()), level: 'info', message: truncateTaskText(text, 1200) }] : [{ at: toIsoTime(Date.now()), level: 'error', message: errorMessage }]) ] }; } @@ -16246,7 +16325,7 @@ async function executeTaskNodeAdapter(node, context = {}) { : [] }; } - return runCodexExecTaskNode(node, context); + return runOpenAiChatTaskNode(node, context); } async function runTaskPlanInternal(plan, options = {}) { diff --git a/lib/task-orchestrator.js b/lib/task-orchestrator.js index 64a69734..6e3ce4f2 100644 --- a/lib/task-orchestrator.js +++ b/lib/task-orchestrator.js @@ -100,6 +100,22 @@ function buildTaskTitle(target, explicitTitle = '') { return truncateText(normalizedTarget.replace(/\s+/g, ' '), 96); } + +function normalizeTaskEngine(value) { + const normalized = normalizeId(value, 'openai-chat'); + if (normalized === 'workflow') return 'workflow'; + // Legacy plans used the name `codex`; the execution base is now OpenAI Chat compatible. + return 'openai-chat'; +} + +function normalizeTaskNodeKind(value) { + const normalized = normalizeId(value, 'openai-chat'); + if (normalized === 'workflow') return 'workflow'; + if (normalized === 'codex') return 'openai-chat'; + if (normalized === 'openai' || normalized === 'openai-chat' || normalized === 'chat') return 'openai-chat'; + return normalized; +} + function normalizeDependencyIds(value = []) { return uniqueStringList(value) .map((depId) => normalizeId(depId, '')) @@ -193,7 +209,7 @@ function detectDependencyCycle(nodes = []) { return false; } -function buildCodexNodePrompt(kind, context = {}) { +function buildOpenAiChatNodePrompt(kind, context = {}) { const title = normalizeText(context.title, 160); const target = normalizeText(context.target, 4000); const item = normalizeText(context.item, 1200); @@ -267,7 +283,7 @@ function buildTaskPlan(request = {}, options = {}) { const workflowMap = normalizeWorkflowCatalog(options.workflowCatalog || []); const target = normalizeText(request.target, 4000); const title = buildTaskTitle(target, request.title); - const engine = normalizeId(request.engine, 'codex') === 'workflow' ? 'workflow' : 'codex'; + const engine = normalizeTaskEngine(request.engine); const allowWrite = request.allowWrite === true; const dryRun = request.dryRun === true; const concurrency = normalizePositiveInteger(request.concurrency, 2, 1, 8); @@ -313,8 +329,8 @@ function buildTaskPlan(request = {}, options = {}) { nodes.push({ id: analysisId, title: '现状分析', - kind: 'codex', - prompt: buildCodexNodePrompt('analysis', { + kind: 'openai-chat', + prompt: buildOpenAiChatNodePrompt('analysis', { title, target, allowWrite: false @@ -333,8 +349,8 @@ function buildTaskPlan(request = {}, options = {}) { nodes.push({ id: nodeId, title: truncateText(item, 72) || `执行 ${executionNodeIds.length}`, - kind: 'codex', - prompt: buildCodexNodePrompt('work', { + kind: 'openai-chat', + prompt: buildOpenAiChatNodePrompt('work', { title, target, item, @@ -351,8 +367,8 @@ function buildTaskPlan(request = {}, options = {}) { nodes.push({ id: verifyId, title: '验证与总结', - kind: 'codex', - prompt: buildCodexNodePrompt('verify', { + kind: 'openai-chat', + prompt: buildOpenAiChatNodePrompt('verify', { title, target, allowWrite: false @@ -371,8 +387,8 @@ function buildTaskPlan(request = {}, options = {}) { nodes.push({ id: nodeId, title: truncateText(followUp, 72) || `Follow-up ${nodes.length + 1}`, - kind: 'codex', - prompt: buildCodexNodePrompt('follow-up', { + kind: 'openai-chat', + prompt: buildOpenAiChatNodePrompt('follow-up', { title, target, allowWrite, @@ -417,7 +433,7 @@ function validateTaskPlan(plan, options = {}) { } const workflowMap = normalizeWorkflowCatalog(options.workflowCatalog || []); - const engine = normalizeId(plan.engine, 'codex') === 'workflow' ? 'workflow' : 'codex'; + const engine = normalizeTaskEngine(plan.engine); const workflowIds = uniqueStringList(plan.workflowIds || []).map((id) => normalizeId(id, '')).filter(Boolean); const nodes = Array.isArray(plan.nodes) ? plan.nodes : []; if (engine === 'workflow' && workflowIds.length === 0) { @@ -445,8 +461,8 @@ function validateTaskPlan(plan, options = {}) { continue; } nodeIds.add(id); - const kind = normalizeId(node.kind, ''); - if (kind !== 'workflow' && kind !== 'codex') { + const kind = normalizeTaskNodeKind(node.kind); + if (kind !== 'workflow' && kind !== 'openai-chat') { issues.push({ code: 'task-node-kind-invalid', message: `task node ${id} has unsupported kind: ${node.kind || ''}` }); } if (kind === 'workflow') { @@ -457,7 +473,7 @@ function validateTaskPlan(plan, options = {}) { issues.push({ code: 'task-node-workflow-unknown', message: `task node ${id} references unknown workflow: ${workflowId}` }); } } - if (kind === 'codex' && !normalizeText(node.prompt, 12000)) { + if (kind === 'openai-chat' && !normalizeText(node.prompt, 12000)) { issues.push({ code: 'task-node-prompt-required', message: `task node ${id} missing prompt` }); } } @@ -492,7 +508,7 @@ function createNodeRunRecord(node, dependencyNodeIds = []) { return { id: normalizeId(node && node.id, ''), title: normalizeText(node && node.title, 160), - kind: normalizeId(node && node.kind, ''), + kind: normalizeTaskNodeKind(node && node.kind), workflowId: normalizeId(node && node.workflowId, ''), dependsOn: normalizeDependencyIds(node && node.dependsOn), dependencyNodeIds: uniqueStringList(dependencyNodeIds), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 313f412c..76d05897 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,6 @@ importers: '@iarna/toml': specifier: ^2.2.5 version: 2.2.5 - '@vue/compiler-dom': - specifier: ^3.5.30 - version: 3.5.34 json5: specifier: ^2.2.3 version: 2.2.3 @@ -24,6 +21,12 @@ importers: specifier: ^1.2.1 version: 1.3.2 devDependencies: + '@vue/compiler-dom': + specifier: ^3.5.30 + version: 3.5.34 + opencc-js: + specifier: ^1.3.1 + version: 1.3.1 vitepress: specifier: ^1.6.4 version: 1.6.4(@algolia/client-search@5.52.1)(postcss@8.5.14)(search-insights@2.17.3) @@ -702,6 +705,9 @@ packages: oniguruma-to-es@3.1.1: resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} + opencc-js@1.3.1: + resolution: {integrity: sha512-EyKDnjHNYlxo3Blll0OGH5qcyZBI3YmXpqeDEity/db50A5TFfCdvylrBlTyx1XvlSRUmh2a5AHvSf89h4u87Q==} + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -1483,6 +1489,8 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 + opencc-js@1.3.1: {} + pend@1.2.0: {} perfect-debounce@1.0.0: {} diff --git a/tests/e2e/test-task-orchestration.js b/tests/e2e/test-task-orchestration.js index 71b9b5db..6d08b28b 100644 --- a/tests/e2e/test-task-orchestration.js +++ b/tests/e2e/test-task-orchestration.js @@ -1,5 +1,156 @@ +const { spawn } = require('child_process'); const { assert, runSync, fs, path } = require('./helpers'); + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function startOpenAiChatMock(tmpHome) { + const scriptPath = path.join(tmpHome, 'task-openai-chat-mock.cjs'); + const portFile = path.join(tmpHome, 'task-openai-chat-mock.port'); + const requestsFile = path.join(tmpHome, 'task-openai-chat-requests.jsonl'); + fs.writeFileSync(scriptPath, ` +const http = require('http'); +const fs = require('fs'); +const portFile = process.argv[2]; +const requestsFile = process.argv[3]; +let requestCount = 0; +const server = http.createServer((req, res) => { + const requestPath = String(req.url || '').split('?')[0]; + let rawBody = ''; + req.setEncoding('utf-8'); + req.on('data', chunk => { rawBody += chunk; }); + req.on('end', () => { + let parsedBody = null; + try { parsedBody = rawBody ? JSON.parse(rawBody) : null; } catch (_) {} + requestCount += 1; + fs.appendFileSync(requestsFile, JSON.stringify({ + n: requestCount, + method: req.method, + path: requestPath, + authorization: req.headers.authorization || '', + body: parsedBody + }) + '\\n'); + if (req.method === 'GET' && requestPath === '/v1/models') { + const body = JSON.stringify({ object: 'list', data: [ + { id: 'deepseek-v4-pro', object: 'model' }, + { id: 'deepseek-v4-flash', object: 'model' } + ] }); + res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(body, 'utf-8') }); + res.end(body, 'utf-8'); + return; + } + if (req.method === 'POST' && requestPath === '/v1/chat/completions') { + const model = parsedBody && parsedBody.model ? parsedBody.model : 'unknown-model'; + const body = JSON.stringify({ + id: 'chatcmpl-task-e2e-' + requestCount, + object: 'chat.completion', + choices: [{ index: 0, message: { role: 'assistant', content: 'openai-chat-e2e-ok model=' + model + ' request=' + requestCount }, finish_reason: 'stop' }], + usage: { prompt_tokens: 10, completion_tokens: 8, total_tokens: 18 } + }); + res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(body, 'utf-8') }); + res.end(body, 'utf-8'); + return; + } + const body = JSON.stringify({ error: { message: 'not found ' + req.method + ' ' + requestPath } }); + res.writeHead(404, { '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, requestsFile], { + 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, + process: child, + readRequests() { + if (!fs.existsSync(requestsFile)) return []; + return fs.readFileSync(requestsFile, 'utf-8') + .split(/\r?\n/g) + .filter(Boolean) + .map(line => JSON.parse(line)); + }, + 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(`OpenAI Chat mock exited early: ${stderr}`); + } + await sleep(100); + } + try { child.kill('SIGKILL'); } catch (_) {} + throw new Error(`OpenAI Chat mock did not start: ${stderr}`); +} + +function writeOpenAiChatConfig(tmpHome, baseUrl) { + const configDir = path.join(tmpHome, '.codex'); + fs.mkdirSync(configDir, { 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 = "deepseek-v4-pro"', + 'model_provider = "local-openai-chat"', + '', + '[model_providers.local-openai-chat]', + 'name = "Local OpenAI Chat"', + `base_url = "${baseUrl}/v1"`, + 'wire_api = "chat_completions"', + 'preferred_auth_method = "sk-task-e2e-secret"', + 'models = ["deepseek-v4-pro", "deepseek-v4-flash"]', + '' + ].join('\n'), 'utf-8'); +} + +function assertOpenAiRunPayload(payload, label) { + assert(payload && payload.run && payload.run.status === 'success', `${label} should succeed`); + const nodes = Array.isArray(payload.run.nodes) ? payload.run.nodes : []; + assert(nodes.length > 0, `${label} should include nodes`); + assert(nodes.every(node => node.kind === 'openai-chat'), `${label} should use OpenAI Chat nodes`); + assert(nodes.every(node => node.status === 'success'), `${label} nodes should all succeed`); + assert(nodes.some(node => node.output && node.output.provider === 'local-openai-chat'), `${label} should record provider`); + assert(JSON.stringify(payload).indexOf('sk-task-e2e-secret') === -1, `${label} must not leak api key`); +} + +function assertOpenAiRequests(mock, minCount, label) { + const chatRequests = mock.readRequests().filter(item => item.path === '/v1/chat/completions'); + assert(chatRequests.length >= minCount, `${label} should call /v1/chat/completions at least ${minCount} times, got ${chatRequests.length}`); + for (const item of chatRequests) { + assert(item.method === 'POST', `${label} chat request should be POST`); + assert(item.authorization === 'Bearer sk-task-e2e-secret', `${label} should pass bearer auth`); + assert(item.body && item.body.model === 'deepseek-v4-pro', `${label} should pass selected model`); + assert(Array.isArray(item.body.messages) && item.body.messages.length >= 2, `${label} should send chat messages`); + assert(item.body.messages.some(message => message.role === 'system'), `${label} should include system prompt`); + assert(item.body.messages.some(message => message.role === 'user'), `${label} should include user prompt`); + } +} + function parseJsonOutput(rawText) { const text = String(rawText || '').trim(); if (!text) { @@ -35,6 +186,8 @@ module.exports = async function testTaskOrchestration(ctx) { 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'); + assert(planPayload.plan.engine === 'openai-chat', 'default task plan should use OpenAI Chat engine'); + assert(planPayload.plan.nodes.every((node) => node.kind === 'openai-chat'), 'default task plan nodes should be OpenAI Chat nodes'); const invalidWorkflowPlanResult = runSync(node, [ cliPath, @@ -171,6 +324,126 @@ module.exports = async function testTaskOrchestration(ctx) { 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 openAiMock = await startOpenAiChatMock(tmpHome); + try { + writeOpenAiChatConfig(tmpHome, `http://127.0.0.1:${openAiMock.port}`); + + const openAiPlanResult = runSync(node, [ + cliPath, + 'task', + 'plan', + '--target', + 'OpenAI Chat provider 端到端模拟', + '--follow-up', + '输出风险说明', + '--engine', + 'openai-chat', + '--json' + ], { env }); + assert(openAiPlanResult.status === 0, `OpenAI Chat task plan failed: ${openAiPlanResult.stderr || openAiPlanResult.stdout}`); + const openAiPlanPayload = parseJsonOutput(openAiPlanResult.stdout); + assert(openAiPlanPayload.ok === true, 'OpenAI Chat task plan should validate'); + assert(openAiPlanPayload.plan && openAiPlanPayload.plan.engine === 'openai-chat', 'OpenAI Chat plan should keep engine'); + assert(openAiPlanPayload.plan.nodes.every((node) => node.kind === 'openai-chat'), 'OpenAI Chat plan should produce OpenAI Chat nodes'); + + const openAiRunResult = runSync(node, [ + cliPath, + 'task', + 'run', + '--target', + 'OpenAI Chat provider CLI 运行链路', + '--follow-up', + '输出验证摘要', + '--engine', + 'openai-chat', + '--concurrency', + '2', + '--json' + ], { env }); + assert(openAiRunResult.status === 0, `OpenAI Chat task run failed: ${openAiRunResult.stderr || openAiRunResult.stdout}`); + const openAiRunPayload = parseJsonOutput(openAiRunResult.stdout); + assertOpenAiRunPayload(openAiRunPayload, 'OpenAI Chat CLI run'); + + const openAiLogsResult = runSync(node, [ + cliPath, + 'task', + 'logs', + openAiRunPayload.runId, + '--json' + ], { env }); + assert(openAiLogsResult.status === 0, `OpenAI Chat task logs failed: ${openAiLogsResult.stderr || openAiLogsResult.stdout}`); + const openAiLogsPayload = parseJsonOutput(openAiLogsResult.stdout); + assert(String(openAiLogsPayload.logs || '').includes('OpenAI Chat request provider=local-openai-chat'), 'OpenAI Chat logs should include provider request'); + + const openAiQueueAddResult = runSync(node, [ + cliPath, + 'task', + 'queue', + 'add', + '--target', + 'OpenAI Chat provider CLI 队列链路', + '--engine', + 'openai-chat', + '--json' + ], { env }); + assert(openAiQueueAddResult.status === 0, `OpenAI Chat queue add failed: ${openAiQueueAddResult.stderr || openAiQueueAddResult.stdout}`); + const openAiQueueAddPayload = parseJsonOutput(openAiQueueAddResult.stdout); + assert(openAiQueueAddPayload.ok === true && openAiQueueAddPayload.task && openAiQueueAddPayload.task.engine === 'openai-chat', 'OpenAI Chat queue add should persist engine'); + + const openAiQueueStartResult = runSync(node, [ + cliPath, + 'task', + 'queue', + 'start', + openAiQueueAddPayload.task.taskId, + '--json' + ], { env }); + assert(openAiQueueStartResult.status === 0, `OpenAI Chat queue start failed: ${openAiQueueStartResult.stderr || openAiQueueStartResult.stdout}`); + const openAiQueueStartPayload = parseJsonOutput(openAiQueueStartResult.stdout); + assert(openAiQueueStartPayload.ok === true, 'OpenAI Chat queue start should succeed'); + assertOpenAiRunPayload(openAiQueueStartPayload.detail, 'OpenAI Chat CLI queue start'); + + const apiOpenAiPlan = await api('task-plan', { + target: 'OpenAI Chat Web API 计划链路', + engine: 'openai-chat', + followUps: ['输出结论'] + }); + assert(apiOpenAiPlan.ok === true, 'OpenAI Chat task-plan API should validate'); + assert(apiOpenAiPlan.plan && apiOpenAiPlan.plan.engine === 'openai-chat', 'OpenAI Chat task-plan API should keep engine'); + assert(apiOpenAiPlan.plan.nodes.every((node) => node.kind === 'openai-chat'), 'OpenAI Chat task-plan API should produce OpenAI Chat nodes'); + + const apiOpenAiRun = await api('task-run', { + target: 'OpenAI Chat Web API 同步运行链路', + engine: 'openai-chat', + concurrency: 1 + }, 15000); + assertOpenAiRunPayload(apiOpenAiRun, 'OpenAI Chat API run'); + + const apiOpenAiDetail = await api('task-run-detail', { runId: apiOpenAiRun.runId }); + assert(apiOpenAiDetail && apiOpenAiDetail.run && apiOpenAiDetail.run.status === 'success', 'OpenAI Chat task-run-detail API should return run detail'); + assert(apiOpenAiDetail.run.nodes.every((node) => node.kind === 'openai-chat'), 'OpenAI Chat task-run-detail API should expose OpenAI Chat nodes'); + + const apiOpenAiQueueAdd = await api('task-queue-add', { + target: 'OpenAI Chat Web API 队列链路', + engine: 'openai-chat' + }); + assert(apiOpenAiQueueAdd.ok === true && apiOpenAiQueueAdd.task && apiOpenAiQueueAdd.task.engine === 'openai-chat', 'OpenAI Chat task-queue-add API should persist engine'); + const apiOpenAiQueueStart = await api('task-queue-start', { + taskId: apiOpenAiQueueAdd.task.taskId, + detach: false + }, 15000); + assert(apiOpenAiQueueStart.ok === true, 'OpenAI Chat task-queue-start API should succeed'); + assertOpenAiRunPayload(apiOpenAiQueueStart.detail, 'OpenAI Chat API queue start'); + + const apiOpenAiOverview = await api('task-overview'); + 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'); + } finally { + await openAiMock.close(); + } + const missingQueueStartResult = runSync(node, [ cliPath, 'task', diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index 6055425b..bbe645d7 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -105,7 +105,7 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(orchestrationPanel, /taskOrchestration.workspaceTab === 'queue'/); assert.match(orchestrationPanel, /taskOrchestration.workspaceTab === 'runs'/); assert.match(orchestrationPanel, /taskOrchestration.workspaceTab === 'detail'/); - assert.match(orchestrationPanel, /taskOrchestration\.queue\.length \|\| taskOrchestration\.runs\.length \|\| taskOrchestration\.selectedRunId \|\| taskOrchestration\.selectedRunError/); + assert.match(orchestrationPanel, /taskOrchestrationActiveQueue\.length \|\| taskOrchestration\.runs\.length \|\| taskOrchestration\.selectedRunId \|\| taskOrchestration\.selectedRunError/); assert.match(orchestrationPanel, /taskOrchestration\.plan \|\| taskOrchestration\.planIssues\.length \|\| taskOrchestration\.planWarnings\.length \|\| taskOrchestration\.lastError/); assert.match(orchestrationPanel, /class="selector-section task-stage-card"/); assert.match(orchestrationPanel, /t\('orchestration\.stage\.title'\)/); diff --git a/tests/unit/task-codex-exec-windows.test.mjs b/tests/unit/task-codex-exec-windows.test.mjs index 5b086372..af171e73 100644 --- a/tests/unit/task-codex-exec-windows.test.mjs +++ b/tests/unit/task-codex-exec-windows.test.mjs @@ -47,77 +47,58 @@ test('resolveSpawnCommand keeps bare command names on windows', () => { assert.strictEqual(resolveSpawnCommand('codex'), 'codex'); }); -test('runCodexExecTaskNode spawns bare codex command through the windows shell', async () => { - const source = extractBlockBySignature(cliSource, 'async function runCodexExecTaskNode(node, context = {}) {'); - const spawnCalls = []; - const runCodexExecTaskNode = instantiateFunction(source, 'runCodexExecTaskNode', { - process: { platform: 'win32' }, - resolveSpawnCommand() { - return 'codex'; - }, - commandExists(command, args) { - assert.strictEqual(command, 'codex'); - assert.strictEqual(args, '--version'); - return true; - }, - toIsoTime() { - return '2026-04-13T03:09:00.000Z'; - }, - truncateTaskText(text) { - return String(text || ''); +test('runOpenAiChatTaskNode uses configured OpenAI Chat provider without spawning codex', async () => { + const source = extractBlockBySignature(cliSource, 'async function runOpenAiChatTaskNode(node, context = {}) {'); + const requests = []; + const runOpenAiChatTaskNode = instantiateFunction(source, 'runOpenAiChatTaskNode', { + resolveTaskOpenAiChatConfig() { + return { + providerName: 'mock-openai', + model: 'deepseek-v4-pro', + endpointUrl: 'http://127.0.0.1:18183/v1/chat/completions', + apiKey: 'sk-unit-secret', + extraHeaders: {} + }; }, - ensureDir() {}, - TASK_RUN_DETAILS_DIR: '/tmp/task-runs', - path: { - join: (...parts) => parts.join('/'), - basename(value) { - const parts = String(value || '').split(/[\\/]/g); - return parts[parts.length - 1] || ''; - } + postOpenAiChatCompletion(config, body) { + requests.push({ config, body }); + return Promise.resolve({ + ok: true, + status: 200, + payload: { + choices: [{ message: { content: 'mock-openai-chat-ok' } }] + } + }); }, - fs: { - mkdtempSync() { - return '/tmp/task-runs/tmp/codex-123'; - }, - rmSync() {} + extractModelResponseText(payload) { + return payload.choices[0].message.content; }, - readCodexLastMessageFile() { - return 'done'; + truncateTaskText(text, limit) { + return String(text || '').slice(0, limit || 1000); }, - findCodexSessionId() { - return ''; + toIsoTime() { + return '2026-06-27T15:30:00.000Z'; }, - spawn(command, args, options) { - spawnCalls.push({ command, args, options }); - return { - stdout: { on() {} }, - stderr: { on() {} }, - on(event, handler) { - if (event === 'close') { - handler(0, ''); - } - }, - kill() {} - }; - } + Date }); - const result = await runCodexExecTaskNode({ + const result = await runOpenAiChatTaskNode({ id: 'analysis-01', - prompt: 'inspect the bug', + prompt: 'inspect the orchestration chain', write: false }, { - cwd: 'C:/repo' + cwd: 'C:/repo', + dependencyResults: [{ id: 'plan-01', summary: 'dependency done' }] }); assert.strictEqual(result.success, true); - assert.strictEqual(spawnCalls.length, 1); - assert.strictEqual(spawnCalls[0].command, 'codex'); - assert.strictEqual(spawnCalls[0].options.shell, true); - assert.deepStrictEqual(spawnCalls[0].args.slice(0, 7), [ - '-a', 'never', - '-s', 'read-only', - '-C', 'C:/repo', - 'exec' - ]); + assert.strictEqual(result.output.provider, 'mock-openai'); + assert.strictEqual(result.output.model, 'deepseek-v4-pro'); + assert.strictEqual(result.output.text, 'mock-openai-chat-ok'); + assert.strictEqual(requests.length, 1); + assert.strictEqual(requests[0].body.model, 'deepseek-v4-pro'); + assert.strictEqual(requests[0].body.messages[0].role, 'system'); + assert.strictEqual(requests[0].body.messages[1].role, 'user'); + assert.match(requests[0].body.messages[1].content, /dependency done/); + assert.ok(!JSON.stringify(result).includes('sk-unit-secret')); }); diff --git a/tests/unit/task-orchestrator.test.mjs b/tests/unit/task-orchestrator.test.mjs index 00a7dd4d..97dad7c3 100644 --- a/tests/unit/task-orchestrator.test.mjs +++ b/tests/unit/task-orchestrator.test.mjs @@ -30,21 +30,21 @@ test('computePlanWaves groups dependencies into waves', () => { ]); }); -test('buildTaskPlan generates codex orchestration nodes and follow-ups', () => { +test('buildTaskPlan generates OpenAI Chat orchestration nodes and follow-ups', () => { const plan = buildTaskPlan({ target: '实现任务编排 Tab\n- 新增前端面板\n- 扩展 CLI', allowWrite: true, concurrency: 3, followUps: ['继续处理 review 评论'] }); - assert.strictEqual(plan.engine, 'codex'); + assert.strictEqual(plan.engine, 'openai-chat'); assert.strictEqual(plan.allowWrite, true); assert.strictEqual(plan.concurrency, 3); assert.ok(Array.isArray(plan.nodes)); - assert.ok(plan.nodes.some((node) => node.kind === 'codex')); + assert.ok(plan.nodes.some((node) => node.kind === 'openai-chat')); assert.ok(plan.nodes.some((node) => node.title.includes('验证'))); assert.strictEqual(plan.followUps.length, 1); - assert.strictEqual(plan.nodes[plan.nodes.length - 1].kind, 'codex'); + assert.strictEqual(plan.nodes[plan.nodes.length - 1].kind, 'openai-chat'); }); test('buildTaskPlan can map workflow ids onto sequential workflow nodes', () => { @@ -107,8 +107,8 @@ test('validateTaskPlan rejects unknown workflow ids instead of silently falling test('validateTaskPlan rejects dependency cycles', () => { const result = validateTaskPlan({ nodes: [ - { id: 'a', kind: 'codex', prompt: 'a', dependsOn: ['b'] }, - { id: 'b', kind: 'codex', prompt: 'b', dependsOn: ['a'] } + { id: 'a', kind: 'openai-chat', prompt: 'a', dependsOn: ['b'] }, + { id: 'b', kind: 'openai-chat', prompt: 'b', dependsOn: ['a'] } ] }); assert.strictEqual(result.ok, false); @@ -120,10 +120,10 @@ test('executeTaskPlan respects dependency blocking and concurrency', async () => const run = await executeTaskPlan({ concurrency: 2, nodes: [ - { id: 'inspect', kind: 'codex', prompt: 'inspect', dependsOn: [], write: false }, - { id: 'work-a', kind: 'codex', prompt: 'work-a', dependsOn: ['inspect'], write: false }, - { id: 'work-b', kind: 'codex', prompt: 'work-b', dependsOn: ['inspect'], write: false }, - { id: 'verify', kind: 'codex', prompt: 'verify', dependsOn: ['work-a', 'work-b'], write: false } + { id: 'inspect', kind: 'openai-chat', prompt: 'inspect', dependsOn: [], write: false }, + { id: 'work-a', kind: 'openai-chat', prompt: 'work-a', dependsOn: ['inspect'], write: false }, + { id: 'work-b', kind: 'openai-chat', prompt: 'work-b', dependsOn: ['inspect'], write: false }, + { id: 'verify', kind: 'openai-chat', prompt: 'verify', dependsOn: ['work-a', 'work-b'], write: false } ] }, { concurrency: 2, @@ -151,7 +151,7 @@ test('executeTaskPlan retries failed nodes within auto-fix rounds', async () => const attempts = new Map(); const run = await executeTaskPlan({ nodes: [ - { id: 'work', kind: 'codex', prompt: 'work', dependsOn: [], write: false, autoFixRounds: 1 } + { id: 'work', kind: 'openai-chat', prompt: 'work', dependsOn: [], write: false, autoFixRounds: 1 } ] }, { async executeNode(node) { @@ -178,8 +178,8 @@ test('executeTaskPlan retries failed nodes within auto-fix rounds', async () => test('executeTaskPlan marks downstream nodes blocked when dependency fails', async () => { const run = await executeTaskPlan({ nodes: [ - { id: 'fail', kind: 'codex', prompt: 'fail', dependsOn: [], write: false }, - { id: 'after', kind: 'codex', prompt: 'after', dependsOn: ['fail'], write: false } + { id: 'fail', kind: 'openai-chat', prompt: 'fail', dependsOn: [], write: false }, + { id: 'after', kind: 'openai-chat', prompt: 'after', dependsOn: ['fail'], write: false } ] }, { async executeNode(node) { @@ -202,7 +202,7 @@ test('executeTaskPlan marks downstream nodes blocked when dependency fails', asy test('executeTaskPlan keeps payload logs without duplicating them', async () => { const run = await executeTaskPlan({ nodes: [ - { id: 'work', kind: 'codex', prompt: 'work', dependsOn: [], write: false } + { id: 'work', kind: 'openai-chat', prompt: 'work', dependsOn: [], write: false } ] }, { async executeNode() { diff --git a/tests/unit/web-ui-behavior-parity.test.mjs b/tests/unit/web-ui-behavior-parity.test.mjs index 37e9b102..9366cbb8 100644 --- a/tests/unit/web-ui-behavior-parity.test.mjs +++ b/tests/unit/web-ui-behavior-parity.test.mjs @@ -883,6 +883,7 @@ test('captured bundled app skeleton only exposes expected data key drift versus 'usageCurrentSessionStats', 'taskOrchestrationSelectedRun', 'taskOrchestrationSelectedRunNodes', + 'taskOrchestrationActiveQueue', 'taskOrchestrationQueueStats', 'taskOrchestrationDraftMetrics', 'taskOrchestrationDraftChecklist', diff --git a/tests/unit/web-ui-logic.test.mjs b/tests/unit/web-ui-logic.test.mjs index 489a5b6e..5a599573 100644 --- a/tests/unit/web-ui-logic.test.mjs +++ b/tests/unit/web-ui-logic.test.mjs @@ -1270,6 +1270,27 @@ test('taskOrchestrationQueueStats counts queue statuses in one pass without chan }); }); +test('taskOrchestrationActiveQueue hides completed and failed queue history from workbench queue tab', () => { + const computed = createMainTabsComputed(); + const queued = { taskId: 'task-queued', status: 'queued' }; + const running = { taskId: 'task-running', status: 'RUNNING' }; + const context = { + taskOrchestration: { + queue: [ + { taskId: 'task-completed', status: 'completed' }, + queued, + { taskId: 'task-failed', status: 'failed' }, + running, + { taskId: 'task-cancelled', status: 'cancelled' } + ] + } + }; + + const activeQueue = computed.taskOrchestrationActiveQueue.call(context); + + assert.deepStrictEqual(activeQueue, [queued, running]); +}); + test('startTaskQueueRunner surfaces already-running queue state distinctly', async () => { const api = async (name) => { if (name === 'task-queue-start') { diff --git a/web-ui/app.js b/web-ui/app.js index 9e2517e4..051bf0ce 100644 --- a/web-ui/app.js +++ b/web-ui/app.js @@ -464,7 +464,7 @@ document.addEventListener('DOMContentLoaded', () => { notes: '', followUpsText: '', workflowIdsText: '', - selectedEngine: 'codex', + selectedEngine: 'openai-chat', runMode: 'write', concurrency: 2, autoFixRounds: 1, diff --git a/web-ui/modules/app.computed.main-tabs.mjs b/web-ui/modules/app.computed.main-tabs.mjs index 47db0c39..d0235c34 100644 --- a/web-ui/modules/app.computed.main-tabs.mjs +++ b/web-ui/modules/app.computed.main-tabs.mjs @@ -12,7 +12,7 @@ function readTaskOrchestrationDraftMetrics(taskOrchestration) { const title = String(state.title || '').trim(); const workflowIds = normalizeTaskDraftLines(state.workflowIdsText); const followUps = normalizeTaskDraftLines(state.followUpsText); - const engine = String(state.selectedEngine || 'codex').trim().toLowerCase() === 'workflow' ? 'workflow' : 'codex'; + const engine = String(state.selectedEngine || 'openai-chat').trim().toLowerCase() === 'workflow' ? 'workflow' : 'openai-chat'; const runMode = String(state.runMode || 'write').trim().toLowerCase(); const allowWrite = runMode === 'write'; const dryRun = runMode === 'dry-run'; @@ -66,7 +66,7 @@ function createTaskDraftChecklist(metrics, t = null) { done: workflowReady, detail: metrics.engine === 'workflow' ? (metrics.workflowCount > 0 ? translateTaskText(t, 'orchestration.readiness.workflow.done', `已选 ${metrics.workflowCount} 个 Workflow`, { count: metrics.workflowCount }) : translateTaskText(t, 'orchestration.readiness.workflow.missing', '还没选 Workflow ID')) - : translateTaskText(t, 'orchestration.readiness.engine.codex', '使用 Codex 规划节点') + : translateTaskText(t, 'orchestration.readiness.engine.openaiChat', '使用 OpenAI Chat-compatible 节点') }, { key: 'scope', @@ -191,6 +191,15 @@ export function createMainTabsComputed() { if (detail && Array.isArray(detail.nodes)) return detail.nodes; return Array.isArray(run.nodes) ? run.nodes : []; }, + taskOrchestrationActiveQueue() { + const queue = this.taskOrchestration && Array.isArray(this.taskOrchestration.queue) + ? this.taskOrchestration.queue + : []; + return queue.filter((item) => { + const status = String(item && item.status || '').trim().toLowerCase(); + return status === 'queued' || status === 'running'; + }); + }, taskOrchestrationQueueStats() { const queue = this.taskOrchestration && Array.isArray(this.taskOrchestration.queue) ? this.taskOrchestration.queue diff --git a/web-ui/modules/app.methods.task-orchestration.mjs b/web-ui/modules/app.methods.task-orchestration.mjs index e18069f0..e9a83fed 100644 --- a/web-ui/modules/app.methods.task-orchestration.mjs +++ b/web-ui/modules/app.methods.task-orchestration.mjs @@ -11,7 +11,7 @@ function createDefaultTaskOrchestrationState() { notes: '', followUpsText: '', workflowIdsText: '', - selectedEngine: 'codex', + selectedEngine: 'openai-chat', runMode: 'write', concurrency: 2, autoFixRounds: 1, @@ -107,7 +107,7 @@ export function createTaskOrchestrationMethods(options = {}) { notes: String(state.notes || '').trim(), followUps: normalizeLines(state.followUpsText), workflowIds: normalizeLines(state.workflowIdsText), - engine: String(state.selectedEngine || 'codex').trim().toLowerCase() === 'workflow' ? 'workflow' : 'codex', + engine: String(state.selectedEngine || 'openai-chat').trim().toLowerCase() === 'workflow' ? 'workflow' : 'openai-chat', allowWrite: flags.allowWrite, dryRun: flags.dryRun, concurrency: normalizePositiveInteger(state.concurrency, 2, 1, 8), @@ -542,7 +542,7 @@ export function createTaskOrchestrationMethods(options = {}) { state.notes = ''; state.followUpsText = ''; state.workflowIdsText = ''; - state.selectedEngine = 'codex'; + state.selectedEngine = 'openai-chat'; state.runMode = 'write'; state.concurrency = 2; state.autoFixRounds = 1; diff --git a/web-ui/modules/i18n/locales/en.mjs b/web-ui/modules/i18n/locales/en.mjs index d4d1f3ab..53d9eb07 100644 --- a/web-ui/modules/i18n/locales/en.mjs +++ b/web-ui/modules/i18n/locales/en.mjs @@ -970,7 +970,7 @@ const en = Object.freeze({ 'orchestration.readiness.target.done': 'Target written', 'orchestration.readiness.target.missing': 'Target not written yet', 'orchestration.readiness.engine.label': 'Execution strategy', - 'orchestration.readiness.engine.codex': 'Use Codex to plan nodes', + 'orchestration.readiness.engine.openaiChat': 'Use OpenAI Chat-compatible nodes', 'orchestration.readiness.workflow.done': '{count} workflow(s) selected', 'orchestration.readiness.workflow.missing': 'No Workflow ID selected yet', 'orchestration.readiness.scope.label': 'Scope', @@ -1013,7 +1013,7 @@ const en = Object.freeze({ 'orchestration.fields.target': 'Target', 'orchestration.fields.target.placeholder': 'e.g. Address PR review comments and add regression tests; avoid unrelated modules', 'orchestration.fields.target.hint': 'One sentence is enough: outcome, constraints, and acceptance criteria.', - 'orchestration.engine.codex': 'Codex', + 'orchestration.engine.openaiChat': 'OpenAI Chat', 'orchestration.engine.workflow': 'Workflow', 'orchestration.runMode.write': 'Write', 'orchestration.runMode.readOnly': 'Read-only', diff --git a/web-ui/modules/i18n/locales/ja.mjs b/web-ui/modules/i18n/locales/ja.mjs index 61722f15..7fa6e1d5 100644 --- a/web-ui/modules/i18n/locales/ja.mjs +++ b/web-ui/modules/i18n/locales/ja.mjs @@ -959,7 +959,7 @@ const ja = Object.freeze({ 'orchestration.readiness.target.done': 'ターゲット入力済み', 'orchestration.readiness.target.missing': 'ターゲット未入力', 'orchestration.readiness.engine.label': '実行方針', - 'orchestration.readiness.engine.codex': 'Codex でノードを計画', + 'orchestration.readiness.engine.openaiChat': 'OpenAI Chat 互換ノードで計画', 'orchestration.readiness.workflow.done': '{count} 件の Workflow を選択済み', 'orchestration.readiness.workflow.missing': 'Workflow ID が未選択', 'orchestration.readiness.scope.label': '範囲', @@ -1002,7 +1002,7 @@ const ja = Object.freeze({ 'orchestration.fields.target': 'ターゲット', 'orchestration.fields.target.placeholder': '例: /path/to/project', 'orchestration.fields.target.hint': '作業ディレクトリまたはファイルパス。', - 'orchestration.engine.codex': 'Codex', + 'orchestration.engine.openaiChat': 'OpenAI Chat', 'orchestration.engine.workflow': 'Workflow', 'orchestration.runMode.write': '書き込み', 'orchestration.runMode.readOnly': '読み取り専用', diff --git a/web-ui/modules/i18n/locales/vi.mjs b/web-ui/modules/i18n/locales/vi.mjs index 9b4ff029..c3a2dd80 100644 --- a/web-ui/modules/i18n/locales/vi.mjs +++ b/web-ui/modules/i18n/locales/vi.mjs @@ -288,7 +288,7 @@ const vi = Object.freeze({ 'orchestration.readiness.target.done': 'Đã viết mục tiêu', 'orchestration.readiness.target.missing': 'Chưa viết mục tiêu', 'orchestration.readiness.engine.label': 'Chiến lược chạy', - 'orchestration.readiness.engine.codex': 'Dùng Codex để lập kế hoạch node', + 'orchestration.readiness.engine.openaiChat': 'Dùng node tương thích OpenAI Chat', 'orchestration.readiness.workflow.done': 'Đã chọn {count} Workflow', 'orchestration.readiness.workflow.missing': 'Chưa chọn Workflow ID', 'orchestration.readiness.scope.label': 'Phạm vi', @@ -1114,7 +1114,7 @@ const vi = Object.freeze({ 'orchestration.fields.target': 'Mục tiêu', 'orchestration.fields.target.placeholder': 'vd: Xử lý comment review PR và thêm test regression; tránh các module không liên quan', 'orchestration.fields.target.hint': 'Một câu là đủ: kết quả, ràng buộc và tiêu chí chấp nhận.', - 'orchestration.engine.codex': 'Codex', + 'orchestration.engine.openaiChat': 'OpenAI Chat', 'orchestration.engine.workflow': 'Workflow', 'orchestration.runMode.write': 'Ghi', 'orchestration.runMode.readOnly': 'Chỉ đọc', diff --git a/web-ui/modules/i18n/locales/zh-tw.mjs b/web-ui/modules/i18n/locales/zh-tw.mjs index 8afcd7b5..90f82b88 100644 --- a/web-ui/modules/i18n/locales/zh-tw.mjs +++ b/web-ui/modules/i18n/locales/zh-tw.mjs @@ -969,7 +969,7 @@ const zhTw = Object.freeze({ 'orchestration.readiness.target.done': '已寫目標', 'orchestration.readiness.target.missing': '還沒寫目標', 'orchestration.readiness.engine.label': '執行策略', - 'orchestration.readiness.engine.codex': '使用 Codex 規劃節點', + 'orchestration.readiness.engine.openaiChat': '使用 OpenAI Chat-compatible 節點', 'orchestration.readiness.workflow.done': '已選 {count} 個 Workflow', 'orchestration.readiness.workflow.missing': '還沒選 Workflow ID', 'orchestration.readiness.scope.label': '邊界', @@ -1012,7 +1012,7 @@ const zhTw = Object.freeze({ 'orchestration.fields.target': '目標', 'orchestration.fields.target.placeholder': '例如:修復目前 PR review 評論,並補對應迴歸測試;不要改無關模塊', 'orchestration.fields.target.hint': '一句話寫清結果、邊界和驗收標準就夠了。', - 'orchestration.engine.codex': 'Codex', + 'orchestration.engine.openaiChat': 'OpenAI Chat', 'orchestration.engine.workflow': 'Workflow', 'orchestration.runMode.write': '寫入', 'orchestration.runMode.readOnly': '只讀', diff --git a/web-ui/modules/i18n/locales/zh.mjs b/web-ui/modules/i18n/locales/zh.mjs index 86578ebd..d60648cd 100644 --- a/web-ui/modules/i18n/locales/zh.mjs +++ b/web-ui/modules/i18n/locales/zh.mjs @@ -969,7 +969,7 @@ const zh = Object.freeze({ 'orchestration.readiness.target.done': '已写目标', 'orchestration.readiness.target.missing': '还没写目标', 'orchestration.readiness.engine.label': '执行策略', - 'orchestration.readiness.engine.codex': '使用 Codex 规划节点', + 'orchestration.readiness.engine.openaiChat': '使用 OpenAI Chat-compatible 节点', 'orchestration.readiness.workflow.done': '已选 {count} 个 Workflow', 'orchestration.readiness.workflow.missing': '还没选 Workflow ID', 'orchestration.readiness.scope.label': '边界', @@ -1012,7 +1012,7 @@ const zh = Object.freeze({ 'orchestration.fields.target': '目标', 'orchestration.fields.target.placeholder': '例如:修复当前 PR review 评论,并补对应回归测试;不要改无关模块', 'orchestration.fields.target.hint': '一句话写清结果、边界和验收标准就够了。', - 'orchestration.engine.codex': 'Codex', + 'orchestration.engine.openaiChat': 'OpenAI Chat', 'orchestration.engine.workflow': 'Workflow', 'orchestration.runMode.write': '写入', 'orchestration.runMode.readOnly': '只读', diff --git a/web-ui/partials/index/panel-orchestration.html b/web-ui/partials/index/panel-orchestration.html index d462faf4..4adcc416 100644 --- a/web-ui/partials/index/panel-orchestration.html +++ b/web-ui/partials/index/panel-orchestration.html @@ -46,8 +46,8 @@ {{ t('orchestration.templates.title') }}
- - + +
@@ -67,7 +67,7 @@
{{ taskOrchestrationDraftReadiness.summary }}
-
{{ taskOrchestration.selectedEngine === 'workflow' ? t('orchestration.engine.workflow') : t('orchestration.engine.codex') }}
+
{{ taskOrchestration.selectedEngine === 'workflow' ? t('orchestration.engine.workflow') : t('orchestration.engine.openaiChat') }}
{{ taskOrchestration.runMode === 'dry-run' ? t('orchestration.runMode.dryRun') : (taskOrchestration.runMode === 'read' ? t('orchestration.runMode.readOnly') : t('orchestration.runMode.write')) }}
{{ t('orchestration.pills.hasTitle') }}
{{ t('orchestration.pills.workflowCount', { count: taskOrchestrationDraftMetrics.workflowCount }) }}
@@ -89,7 +89,7 @@ @@ -176,7 +176,7 @@
@@ -251,7 +251,7 @@
@@ -262,30 +262,30 @@ -
- +
-
-
+
+
{{ t('orchestration.queue.empty.title') }}
{{ t('orchestration.queue.empty.subtitle') }}
{{ item.status }} - +
-
+
{{ t('orchestration.runs.empty.title') }}
{{ t('orchestration.runs.empty.subtitle') }}
@@ -339,7 +339,7 @@ -
diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index 7960a8f5..f17a2dee 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -2939,7 +2939,7 @@ return function render(_ctx, _cache) { }, [ (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.visibleSessionsList, (session, __, ___, _cached) => { const _memo = ([_ctx.activeSessionExportKey === _ctx.getSessionExportKey(session), session.messageCount, session.updatedAt, session.title, session.sourceLabel, session.cwd, _ctx.isSessionPinned(session), _ctx.sessionsLoading, session.match && session.match.count]) - if (_cached && _cached.key === session.source + '-' + session.sessionId + '-' + session.filePath && _isMemoSame(_cached, _memo)) return _cached + if (_cached && _cached.el && _cached.key === session.source + '-' + session.sessionId + '-' + session.filePath && _isMemoSame(_cached, _memo)) return _cached const _item = (_openBlock(), _createElementBlock("div", { key: session.source + '-' + session.sessionId + '-' + session.filePath, class: _normalizeClass([ @@ -3435,7 +3435,7 @@ return function render(_ctx, _cache) { : _createCommentVNode("v-if", true), (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.activeSessionVisibleMessages, (msg, idx, ___, _cached) => { const _memo = ([msg.text, msg.timestamp, msg.roleLabel, msg.normalizedRole]) - if (_cached && _cached.key === _ctx.getRecordRenderKey(msg, idx) && _isMemoSame(_cached, _memo)) return _cached + if (_cached && _cached.el && _cached.key === _ctx.getRecordRenderKey(msg, idx) && _isMemoSame(_cached, _memo)) return _cached const _item = (_openBlock(), _createElementBlock("div", { key: _ctx.getRecordRenderKey(msg, idx), "data-message-key": _ctx.getRecordRenderKey(msg, idx), @@ -3486,7 +3486,7 @@ return function render(_ctx, _cache) { _createElementVNode("div", { class: "session-timeline-track" }), (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.sessionTimelineNodes, (node, __, ___, _cached) => { const _memo = ([_ctx.sessionTimelineActiveKey === node.key, node.safePercent, node.title]) - if (_cached && _cached.key === 'timeline-' + node.key && _isMemoSame(_cached, _memo)) return _cached + if (_cached && _cached.el && _cached.key === 'timeline-' + node.key && _isMemoSame(_cached, _memo)) return _cached const _item = (_openBlock(), _createElementBlock("button", { key: 'timeline-' + node.key, type: "button", @@ -3518,7 +3518,7 @@ return function render(_ctx, _cache) { ]), (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.sessionTimelineNodes, (node, __, ___, _cached) => { const _memo = ([_ctx.sessionTimelineActiveKey === node.key, node.safePercent, node.title]) - if (_cached && _cached.key === 'timeline-bar-' + node.key && _isMemoSame(_cached, _memo)) return _cached + if (_cached && _cached.el && _cached.key === 'timeline-bar-' + node.key && _isMemoSame(_cached, _memo)) return _cached const _item = (_openBlock(), _createElementBlock("button", { key: 'timeline-bar-' + node.key, type: "button", @@ -4070,12 +4070,12 @@ return function render(_ctx, _cache) { _createElementVNode("button", { type: "button", class: "task-template-chip", - onClick: $event => {_ctx.taskOrchestration.target = _ctx.t('orchestration.templates.reviewFix.target'); _ctx.taskOrchestration.selectedEngine = 'codex'; _ctx.taskOrchestration.workflowIdsText = ''; _ctx.taskOrchestration.notes = _ctx.t('orchestration.templates.reviewFix.notes'); _ctx.taskOrchestration.followUpsText = _ctx.t('orchestration.templates.reviewFix.followUps')} + onClick: $event => {_ctx.taskOrchestration.target = _ctx.t('orchestration.templates.reviewFix.target'); _ctx.taskOrchestration.selectedEngine = 'openai-chat'; _ctx.taskOrchestration.workflowIdsText = ''; _ctx.taskOrchestration.notes = _ctx.t('orchestration.templates.reviewFix.notes'); _ctx.taskOrchestration.followUpsText = _ctx.t('orchestration.templates.reviewFix.followUps')} }, _toDisplayString(_ctx.t('orchestration.templates.reviewFix.label')), 9 /* TEXT, PROPS */, ["onClick"]), _createElementVNode("button", { type: "button", class: "task-template-chip", - onClick: $event => {_ctx.taskOrchestration.target = _ctx.t('orchestration.templates.planOnly.target'); _ctx.taskOrchestration.selectedEngine = 'codex'; _ctx.taskOrchestration.workflowIdsText = ''; _ctx.taskOrchestration.notes = _ctx.t('orchestration.templates.planOnly.notes'); _ctx.taskOrchestration.followUpsText = ''} + onClick: $event => {_ctx.taskOrchestration.target = _ctx.t('orchestration.templates.planOnly.target'); _ctx.taskOrchestration.selectedEngine = 'openai-chat'; _ctx.taskOrchestration.workflowIdsText = ''; _ctx.taskOrchestration.notes = _ctx.t('orchestration.templates.planOnly.notes'); _ctx.taskOrchestration.followUpsText = ''} }, _toDisplayString(_ctx.t('orchestration.templates.planOnly.label')), 9 /* TEXT, PROPS */, ["onClick"]), _createElementVNode("button", { type: "button", @@ -4108,7 +4108,7 @@ return function render(_ctx, _cache) { _createElementVNode("div", { class: "task-readiness-copy task-draft-inline-copy" }, _toDisplayString(_ctx.taskOrchestrationDraftReadiness.summary), 1 /* TEXT */) ]), _createElementVNode("div", { class: "task-config-strip" }, [ - _createElementVNode("div", { class: "task-config-pill" }, _toDisplayString(_ctx.taskOrchestration.selectedEngine === 'workflow' ? _ctx.t('orchestration.engine.workflow') : _ctx.t('orchestration.engine.codex')), 1 /* TEXT */), + _createElementVNode("div", { class: "task-config-pill" }, _toDisplayString(_ctx.taskOrchestration.selectedEngine === 'workflow' ? _ctx.t('orchestration.engine.workflow') : _ctx.t('orchestration.engine.openaiChat')), 1 /* TEXT */), _createElementVNode("div", { class: "task-config-pill" }, _toDisplayString(_ctx.taskOrchestration.runMode === 'dry-run' ? _ctx.t('orchestration.runMode.dryRun') : (_ctx.taskOrchestration.runMode === 'read' ? _ctx.t('orchestration.runMode.readOnly') : _ctx.t('orchestration.runMode.write'))), 1 /* TEXT */), (_ctx.taskOrchestration.title.trim()) ? (_openBlock(), _createElementBlock("div", { @@ -4147,7 +4147,7 @@ return function render(_ctx, _cache) { class: "provider-fast-switch-select", disabled: "" }, [ - _createElementVNode("option", { value: "codex" }, _toDisplayString(_ctx.t('orchestration.engine.codex')), 1 /* TEXT */), + _createElementVNode("option", { value: "openai-chat" }, _toDisplayString(_ctx.t('orchestration.engine.openaiChat')), 1 /* TEXT */), _createElementVNode("option", { value: "workflow" }, _toDisplayString(_ctx.t('orchestration.engine.workflow')), 1 /* TEXT */) ], 8 /* PROPS */, ["onUpdate:modelValue"]), [ [_vModelSelect, _ctx.taskOrchestration.selectedEngine] @@ -4305,7 +4305,7 @@ return function render(_ctx, _cache) { ]) ]) ]), - (!(_ctx.taskOrchestration.plan || _ctx.taskOrchestration.planIssues.length || _ctx.taskOrchestration.planWarnings.length || _ctx.taskOrchestration.lastError || _ctx.taskOrchestration.queue.length || _ctx.taskOrchestration.runs.length || _ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.selectedRunError)) + (!(_ctx.taskOrchestration.plan || _ctx.taskOrchestration.planIssues.length || _ctx.taskOrchestration.planWarnings.length || _ctx.taskOrchestration.lastError || _ctx.taskOrchestrationActiveQueue.length || _ctx.taskOrchestration.runs.length || _ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.selectedRunError)) ? (_openBlock(), _createElementBlock("section", { key: 0, class: "selector-section task-stage-card" @@ -4424,7 +4424,7 @@ return function render(_ctx, _cache) { : _createCommentVNode("v-if", true) ])) : _createCommentVNode("v-if", true), - (_ctx.taskOrchestration.queue.length || _ctx.taskOrchestration.runs.length || _ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.selectedRunError) + (_ctx.taskOrchestrationActiveQueue.length || _ctx.taskOrchestration.runs.length || _ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.selectedRunError) ? (_openBlock(), _createElementBlock("section", { key: 1, class: "selector-section task-workbench-card" @@ -4441,7 +4441,7 @@ return function render(_ctx, _cache) { onClick: $event => (_ctx.loadTaskOrchestrationOverview({ forceRefresh: true, includeDetail: true })), disabled: _ctx.taskOrchestration.loading }, _toDisplayString(_ctx.taskOrchestration.loading ? _ctx.t('common.refreshing') : _ctx.t('common.refresh')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), - (_ctx.taskOrchestration.queue.length) + (_ctx.taskOrchestrationActiveQueue.length) ? (_openBlock(), _createElementBlock("button", { key: 0, type: "button", @@ -4452,20 +4452,20 @@ return function render(_ctx, _cache) { : _createCommentVNode("v-if", true) ]) ]), - ((_ctx.taskOrchestration.queue.length ? 1 : 0) + (_ctx.taskOrchestration.runs.length ? 1 : 0) + ((_ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.selectedRunError) ? 1 : 0) > 1) + ((_ctx.taskOrchestrationActiveQueue.length ? 1 : 0) + (_ctx.taskOrchestration.runs.length ? 1 : 0) + ((_ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.selectedRunError) ? 1 : 0) > 1) ? (_openBlock(), _createElementBlock("div", { key: 0, class: "task-workbench-tabs", role: "group", "aria-label": _ctx.t('orchestration.workbench.tabs.aria') }, [ - (_ctx.taskOrchestration.queue.length) + (_ctx.taskOrchestrationActiveQueue.length) ? (_openBlock(), _createElementBlock("button", { key: 0, type: "button", class: _normalizeClass(["task-workbench-tab", { active: _ctx.taskOrchestration.workspaceTab === 'queue' }]), onClick: $event => (_ctx.taskOrchestration.workspaceTab = 'queue') - }, _toDisplayString(_ctx.t('orchestration.workbench.tabs.queue', { count: _ctx.taskOrchestration.queue.length })), 11 /* TEXT, CLASS, PROPS */, ["onClick"])) + }, _toDisplayString(_ctx.t('orchestration.workbench.tabs.queue', { count: _ctx.taskOrchestrationActiveQueue.length })), 11 /* TEXT, CLASS, PROPS */, ["onClick"])) : _createCommentVNode("v-if", true), (_ctx.taskOrchestration.runs.length) ? (_openBlock(), _createElementBlock("button", { @@ -4485,12 +4485,12 @@ return function render(_ctx, _cache) { : _createCommentVNode("v-if", true) ], 8 /* PROPS */, ["aria-label"])) : _createCommentVNode("v-if", true), - (_ctx.taskOrchestration.workspaceTab === 'queue' || (!_ctx.taskOrchestration.runs.length && !_ctx.taskOrchestration.selectedRunId && !_ctx.taskOrchestration.selectedRunError)) + ((_ctx.taskOrchestrationActiveQueue.length && _ctx.taskOrchestration.workspaceTab === 'queue') || (!_ctx.taskOrchestration.runs.length && !_ctx.taskOrchestration.selectedRunId && !_ctx.taskOrchestration.selectedRunError)) ? (_openBlock(), _createElementBlock("div", { key: 1, class: "task-workbench-panel" }, [ - (!_ctx.taskOrchestration.queue.length) + (!_ctx.taskOrchestrationActiveQueue.length) ? (_openBlock(), _createElementBlock("div", { key: 0, class: "task-empty-state" @@ -4502,7 +4502,7 @@ return function render(_ctx, _cache) { key: 1, class: "task-runtime-list" }, [ - (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestration.queue, (item) => { + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestrationActiveQueue, (item) => { return (_openBlock(), _createElementBlock("div", { key: item.taskId, class: _normalizeClass(['task-runtime-item', { active: item.lastRunId && _ctx.taskOrchestration.selectedRunId === item.lastRunId, clickable: !!item.lastRunId }]), @@ -4529,18 +4529,20 @@ return function render(_ctx, _cache) { _createElementVNode("span", { class: _normalizeClass(['pill', _ctx.taskRunStatusTone(item.status)]) }, _toDisplayString(item.status), 3 /* TEXT, CLASS */), - _createElementVNode("button", { - type: "button", - class: "btn-mini", - onClick: _withModifiers($event => (_ctx.cancelTaskRunFromUi(item.taskId)), ["stop"]), - disabled: item.status !== 'queued' && item.status !== 'running' - }, _toDisplayString(_ctx.t('common.cancel')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]) + (_ctx.isTaskRunActive(item.status)) + ? (_openBlock(), _createElementBlock("button", { + key: 0, + type: "button", + class: "btn-mini", + onClick: _withModifiers($event => (_ctx.cancelTaskRunFromUi(item.taskId)), ["stop"]) + }, _toDisplayString(_ctx.t('common.cancel')), 9 /* TEXT, PROPS */, ["onClick"])) + : _createCommentVNode("v-if", true) ]) ], 42 /* CLASS, PROPS, NEED_HYDRATION */, ["role", "tabindex", "aria-disabled", "onClick", "onKeydown"])) }), 128 /* KEYED_FRAGMENT */)) ])) ])) - : (_ctx.taskOrchestration.workspaceTab === 'runs' || (!_ctx.taskOrchestration.queue.length && _ctx.taskOrchestration.runs.length && !_ctx.taskOrchestration.selectedRunId && !_ctx.taskOrchestration.selectedRunError)) + : (_ctx.taskOrchestration.workspaceTab === 'runs' || (!_ctx.taskOrchestrationActiveQueue.length && _ctx.taskOrchestration.runs.length && !_ctx.taskOrchestration.selectedRunId && !_ctx.taskOrchestration.selectedRunError)) ? (_openBlock(), _createElementBlock("div", { key: 2, class: "task-workbench-panel" @@ -4600,12 +4602,14 @@ return function render(_ctx, _cache) { onClick: $event => (_ctx.retryTaskRunFromUi(_ctx.taskOrchestration.selectedRunId)), disabled: !_ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.retrying }, _toDisplayString(_ctx.taskOrchestration.retrying ? _ctx.t('orchestration.detail.retrying') : _ctx.t('orchestration.detail.retry')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), - _createElementVNode("button", { - type: "button", - class: "btn-tool btn-tool-compact", - onClick: $event => (_ctx.cancelTaskRunFromUi(_ctx.taskOrchestration.selectedRunId)), - disabled: !_ctx.taskOrchestrationSelectedRun || !_ctx.taskOrchestrationSelectedRun.run || !_ctx.isTaskRunActive(_ctx.taskOrchestrationSelectedRun.run.status) - }, _toDisplayString(_ctx.t('common.cancel')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]) + (_ctx.taskOrchestrationSelectedRun && _ctx.taskOrchestrationSelectedRun.run && _ctx.isTaskRunActive(_ctx.taskOrchestrationSelectedRun.run.status)) + ? (_openBlock(), _createElementBlock("button", { + key: 0, + type: "button", + class: "btn-tool btn-tool-compact", + onClick: $event => (_ctx.cancelTaskRunFromUi(_ctx.taskOrchestration.selectedRunId)) + }, _toDisplayString(_ctx.t('common.cancel')), 9 /* TEXT, PROPS */, ["onClick"])) + : _createCommentVNode("v-if", true) ]), (_ctx.taskOrchestration.selectedRunError) ? (_openBlock(), _createElementBlock("div", { From 906100cd8077063fb0c622f85e126babbba8028c Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sat, 27 Jun 2026 15:58:59 +0000 Subject: [PATCH 02/54] fix(task-orchestration): align precompiled web render with ci --- web-ui/res/web-ui-render.precompiled.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index f17a2dee..8001f4f2 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -2939,7 +2939,7 @@ return function render(_ctx, _cache) { }, [ (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.visibleSessionsList, (session, __, ___, _cached) => { const _memo = ([_ctx.activeSessionExportKey === _ctx.getSessionExportKey(session), session.messageCount, session.updatedAt, session.title, session.sourceLabel, session.cwd, _ctx.isSessionPinned(session), _ctx.sessionsLoading, session.match && session.match.count]) - if (_cached && _cached.el && _cached.key === session.source + '-' + session.sessionId + '-' + session.filePath && _isMemoSame(_cached, _memo)) return _cached + if (_cached && _cached.key === session.source + '-' + session.sessionId + '-' + session.filePath && _isMemoSame(_cached, _memo)) return _cached const _item = (_openBlock(), _createElementBlock("div", { key: session.source + '-' + session.sessionId + '-' + session.filePath, class: _normalizeClass([ @@ -3435,7 +3435,7 @@ return function render(_ctx, _cache) { : _createCommentVNode("v-if", true), (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.activeSessionVisibleMessages, (msg, idx, ___, _cached) => { const _memo = ([msg.text, msg.timestamp, msg.roleLabel, msg.normalizedRole]) - if (_cached && _cached.el && _cached.key === _ctx.getRecordRenderKey(msg, idx) && _isMemoSame(_cached, _memo)) return _cached + if (_cached && _cached.key === _ctx.getRecordRenderKey(msg, idx) && _isMemoSame(_cached, _memo)) return _cached const _item = (_openBlock(), _createElementBlock("div", { key: _ctx.getRecordRenderKey(msg, idx), "data-message-key": _ctx.getRecordRenderKey(msg, idx), @@ -3486,7 +3486,7 @@ return function render(_ctx, _cache) { _createElementVNode("div", { class: "session-timeline-track" }), (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.sessionTimelineNodes, (node, __, ___, _cached) => { const _memo = ([_ctx.sessionTimelineActiveKey === node.key, node.safePercent, node.title]) - if (_cached && _cached.el && _cached.key === 'timeline-' + node.key && _isMemoSame(_cached, _memo)) return _cached + if (_cached && _cached.key === 'timeline-' + node.key && _isMemoSame(_cached, _memo)) return _cached const _item = (_openBlock(), _createElementBlock("button", { key: 'timeline-' + node.key, type: "button", @@ -3518,7 +3518,7 @@ return function render(_ctx, _cache) { ]), (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.sessionTimelineNodes, (node, __, ___, _cached) => { const _memo = ([_ctx.sessionTimelineActiveKey === node.key, node.safePercent, node.title]) - if (_cached && _cached.el && _cached.key === 'timeline-bar-' + node.key && _isMemoSame(_cached, _memo)) return _cached + if (_cached && _cached.key === 'timeline-bar-' + node.key && _isMemoSame(_cached, _memo)) return _cached const _item = (_openBlock(), _createElementBlock("button", { key: 'timeline-bar-' + node.key, type: "button", From ffeeb587bedb5aa207cc8077038d2b1aa9b7cf86 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sat, 27 Jun 2026 16:05:56 +0000 Subject: [PATCH 03/54] fix(task-orchestration): align vue compiler locks --- package-lock.json | 96 +++++++++++++++++++------ package.json | 2 +- pnpm-lock.yaml | 2 +- web-ui/res/web-ui-render.precompiled.js | 8 +-- 4 files changed, 81 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7edabef1..1a2ff6d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "codexmate": "cli.js" }, "devDependencies": { - "@vue/compiler-dom": "^3.5.30", + "@vue/compiler-dom": "^3.5.34", "opencc-js": "^1.3.1", "vitepress": "^1.6.4" }, @@ -285,9 +285,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -295,9 +295,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -305,13 +305,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -321,14 +321,14 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1345,16 +1345,37 @@ } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.30", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", - "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", + "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", "dev": true, "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.30", - "@vue/shared": "3.5.30" + "@vue/compiler-core": "3.5.34", + "@vue/shared": "3.5.34" } }, + "node_modules/@vue/compiler-dom/node_modules/@vue/compiler-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz", + "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.34", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom/node_modules/@vue/shared": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz", + "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", + "dev": true, + "license": "MIT" + }, "node_modules/@vue/compiler-sfc": { "version": "3.5.30", "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", @@ -1373,6 +1394,17 @@ "source-map-js": "^1.2.1" } }, + "node_modules/@vue/compiler-sfc/node_modules/@vue/compiler-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", + "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" + } + }, "node_modules/@vue/compiler-ssr": { "version": "3.5.30", "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", @@ -1384,6 +1416,17 @@ "@vue/shared": "3.5.30" } }, + "node_modules/@vue/compiler-ssr/node_modules/@vue/compiler-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", + "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" + } + }, "node_modules/@vue/devtools-api": { "version": "7.7.9", "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", @@ -2553,6 +2596,17 @@ } } }, + "node_modules/vue/node_modules/@vue/compiler-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", + "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" + } + }, "node_modules/yauzl": { "version": "3.2.1", "resolved": "https://registry.npmmirror.com/yauzl/-/yauzl-3.2.1.tgz", diff --git a/package.json b/package.json index 6957418d..de403aec 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "author": "ymkiux", "license": "Apache-2.0", "devDependencies": { - "@vue/compiler-dom": "^3.5.30", + "@vue/compiler-dom": "^3.5.34", "opencc-js": "^1.3.1", "vitepress": "^1.6.4" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76d05897..ef6e3513 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: version: 1.3.2 devDependencies: '@vue/compiler-dom': - specifier: ^3.5.30 + specifier: ^3.5.34 version: 3.5.34 opencc-js: specifier: ^1.3.1 diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index 8001f4f2..f17a2dee 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -2939,7 +2939,7 @@ return function render(_ctx, _cache) { }, [ (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.visibleSessionsList, (session, __, ___, _cached) => { const _memo = ([_ctx.activeSessionExportKey === _ctx.getSessionExportKey(session), session.messageCount, session.updatedAt, session.title, session.sourceLabel, session.cwd, _ctx.isSessionPinned(session), _ctx.sessionsLoading, session.match && session.match.count]) - if (_cached && _cached.key === session.source + '-' + session.sessionId + '-' + session.filePath && _isMemoSame(_cached, _memo)) return _cached + if (_cached && _cached.el && _cached.key === session.source + '-' + session.sessionId + '-' + session.filePath && _isMemoSame(_cached, _memo)) return _cached const _item = (_openBlock(), _createElementBlock("div", { key: session.source + '-' + session.sessionId + '-' + session.filePath, class: _normalizeClass([ @@ -3435,7 +3435,7 @@ return function render(_ctx, _cache) { : _createCommentVNode("v-if", true), (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.activeSessionVisibleMessages, (msg, idx, ___, _cached) => { const _memo = ([msg.text, msg.timestamp, msg.roleLabel, msg.normalizedRole]) - if (_cached && _cached.key === _ctx.getRecordRenderKey(msg, idx) && _isMemoSame(_cached, _memo)) return _cached + if (_cached && _cached.el && _cached.key === _ctx.getRecordRenderKey(msg, idx) && _isMemoSame(_cached, _memo)) return _cached const _item = (_openBlock(), _createElementBlock("div", { key: _ctx.getRecordRenderKey(msg, idx), "data-message-key": _ctx.getRecordRenderKey(msg, idx), @@ -3486,7 +3486,7 @@ return function render(_ctx, _cache) { _createElementVNode("div", { class: "session-timeline-track" }), (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.sessionTimelineNodes, (node, __, ___, _cached) => { const _memo = ([_ctx.sessionTimelineActiveKey === node.key, node.safePercent, node.title]) - if (_cached && _cached.key === 'timeline-' + node.key && _isMemoSame(_cached, _memo)) return _cached + if (_cached && _cached.el && _cached.key === 'timeline-' + node.key && _isMemoSame(_cached, _memo)) return _cached const _item = (_openBlock(), _createElementBlock("button", { key: 'timeline-' + node.key, type: "button", @@ -3518,7 +3518,7 @@ return function render(_ctx, _cache) { ]), (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.sessionTimelineNodes, (node, __, ___, _cached) => { const _memo = ([_ctx.sessionTimelineActiveKey === node.key, node.safePercent, node.title]) - if (_cached && _cached.key === 'timeline-bar-' + node.key && _isMemoSame(_cached, _memo)) return _cached + if (_cached && _cached.el && _cached.key === 'timeline-bar-' + node.key && _isMemoSame(_cached, _memo)) return _cached const _item = (_openBlock(), _createElementBlock("button", { key: 'timeline-bar-' + node.key, type: "button", From e45b9f6a83ebffd470d21928ad90705fac32aef4 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sat, 27 Jun 2026 16:13:30 +0000 Subject: [PATCH 04/54] fix(task-orchestration): close review edge cases --- cli.js | 13 +++++++- lib/task-orchestrator.js | 5 +-- tests/unit/task-codex-exec-windows.test.mjs | 37 +++++++++++++++++++++ tests/unit/task-orchestrator.test.mjs | 17 ++++++++++ tests/unit/web-ui-logic.test.mjs | 4 ++- web-ui/modules/app.computed.main-tabs.mjs | 2 +- 6 files changed, 73 insertions(+), 5 deletions(-) diff --git a/cli.js b/cli.js index 01ba4411..26129c9c 100644 --- a/cli.js +++ b/cli.js @@ -16174,13 +16174,24 @@ function postOpenAiChatCompletion(requestConfig, body, options = {}) { let raw = ''; let receivedBytes = 0; res.on('data', (chunk) => { + if (settled) return; receivedBytes += chunk.length || 0; if (receivedBytes > TASK_OPENAI_CHAT_MAX_RESPONSE_BYTES) { - res.destroy(new Error('response too large')); + finish({ ok: false, status, error: 'OpenAI Chat response too large', payload: null, body: raw }); + res.destroy(); return; } raw += chunk; }); + res.on('error', (error) => { + finish({ + ok: false, + status, + error: error && error.message ? error.message : 'OpenAI Chat request failed', + payload: null, + body: raw + }); + }); res.on('end', () => { if (settled) return; let parsedPayload = null; diff --git a/lib/task-orchestrator.js b/lib/task-orchestrator.js index 6e3ce4f2..f15cf97a 100644 --- a/lib/task-orchestrator.js +++ b/lib/task-orchestrator.js @@ -283,7 +283,7 @@ function buildTaskPlan(request = {}, options = {}) { const workflowMap = normalizeWorkflowCatalog(options.workflowCatalog || []); const target = normalizeText(request.target, 4000); const title = buildTaskTitle(target, request.title); - const engine = normalizeTaskEngine(request.engine); + const requestedEngine = normalizeTaskEngine(request.engine); const allowWrite = request.allowWrite === true; const dryRun = request.dryRun === true; const concurrency = normalizePositiveInteger(request.concurrency, 2, 1, 8); @@ -297,7 +297,8 @@ function buildTaskPlan(request = {}, options = {}) { const nodes = []; let nodeSequence = 0; const nextNodeId = (prefix) => `${prefix}-${String(++nodeSequence).padStart(2, '0')}`; - const shouldBuildWorkflowNodes = requestedWorkflowIds.length > 0 || engine === 'workflow'; + const shouldBuildWorkflowNodes = requestedWorkflowIds.length > 0 || requestedEngine === 'workflow'; + const engine = shouldBuildWorkflowNodes ? 'workflow' : requestedEngine; if (shouldBuildWorkflowNodes) { let previousId = ''; diff --git a/tests/unit/task-codex-exec-windows.test.mjs b/tests/unit/task-codex-exec-windows.test.mjs index af171e73..727a623e 100644 --- a/tests/unit/task-codex-exec-windows.test.mjs +++ b/tests/unit/task-codex-exec-windows.test.mjs @@ -1,4 +1,6 @@ import assert from 'assert'; +import http from 'node:http'; +import https from 'node:https'; import { readProjectFile } from './helpers/web-ui-source.mjs'; const cliSource = readProjectFile('cli.js'); @@ -47,6 +49,41 @@ test('resolveSpawnCommand keeps bare command names on windows', () => { assert.strictEqual(resolveSpawnCommand('codex'), 'codex'); }); +test('postOpenAiChatCompletion fails fast on oversized responses', async () => { + const source = extractBlockBySignature(cliSource, 'function postOpenAiChatCompletion(requestConfig, body, options = {}) {'); + const postOpenAiChatCompletion = instantiateFunction(source, 'postOpenAiChatCompletion', { + URL, + http, + https, + HTTP_KEEP_ALIVE_AGENT: false, + HTTPS_KEEP_ALIVE_AGENT: false, + TASK_OPENAI_CHAT_MAX_RESPONSE_BYTES: 8, + TASK_OPENAI_CHAT_TIMEOUT_MS: 1000, + Buffer, + truncateTaskText(text, limit) { + return String(text || '').slice(0, limit || 1000); + } + }); + const server = http.createServer((req, res) => { + req.resume(); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ choices: [{ message: { content: 'x'.repeat(64) } }] })); + }); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + try { + const { port } = server.address(); + const result = await postOpenAiChatCompletion({ + endpointUrl: `http://127.0.0.1:${port}/v1/chat/completions`, + extraHeaders: {} + }, { model: 'mock', messages: [] }); + assert.strictEqual(result.ok, false); + assert.strictEqual(result.status, 200); + assert.strictEqual(result.error, 'OpenAI Chat response too large'); + } finally { + await new Promise((resolve) => server.close(resolve)); + } +}); + test('runOpenAiChatTaskNode uses configured OpenAI Chat provider without spawning codex', async () => { const source = extractBlockBySignature(cliSource, 'async function runOpenAiChatTaskNode(node, context = {}) {'); const requests = []; diff --git a/tests/unit/task-orchestrator.test.mjs b/tests/unit/task-orchestrator.test.mjs index 97dad7c3..4a730d9a 100644 --- a/tests/unit/task-orchestrator.test.mjs +++ b/tests/unit/task-orchestrator.test.mjs @@ -58,12 +58,29 @@ test('buildTaskPlan can map workflow ids onto sequential workflow nodes', () => { id: 'safe-provider-switch', name: '安全切换', readOnly: false } ] }); + assert.strictEqual(plan.engine, 'workflow'); assert.strictEqual(plan.nodes.length, 2); assert.strictEqual(plan.nodes[0].kind, 'workflow'); assert.deepStrictEqual(plan.nodes[1].dependsOn, [plan.nodes[0].id]); assert.strictEqual(plan.nodes[1].write, true); }); +test('buildTaskPlan keeps workflowIds requests in workflow mode without explicit engine', () => { + const plan = buildTaskPlan({ + target: '诊断并整理 provider 配置', + workflowIds: ['diagnose-config'], + followUps: ['总结风险'] + }, { + workflowCatalog: [ + { id: 'diagnose-config', name: '诊断配置', readOnly: true } + ] + }); + assert.strictEqual(plan.engine, 'workflow'); + assert.strictEqual(plan.nodes.length, 1); + assert.strictEqual(plan.nodes[0].kind, 'workflow'); + assert.deepStrictEqual(plan.nodes[0].input.followUps, ['总结风险']); +}); + test('buildTaskPlan keeps workflow follow-ups inside the final workflow node payload', () => { const plan = buildTaskPlan({ target: '诊断并整理结果', diff --git a/tests/unit/web-ui-logic.test.mjs b/tests/unit/web-ui-logic.test.mjs index 5a599573..d91025ae 100644 --- a/tests/unit/web-ui-logic.test.mjs +++ b/tests/unit/web-ui-logic.test.mjs @@ -1274,6 +1274,7 @@ test('taskOrchestrationActiveQueue hides completed and failed queue history from const computed = createMainTabsComputed(); const queued = { taskId: 'task-queued', status: 'queued' }; const running = { taskId: 'task-running', status: 'RUNNING' }; + const runStatusOnly = { taskId: 'task-run-status-only', runStatus: 'running' }; const context = { taskOrchestration: { queue: [ @@ -1281,6 +1282,7 @@ test('taskOrchestrationActiveQueue hides completed and failed queue history from queued, { taskId: 'task-failed', status: 'failed' }, running, + runStatusOnly, { taskId: 'task-cancelled', status: 'cancelled' } ] } @@ -1288,7 +1290,7 @@ test('taskOrchestrationActiveQueue hides completed and failed queue history from const activeQueue = computed.taskOrchestrationActiveQueue.call(context); - assert.deepStrictEqual(activeQueue, [queued, running]); + assert.deepStrictEqual(activeQueue, [queued, running, runStatusOnly]); }); test('startTaskQueueRunner surfaces already-running queue state distinctly', async () => { diff --git a/web-ui/modules/app.computed.main-tabs.mjs b/web-ui/modules/app.computed.main-tabs.mjs index d0235c34..eaac21fe 100644 --- a/web-ui/modules/app.computed.main-tabs.mjs +++ b/web-ui/modules/app.computed.main-tabs.mjs @@ -196,7 +196,7 @@ export function createMainTabsComputed() { ? this.taskOrchestration.queue : []; return queue.filter((item) => { - const status = String(item && item.status || '').trim().toLowerCase(); + const status = String((item && (item.status || item.runStatus)) || '').trim().toLowerCase(); return status === 'queued' || status === 'running'; }); }, From 2aadac7ef98cd9d07a13ac687ea45ff3178b6391 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sun, 28 Jun 2026 01:35:57 +0000 Subject: [PATCH 05/54] feat(task-orchestration): simplify console layout --- tests/unit/config-tabs-ui.test.mjs | 7 +- web-ui/modules/i18n/locales/en.mjs | 10 + web-ui/modules/i18n/locales/ja.mjs | 10 + web-ui/modules/i18n/locales/vi.mjs | 10 + web-ui/modules/i18n/locales/zh-tw.mjs | 10 + web-ui/modules/i18n/locales/zh.mjs | 10 + .../partials/index/panel-orchestration.html | 159 ++++++----- web-ui/res/web-ui-render.precompiled.js | 266 +++++++++--------- web-ui/styles/task-orchestration.css | 181 ++++++++++++ 9 files changed, 451 insertions(+), 212 deletions(-) diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index bbe645d7..42850161 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -93,12 +93,13 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(orchestrationPanel, /@click="startTaskQueueRunner\(\)"/); assert.match(orchestrationPanel, /@click="retryTaskRunFromUi\(taskOrchestration.selectedRunId\)"/); assert.match(orchestrationPanel, /class="selector-section task-hero-card"/); - assert.match(orchestrationPanel, /class="task-layout-grid task-layout-grid-primary"/); - assert.match(orchestrationPanel, /class="task-template-chip-group"/); + assert.match(orchestrationPanel, /class="[^"]*task-layout-grid task-layout-grid-primary[^"]*"/); + assert.match(orchestrationPanel, /class="[^"]*task-quick-card[^"]*"/); + assert.match(orchestrationPanel, /class="task-quick-template-grid"/); assert.match(orchestrationPanel, /class="task-workflow-suggestions"/); assert.match(orchestrationPanel, /@click="appendTaskWorkflowId\(workflow.id \|\| workflow.name\)"/); assert.match(orchestrationPanel, /class="[^"]*task-draft-overview[^"]*"/); - assert.match(orchestrationPanel, /class="task-advanced-panel"/); + assert.match(orchestrationPanel, /class="[^"]*task-advanced-panel[^"]*"/); assert.match(orchestrationPanel, /taskOrchestrationDraftReadiness.summary/); assert.match(orchestrationPanel, /taskOrchestrationDraftReadiness.title/); assert.match(orchestrationPanel, /class="task-config-strip"/); diff --git a/web-ui/modules/i18n/locales/en.mjs b/web-ui/modules/i18n/locales/en.mjs index 53d9eb07..9b6737e6 100644 --- a/web-ui/modules/i18n/locales/en.mjs +++ b/web-ui/modules/i18n/locales/en.mjs @@ -961,6 +961,16 @@ const en = Object.freeze({ 'orchestration.hero.kicker': 'Task orchestration', 'orchestration.hero.title': 'Turn goals into executable steps', 'orchestration.hero.subtitle': 'Write a target, preview a plan, then run.', + 'orchestration.quick.kicker': 'Quick task', + 'orchestration.quick.title': 'Tell Codexmate what to finish', + 'orchestration.quick.subtitle': 'Start with the outcome. Codexmate will preview a plan, run it, and keep advanced options out of the way until needed.', + 'orchestration.quick.caption': 'The primary action previews the plan before running. Use queue mode for batches or long-running work.', + 'orchestration.quick.templates.reviewFix.meta': 'Fix review feedback and validate regressions.', + 'orchestration.quick.templates.planOnly.meta': 'Investigate first without writing files.', + 'orchestration.quick.templates.workflowBatch.meta': 'Reuse local workflows for repeatable checks.', + 'orchestration.quick.checklist.title': 'Ready check', + 'orchestration.quick.checklist.subtitle': 'Only the blockers that affect execution stay visible.', + 'orchestration.quick.status.title': 'Current draft', 'orchestration.draft.reset': 'Reset draft', 'orchestration.summary.aria': 'Task orchestration summary', 'orchestration.summary.running': 'Running', diff --git a/web-ui/modules/i18n/locales/ja.mjs b/web-ui/modules/i18n/locales/ja.mjs index 7fa6e1d5..1a3a0c8c 100644 --- a/web-ui/modules/i18n/locales/ja.mjs +++ b/web-ui/modules/i18n/locales/ja.mjs @@ -950,6 +950,16 @@ const ja = Object.freeze({ 'orchestration.hero.kicker': 'タスクオーケストレーション', 'orchestration.hero.title': '要件を実行可能なステップに分解', 'orchestration.hero.subtitle': '最初に目標を書き、計画をプレビューし、実行します。', + 'orchestration.quick.kicker': 'クイックタスク', + 'orchestration.quick.title': 'Codexmate に何を完了させますか?', + 'orchestration.quick.subtitle': 'まずゴールを書くだけで、計画をプレビューしてから実行します。詳細設定は必要になるまで隠します。', + 'orchestration.quick.caption': 'メイン操作は計画を確認してから実行します。長い作業や一括処理はキューを使います。', + 'orchestration.quick.templates.reviewFix.meta': 'レビュー指摘を直し、回帰確認します。', + 'orchestration.quick.templates.planOnly.meta': '書き込みなしで調査と計画だけ行います。', + 'orchestration.quick.templates.workflowBatch.meta': 'ローカル Workflow で定型チェックします。', + 'orchestration.quick.checklist.title': '実行前チェック', + 'orchestration.quick.checklist.subtitle': '実行に影響するブロッカーだけを表示します。', + 'orchestration.quick.status.title': '現在の下書き', 'orchestration.draft.reset': '下書きをリセット', 'orchestration.summary.aria': 'タスクオーケストレーション概要', 'orchestration.summary.running': '実行中', diff --git a/web-ui/modules/i18n/locales/vi.mjs b/web-ui/modules/i18n/locales/vi.mjs index c3a2dd80..97faeae4 100644 --- a/web-ui/modules/i18n/locales/vi.mjs +++ b/web-ui/modules/i18n/locales/vi.mjs @@ -1092,6 +1092,16 @@ const vi = Object.freeze({ 'orchestration.hero.kicker': 'Điều phối tác vụ', 'orchestration.hero.title': 'Biến mục tiêu thành các bước thực thi', 'orchestration.hero.subtitle': 'Viết mục tiêu, xem trước kế hoạch, rồi chạy.', + 'orchestration.quick.kicker': 'Tác vụ nhanh', + 'orchestration.quick.title': 'Bạn muốn Codexmate hoàn thành việc gì?', + 'orchestration.quick.subtitle': 'Chỉ cần mô tả kết quả. Codexmate sẽ xem trước kế hoạch rồi chạy; thiết lập nâng cao được ẩn cho tới khi cần.', + 'orchestration.quick.caption': 'Nút chính sẽ tạo kế hoạch trước khi chạy. Dùng hàng đợi cho tác vụ dài hoặc theo lô.', + 'orchestration.quick.templates.reviewFix.meta': 'Sửa góp ý review và kiểm thử hồi quy.', + 'orchestration.quick.templates.planOnly.meta': 'Chỉ điều tra và lập kế hoạch, không ghi file.', + 'orchestration.quick.templates.workflowBatch.meta': 'Dùng Workflow cục bộ cho kiểm tra lặp lại.', + 'orchestration.quick.checklist.title': 'Kiểm tra sẵn sàng', + 'orchestration.quick.checklist.subtitle': 'Chỉ hiển thị blocker ảnh hưởng tới việc chạy.', + 'orchestration.quick.status.title': 'Bản nháp hiện tại', 'orchestration.draft.reset': 'Đặt lại bản nháp', 'orchestration.summary.aria': 'Tóm tắt điều phối tác vụ', 'orchestration.summary.running': 'Đang chạy', diff --git a/web-ui/modules/i18n/locales/zh-tw.mjs b/web-ui/modules/i18n/locales/zh-tw.mjs index 90f82b88..bdcc9104 100644 --- a/web-ui/modules/i18n/locales/zh-tw.mjs +++ b/web-ui/modules/i18n/locales/zh-tw.mjs @@ -960,6 +960,16 @@ const zhTw = Object.freeze({ 'orchestration.hero.kicker': '任務編排', 'orchestration.hero.title': '把需求拆成可執行步驟', 'orchestration.hero.subtitle': '先寫目標,再預覽計劃,再執行。', + 'orchestration.quick.kicker': '快速任務', + 'orchestration.quick.title': '你想讓 Codexmate 完成什麼?', + 'orchestration.quick.subtitle': '先寫結果,編排器會預覽計畫、再執行;進階設定預設收起,不把引擎艙塞到使用者面前。', + 'orchestration.quick.caption': '主按鈕會先生成計畫再執行;批次或長任務再使用佇列模式。', + 'orchestration.quick.templates.reviewFix.meta': '處理 review 回饋並補回歸驗證。', + 'orchestration.quick.templates.planOnly.meta': '先排查規劃,不寫入檔案。', + 'orchestration.quick.templates.workflowBatch.meta': '複用本地 Workflow 跑固定檢查。', + 'orchestration.quick.checklist.title': '發布前檢查', + 'orchestration.quick.checklist.subtitle': '只保留真正影響執行的阻塞項。', + 'orchestration.quick.status.title': '目前草稿', 'orchestration.draft.reset': '重置草稿', 'orchestration.summary.aria': '任務編排概覽', 'orchestration.summary.running': '運行中', diff --git a/web-ui/modules/i18n/locales/zh.mjs b/web-ui/modules/i18n/locales/zh.mjs index d60648cd..ba1a552c 100644 --- a/web-ui/modules/i18n/locales/zh.mjs +++ b/web-ui/modules/i18n/locales/zh.mjs @@ -960,6 +960,16 @@ const zh = Object.freeze({ 'orchestration.hero.kicker': '任务编排', 'orchestration.hero.title': '把需求拆成可执行步骤', 'orchestration.hero.subtitle': '先写目标,再预览计划,再执行。', + 'orchestration.quick.kicker': '快速任务', + 'orchestration.quick.title': '你想让 Codexmate 完成什么?', + 'orchestration.quick.subtitle': '先写结果,编排器会预览计划、再执行;高级设置默认收起,不把发动机舱塞到用户脸上。', + 'orchestration.quick.caption': '主按钮会先生成计划再执行;批量或长任务再使用入队模式。', + 'orchestration.quick.templates.reviewFix.meta': '处理 review 反馈并补回归验证。', + 'orchestration.quick.templates.planOnly.meta': '先排查规划,不写入文件。', + 'orchestration.quick.templates.workflowBatch.meta': '复用本地 Workflow 跑固定检查。', + 'orchestration.quick.checklist.title': '发布前检查', + 'orchestration.quick.checklist.subtitle': '只保留真正影响执行的阻塞项。', + 'orchestration.quick.status.title': '当前草稿', 'orchestration.draft.reset': '重置草稿', 'orchestration.summary.aria': '任务编排概览', 'orchestration.summary.running': '运行中', diff --git a/web-ui/partials/index/panel-orchestration.html b/web-ui/partials/index/panel-orchestration.html index 4adcc416..b6852966 100644 --- a/web-ui/partials/index/panel-orchestration.html +++ b/web-ui/partials/index/panel-orchestration.html @@ -31,85 +31,92 @@
-
-
-
-
-
1
-
-
{{ t('orchestration.step1.title') }}
-
{{ t('orchestration.step1.subtitle') }}
-
+
+
+
+
+
{{ t('orchestration.quick.kicker') }}
+
{{ t('orchestration.quick.title') }}
+
{{ t('orchestration.quick.subtitle') }}
-
- {{ t('orchestration.templates.title') }} -
-
- - - -
-
-
- -
-
diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index f17a2dee..5a125fab 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -4049,126 +4049,111 @@ return function render(_ctx, _cache) { ], 8 /* PROPS */, ["aria-label"])) : _createCommentVNode("v-if", true) ]), - _createElementVNode("div", { class: "task-layout-grid task-layout-grid-primary" }, [ - _createElementVNode("section", { class: "selector-section task-compose-flow-card" }, [ - _createElementVNode("div", { class: "task-flow-section task-flow-section-compact" }, [ - _createElementVNode("div", { class: "task-flow-head" }, [ - _createElementVNode("div", { class: "task-flow-step" }, "1"), - _createElementVNode("div", null, [ - _createElementVNode("div", { class: "task-flow-title" }, _toDisplayString(_ctx.t('orchestration.step1.title')), 1 /* TEXT */), - _createElementVNode("div", { class: "task-flow-copy" }, _toDisplayString(_ctx.t('orchestration.step1.subtitle')), 1 /* TEXT */) - ]) + _createElementVNode("div", { class: "task-layout-grid task-layout-grid-primary task-quick-layout" }, [ + _createElementVNode("section", { class: "selector-section task-compose-flow-card task-quick-card" }, [ + _createElementVNode("div", { class: "task-quick-main" }, [ + _createElementVNode("div", { class: "task-quick-copy" }, [ + _createElementVNode("div", { class: "task-hero-kicker" }, _toDisplayString(_ctx.t('orchestration.quick.kicker')), 1 /* TEXT */), + _createElementVNode("div", { class: "selector-title task-quick-title" }, _toDisplayString(_ctx.t('orchestration.quick.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "skills-panel-note task-hero-copy" }, _toDisplayString(_ctx.t('orchestration.quick.subtitle')), 1 /* TEXT */) ]), - (!_ctx.taskOrchestration.target.trim()) - ? (_openBlock(), _createElementBlock("details", { - key: 0, - class: "task-template-panel" - }, [ - _createElementVNode("summary", { class: "task-advanced-summary" }, _toDisplayString(_ctx.t('orchestration.templates.title')), 1 /* TEXT */), - _createElementVNode("div", { class: "task-template-block" }, [ - _createElementVNode("div", { class: "task-template-chip-group" }, [ - _createElementVNode("button", { - type: "button", - class: "task-template-chip", - onClick: $event => {_ctx.taskOrchestration.target = _ctx.t('orchestration.templates.reviewFix.target'); _ctx.taskOrchestration.selectedEngine = 'openai-chat'; _ctx.taskOrchestration.workflowIdsText = ''; _ctx.taskOrchestration.notes = _ctx.t('orchestration.templates.reviewFix.notes'); _ctx.taskOrchestration.followUpsText = _ctx.t('orchestration.templates.reviewFix.followUps')} - }, _toDisplayString(_ctx.t('orchestration.templates.reviewFix.label')), 9 /* TEXT, PROPS */, ["onClick"]), - _createElementVNode("button", { - type: "button", - class: "task-template-chip", - onClick: $event => {_ctx.taskOrchestration.target = _ctx.t('orchestration.templates.planOnly.target'); _ctx.taskOrchestration.selectedEngine = 'openai-chat'; _ctx.taskOrchestration.workflowIdsText = ''; _ctx.taskOrchestration.notes = _ctx.t('orchestration.templates.planOnly.notes'); _ctx.taskOrchestration.followUpsText = ''} - }, _toDisplayString(_ctx.t('orchestration.templates.planOnly.label')), 9 /* TEXT, PROPS */, ["onClick"]), - _createElementVNode("button", { - type: "button", - class: "task-template-chip", - onClick: $event => {_ctx.taskOrchestration.target = _ctx.t('orchestration.templates.workflowBatch.target'); _ctx.taskOrchestration.selectedEngine = 'workflow'; _ctx.taskOrchestration.workflowIdsText = _ctx.t('orchestration.templates.workflowBatch.workflowIds'); _ctx.taskOrchestration.notes = _ctx.t('orchestration.templates.workflowBatch.notes'); _ctx.taskOrchestration.followUpsText = ''} - }, _toDisplayString(_ctx.t('orchestration.templates.workflowBatch.label')), 9 /* TEXT, PROPS */, ["onClick"]) - ]) - ]) - ])) - : _createCommentVNode("v-if", true), - _createElementVNode("div", { class: "selector-grid task-composer-grid task-composer-grid-primary" }, [ - _createElementVNode("label", { class: "selector-field task-field task-field-wide task-goal-field" }, [ + _createElementVNode("div", { class: "task-quick-input-card" }, [ + _createElementVNode("label", { class: "task-quick-target-field" }, [ _createElementVNode("span", { class: "selector-label" }, _toDisplayString(_ctx.t('orchestration.fields.target')), 1 /* TEXT */), _withDirectives(_createElementVNode("textarea", { "onUpdate:modelValue": $event => ((_ctx.taskOrchestration.target) = $event), - class: "task-textarea task-textarea-goal", + class: "task-textarea task-textarea-goal task-quick-target", rows: "5", placeholder: _ctx.t('orchestration.fields.target.placeholder') }, null, 8 /* PROPS */, ["onUpdate:modelValue", "placeholder"]), [ [_vModelText, _ctx.taskOrchestration.target] ]), _createElementVNode("span", { class: "task-field-hint" }, _toDisplayString(_ctx.t('orchestration.fields.target.hint')), 1 /* TEXT */) - ]) - ]), - _createElementVNode("div", { class: "task-draft-overview task-draft-inline" }, [ - _createElementVNode("div", { class: "task-draft-inline-head" }, [ - _createElementVNode("span", { - class: _normalizeClass(['pill', _ctx.taskOrchestrationDraftReadiness.tone]) - }, _toDisplayString(_ctx.taskOrchestrationDraftReadiness.title), 3 /* TEXT, CLASS */), - _createElementVNode("div", { class: "task-readiness-copy task-draft-inline-copy" }, _toDisplayString(_ctx.taskOrchestrationDraftReadiness.summary), 1 /* TEXT */) ]), - _createElementVNode("div", { class: "task-config-strip" }, [ - _createElementVNode("div", { class: "task-config-pill" }, _toDisplayString(_ctx.taskOrchestration.selectedEngine === 'workflow' ? _ctx.t('orchestration.engine.workflow') : _ctx.t('orchestration.engine.openaiChat')), 1 /* TEXT */), - _createElementVNode("div", { class: "task-config-pill" }, _toDisplayString(_ctx.taskOrchestration.runMode === 'dry-run' ? _ctx.t('orchestration.runMode.dryRun') : (_ctx.taskOrchestration.runMode === 'read' ? _ctx.t('orchestration.runMode.readOnly') : _ctx.t('orchestration.runMode.write'))), 1 /* TEXT */), - (_ctx.taskOrchestration.title.trim()) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "task-config-pill" - }, _toDisplayString(_ctx.t('orchestration.pills.hasTitle')), 1 /* TEXT */)) - : _createCommentVNode("v-if", true), - (_ctx.taskOrchestration.selectedEngine === 'workflow' && _ctx.taskOrchestrationDraftMetrics.workflowCount > 0) - ? (_openBlock(), _createElementBlock("div", { - key: 1, - class: "task-config-pill" - }, _toDisplayString(_ctx.t('orchestration.pills.workflowCount', { count: _ctx.taskOrchestrationDraftMetrics.workflowCount })), 1 /* TEXT */)) - : _createCommentVNode("v-if", true), - (_ctx.taskOrchestration.plan) - ? (_openBlock(), _createElementBlock("div", { - key: 2, - class: "task-config-pill" - }, _toDisplayString(_ctx.t('orchestration.pills.planNodes', { count: _ctx.taskOrchestrationDraftMetrics.planNodeCount })), 1 /* TEXT */)) - : _createCommentVNode("v-if", true) - ]) - ]) - ]), - _createElementVNode("div", { class: "task-flow-section task-flow-section-compact" }, [ - _createElementVNode("div", { class: "task-flow-head" }, [ - _createElementVNode("div", { class: "task-flow-step" }, "2"), - _createElementVNode("div", null, [ - _createElementVNode("div", { class: "task-flow-title" }, _toDisplayString(_ctx.t('orchestration.step2.title')), 1 /* TEXT */), - _createElementVNode("div", { class: "task-flow-copy" }, _toDisplayString(_ctx.t('orchestration.step2.subtitle')), 1 /* TEXT */) - ]) - ]), - _createElementVNode("div", { class: "selector-grid task-composer-grid task-composer-grid-compact task-composer-grid-inline" }, [ - _createElementVNode("label", { class: "selector-field" }, [ - _createElementVNode("span", { class: "selector-label" }, _toDisplayString(_ctx.t('orchestration.fields.engine')), 1 /* TEXT */), - _withDirectives(_createElementVNode("select", { - "onUpdate:modelValue": $event => ((_ctx.taskOrchestration.selectedEngine) = $event), - class: "provider-fast-switch-select", - disabled: "" - }, [ - _createElementVNode("option", { value: "openai-chat" }, _toDisplayString(_ctx.t('orchestration.engine.openaiChat')), 1 /* TEXT */), - _createElementVNode("option", { value: "workflow" }, _toDisplayString(_ctx.t('orchestration.engine.workflow')), 1 /* TEXT */) - ], 8 /* PROPS */, ["onUpdate:modelValue"]), [ - [_vModelSelect, _ctx.taskOrchestration.selectedEngine] + _createElementVNode("div", { class: "task-draft-overview task-draft-inline task-quick-readiness" }, [ + _createElementVNode("div", { class: "task-draft-inline-head" }, [ + _createElementVNode("span", { + class: _normalizeClass(['pill', _ctx.taskOrchestrationDraftReadiness.tone]) + }, _toDisplayString(_ctx.taskOrchestrationDraftReadiness.title), 3 /* TEXT, CLASS */), + _createElementVNode("div", { class: "task-readiness-copy task-draft-inline-copy" }, _toDisplayString(_ctx.taskOrchestrationDraftReadiness.summary), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-config-strip" }, [ + _createElementVNode("div", { class: "task-config-pill" }, _toDisplayString(_ctx.taskOrchestration.selectedEngine === 'workflow' ? _ctx.t('orchestration.engine.workflow') : _ctx.t('orchestration.engine.openaiChat')), 1 /* TEXT */), + _createElementVNode("div", { class: "task-config-pill" }, _toDisplayString(_ctx.taskOrchestration.runMode === 'dry-run' ? _ctx.t('orchestration.runMode.dryRun') : (_ctx.taskOrchestration.runMode === 'read' ? _ctx.t('orchestration.runMode.readOnly') : _ctx.t('orchestration.runMode.write') )), 1 /* TEXT */), + (_ctx.taskOrchestration.title.trim()) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-config-pill" + }, _toDisplayString(_ctx.t('orchestration.pills.hasTitle')), 1 /* TEXT */)) + : _createCommentVNode("v-if", true), + (_ctx.taskOrchestration.selectedEngine === 'workflow' && _ctx.taskOrchestrationDraftMetrics.workflowCount > 0) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-config-pill" + }, _toDisplayString(_ctx.t('orchestration.pills.workflowCount', { count: _ctx.taskOrchestrationDraftMetrics.workflowCount })), 1 /* TEXT */)) + : _createCommentVNode("v-if", true), + (_ctx.taskOrchestration.plan) + ? (_openBlock(), _createElementBlock("div", { + key: 2, + class: "task-config-pill" + }, _toDisplayString(_ctx.t('orchestration.pills.planNodes', { count: _ctx.taskOrchestrationDraftMetrics.planNodeCount })), 1 /* TEXT */)) + : _createCommentVNode("v-if", true) ]) ]), - _createElementVNode("label", { class: "selector-field" }, [ - _createElementVNode("span", { class: "selector-label" }, _toDisplayString(_ctx.t('orchestration.fields.runMode')), 1 /* TEXT */), - _withDirectives(_createElementVNode("select", { - "onUpdate:modelValue": $event => ((_ctx.taskOrchestration.runMode) = $event), - class: "provider-fast-switch-select", - disabled: "" - }, [ - _createElementVNode("option", { value: "write" }, _toDisplayString(_ctx.t('orchestration.runMode.write')), 1 /* TEXT */), - _createElementVNode("option", { value: "read" }, _toDisplayString(_ctx.t('orchestration.runMode.readOnly')), 1 /* TEXT */), - _createElementVNode("option", { value: "dry-run" }, _toDisplayString(_ctx.t('orchestration.runMode.dryRun')), 1 /* TEXT */) - ], 8 /* PROPS */, ["onUpdate:modelValue"]), [ - [_vModelSelect, _ctx.taskOrchestration.runMode] + _createElementVNode("div", { class: "task-action-row task-action-row-prominent task-quick-actions" }, [ + _createElementVNode("button", { + type: "button", + class: "btn-tool task-action-preview", + onClick: $event => (_ctx.previewTaskPlan()), + disabled: _ctx.taskOrchestration.planning || _ctx.taskOrchestration.running || !_ctx.taskOrchestration.target.trim() + }, _toDisplayString(_ctx.taskOrchestration.planning ? _ctx.t('orchestration.actions.planning') : _ctx.t('orchestration.actions.previewOnly')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), + _createElementVNode("div", { class: "task-action-row-right task-action-row-right-prominent" }, [ + _createElementVNode("button", { + type: "button", + class: "btn-tool btn-primary", + onClick: $event => (_ctx.planAndRunTaskOrchestration()), + disabled: _ctx.taskOrchestration.running || _ctx.taskOrchestration.planning || !_ctx.taskOrchestration.target.trim() + }, _toDisplayString((_ctx.taskOrchestration.running || _ctx.taskOrchestration.planning) ? _ctx.t('orchestration.actions.preparing') : (_ctx.taskOrchestration.runMode === 'dry-run' ? _ctx.t('orchestration.actions.generatePlan') : _ctx.t('orchestration.actions.planAndRun'))), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), + _createElementVNode("button", { + type: "button", + class: "btn-tool", + onClick: $event => (_ctx.queueTaskOrchestrationAndStart()), + disabled: _ctx.taskOrchestration.queueAdding || _ctx.taskOrchestration.queueStarting || _ctx.taskOrchestration.planning || !_ctx.taskOrchestration.target.trim() + }, _toDisplayString((_ctx.taskOrchestration.queueAdding || _ctx.taskOrchestration.queueStarting) ? _ctx.t('orchestration.actions.processing') : _ctx.t('orchestration.actions.queueAndStart')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]) ]) + ]), + _createElementVNode("div", { class: "task-action-caption" }, _toDisplayString(_ctx.t('orchestration.quick.caption')), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-template-block task-quick-template-block" }, [ + _createElementVNode("div", { class: "task-template-label" }, _toDisplayString(_ctx.t('orchestration.templates.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "task-quick-template-grid" }, [ + _createElementVNode("button", { + type: "button", + class: "task-quick-template-card", + onClick: $event => {_ctx.taskOrchestration.target = _ctx.t('orchestration.templates.reviewFix.target'); _ctx.taskOrchestration.selectedEngine = 'openai-chat'; _ctx.taskOrchestration.workflowIdsText = ''; _ctx.taskOrchestration.notes = _ctx.t('orchestration.templates.reviewFix.notes'); _ctx.taskOrchestration.followUpsText = _ctx.t('orchestration.templates.reviewFix.followUps')} + }, [ + _createElementVNode("strong", null, _toDisplayString(_ctx.t('orchestration.templates.reviewFix.label')), 1 /* TEXT */), + _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.quick.templates.reviewFix.meta')), 1 /* TEXT */) + ], 8 /* PROPS */, ["onClick"]), + _createElementVNode("button", { + type: "button", + class: "task-quick-template-card", + onClick: $event => {_ctx.taskOrchestration.target = _ctx.t('orchestration.templates.planOnly.target'); _ctx.taskOrchestration.selectedEngine = 'openai-chat'; _ctx.taskOrchestration.workflowIdsText = ''; _ctx.taskOrchestration.notes = _ctx.t('orchestration.templates.planOnly.notes'); _ctx.taskOrchestration.followUpsText = ''} + }, [ + _createElementVNode("strong", null, _toDisplayString(_ctx.t('orchestration.templates.planOnly.label')), 1 /* TEXT */), + _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.quick.templates.planOnly.meta')), 1 /* TEXT */) + ], 8 /* PROPS */, ["onClick"]), + _createElementVNode("button", { + type: "button", + class: "task-quick-template-card", + onClick: $event => {_ctx.taskOrchestration.target = _ctx.t('orchestration.templates.workflowBatch.target'); _ctx.taskOrchestration.selectedEngine = 'workflow'; _ctx.taskOrchestration.workflowIdsText = _ctx.t('orchestration.templates.workflowBatch.workflowIds'); _ctx.taskOrchestration.notes = _ctx.t('orchestration.templates.workflowBatch.notes'); _ctx.taskOrchestration.followUpsText = ''} + }, [ + _createElementVNode("strong", null, _toDisplayString(_ctx.t('orchestration.templates.workflowBatch.label')), 1 /* TEXT */), + _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.quick.templates.workflowBatch.meta')), 1 /* TEXT */) + ], 8 /* PROPS */, ["onClick"]) ]) ]), - _createElementVNode("details", { class: "task-advanced-panel" }, [ + _createElementVNode("details", { class: "task-advanced-panel task-quick-advanced" }, [ _createElementVNode("summary", { class: "task-advanced-summary" }, _toDisplayString(_ctx.t('orchestration.advanced.title')), 1 /* TEXT */), _createElementVNode("div", { class: "selector-grid task-composer-grid task-composer-grid-secondary" }, [ _createElementVNode("label", { class: "selector-field task-field-wide" }, [ @@ -4182,6 +4167,33 @@ return function render(_ctx, _cache) { [_vModelText, _ctx.taskOrchestration.title] ]) ]), + _createElementVNode("label", { class: "selector-field" }, [ + _createElementVNode("span", { class: "selector-label" }, _toDisplayString(_ctx.t('orchestration.fields.engine')), 1 /* TEXT */), + _withDirectives(_createElementVNode("select", { + "onUpdate:modelValue": $event => ((_ctx.taskOrchestration.selectedEngine) = $event), + class: "provider-fast-switch-select", + disabled: "" + }, [ + _createElementVNode("option", { value: "openai-chat" }, _toDisplayString(_ctx.t('orchestration.engine.openaiChat')), 1 /* TEXT */), + _createElementVNode("option", { value: "workflow" }, _toDisplayString(_ctx.t('orchestration.engine.workflow')), 1 /* TEXT */) + ], 8 /* PROPS */, ["onUpdate:modelValue"]), [ + [_vModelSelect, _ctx.taskOrchestration.selectedEngine] + ]) + ]), + _createElementVNode("label", { class: "selector-field" }, [ + _createElementVNode("span", { class: "selector-label" }, _toDisplayString(_ctx.t('orchestration.fields.runMode')), 1 /* TEXT */), + _withDirectives(_createElementVNode("select", { + "onUpdate:modelValue": $event => ((_ctx.taskOrchestration.runMode) = $event), + class: "provider-fast-switch-select", + disabled: "" + }, [ + _createElementVNode("option", { value: "write" }, _toDisplayString(_ctx.t('orchestration.runMode.write')), 1 /* TEXT */), + _createElementVNode("option", { value: "read" }, _toDisplayString(_ctx.t('orchestration.runMode.readOnly')), 1 /* TEXT */), + _createElementVNode("option", { value: "dry-run" }, _toDisplayString(_ctx.t('orchestration.runMode.dryRun')), 1 /* TEXT */) + ], 8 /* PROPS */, ["onUpdate:modelValue"]), [ + [_vModelSelect, _ctx.taskOrchestration.runMode] + ]) + ]), _createElementVNode("label", { class: "selector-field task-field-wide" }, [ _createElementVNode("span", { class: "selector-label" }, _toDisplayString(_ctx.t('orchestration.fields.notes')), 1 /* TEXT */), _withDirectives(_createElementVNode("textarea", { @@ -4271,37 +4283,29 @@ return function render(_ctx, _cache) { ]) ]) ]), - _createElementVNode("div", { class: "task-flow-section task-flow-section-actions task-flow-section-compact" }, [ - _createElementVNode("div", { class: "task-flow-head" }, [ - _createElementVNode("div", { class: "task-flow-step" }, "3"), + _createElementVNode("aside", { class: "task-quick-side-card" }, [ + _createElementVNode("div", { class: "task-readiness-head" }, [ _createElementVNode("div", null, [ - _createElementVNode("div", { class: "task-flow-title" }, _toDisplayString(_ctx.t('orchestration.step3.title')), 1 /* TEXT */), - _createElementVNode("div", { class: "task-flow-copy" }, _toDisplayString(_ctx.t('orchestration.step3.subtitle')), 1 /* TEXT */) + _createElementVNode("div", { class: "task-readiness-title" }, _toDisplayString(_ctx.t('orchestration.quick.checklist.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "task-readiness-copy" }, _toDisplayString(_ctx.t('orchestration.quick.checklist.subtitle')), 1 /* TEXT */) ]) ]), - _createElementVNode("div", { class: "task-action-row task-action-row-prominent" }, [ - _createElementVNode("button", { - type: "button", - class: "btn-tool task-action-preview", - onClick: $event => (_ctx.previewTaskPlan()), - disabled: _ctx.taskOrchestration.planning || _ctx.taskOrchestration.running || !_ctx.taskOrchestration.target.trim() - }, _toDisplayString(_ctx.taskOrchestration.planning ? _ctx.t('orchestration.actions.planning') : _ctx.t('orchestration.actions.previewOnly')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), - _createElementVNode("div", { class: "task-action-row-right task-action-row-right-prominent" }, [ - _createElementVNode("button", { - type: "button", - class: "btn-tool btn-primary", - onClick: $event => (_ctx.planAndRunTaskOrchestration()), - disabled: _ctx.taskOrchestration.running || _ctx.taskOrchestration.planning || !_ctx.taskOrchestration.target.trim() - }, _toDisplayString((_ctx.taskOrchestration.running || _ctx.taskOrchestration.planning) ? _ctx.t('orchestration.actions.preparing') : (_ctx.taskOrchestration.runMode === 'dry-run' ? _ctx.t('orchestration.actions.generatePlan') : _ctx.t('orchestration.actions.planAndRun'))), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), - _createElementVNode("button", { - type: "button", - class: "btn-tool", - onClick: $event => (_ctx.queueTaskOrchestrationAndStart()), - disabled: _ctx.taskOrchestration.queueAdding || _ctx.taskOrchestration.queueStarting || _ctx.taskOrchestration.planning || !_ctx.taskOrchestration.target.trim() - }, _toDisplayString((_ctx.taskOrchestration.queueAdding || _ctx.taskOrchestration.queueStarting) ? _ctx.t('orchestration.actions.processing') : _ctx.t('orchestration.actions.queueAndStart')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]) - ]) + _createElementVNode("div", { class: "task-readiness-grid task-quick-checklist" }, [ + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestrationDraftChecklist, (item) => { + return (_openBlock(), _createElementBlock("div", { + key: item.key, + class: _normalizeClass(['task-readiness-item', { done: item.done }]) + }, [ + _createElementVNode("strong", null, _toDisplayString(item.label), 1 /* TEXT */), + _createElementVNode("span", null, _toDisplayString(item.detail), 1 /* TEXT */) + ], 2 /* CLASS */)) + }), 128 /* KEYED_FRAGMENT */)) ]), - _createElementVNode("div", { class: "task-action-caption" }, _toDisplayString(_ctx.t('orchestration.actions.caption')), 1 /* TEXT */) + _createElementVNode("div", { class: "task-quick-status-card" }, [ + _createElementVNode("div", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.quick.status.title')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationDraftReadiness.title), 1 /* TEXT */), + _createElementVNode("span", null, _toDisplayString(_ctx.taskOrchestrationDraftReadiness.summary), 1 /* TEXT */) + ]) ]) ]) ]), diff --git a/web-ui/styles/task-orchestration.css b/web-ui/styles/task-orchestration.css index 62c4d194..495b647b 100644 --- a/web-ui/styles/task-orchestration.css +++ b/web-ui/styles/task-orchestration.css @@ -820,3 +820,184 @@ flex-wrap: wrap; } } + +.task-quick-layout { + margin-bottom: 18px; +} + +.task-quick-card { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(260px, 340px); + gap: 18px; + align-items: stretch; +} + +.task-quick-main { + min-width: 0; + display: flex; + flex-direction: column; + gap: 14px; +} + +.task-quick-copy { + max-width: 780px; +} + +.task-quick-title { + font-size: clamp(22px, 2vw, 30px); + line-height: 1.18; +} + +.task-quick-input-card { + padding: 18px; + border-radius: 22px; + border: 1px solid rgba(199, 116, 98, 0.20); + background: + linear-gradient(180deg, rgba(199, 116, 98, 0.08) 0%, rgba(199, 116, 98, 0.02) 100%), + var(--color-surface-alt); + box-shadow: var(--shadow-subtle); +} + +.task-quick-target-field { + display: block; +} + +.task-quick-target { + min-height: 150px; + border-radius: 18px; + background: var(--color-surface); + font-size: 15px; +} + +.task-quick-readiness { + margin-top: 14px; + background: rgba(255, 255, 255, 0.58); +} + +.task-quick-actions { + justify-content: space-between; + gap: 12px; + margin-top: 16px; +} + +.task-quick-actions > .task-action-row-right { + margin-left: auto; +} + +.task-quick-template-block { + padding: 14px; +} + +.task-quick-template-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + margin-top: 10px; +} + +.task-quick-template-card { + min-width: 0; + padding: 14px; + border-radius: 16px; + border: 1px solid var(--color-border-soft); + background: var(--color-surface); + color: var(--color-text-primary); + text-align: left; + cursor: pointer; + transition: + transform var(--transition-fast) var(--ease-smooth), + border-color var(--transition-fast) var(--ease-smooth), + box-shadow var(--transition-fast) var(--ease-smooth), + background var(--transition-fast) var(--ease-smooth); +} + +.task-quick-template-card:hover { + transform: translateY(-1px); + border-color: rgba(199, 116, 98, 0.34); + background: var(--color-brand-light); + box-shadow: var(--shadow-subtle); +} + +.task-quick-template-card strong, +.task-quick-template-card span { + display: block; +} + +.task-quick-template-card strong { + font-size: 13px; + color: var(--color-text-primary); +} + +.task-quick-template-card span { + margin-top: 6px; + font-size: 12px; + line-height: 1.5; + color: var(--color-text-secondary); +} + +.task-quick-advanced { + margin-top: 0; +} + +.task-quick-side-card { + min-width: 0; + padding: 16px; + border-radius: 20px; + border: 1px solid var(--color-border-soft); + background: var(--color-surface-alt); + box-shadow: var(--shadow-subtle); +} + +.task-quick-checklist { + grid-template-columns: 1fr; +} + +.task-quick-status-card { + margin-top: 14px; + padding: 14px; + border-radius: 16px; + border: 1px solid rgba(199, 116, 98, 0.18); + background: var(--color-surface); + display: flex; + flex-direction: column; + gap: 6px; +} + +.task-quick-status-card strong { + color: var(--color-text-primary); +} + +.task-quick-status-card span { + font-size: 12px; + line-height: 1.55; + color: var(--color-text-secondary); +} + +@media (max-width: 1200px) { + .task-quick-card { + grid-template-columns: 1fr; + } + + .task-quick-checklist { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 720px) { + .task-quick-template-grid, + .task-quick-checklist { + grid-template-columns: 1fr; + } + + .task-quick-actions, + .task-quick-actions > .task-action-row-right { + align-items: stretch; + width: 100%; + margin-left: 0; + } + + .task-quick-actions > .btn-tool, + .task-quick-actions > .task-action-row-right > .btn-tool { + width: 100%; + } +} From 75d94172fd1b601316ac9c1199e242b403afe2c4 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sun, 28 Jun 2026 02:50:37 +0000 Subject: [PATCH 06/54] fix(task-orchestration): materialize chat artifacts --- cli.js | 122 +++++++++++++++++++++++++-- tests/e2e/test-task-orchestration.js | 48 ++++++++++- 2 files changed, 164 insertions(+), 6 deletions(-) diff --git a/cli.js b/cli.js index 26129c9c..957bb6f7 100644 --- a/cli.js +++ b/cli.js @@ -73,7 +73,8 @@ const { truncateText: truncateTaskText, buildTaskPlan, validateTaskPlan, - executeTaskPlan + executeTaskPlan, + computePlanWaves } = require('./lib/task-orchestrator'); const { readAutomationConfig, @@ -16226,6 +16227,106 @@ function postOpenAiChatCompletion(requestConfig, body, options = {}) { }); } +const TASK_OPENAI_MATERIALIZED_ARTIFACT_MAX_BYTES = 512 * 1024; +const TASK_OPENAI_MATERIALIZED_ARTIFACT_EXTENSIONS = new Set(['.html', '.htm', '.css', '.js', '.mjs', '.json', '.md', '.txt', '.svg']); + +function normalizeTaskMaterializedArtifactPath(rawPath, cwd) { + const baseDir = path.resolve(typeof cwd === 'string' && cwd.trim() ? cwd.trim() : process.cwd()); + const text = typeof rawPath === 'string' ? rawPath.trim() : ''; + if (!text || text.length > 200) { + return { error: 'artifact path is empty or too long' }; + } + if (path.isAbsolute(text) || text.includes('\\')) { + return { error: `artifact path must be a safe relative path: ${text}` }; + } + const normalized = path.normalize(text); + if (!normalized || normalized === '.' || normalized.startsWith('..') || normalized.split(path.sep).includes('..')) { + return { error: `artifact path escapes cwd: ${text}` }; + } + const ext = path.extname(normalized).toLowerCase(); + if (!TASK_OPENAI_MATERIALIZED_ARTIFACT_EXTENSIONS.has(ext)) { + return { error: `artifact extension is not allowed: ${ext || '(none)'}` }; + } + const targetPath = path.resolve(baseDir, normalized); + const relative = path.relative(baseDir, targetPath); + if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) { + return { error: `artifact path escapes cwd: ${text}` }; + } + return { path: targetPath, relativePath: normalized, baseDir }; +} + +function inferTaskMaterializedArtifactPath(info, prefix, hints) { + const sources = [info, prefix, hints].map((item) => String(item || '')).filter(Boolean); + const explicitPatterns = [ + /(?:file|filename|path)\s*[:=]\s*[`"']?([A-Za-z0-9._/-]+\.[A-Za-z0-9]+)[`"']?/i, + /(?:文件|路径|保存为|写入到?|输出到?)\s*[::]?\s*[`"']?([A-Za-z0-9._/-]+\.[A-Za-z0-9]+)[`"']?/i, + /[`"']([A-Za-z0-9._/-]+\.[A-Za-z0-9]+)[`"']/i + ]; + for (const source of sources) { + for (const pattern of explicitPatterns) { + const match = source.match(pattern); + if (match && match[1]) { + return match[1]; + } + } + } + const normalizedInfo = String(info || '').trim().toLowerCase(); + const hintText = sources.join('\n').toLowerCase(); + if ((normalizedInfo === 'html' || normalizedInfo.startsWith('html ')) && hintText.includes('index.html')) { + return 'index.html'; + } + return ''; +} + +function materializeOpenAiChatTaskArtifacts(text, options = {}) { + const content = typeof text === 'string' ? text : ''; + if (!content.trim()) { + return { files: [], warnings: [] }; + } + const cwd = typeof options.cwd === 'string' && options.cwd.trim() ? options.cwd.trim() : process.cwd(); + const hints = [options.target, options.notes, options.prompt] + .map((item) => String(item || '').trim()) + .filter(Boolean) + .join('\n'); + const files = []; + const warnings = []; + const seen = new Set(); + const fencePattern = /```([^\n`]*)\n([\s\S]*?)```/g; + let match = null; + while ((match = fencePattern.exec(content)) !== null) { + const info = String(match[1] || '').trim(); + const body = String(match[2] || ''); + const prefix = content.slice(Math.max(0, match.index - 240), match.index); + const inferredPath = inferTaskMaterializedArtifactPath(info, prefix, hints); + if (!inferredPath) { + continue; + } + if (Buffer.byteLength(body, 'utf-8') > TASK_OPENAI_MATERIALIZED_ARTIFACT_MAX_BYTES) { + warnings.push(`artifact too large and skipped: ${inferredPath}`); + continue; + } + const normalized = normalizeTaskMaterializedArtifactPath(inferredPath, cwd); + if (normalized.error) { + warnings.push(normalized.error); + continue; + } + if (seen.has(normalized.path)) { + warnings.push(`duplicate artifact skipped: ${normalized.relativePath}`); + continue; + } + seen.add(normalized.path); + try { + const fileContent = body.trimStart(); + ensureDir(path.dirname(normalized.path)); + fs.writeFileSync(normalized.path, fileContent, { encoding: 'utf-8', mode: 0o600 }); + files.push({ path: normalized.path, relativePath: normalized.relativePath, bytes: Buffer.byteLength(fileContent, 'utf-8') }); + } catch (error) { + warnings.push(`failed to write artifact ${normalized.relativePath}: ${error && error.message ? error.message : String(error)}`); + } + } + return { files, warnings }; +} + async function runOpenAiChatTaskNode(node, context = {}) { const requestConfig = resolveTaskOpenAiChatConfig(); if (requestConfig.error) { @@ -16237,7 +16338,7 @@ async function runOpenAiChatTaskNode(node, context = {}) { logs: [{ at: toIsoTime(Date.now()), level: 'error', message: requestConfig.error }] }; } - const allowWrite = context.allowWrite === true && node.write === true; + const allowWrite = context.allowWrite === true && node.write !== false && context.dryRun !== true; const dependencyResults = Array.isArray(context.dependencyResults) ? context.dependencyResults : []; const dependencyLines = dependencyResults .map((item) => { @@ -16261,7 +16362,7 @@ async function runOpenAiChatTaskNode(node, context = {}) { '你是 codexmate 任务编排中的 OpenAI Chat-compatible 执行节点。', '基于用户给出的目标、前置节点摘要和当前节点范围,输出可执行、可验证、事实谨慎的结果。', allowWrite - ? '当前任务允许写入,但本 OpenAI Chat 节点没有直接本地工具能力;如果需要改文件,请输出精确改动方案、补丁建议、验证命令与风险说明,不要声称已经实际写入。' + ? '当前任务允许写入;如果需要创建或更新文件,请明确写出安全的相对文件名,并用 fenced code block 给出完整文件内容。codexmate 只会物化明确、安全、位于任务工作目录内的文本类 artifact。' : '当前任务是只读/验证节点,不要声称修改了文件。', '回答使用中文,避免空泛总结。' ].join('\n'); @@ -16283,6 +16384,14 @@ async function runOpenAiChatTaskNode(node, context = {}) { const errorMessage = success ? '' : (result.error || 'OpenAI Chat response did not contain text'); const summary = truncateTaskText(text || errorMessage, 400); const safeEndpoint = requestConfig.endpointUrl.replace(/([?&](?:key|api_key|token)=)[^&]+/ig, '$1***'); + const materialized = success && allowWrite + ? materializeOpenAiChatTaskArtifacts(text, { + cwd: context.cwd || process.cwd(), + target: context.plan && context.plan.target ? context.plan.target : '', + notes: context.plan && context.plan.notes ? context.plan.notes : '', + prompt: node.prompt || '' + }) + : { files: [], warnings: [] }; return { success, error: errorMessage, @@ -16294,11 +16403,14 @@ async function runOpenAiChatTaskNode(node, context = {}) { status: result.status || 0, text, response: result.payload || null, - durationMs: Date.now() - startedAt + durationMs: Date.now() - startedAt, + materializedFiles: materialized.files }, logs: [ { at: toIsoTime(Date.now()), level: 'info', message: `OpenAI Chat request provider=${requestConfig.providerName} model=${requestConfig.model} status=${result.status || 0}` }, - ...(success ? [{ at: toIsoTime(Date.now()), level: 'info', message: truncateTaskText(text, 1200) }] : [{ at: toIsoTime(Date.now()), level: 'error', message: errorMessage }]) + ...(success ? [{ at: toIsoTime(Date.now()), level: 'info', message: truncateTaskText(text, 1200) }] : [{ at: toIsoTime(Date.now()), level: 'error', message: errorMessage }]), + ...materialized.files.map((file) => ({ at: toIsoTime(Date.now()), level: 'info', message: `materialized artifact ${file.relativePath} (${file.bytes} bytes)` })), + ...materialized.warnings.map((warning) => ({ at: toIsoTime(Date.now()), level: 'warn', message: warning })) ] }; } diff --git a/tests/e2e/test-task-orchestration.js b/tests/e2e/test-task-orchestration.js index 6d08b28b..10a76563 100644 --- a/tests/e2e/test-task-orchestration.js +++ b/tests/e2e/test-task-orchestration.js @@ -43,10 +43,14 @@ const server = http.createServer((req, res) => { } if (req.method === 'POST' && requestPath === '/v1/chat/completions') { const model = parsedBody && parsedBody.model ? parsedBody.model : 'unknown-model'; + const requestText = JSON.stringify(parsedBody || {}); + const content = requestText.includes('index.html') + ? '输出文件:index.html\\n\`\`\`html\\n2048 Probe

2048

2 4 8 16
\\n\`\`\`' + : 'openai-chat-e2e-ok model=' + model + ' request=' + requestCount; const body = JSON.stringify({ id: 'chatcmpl-task-e2e-' + requestCount, object: 'chat.completion', - choices: [{ index: 0, message: { role: 'assistant', content: 'openai-chat-e2e-ok model=' + model + ' request=' + requestCount }, finish_reason: 'stop' }], + choices: [{ index: 0, message: { role: 'assistant', content }, finish_reason: 'stop' }], usage: { prompt_tokens: 10, completion_tokens: 8, total_tokens: 18 } }); res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(body, 'utf-8') }); @@ -375,6 +379,48 @@ module.exports = async function testTaskOrchestration(ctx) { const openAiLogsPayload = parseJsonOutput(openAiLogsResult.stdout); assert(String(openAiLogsPayload.logs || '').includes('OpenAI Chat request provider=local-openai-chat'), 'OpenAI Chat logs should include provider request'); + const directPlanPath = path.join(tmpHome, 'task-direct-openai-plan.json'); + const directPlanCwd = path.join(tmpHome, 'task-direct-plan-workspace'); + fs.mkdirSync(directPlanCwd, { recursive: true }); + fs.writeFileSync(directPlanPath, JSON.stringify({ + id: 'task-direct-openai-plan', + title: 'Direct OpenAI Chat plan', + target: 'Create index.html for direct OpenAI Chat plan execution', + notes: 'Write index.html only inside the provided cwd.', + cwd: directPlanCwd, + engine: 'openai-chat', + allowWrite: true, + dryRun: false, + concurrency: 1, + nodes: [ + { + id: 'direct-openai-node', + title: 'Direct OpenAI node', + kind: 'openai-chat', + prompt: 'Create index.html with a tiny 2048 probe page and return the full file in an html fenced block.', + dependsOn: [] + } + ] + }, null, 2), 'utf-8'); + const directPlanRunResult = runSync(node, [ + cliPath, + 'task', + 'run', + '--plan', + `@${directPlanPath}`, + '--allow-write', + '--json' + ], { env }); + assert(directPlanRunResult.status === 0, `OpenAI Chat direct plan run failed: ${directPlanRunResult.stderr || directPlanRunResult.stdout}`); + const directPlanRunPayload = parseJsonOutput(directPlanRunResult.stdout); + assertOpenAiRunPayload(directPlanRunPayload, 'OpenAI Chat direct plan run'); + assert(Array.isArray(directPlanRunPayload.plan && directPlanRunPayload.plan.waves), 'OpenAI Chat direct plan run should compute waves'); + const directPlanIndexPath = path.join(directPlanCwd, 'index.html'); + assert(fs.existsSync(directPlanIndexPath), 'OpenAI Chat direct plan run should materialize index.html when allow-write is enabled'); + assert(fs.readFileSync(directPlanIndexPath, 'utf-8').includes('2048'), 'materialized index.html should contain generated page content'); + const materializedFiles = directPlanRunPayload.run.nodes.flatMap(item => item && item.output && Array.isArray(item.output.materializedFiles) ? item.output.materializedFiles : []); + assert(materializedFiles.some(item => item.relativePath === 'index.html'), 'OpenAI Chat direct plan run should report materialized index.html'); + const openAiQueueAddResult = runSync(node, [ cliPath, 'task', From 268130c3db7855788c798ab84028538382f6ec97 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sun, 28 Jun 2026 03:47:21 +0000 Subject: [PATCH 07/54] fix(task-orchestration): preserve web workspace threads --- cli.js | 78 ++++++++++++- lib/task-orchestrator.js | 12 ++ tests/e2e/test-task-orchestration.js | 45 +++++++- tests/unit/task-orchestrator.test.mjs | 13 +++ tests/unit/web-ui-behavior-parity.test.mjs | 1 + tests/unit/web-ui-logic.test.mjs | 68 +++++++++++ web-ui/app.js | 2 + .../app.methods.task-orchestration.mjs | 46 ++++++++ web-ui/modules/i18n/locales/en.mjs | 9 ++ web-ui/modules/i18n/locales/ja.mjs | 9 ++ web-ui/modules/i18n/locales/vi.mjs | 9 ++ web-ui/modules/i18n/locales/zh-tw.mjs | 9 ++ web-ui/modules/i18n/locales/zh.mjs | 9 ++ .../partials/index/panel-orchestration.html | 57 ++++++++-- web-ui/res/web-ui-render.precompiled.js | 106 +++++++++++++++++- 15 files changed, 456 insertions(+), 17 deletions(-) diff --git a/cli.js b/cli.js index 957bb6f7..54c3d12a 100644 --- a/cli.js +++ b/cli.js @@ -13018,6 +13018,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser detached: true, taskId, runId, + threadId: plan.threadId || '', + cwd: plan.cwd || '', warnings: validation.warnings || [] }; } else { @@ -14032,6 +14034,38 @@ function parseTaskCliOptions(args = []) { options.explicit.autoFixRounds = true; continue; } + if (arg === '--cwd') { + options.cwd = String(args[i + 1] || '').trim(); + options.explicit.cwd = true; + i += 1; + continue; + } + if (arg.startsWith('--cwd=')) { + options.cwd = arg.slice('--cwd='.length).trim(); + options.explicit.cwd = true; + continue; + } + if (arg === '--thread-id' || arg === '--conversation-id' || arg === '--session-id') { + options.threadId = String(args[i + 1] || '').trim(); + options.explicit.threadId = true; + i += 1; + continue; + } + if (arg.startsWith('--thread-id=')) { + options.threadId = arg.slice('--thread-id='.length).trim(); + options.explicit.threadId = true; + continue; + } + if (arg.startsWith('--conversation-id=')) { + options.threadId = arg.slice('--conversation-id='.length).trim(); + options.explicit.threadId = true; + continue; + } + if (arg.startsWith('--session-id=')) { + options.threadId = arg.slice('--session-id='.length).trim(); + options.explicit.threadId = true; + continue; + } if (arg === '--limit') { const value = parseInt(args[i + 1], 10); if (Number.isFinite(value)) options.limit = value; @@ -14092,6 +14126,8 @@ function buildTaskCliPayload(options = {}, rest = []) { if (explicit.engine) payload.engine = options.engine || 'openai-chat'; if (explicit.concurrency) payload.concurrency = options.concurrency; if (explicit.autoFixRounds) payload.autoFixRounds = options.autoFixRounds; + if (explicit.cwd && options.cwd) payload.cwd = options.cwd; + if (explicit.threadId && options.threadId) payload.threadId = options.threadId; if (explicit.taskId && options.taskId) payload.taskId = options.taskId; if (explicit.runId && options.runId) payload.runId = options.runId; if (!payload.target && Array.isArray(rest) && rest.length > 0) { @@ -15559,6 +15595,10 @@ function createTaskRunId() { return `tr-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`; } +function createTaskThreadId() { + return `tt-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`; +} + function validateTaskRunId(value) { const runId = typeof value === 'string' ? value.trim() : ''; if (!runId) { @@ -15570,6 +15610,14 @@ function validateTaskRunId(value) { return { ok: true, error: '', runId }; } +function normalizeTaskThreadId(value) { + const threadId = typeof value === 'string' ? value.trim() : ''; + if (!threadId) { + return ''; + } + return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(threadId) ? threadId : ''; +} + function normalizeTaskEngine(value) { const normalized = typeof value === 'string' ? value.trim().toLowerCase().replace(/[\s_/]+/g, '-') : ''; if (normalized === 'workflow') return 'workflow'; @@ -15615,6 +15663,7 @@ function normalizeTaskPlanRequest(params = {}) { : (typeof source.followUp === 'string' && source.followUp.trim() ? [source.followUp.trim()] : []); return { id: typeof source.id === 'string' ? source.id.trim() : '', + threadId: normalizeTaskThreadId(source.threadId || source.conversationId || source.sessionId), title: typeof source.title === 'string' ? source.title.trim() : '', target: typeof source.target === 'string' ? source.target.trim() : '', notes: typeof source.notes === 'string' ? source.notes.trim() : '', @@ -15632,12 +15681,17 @@ function normalizeTaskPlanRequest(params = {}) { function coerceTaskPlanPayload(params = {}) { if (params && params.plan && typeof params.plan === 'object' && !Array.isArray(params.plan)) { const plan = cloneJson(params.plan, {}); - const overrideKeys = ['id', 'title', 'target', 'notes', 'cwd', 'engine', 'allowWrite', 'dryRun', 'concurrency', 'autoFixRounds', 'workflowIds', 'followUps']; + const overrideKeys = ['id', 'threadId', 'conversationId', 'sessionId', 'title', 'target', 'notes', 'cwd', 'engine', 'allowWrite', 'dryRun', 'concurrency', 'autoFixRounds', 'workflowIds', 'followUps']; for (const key of overrideKeys) { if (Object.prototype.hasOwnProperty.call(params, key) && params[key] !== undefined) { - plan[key] = cloneJson(params[key], params[key]); + if (key === 'conversationId' || key === 'sessionId') { + plan.threadId = cloneJson(params[key], params[key]); + } else { + plan[key] = cloneJson(params[key], params[key]); + } } } + plan.threadId = normalizeTaskThreadId(plan.threadId) || createTaskThreadId(); plan.engine = normalizeTaskEngine(plan.engine); plan.workflowIds = normalizeTaskFollowUps(plan.workflowIds || []).map((id) => normalizeWorkflowId(id)).filter(Boolean); plan.followUps = normalizeTaskFollowUps(plan.followUps || []); @@ -15652,6 +15706,7 @@ function coerceTaskPlanPayload(params = {}) { }); return { ...plan, + threadId: request.threadId || createTaskThreadId(), engine: normalizeTaskEngine(request.engine || plan.engine) }; } @@ -15674,8 +15729,10 @@ function normalizeTaskQueueItem(raw = {}) { const taskId = typeof raw.taskId === 'string' ? raw.taskId.trim() : ''; return { taskId: taskId || createTaskId(), + threadId: normalizeTaskThreadId(raw.threadId || plan.threadId) || createTaskThreadId(), title: typeof raw.title === 'string' ? raw.title.trim() : (typeof plan.title === 'string' ? plan.title.trim() : ''), target: typeof raw.target === 'string' ? raw.target.trim() : (typeof plan.target === 'string' ? plan.target.trim() : ''), + cwd: typeof raw.cwd === 'string' && raw.cwd.trim() ? raw.cwd.trim() : (typeof plan.cwd === 'string' ? plan.cwd.trim() : ''), status: typeof raw.status === 'string' ? raw.status.trim().toLowerCase() : 'queued', createdAt: toIsoTime(raw.createdAt || Date.now(), ''), updatedAt: toIsoTime(raw.updatedAt || raw.createdAt || Date.now(), ''), @@ -15876,8 +15933,10 @@ function collectTaskRunSummary(detail = {}) { return { runId: detail.runId || '', taskId: detail.taskId || '', + threadId: detail.threadId || '', title: detail.title || '', target: detail.target || '', + cwd: detail.cwd || '', engine: detail.engine || '', allowWrite: detail.allowWrite === true, dryRun: detail.dryRun === true, @@ -16462,13 +16521,18 @@ async function runTaskPlanInternal(plan, options = {}) { } const taskId = typeof options.taskId === 'string' && options.taskId.trim() ? options.taskId.trim() : (plan.id || createTaskId()); const runId = typeof options.runId === 'string' && options.runId.trim() ? options.runId.trim() : createTaskRunId(); + const threadId = normalizeTaskThreadId(options.threadId || plan.threadId) || createTaskThreadId(); + plan.threadId = threadId; + const cwd = typeof plan.cwd === 'string' && plan.cwd.trim() ? plan.cwd.trim() : process.cwd(); const controller = new AbortController(); const baseDetail = { runId, taskId, + threadId, workerPid: process.pid, title: plan.title || '', target: plan.target || '', + cwd, engine: normalizeTaskEngine(plan.engine), allowWrite: plan.allowWrite === true, dryRun: plan.dryRun === true, @@ -16504,6 +16568,8 @@ async function runTaskPlanInternal(plan, options = {}) { const queued = upsertTaskQueueItem({ ...options.queueItem, taskId, + threadId, + cwd, status: 'running', runStatus: 'running', lastRunId: runId, @@ -16524,7 +16590,7 @@ async function runTaskPlanInternal(plan, options = {}) { runId, allowWrite: plan.allowWrite === true, dryRun: plan.dryRun === true, - cwd: plan.cwd || process.cwd() + cwd }), onUpdate: async (snapshot) => { const nextDetail = { @@ -16538,6 +16604,8 @@ async function runTaskPlanInternal(plan, options = {}) { const queued = upsertTaskQueueItem({ ...options.queueItem, taskId, + threadId, + cwd, status: snapshot.status === 'success' ? 'completed' : (snapshot.status === 'failed' ? 'failed' : (snapshot.status === 'cancelled' ? 'cancelled' : 'running')), @@ -16567,6 +16635,8 @@ async function runTaskPlanInternal(plan, options = {}) { const queued = upsertTaskQueueItem({ ...options.queueItem, taskId, + threadId, + cwd, status: run.status === 'success' ? 'completed' : (run.status === 'cancelled' ? 'cancelled' : 'failed'), @@ -16597,8 +16667,10 @@ function addTaskToQueue(params = {}) { const taskId = typeof params.taskId === 'string' && params.taskId.trim() ? params.taskId.trim() : createTaskId(); const item = upsertTaskQueueItem({ taskId, + threadId: plan.threadId || createTaskThreadId(), title: plan.title, target: plan.target, + cwd: plan.cwd || process.cwd(), status: 'queued', createdAt: toIsoTime(Date.now()), updatedAt: toIsoTime(Date.now()), diff --git a/lib/task-orchestrator.js b/lib/task-orchestrator.js index f15cf97a..a63c8faa 100644 --- a/lib/task-orchestrator.js +++ b/lib/task-orchestrator.js @@ -214,6 +214,7 @@ function buildOpenAiChatNodePrompt(kind, context = {}) { const target = normalizeText(context.target, 4000); const item = normalizeText(context.item, 1200); const followUp = normalizeText(context.followUp, 1200); + const cwd = normalizeText(context.cwd, 1200); const dependencySummaries = uniqueStringList(context.dependencySummaries || []).slice(0, 6); const allowWrite = context.allowWrite === true; const dependencyBlock = dependencySummaries.length > 0 @@ -222,10 +223,12 @@ function buildOpenAiChatNodePrompt(kind, context = {}) { const writeRule = allowWrite ? '允许直接修改本地工作区,但必须控制变更范围并完成最小必要验证。' : '只允许只读调查,不要修改任何文件,不要执行写入型操作。'; + const cwdLine = cwd ? `工作区路径: ${cwd}` : ''; if (kind === 'analysis') { return [ `任务标题: ${title || '任务分析'}`, + cwdLine, `任务目标:\n${target}`, writeRule, '请先调查当前仓库与上下文,给出简洁的现状判断、主要风险、建议执行顺序,以及你认为最小可行的验证方案。', @@ -236,6 +239,7 @@ function buildOpenAiChatNodePrompt(kind, context = {}) { if (kind === 'work') { return [ `任务标题: ${title || '执行子任务'}`, + cwdLine, `总目标:\n${target}`, `当前子任务:\n${item || title || target}`, dependencyBlock, @@ -247,6 +251,7 @@ function buildOpenAiChatNodePrompt(kind, context = {}) { if (kind === 'verify') { return [ `任务标题: ${title || '验证与总结'}`, + cwdLine, `总目标:\n${target}`, dependencyBlock, writeRule, @@ -256,6 +261,7 @@ function buildOpenAiChatNodePrompt(kind, context = {}) { return [ `任务标题: ${title || '后续任务'}`, + cwdLine, `总目标:\n${target}`, dependencyBlock, writeRule, @@ -294,6 +300,7 @@ function buildTaskPlan(request = {}, options = {}) { const followUps = uniqueStringList(request.followUps || []); const notes = normalizeText(request.notes, 2000); const cwd = normalizeText(request.cwd || options.cwd, 1200); + const threadId = normalizeText(request.threadId || request.conversationId || request.sessionId, 160); const nodes = []; let nodeSequence = 0; const nextNodeId = (prefix) => `${prefix}-${String(++nodeSequence).padStart(2, '0')}`; @@ -334,6 +341,7 @@ function buildTaskPlan(request = {}, options = {}) { prompt: buildOpenAiChatNodePrompt('analysis', { title, target, + cwd, allowWrite: false }), dependsOn: [], @@ -355,6 +363,7 @@ function buildTaskPlan(request = {}, options = {}) { title, target, item, + cwd, allowWrite }), dependsOn: [analysisId], @@ -372,6 +381,7 @@ function buildTaskPlan(request = {}, options = {}) { prompt: buildOpenAiChatNodePrompt('verify', { title, target, + cwd, allowWrite: false }), dependsOn: executionNodeIds.slice(), @@ -392,6 +402,7 @@ function buildTaskPlan(request = {}, options = {}) { prompt: buildOpenAiChatNodePrompt('follow-up', { title, target, + cwd, allowWrite, followUp }), @@ -406,6 +417,7 @@ function buildTaskPlan(request = {}, options = {}) { const plan = { id: normalizeId(request.id, ''), + threadId, title, target, notes, diff --git a/tests/e2e/test-task-orchestration.js b/tests/e2e/test-task-orchestration.js index 10a76563..ce326520 100644 --- a/tests/e2e/test-task-orchestration.js +++ b/tests/e2e/test-task-orchestration.js @@ -338,6 +338,10 @@ module.exports = async function testTaskOrchestration(ctx) { 'plan', '--target', 'OpenAI Chat provider 端到端模拟', + '--cwd', + path.join(tmpHome, 'task-plan-workspace'), + '--thread-id', + 'thread-cli-plan', '--follow-up', '输出风险说明', '--engine', @@ -348,8 +352,12 @@ module.exports = async function testTaskOrchestration(ctx) { const openAiPlanPayload = parseJsonOutput(openAiPlanResult.stdout); assert(openAiPlanPayload.ok === true, 'OpenAI Chat task plan should validate'); assert(openAiPlanPayload.plan && openAiPlanPayload.plan.engine === 'openai-chat', 'OpenAI Chat plan should keep engine'); + assert(openAiPlanPayload.plan.threadId === 'thread-cli-plan', 'OpenAI Chat plan should preserve CLI thread id'); + assert(openAiPlanPayload.plan.cwd === path.join(tmpHome, 'task-plan-workspace'), 'OpenAI Chat plan should preserve CLI cwd'); assert(openAiPlanPayload.plan.nodes.every((node) => node.kind === 'openai-chat'), 'OpenAI Chat plan should produce OpenAI Chat nodes'); + const openAiRunCwd = path.join(tmpHome, 'task-run-workspace'); + fs.mkdirSync(openAiRunCwd, { recursive: true }); const openAiRunResult = runSync(node, [ cliPath, 'task', @@ -360,6 +368,10 @@ module.exports = async function testTaskOrchestration(ctx) { '输出验证摘要', '--engine', 'openai-chat', + '--cwd', + openAiRunCwd, + '--thread-id', + 'thread-cli-run', '--concurrency', '2', '--json' @@ -367,6 +379,8 @@ module.exports = async function testTaskOrchestration(ctx) { assert(openAiRunResult.status === 0, `OpenAI Chat task run failed: ${openAiRunResult.stderr || openAiRunResult.stdout}`); const openAiRunPayload = parseJsonOutput(openAiRunResult.stdout); assertOpenAiRunPayload(openAiRunPayload, 'OpenAI Chat CLI run'); + assert(openAiRunPayload.threadId === 'thread-cli-run', 'OpenAI Chat CLI run should preserve thread id'); + assert(openAiRunPayload.cwd === openAiRunCwd, 'OpenAI Chat CLI run should preserve cwd'); const openAiLogsResult = runSync(node, [ cliPath, @@ -388,6 +402,7 @@ module.exports = async function testTaskOrchestration(ctx) { target: 'Create index.html for direct OpenAI Chat plan execution', notes: 'Write index.html only inside the provided cwd.', cwd: directPlanCwd, + threadId: 'thread-direct-plan', engine: 'openai-chat', allowWrite: true, dryRun: false, @@ -415,6 +430,8 @@ module.exports = async function testTaskOrchestration(ctx) { const directPlanRunPayload = parseJsonOutput(directPlanRunResult.stdout); assertOpenAiRunPayload(directPlanRunPayload, 'OpenAI Chat direct plan run'); assert(Array.isArray(directPlanRunPayload.plan && directPlanRunPayload.plan.waves), 'OpenAI Chat direct plan run should compute waves'); + assert(directPlanRunPayload.threadId === 'thread-direct-plan', 'OpenAI Chat direct plan run should preserve plan thread id'); + assert(directPlanRunPayload.cwd === directPlanCwd, 'OpenAI Chat direct plan run should preserve plan cwd'); const directPlanIndexPath = path.join(directPlanCwd, 'index.html'); assert(fs.existsSync(directPlanIndexPath), 'OpenAI Chat direct plan run should materialize index.html when allow-write is enabled'); assert(fs.readFileSync(directPlanIndexPath, 'utf-8').includes('2048'), 'materialized index.html should contain generated page content'); @@ -430,11 +447,17 @@ module.exports = async function testTaskOrchestration(ctx) { 'OpenAI Chat provider CLI 队列链路', '--engine', 'openai-chat', + '--cwd', + path.join(tmpHome, 'task-queue-workspace'), + '--thread-id', + 'thread-cli-queue', '--json' ], { env }); assert(openAiQueueAddResult.status === 0, `OpenAI Chat queue add failed: ${openAiQueueAddResult.stderr || openAiQueueAddResult.stdout}`); const openAiQueueAddPayload = parseJsonOutput(openAiQueueAddResult.stdout); assert(openAiQueueAddPayload.ok === true && openAiQueueAddPayload.task && openAiQueueAddPayload.task.engine === 'openai-chat', 'OpenAI Chat queue add should persist engine'); + assert(openAiQueueAddPayload.task.threadId === 'thread-cli-queue', 'OpenAI Chat queue add should persist thread id'); + assert(openAiQueueAddPayload.task.cwd === path.join(tmpHome, 'task-queue-workspace'), 'OpenAI Chat queue add should persist cwd'); const openAiQueueStartResult = runSync(node, [ cliPath, @@ -448,38 +471,58 @@ module.exports = async function testTaskOrchestration(ctx) { const openAiQueueStartPayload = parseJsonOutput(openAiQueueStartResult.stdout); assert(openAiQueueStartPayload.ok === true, 'OpenAI Chat queue start should succeed'); assertOpenAiRunPayload(openAiQueueStartPayload.detail, 'OpenAI Chat CLI queue start'); + assert(openAiQueueStartPayload.detail.threadId === 'thread-cli-queue', 'OpenAI Chat queue start should preserve thread id'); + assert(openAiQueueStartPayload.detail.cwd === path.join(tmpHome, 'task-queue-workspace'), 'OpenAI Chat queue start should preserve cwd'); const apiOpenAiPlan = await api('task-plan', { target: 'OpenAI Chat Web API 计划链路', engine: 'openai-chat', + cwd: path.join(tmpHome, 'api-plan-workspace'), + threadId: 'thread-api-plan', followUps: ['输出结论'] }); assert(apiOpenAiPlan.ok === true, 'OpenAI Chat task-plan API should validate'); assert(apiOpenAiPlan.plan && apiOpenAiPlan.plan.engine === 'openai-chat', 'OpenAI Chat task-plan API should keep engine'); + assert(apiOpenAiPlan.plan.threadId === 'thread-api-plan', 'OpenAI Chat task-plan API should preserve thread id'); + assert(apiOpenAiPlan.plan.cwd === path.join(tmpHome, 'api-plan-workspace'), 'OpenAI Chat task-plan API should preserve cwd'); assert(apiOpenAiPlan.plan.nodes.every((node) => node.kind === 'openai-chat'), 'OpenAI Chat task-plan API should produce OpenAI Chat nodes'); + const apiRunCwd = path.join(tmpHome, 'api-run-workspace'); + fs.mkdirSync(apiRunCwd, { recursive: true }); const apiOpenAiRun = await api('task-run', { target: 'OpenAI Chat Web API 同步运行链路', engine: 'openai-chat', + cwd: apiRunCwd, + threadId: 'thread-api-run', concurrency: 1 }, 15000); assertOpenAiRunPayload(apiOpenAiRun, 'OpenAI Chat API run'); + assert(apiOpenAiRun.threadId === 'thread-api-run', 'OpenAI Chat API run should preserve thread id'); + assert(apiOpenAiRun.cwd === apiRunCwd, 'OpenAI Chat API run should preserve cwd'); const apiOpenAiDetail = await api('task-run-detail', { runId: apiOpenAiRun.runId }); assert(apiOpenAiDetail && apiOpenAiDetail.run && apiOpenAiDetail.run.status === 'success', 'OpenAI Chat task-run-detail API should return run detail'); + assert(apiOpenAiDetail.threadId === 'thread-api-run', 'OpenAI Chat task-run-detail API should expose thread id'); + assert(apiOpenAiDetail.cwd === apiRunCwd, 'OpenAI Chat task-run-detail API should expose cwd'); assert(apiOpenAiDetail.run.nodes.every((node) => node.kind === 'openai-chat'), 'OpenAI Chat task-run-detail API should expose OpenAI Chat nodes'); const apiOpenAiQueueAdd = await api('task-queue-add', { target: 'OpenAI Chat Web API 队列链路', - engine: 'openai-chat' + engine: 'openai-chat', + cwd: path.join(tmpHome, 'api-queue-workspace'), + threadId: 'thread-api-queue' }); assert(apiOpenAiQueueAdd.ok === true && apiOpenAiQueueAdd.task && apiOpenAiQueueAdd.task.engine === 'openai-chat', 'OpenAI Chat task-queue-add API should persist engine'); + assert(apiOpenAiQueueAdd.task.threadId === 'thread-api-queue', 'OpenAI Chat task-queue-add API should persist thread id'); + assert(apiOpenAiQueueAdd.task.cwd === path.join(tmpHome, 'api-queue-workspace'), 'OpenAI Chat task-queue-add API should persist cwd'); const apiOpenAiQueueStart = await api('task-queue-start', { taskId: apiOpenAiQueueAdd.task.taskId, detach: false }, 15000); assert(apiOpenAiQueueStart.ok === true, 'OpenAI Chat task-queue-start API should succeed'); assertOpenAiRunPayload(apiOpenAiQueueStart.detail, 'OpenAI Chat API queue start'); + assert(apiOpenAiQueueStart.detail.threadId === 'thread-api-queue', 'OpenAI Chat task-queue-start API should preserve thread id'); + assert(apiOpenAiQueueStart.detail.cwd === path.join(tmpHome, 'api-queue-workspace'), 'OpenAI Chat task-queue-start API should preserve cwd'); const apiOpenAiOverview = await api('task-overview'); assert(Array.isArray(apiOpenAiOverview.runs), 'OpenAI Chat task-overview API should return runs after execution'); diff --git a/tests/unit/task-orchestrator.test.mjs b/tests/unit/task-orchestrator.test.mjs index 4a730d9a..c83bcfa8 100644 --- a/tests/unit/task-orchestrator.test.mjs +++ b/tests/unit/task-orchestrator.test.mjs @@ -47,6 +47,19 @@ test('buildTaskPlan generates OpenAI Chat orchestration nodes and follow-ups', ( assert.strictEqual(plan.nodes[plan.nodes.length - 1].kind, 'openai-chat'); }); +test('buildTaskPlan carries workspace and thread context into OpenAI Chat nodes', () => { + const plan = buildTaskPlan({ + target: '在指定工作区创建 2048 页面', + cwd: '/tmp/codexmate-workspace', + threadId: 'thread-2048', + allowWrite: true + }); + + assert.strictEqual(plan.cwd, '/tmp/codexmate-workspace'); + assert.strictEqual(plan.threadId, 'thread-2048'); + assert.ok(plan.nodes.some((node) => String(node.prompt || '').includes('工作区路径: /tmp/codexmate-workspace'))); +}); + test('buildTaskPlan can map workflow ids onto sequential workflow nodes', () => { const plan = buildTaskPlan({ target: '诊断并切换 provider', diff --git a/tests/unit/web-ui-behavior-parity.test.mjs b/tests/unit/web-ui-behavior-parity.test.mjs index 9366cbb8..256f7068 100644 --- a/tests/unit/web-ui-behavior-parity.test.mjs +++ b/tests/unit/web-ui-behavior-parity.test.mjs @@ -629,6 +629,7 @@ test('captured bundled app skeleton only exposes expected data key drift versus 'selectTaskRun', 'retryTaskRunFromUi', 'cancelTaskRunFromUi', + 'continueTaskThreadFromUi', 'taskOrchestrationHasLiveActivity', 'stopTaskOrchestrationPolling', 'syncTaskOrchestrationPolling', diff --git a/tests/unit/web-ui-logic.test.mjs b/tests/unit/web-ui-logic.test.mjs index d91025ae..42dc9cbe 100644 --- a/tests/unit/web-ui-logic.test.mjs +++ b/tests/unit/web-ui-logic.test.mjs @@ -1208,6 +1208,8 @@ test('ensureTaskOrchestrationState creates workbench defaults including workspac const state = methods.ensureTaskOrchestrationState.call(context); assert.strictEqual(state.workspaceTab, 'queue'); + assert.strictEqual(state.workspacePath, ''); + assert.strictEqual(state.threadId, ''); assert.strictEqual(state.selectedRunError, ''); assert.strictEqual(state.detailRequestToken, 0); assert.deepStrictEqual(state.overviewWarnings, []); @@ -1226,11 +1228,40 @@ test('ensureTaskOrchestrationState backfills missing workbench fields on existin assert.strictEqual(state.target, 'keep-me'); assert.strictEqual(state.workspaceTab, 'runs'); + assert.strictEqual(state.workspacePath, ''); + assert.strictEqual(state.threadId, ''); assert.strictEqual(state.selectedRunError, ''); assert.strictEqual(state.detailRequestToken, 0); assert.deepStrictEqual(state.overviewWarnings, []); }); +test('buildTaskOrchestrationRequest includes workspace path and thread id', () => { + const methods = createTaskOrchestrationMethods({ api: async () => ({}) }); + const context = { + taskOrchestration: { + title: '2048 task', + target: 'Create 2048 page', + notes: 'Only write inside cwd', + workspacePath: ' /tmp/codexmate-web-workspace ', + threadId: ' thread-2048 ', + followUpsText: 'Verify page', + workflowIdsText: '', + selectedEngine: 'openai-chat', + runMode: 'write', + concurrency: 1, + autoFixRounds: 0 + }, + ensureTaskOrchestrationState: methods.ensureTaskOrchestrationState + }; + + const req = methods.buildTaskOrchestrationRequest.call(context); + + assert.strictEqual(req.cwd, '/tmp/codexmate-web-workspace'); + assert.strictEqual(req.threadId, 'thread-2048'); + assert.strictEqual(req.allowWrite, true); + assert.deepStrictEqual(req.followUps, ['Verify page']); +}); + test('taskOrchestrationSelectedRunNodes prefers top-level detail nodes when present', () => { const computed = createMainTabsComputed(); const context = { @@ -1394,3 +1425,40 @@ test('selectTaskRun switches workbench to detail and keeps latest detail respons assert.strictEqual(context.taskOrchestration.selectedRunDetail.run.runId, 'run-2'); assert.strictEqual(context.taskOrchestration.selectedRunError, ''); }); + +test('continueTaskThreadFromUi inherits selected run workspace and thread', () => { + const methods = createTaskOrchestrationMethods({ api: async () => ({}) }); + const messages = []; + const context = { + taskOrchestration: { + selectedRunDetail: { + threadId: 'thread-existing', + cwd: '/tmp/existing-workspace', + title: 'Existing task', + target: 'Create 2048 page', + engine: 'openai-chat', + allowWrite: true, + dryRun: false + }, + selectedEngine: 'workflow', + runMode: 'read', + plan: { nodes: [{ id: 'old' }] }, + planIssues: ['old issue'], + planWarnings: ['old warning'] + }, + ensureTaskOrchestrationState: methods.ensureTaskOrchestrationState, + showMessage(message, tone) { + messages.push({ message, tone }); + } + }; + + methods.continueTaskThreadFromUi.call(context); + + assert.strictEqual(context.taskOrchestration.threadId, 'thread-existing'); + assert.strictEqual(context.taskOrchestration.workspacePath, '/tmp/existing-workspace'); + assert.strictEqual(context.taskOrchestration.target, 'Create 2048 page'); + assert.strictEqual(context.taskOrchestration.runMode, 'write'); + assert.strictEqual(context.taskOrchestration.plan, null); + assert.deepStrictEqual(context.taskOrchestration.planIssues, []); + assert.deepStrictEqual(messages, [{ message: '已继承该任务的会话与工作区,可继续追加要求', tone: 'success' }]); +}); diff --git a/web-ui/app.js b/web-ui/app.js index 051bf0ce..5a165695 100644 --- a/web-ui/app.js +++ b/web-ui/app.js @@ -462,6 +462,8 @@ document.addEventListener('DOMContentLoaded', () => { target: '', title: '', notes: '', + workspacePath: '', + threadId: '', followUpsText: '', workflowIdsText: '', selectedEngine: 'openai-chat', diff --git a/web-ui/modules/app.methods.task-orchestration.mjs b/web-ui/modules/app.methods.task-orchestration.mjs index e9a83fed..5a3d6049 100644 --- a/web-ui/modules/app.methods.task-orchestration.mjs +++ b/web-ui/modules/app.methods.task-orchestration.mjs @@ -9,6 +9,8 @@ function createDefaultTaskOrchestrationState() { target: '', title: '', notes: '', + workspacePath: '', + threadId: '', followUpsText: '', workflowIdsText: '', selectedEngine: 'openai-chat', @@ -105,6 +107,8 @@ export function createTaskOrchestrationMethods(options = {}) { title: String(state.title || '').trim(), target: String(state.target || '').trim(), notes: String(state.notes || '').trim(), + cwd: String(state.workspacePath || '').trim(), + threadId: String(state.threadId || '').trim(), followUps: normalizeLines(state.followUpsText), workflowIds: normalizeLines(state.workflowIdsText), engine: String(state.selectedEngine || 'openai-chat').trim().toLowerCase() === 'workflow' ? 'workflow' : 'openai-chat', @@ -121,6 +125,8 @@ export function createTaskOrchestrationMethods(options = {}) { title: req.title, target: req.target, notes: req.notes, + cwd: req.cwd, + threadId: req.threadId, followUps: req.followUps, workflowIds: req.workflowIds, engine: req.engine, @@ -222,6 +228,9 @@ export function createTaskOrchestrationMethods(options = {}) { try { const res = await api('task-plan', this.buildTaskOrchestrationRequest()); state.plan = res && res.plan ? res.plan : null; + if (state.plan && state.plan.threadId && !state.threadId) { + state.threadId = state.plan.threadId; + } state.planIssues = Array.isArray(res && res.issues) ? res.issues : []; state.planWarnings = Array.isArray(res && res.warnings) ? res.warnings : []; state.planFingerprint = state.plan ? this.buildTaskOrchestrationFingerprint() : ''; @@ -265,6 +274,9 @@ export function createTaskOrchestrationMethods(options = {}) { return res; } state.selectedRunId = res.runId || state.selectedRunId; + if (res && res.threadId) { + state.threadId = res.threadId; + } await this.loadTaskOrchestrationOverview({ silent: true, includeDetail: false }); if (state.selectedRunId) { await this.loadTaskRunDetail(state.selectedRunId, { silent: true }); @@ -295,6 +307,9 @@ export function createTaskOrchestrationMethods(options = {}) { } return res; } + if (res && res.task && res.task.threadId) { + state.threadId = res.task.threadId; + } if (!options.deferRefresh) { await this.loadTaskOrchestrationOverview({ silent: true, includeDetail: false }); } @@ -417,6 +432,12 @@ export function createTaskOrchestrationMethods(options = {}) { return res; } state.selectedRunDetail = res; + if (res && res.threadId) { + state.threadId = res.threadId; + } + if (res && res.cwd && !state.workspacePath) { + state.workspacePath = res.cwd; + } state.selectedRunError = ''; this.syncTaskOrchestrationPolling(); return res; @@ -455,6 +476,9 @@ export function createTaskOrchestrationMethods(options = {}) { return res; } state.selectedRunId = res.runId || state.selectedRunId; + if (res && res.threadId) { + state.threadId = res.threadId; + } await this.loadTaskOrchestrationOverview({ silent: true, includeDetail: false }); if (state.selectedRunId) { await this.loadTaskRunDetail(state.selectedRunId, { silent: true }); @@ -496,6 +520,26 @@ export function createTaskOrchestrationMethods(options = {}) { } }, + continueTaskThreadFromUi() { + const state = this.ensureTaskOrchestrationState(); + const detail = state.selectedRunDetail && typeof state.selectedRunDetail === 'object' ? state.selectedRunDetail : null; + if (!detail) { + return; + } + state.threadId = String(detail.threadId || state.threadId || '').trim(); + state.workspacePath = String(detail.cwd || state.workspacePath || '').trim(); + state.title = String(detail.title || state.title || '').trim(); + state.target = String(detail.target || state.target || '').trim(); + state.selectedEngine = String(detail.engine || state.selectedEngine || 'openai-chat').trim().toLowerCase() === 'workflow' ? 'workflow' : 'openai-chat'; + state.runMode = detail.dryRun ? 'dry-run' : (detail.allowWrite ? 'write' : 'read'); + state.notes = ''; + state.followUpsText = ''; + state.plan = null; + state.planIssues = []; + state.planWarnings = []; + this.showMessage('已继承该任务的会话与工作区,可继续追加要求', 'success'); + }, + taskOrchestrationHasLiveActivity() { const state = this.ensureTaskOrchestrationState(); const queue = Array.isArray(state.queue) ? state.queue : []; @@ -540,6 +584,8 @@ export function createTaskOrchestrationMethods(options = {}) { state.target = ''; state.title = ''; state.notes = ''; + state.workspacePath = ''; + state.threadId = ''; state.followUpsText = ''; state.workflowIdsText = ''; state.selectedEngine = 'openai-chat'; diff --git a/web-ui/modules/i18n/locales/en.mjs b/web-ui/modules/i18n/locales/en.mjs index 9b6737e6..7a0dc708 100644 --- a/web-ui/modules/i18n/locales/en.mjs +++ b/web-ui/modules/i18n/locales/en.mjs @@ -1041,6 +1041,12 @@ const en = Object.freeze({ 'orchestration.fields.notes': 'Notes', 'orchestration.fields.notes.placeholder': 'e.g. Do incremental changes only; do not rewrite architecture', 'orchestration.fields.notes.hint': 'Add boundaries, constraints, style rules, or verification requirements.', + 'orchestration.fields.workspacePath': 'Workspace path', + 'orchestration.fields.workspacePath.placeholder': 'e.g. /home/me/project', + 'orchestration.fields.workspacePath.hint': 'Plan, run, and queue use this cwd; leave blank to use the server current directory.', + 'orchestration.fields.threadId': 'Task thread ID', + 'orchestration.fields.threadId.placeholder': 'Optional; generated automatically when empty', + 'orchestration.fields.threadId.hint': 'Use the same thread for follow-up requirements, retries, and queue tracking.', 'orchestration.fields.followUps': 'Follow-ups (one per line)', 'orchestration.fields.followUps.placeholder': 'e.g.\nAddress new review comments\nAdd regression tests', 'orchestration.fields.concurrency': 'Concurrency', @@ -1071,6 +1077,8 @@ const en = Object.freeze({ 'orchestration.plan.summary.nodes': 'Nodes', 'orchestration.plan.summary.waves': 'Waves', 'orchestration.plan.summary.engine': 'Engine', + 'orchestration.plan.summary.threadId': 'Thread ID', + 'orchestration.plan.summary.cwd': 'Workspace', 'orchestration.plan.node.write': 'write', 'orchestration.plan.node.readOnly': 'read-only', 'orchestration.labels.dependencies': 'Dependencies: ', @@ -1090,6 +1098,7 @@ const en = Object.freeze({ 'orchestration.detail.refresh': 'Refresh detail', 'orchestration.detail.retry': 'Retry', 'orchestration.detail.retrying': 'Retrying...', + 'orchestration.detail.continueThread': 'Continue thread', 'orchestration.detail.empty.title': 'Select a run to view details', 'orchestration.detail.empty.subtitle': 'This view shows node statuses, summaries, and logs.', 'orchestration.detail.summary.status': 'Status', diff --git a/web-ui/modules/i18n/locales/ja.mjs b/web-ui/modules/i18n/locales/ja.mjs index 1a3a0c8c..3bdd0b66 100644 --- a/web-ui/modules/i18n/locales/ja.mjs +++ b/web-ui/modules/i18n/locales/ja.mjs @@ -1030,6 +1030,12 @@ const ja = Object.freeze({ 'orchestration.fields.notes': '説明', 'orchestration.fields.notes.placeholder': '例:既存アーキテクチャを書き換えず、増分実装のみ行う', 'orchestration.fields.notes.hint': '境界条件、禁止事項、スタイル要件、検証要件を補足してください。', + 'orchestration.fields.workspacePath': 'Workspace path', + 'orchestration.fields.workspacePath.placeholder': 'e.g. /home/me/project', + 'orchestration.fields.workspacePath.hint': 'Plan, run, and queue use this cwd; leave blank to use the server current directory.', + 'orchestration.fields.threadId': 'Task thread ID', + 'orchestration.fields.threadId.placeholder': 'Optional; generated automatically when empty', + 'orchestration.fields.threadId.hint': 'Use the same thread for follow-up requirements, retries, and queue tracking.', 'orchestration.fields.followUps': '後続アクション(1行1件)', 'orchestration.fields.followUps.placeholder': '例:\nレビューコメントの続きを処理\n最後に回帰テストを実行', 'orchestration.fields.concurrency': '並列度', @@ -1060,6 +1066,8 @@ const ja = Object.freeze({ 'orchestration.plan.summary.nodes': 'ノード数', 'orchestration.plan.summary.waves': '波数', 'orchestration.plan.summary.engine': 'エンジン', + 'orchestration.plan.summary.threadId': 'Thread ID', + 'orchestration.plan.summary.cwd': 'Workspace', 'orchestration.plan.node.write': '書き込み', 'orchestration.plan.node.readOnly': '読み取り専用', 'orchestration.labels.dependencies': '依存', @@ -1079,6 +1087,7 @@ const ja = Object.freeze({ 'orchestration.detail.refresh': '詳細を更新', 'orchestration.detail.retry': '再試行', 'orchestration.detail.retrying': '再試行中...', + 'orchestration.detail.continueThread': 'Continue thread', 'orchestration.detail.empty.title': '実行記録を選択して詳細を表示', 'orchestration.detail.empty.subtitle': 'ここにノードの状態、サマリー、ログが表示されます。', 'orchestration.detail.summary.status': '状態', diff --git a/web-ui/modules/i18n/locales/vi.mjs b/web-ui/modules/i18n/locales/vi.mjs index 97faeae4..eb509b81 100644 --- a/web-ui/modules/i18n/locales/vi.mjs +++ b/web-ui/modules/i18n/locales/vi.mjs @@ -1142,6 +1142,12 @@ const vi = Object.freeze({ 'orchestration.fields.notes': 'Ghi chú', 'orchestration.fields.notes.placeholder': 'vd: Chỉ thay đổi tăng dần; không viết lại kiến trúc', 'orchestration.fields.notes.hint': 'Thêm ranh giới, ràng buộc, quy tắc style hoặc yêu cầu xác minh.', + 'orchestration.fields.workspacePath': 'Workspace path', + 'orchestration.fields.workspacePath.placeholder': 'e.g. /home/me/project', + 'orchestration.fields.workspacePath.hint': 'Plan, run, and queue use this cwd; leave blank to use the server current directory.', + 'orchestration.fields.threadId': 'Task thread ID', + 'orchestration.fields.threadId.placeholder': 'Optional; generated automatically when empty', + 'orchestration.fields.threadId.hint': 'Use the same thread for follow-up requirements, retries, and queue tracking.', 'orchestration.fields.followUps': 'Theo dõi (mỗi dòng một mục)', 'orchestration.fields.followUps.placeholder': 'vd:\\nXử lý comment review mới\\nThêm test regression', 'orchestration.fields.concurrency': 'Đồng thời', @@ -1172,6 +1178,8 @@ const vi = Object.freeze({ 'orchestration.plan.summary.nodes': 'Nút', 'orchestration.plan.summary.waves': 'Đợt', 'orchestration.plan.summary.engine': 'Engine', + 'orchestration.plan.summary.threadId': 'Thread ID', + 'orchestration.plan.summary.cwd': 'Workspace', 'orchestration.plan.node.write': 'ghi', 'orchestration.plan.node.readOnly': 'chỉ đọc', 'orchestration.labels.dependencies': 'Phụ thuộc: ', @@ -1191,6 +1199,7 @@ const vi = Object.freeze({ 'orchestration.detail.refresh': 'Làm mới chi tiết', 'orchestration.detail.retry': 'Thử lại', 'orchestration.detail.retrying': 'Đang thử lại...', + 'orchestration.detail.continueThread': 'Continue thread', 'orchestration.detail.empty.title': 'Chọn một lần chạy để xem chi tiết', 'orchestration.detail.empty.subtitle': 'Chế độ xem này hiển thị trạng thái nút, tóm tắt và log.', 'orchestration.detail.summary.status': 'Trạng thái', diff --git a/web-ui/modules/i18n/locales/zh-tw.mjs b/web-ui/modules/i18n/locales/zh-tw.mjs index bdcc9104..7669231e 100644 --- a/web-ui/modules/i18n/locales/zh-tw.mjs +++ b/web-ui/modules/i18n/locales/zh-tw.mjs @@ -1040,6 +1040,12 @@ const zhTw = Object.freeze({ 'orchestration.fields.notes': '說明', 'orchestration.fields.notes.placeholder': '例如:不要重寫現有架構,只做增量實現', 'orchestration.fields.notes.hint': '補邊界、禁區、風格要求或驗證要求。', + 'orchestration.fields.workspacePath': '工作区路径', + 'orchestration.fields.workspacePath.placeholder': '例如:/home/me/project', + 'orchestration.fields.workspacePath.hint': '计划、执行和队列都会使用这个 cwd;留空则使用服务当前目录。', + 'orchestration.fields.threadId': '任务线程 ID', + 'orchestration.fields.threadId.placeholder': '可选;留空自动生成', + 'orchestration.fields.threadId.hint': '同一线程用于连续追加要求、重试和队列追踪。', 'orchestration.fields.followUps': '後續動作(每行一條)', 'orchestration.fields.followUps.placeholder': '例如:\n繼續處理 review 評論\n最後補回歸測試', 'orchestration.fields.concurrency': '併發', @@ -1070,6 +1076,8 @@ const zhTw = Object.freeze({ 'orchestration.plan.summary.nodes': '節點數', 'orchestration.plan.summary.waves': '波次', 'orchestration.plan.summary.engine': '引擎', + 'orchestration.plan.summary.threadId': '线程 ID', + 'orchestration.plan.summary.cwd': '工作区', 'orchestration.plan.node.write': 'write', 'orchestration.plan.node.readOnly': 'read-only', 'orchestration.labels.dependencies': '依賴:', @@ -1089,6 +1097,7 @@ const zhTw = Object.freeze({ 'orchestration.detail.refresh': '重新整理詳情', 'orchestration.detail.retry': '重試', 'orchestration.detail.retrying': '重試中...', + 'orchestration.detail.continueThread': '继续此线程', 'orchestration.detail.empty.title': '選擇一條運行記錄查看詳情', 'orchestration.detail.empty.subtitle': '這裡會顯示節點狀態、摘要和日誌。', 'orchestration.detail.summary.status': '狀態', diff --git a/web-ui/modules/i18n/locales/zh.mjs b/web-ui/modules/i18n/locales/zh.mjs index ba1a552c..13463f3e 100644 --- a/web-ui/modules/i18n/locales/zh.mjs +++ b/web-ui/modules/i18n/locales/zh.mjs @@ -1040,6 +1040,12 @@ const zh = Object.freeze({ 'orchestration.fields.notes': '说明', 'orchestration.fields.notes.placeholder': '例如:不要重写现有架构,只做增量实现', 'orchestration.fields.notes.hint': '补边界、禁区、风格要求或验证要求。', + 'orchestration.fields.workspacePath': '工作区路径', + 'orchestration.fields.workspacePath.placeholder': '例如:/home/me/project', + 'orchestration.fields.workspacePath.hint': '计划、执行和队列都会使用这个 cwd;留空则使用服务当前目录。', + 'orchestration.fields.threadId': '任务线程 ID', + 'orchestration.fields.threadId.placeholder': '可选;留空自动生成', + 'orchestration.fields.threadId.hint': '同一线程用于连续追加要求、重试和队列追踪。', 'orchestration.fields.followUps': '后续动作(每行一条)', 'orchestration.fields.followUps.placeholder': '例如:\n继续处理 review 评论\n最后补回归测试', 'orchestration.fields.concurrency': '并发', @@ -1070,6 +1076,8 @@ const zh = Object.freeze({ 'orchestration.plan.summary.nodes': '节点数', 'orchestration.plan.summary.waves': '波次', 'orchestration.plan.summary.engine': '引擎', + 'orchestration.plan.summary.threadId': '线程 ID', + 'orchestration.plan.summary.cwd': '工作区', 'orchestration.plan.node.write': 'write', 'orchestration.plan.node.readOnly': 'read-only', 'orchestration.labels.dependencies': '依赖:', @@ -1089,6 +1097,7 @@ const zh = Object.freeze({ 'orchestration.detail.refresh': '刷新详情', 'orchestration.detail.retry': '重试', 'orchestration.detail.retrying': '重试中...', + 'orchestration.detail.continueThread': '继续此线程', 'orchestration.detail.empty.title': '选择一条运行记录查看详情', 'orchestration.detail.empty.subtitle': '这里会显示节点状态、摘要和日志。', 'orchestration.detail.summary.status': '状态', diff --git a/web-ui/partials/index/panel-orchestration.html b/web-ui/partials/index/panel-orchestration.html index b6852966..ad915278 100644 --- a/web-ui/partials/index/panel-orchestration.html +++ b/web-ui/partials/index/panel-orchestration.html @@ -122,6 +122,16 @@ {{ t('orchestration.fields.notes.hint') }} + +
-
- {{ t('orchestration.plan.summary.engine') }} - {{ taskOrchestration.plan.engine }} +
+ {{ t('orchestration.plan.summary.engine') }} + {{ taskOrchestration.plan.engine }} +
+
+ {{ t('orchestration.plan.summary.threadId') }} + {{ taskOrchestration.plan.threadId }} +
+
+ {{ t('orchestration.plan.summary.cwd') }} + {{ taskOrchestration.plan.cwd }} +
-
{{ wave.label }}
@@ -297,11 +315,16 @@ @click="item.lastRunId ? (taskOrchestration.workspaceTab = 'detail', selectTaskRun(item.lastRunId)) : null" @keydown.enter.self.prevent="item.lastRunId ? (taskOrchestration.workspaceTab = 'detail', selectTaskRun(item.lastRunId)) : null" @keydown.space.self.prevent="item.lastRunId ? (taskOrchestration.workspaceTab = 'detail', selectTaskRun(item.lastRunId)) : null"> -
-
{{ item.title || item.target || item.taskId }}
-
{{ item.taskId }} · {{ item.updatedAt || item.createdAt }}
-
{{ item.lastSummary }}
-
+
+
{{ item.title || item.target || item.taskId }}
+
{{ item.taskId }} · {{ item.updatedAt || item.createdAt }}
+
+ {{ t('orchestration.plan.summary.threadId') }}: {{ item.threadId }} + · + {{ t('orchestration.plan.summary.cwd') }}: {{ item.cwd }} +
+
{{ item.lastSummary }}
+
{{ item.status }} @@ -325,6 +348,11 @@
{{ item.title || item.taskId || item.runId }}
{{ item.runId }} · {{ item.durationMs || 0 }}ms
+
+ {{ t('orchestration.plan.summary.threadId') }}: {{ item.threadId }} + · + {{ t('orchestration.plan.summary.cwd') }}: {{ item.cwd }} +
{{ item.summary }}
@@ -342,6 +370,9 @@ + @@ -370,6 +401,14 @@ {{ t('orchestration.detail.summary.summary') }} {{ taskOrchestrationSelectedRun.run.summary || t('common.none') }}
+
+ {{ t('orchestration.plan.summary.threadId') }} + {{ taskOrchestrationSelectedRun.threadId }} +
+
+ {{ t('orchestration.plan.summary.cwd') }} + {{ taskOrchestrationSelectedRun.cwd }} +
{{ taskOrchestrationSelectedRun.run.error }}
diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index 5a125fab..7b744690 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -4206,6 +4206,30 @@ return function render(_ctx, _cache) { ]), _createElementVNode("span", { class: "task-field-hint" }, _toDisplayString(_ctx.t('orchestration.fields.notes.hint')), 1 /* TEXT */) ]), + _createElementVNode("label", { class: "selector-field task-field-wide" }, [ + _createElementVNode("span", { class: "selector-label" }, _toDisplayString(_ctx.t('orchestration.fields.workspacePath')), 1 /* TEXT */), + _withDirectives(_createElementVNode("input", { + "onUpdate:modelValue": $event => ((_ctx.taskOrchestration.workspacePath) = $event), + class: "model-input", + type: "text", + placeholder: _ctx.t('orchestration.fields.workspacePath.placeholder') + }, null, 8 /* PROPS */, ["onUpdate:modelValue", "placeholder"]), [ + [_vModelText, _ctx.taskOrchestration.workspacePath] + ]), + _createElementVNode("span", { class: "task-field-hint" }, _toDisplayString(_ctx.t('orchestration.fields.workspacePath.hint')), 1 /* TEXT */) + ]), + _createElementVNode("label", { class: "selector-field" }, [ + _createElementVNode("span", { class: "selector-label" }, _toDisplayString(_ctx.t('orchestration.fields.threadId')), 1 /* TEXT */), + _withDirectives(_createElementVNode("input", { + "onUpdate:modelValue": $event => ((_ctx.taskOrchestration.threadId) = $event), + class: "model-input", + type: "text", + placeholder: _ctx.t('orchestration.fields.threadId.placeholder') + }, null, 8 /* PROPS */, ["onUpdate:modelValue", "placeholder"]), [ + [_vModelText, _ctx.taskOrchestration.threadId] + ]), + _createElementVNode("span", { class: "task-field-hint" }, _toDisplayString(_ctx.t('orchestration.fields.threadId.hint')), 1 /* TEXT */) + ]), _createElementVNode("label", { class: "selector-field task-field-wide" }, [ _createElementVNode("span", { class: "selector-label" }, _toDisplayString(_ctx.t('orchestration.fields.followUps')), 1 /* TEXT */), _withDirectives(_createElementVNode("textarea", { @@ -4387,7 +4411,25 @@ return function render(_ctx, _cache) { _createElementVNode("div", { class: "task-plan-summary-item" }, [ _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.engine')), 1 /* TEXT */), _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.plan.engine), 1 /* TEXT */) - ]) + ]), + (_ctx.taskOrchestration.plan.threadId) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-plan-summary-item" + }, [ + _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.threadId')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.plan.threadId), 1 /* TEXT */) + ])) + : _createCommentVNode("v-if", true), + (_ctx.taskOrchestration.plan.cwd) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-plan-summary-item" + }, [ + _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.cwd')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.plan.cwd), 1 /* TEXT */) + ])) + : _createCommentVNode("v-if", true) ]), _createElementVNode("div", { class: "task-wave-list" }, [ (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestration.plan.waves, (wave) => { @@ -4522,9 +4564,25 @@ return function render(_ctx, _cache) { _createElementVNode("div", { class: "task-runtime-item-main" }, [ _createElementVNode("div", { class: "task-runtime-item-title" }, _toDisplayString(item.title || item.target || item.taskId), 1 /* TEXT */), _createElementVNode("div", { class: "task-runtime-item-meta" }, _toDisplayString(item.taskId) + " · " + _toDisplayString(item.updatedAt || item.createdAt), 1 /* TEXT */), - (item.lastSummary) + (item.threadId || item.cwd) ? (_openBlock(), _createElementBlock("div", { key: 0, + class: "task-runtime-item-meta" + }, [ + (item.threadId) + ? (_openBlock(), _createElementBlock("span", { key: 0 }, _toDisplayString(_ctx.t('orchestration.plan.summary.threadId')) + ": " + _toDisplayString(item.threadId), 1 /* TEXT */)) + : _createCommentVNode("v-if", true), + (item.threadId && item.cwd) + ? (_openBlock(), _createElementBlock("span", { key: 1 }, " · ")) + : _createCommentVNode("v-if", true), + (item.cwd) + ? (_openBlock(), _createElementBlock("span", { key: 2 }, _toDisplayString(_ctx.t('orchestration.plan.summary.cwd')) + ": " + _toDisplayString(item.cwd), 1 /* TEXT */)) + : _createCommentVNode("v-if", true) + ])) + : _createCommentVNode("v-if", true), + (item.lastSummary) + ? (_openBlock(), _createElementBlock("div", { + key: 1, class: "task-runtime-item-summary" }, _toDisplayString(item.lastSummary), 1 /* TEXT */)) : _createCommentVNode("v-if", true) @@ -4573,9 +4631,25 @@ return function render(_ctx, _cache) { _createElementVNode("div", { class: "task-runtime-item-main" }, [ _createElementVNode("div", { class: "task-runtime-item-title" }, _toDisplayString(item.title || item.taskId || item.runId), 1 /* TEXT */), _createElementVNode("div", { class: "task-runtime-item-meta" }, _toDisplayString(item.runId) + " · " + _toDisplayString(item.durationMs || 0) + "ms", 1 /* TEXT */), - (item.summary) + (item.threadId || item.cwd) ? (_openBlock(), _createElementBlock("div", { key: 0, + class: "task-runtime-item-meta" + }, [ + (item.threadId) + ? (_openBlock(), _createElementBlock("span", { key: 0 }, _toDisplayString(_ctx.t('orchestration.plan.summary.threadId')) + ": " + _toDisplayString(item.threadId), 1 /* TEXT */)) + : _createCommentVNode("v-if", true), + (item.threadId && item.cwd) + ? (_openBlock(), _createElementBlock("span", { key: 1 }, " · ")) + : _createCommentVNode("v-if", true), + (item.cwd) + ? (_openBlock(), _createElementBlock("span", { key: 2 }, _toDisplayString(_ctx.t('orchestration.plan.summary.cwd')) + ": " + _toDisplayString(item.cwd), 1 /* TEXT */)) + : _createCommentVNode("v-if", true) + ])) + : _createCommentVNode("v-if", true), + (item.summary) + ? (_openBlock(), _createElementBlock("div", { + key: 1, class: "task-runtime-item-summary" }, _toDisplayString(item.summary), 1 /* TEXT */)) : _createCommentVNode("v-if", true) @@ -4606,6 +4680,12 @@ return function render(_ctx, _cache) { onClick: $event => (_ctx.retryTaskRunFromUi(_ctx.taskOrchestration.selectedRunId)), disabled: !_ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.retrying }, _toDisplayString(_ctx.taskOrchestration.retrying ? _ctx.t('orchestration.detail.retrying') : _ctx.t('orchestration.detail.retry')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), + _createElementVNode("button", { + type: "button", + class: "btn-tool btn-tool-compact", + onClick: _ctx.continueTaskThreadFromUi, + disabled: !_ctx.taskOrchestrationSelectedRun + }, _toDisplayString(_ctx.t('orchestration.detail.continueThread')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), (_ctx.taskOrchestrationSelectedRun && _ctx.taskOrchestrationSelectedRun.run && _ctx.isTaskRunActive(_ctx.taskOrchestrationSelectedRun.run.status)) ? (_openBlock(), _createElementBlock("button", { key: 0, @@ -4646,7 +4726,25 @@ return function render(_ctx, _cache) { _createElementVNode("div", { class: "task-plan-summary-item" }, [ _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.detail.summary.summary')), 1 /* TEXT */), _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationSelectedRun.run.summary || _ctx.t('common.none')), 1 /* TEXT */) - ]) + ]), + (_ctx.taskOrchestrationSelectedRun.threadId) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-plan-summary-item" + }, [ + _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.threadId')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationSelectedRun.threadId), 1 /* TEXT */) + ])) + : _createCommentVNode("v-if", true), + (_ctx.taskOrchestrationSelectedRun.cwd) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-plan-summary-item" + }, [ + _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.cwd')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationSelectedRun.cwd), 1 /* TEXT */) + ])) + : _createCommentVNode("v-if", true) ]), (_ctx.taskOrchestrationSelectedRun.run.error) ? (_openBlock(), _createElementBlock("div", { From 0d5a772f0e069b57fef9c88efc1394d9aa238c13 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sun, 28 Jun 2026 03:58:35 +0000 Subject: [PATCH 08/54] fix(task-orchestration): harden artifact run state --- cli.js | 62 ++++++++++++++++++- lib/task-orchestrator.js | 3 +- tests/e2e/test-task-orchestration.js | 54 +++++++++++++++- tests/unit/task-codex-exec-windows.test.mjs | 3 + tests/unit/task-orchestrator.test.mjs | 10 +++ tests/unit/web-ui-logic.test.mjs | 6 +- web-ui/modules/app.computed.main-tabs.mjs | 2 +- .../app.methods.task-orchestration.mjs | 9 ++- .../partials/index/panel-orchestration.html | 8 +-- web-ui/res/web-ui-render.precompiled.js | 12 ++-- 10 files changed, 150 insertions(+), 19 deletions(-) diff --git a/cli.js b/cli.js index 54c3d12a..276ea373 100644 --- a/cli.js +++ b/cli.js @@ -16131,6 +16131,27 @@ function buildOpenAiChatEndpointUrl(baseUrl) { return joinApiUrl(trimmed, 'chat/completions'); } +function redactTaskEndpointUrl(endpointUrl = '') { + const raw = String(endpointUrl || '').trim(); + if (!raw) return ''; + try { + const parsed = new URL(raw); + if (parsed.username) parsed.username = '***'; + if (parsed.password) parsed.password = '***'; + const secretKeyPattern = /(?:^|[_-])(?:key|api[-_]?key|token|access[-_]?token|refresh[-_]?token|secret|password|passwd|pwd|credential|credentials|signature|sig)(?:$|[_-])/i; + for (const key of [...parsed.searchParams.keys()]) { + if (secretKeyPattern.test(key)) { + parsed.searchParams.set(key, '***'); + } + } + return parsed.toString(); + } catch (_) { + return raw + .replace(/\/\/([^/@:]+):([^/@]+)@/g, '//***:***@') + .replace(/([?&][^=&]*(?:key|token|secret|password|passwd|pwd|credential|signature|sig)[^=]*=)[^&]*/ig, '$1***'); + } +} + function pickTaskProviderModel(providerName, provider, config) { const currentModels = readCurrentModels(); const savedModel = currentModels && typeof currentModels[providerName] === 'string' @@ -16314,6 +16335,40 @@ function normalizeTaskMaterializedArtifactPath(rawPath, cwd) { return { path: targetPath, relativePath: normalized, baseDir }; } +function validateTaskMaterializedArtifactParents(targetPath, baseDir) { + const resolvedBase = fs.realpathSync.native(baseDir); + let current = path.dirname(targetPath); + const pending = []; + while (true) { + if (current === resolvedBase) { + return { ok: true }; + } + if (current === path.dirname(current)) { + return { ok: false, error: 'artifact parent escapes cwd' }; + } + if (fs.existsSync(current)) { + const stats = fs.lstatSync(current); + if (stats.isSymbolicLink()) { + return { ok: false, error: `artifact parent is a symlink: ${path.relative(baseDir, current) || current}` }; + } + const resolvedCurrent = fs.realpathSync.native(current); + const relative = path.relative(resolvedBase, resolvedCurrent); + if (relative.startsWith('..') || path.isAbsolute(relative)) { + return { ok: false, error: 'artifact parent resolves outside cwd' }; + } + for (const child of pending) { + const childRelative = path.relative(resolvedBase, child); + if (childRelative.startsWith('..') || path.isAbsolute(childRelative)) { + return { ok: false, error: 'artifact parent escapes cwd' }; + } + } + return { ok: true }; + } + pending.push(current); + current = path.dirname(current); + } +} + function inferTaskMaterializedArtifactPath(info, prefix, hints) { const sources = [info, prefix, hints].map((item) => String(item || '')).filter(Boolean); const explicitPatterns = [ @@ -16376,6 +16431,11 @@ function materializeOpenAiChatTaskArtifacts(text, options = {}) { seen.add(normalized.path); try { const fileContent = body.trimStart(); + const parentValidation = validateTaskMaterializedArtifactParents(normalized.path, normalized.baseDir); + if (!parentValidation.ok) { + warnings.push(parentValidation.error || `artifact parent is unsafe: ${normalized.relativePath}`); + continue; + } ensureDir(path.dirname(normalized.path)); fs.writeFileSync(normalized.path, fileContent, { encoding: 'utf-8', mode: 0o600 }); files.push({ path: normalized.path, relativePath: normalized.relativePath, bytes: Buffer.byteLength(fileContent, 'utf-8') }); @@ -16442,7 +16502,7 @@ async function runOpenAiChatTaskNode(node, context = {}) { const success = result.ok && !!text; const errorMessage = success ? '' : (result.error || 'OpenAI Chat response did not contain text'); const summary = truncateTaskText(text || errorMessage, 400); - const safeEndpoint = requestConfig.endpointUrl.replace(/([?&](?:key|api_key|token)=)[^&]+/ig, '$1***'); + const safeEndpoint = redactTaskEndpointUrl(requestConfig.endpointUrl); const materialized = success && allowWrite ? materializeOpenAiChatTaskArtifacts(text, { cwd: context.cwd || process.cwd(), diff --git a/lib/task-orchestrator.js b/lib/task-orchestrator.js index a63c8faa..439e876d 100644 --- a/lib/task-orchestrator.js +++ b/lib/task-orchestrator.js @@ -109,7 +109,8 @@ function normalizeTaskEngine(value) { } function normalizeTaskNodeKind(value) { - const normalized = normalizeId(value, 'openai-chat'); + const normalized = normalizeId(value, ''); + if (!normalized) return ''; if (normalized === 'workflow') return 'workflow'; if (normalized === 'codex') return 'openai-chat'; if (normalized === 'openai' || normalized === 'openai-chat' || normalized === 'chat') return 'openai-chat'; diff --git a/tests/e2e/test-task-orchestration.js b/tests/e2e/test-task-orchestration.js index ce326520..fe88d3a5 100644 --- a/tests/e2e/test-task-orchestration.js +++ b/tests/e2e/test-task-orchestration.js @@ -44,7 +44,9 @@ const server = http.createServer((req, res) => { if (req.method === 'POST' && requestPath === '/v1/chat/completions') { const model = parsedBody && parsedBody.model ? parsedBody.model : 'unknown-model'; const requestText = JSON.stringify(parsedBody || {}); - const content = requestText.includes('index.html') + const content = requestText.includes('symlink-artifact-probe') + ? '输出文件:link/index.html\\n\`\`\`html\\n

symlink escape

\\n\`\`\`' + : requestText.includes('index.html') ? '输出文件:index.html\\n\`\`\`html\\n2048 Probe

2048

2 4 8 16
\\n\`\`\`' : 'openai-chat-e2e-ok model=' + model + ' request=' + requestCount; const body = JSON.stringify({ @@ -330,7 +332,7 @@ module.exports = async function testTaskOrchestration(ctx) { const openAiMock = await startOpenAiChatMock(tmpHome); try { - writeOpenAiChatConfig(tmpHome, `http://127.0.0.1:${openAiMock.port}`); + writeOpenAiChatConfig(tmpHome, `http://codex-user:codex-secret@127.0.0.1:${openAiMock.port}`); const openAiPlanResult = runSync(node, [ cliPath, @@ -437,6 +439,54 @@ module.exports = async function testTaskOrchestration(ctx) { assert(fs.readFileSync(directPlanIndexPath, 'utf-8').includes('2048'), 'materialized index.html should contain generated page content'); const materializedFiles = directPlanRunPayload.run.nodes.flatMap(item => item && item.output && Array.isArray(item.output.materializedFiles) ? item.output.materializedFiles : []); assert(materializedFiles.some(item => item.relativePath === 'index.html'), 'OpenAI Chat direct plan run should report materialized index.html'); + const directNodeOutputs = JSON.stringify(directPlanRunPayload.run.nodes.map(item => item && item.output ? item.output : {})); + assert(!directNodeOutputs.includes('codex-secret'), 'OpenAI Chat node output should redact endpoint URL userinfo secrets'); + assert(directNodeOutputs.includes('***'), 'OpenAI Chat node output should keep a redacted endpoint marker'); + + const symlinkPlanPath = path.join(tmpHome, 'task-symlink-openai-plan.json'); + const symlinkPlanCwd = path.join(tmpHome, 'task-symlink-plan-workspace'); + const symlinkEscapeDir = path.join(tmpHome, 'task-symlink-escape-target'); + fs.mkdirSync(symlinkPlanCwd, { recursive: true }); + fs.mkdirSync(symlinkEscapeDir, { recursive: true }); + fs.symlinkSync(symlinkEscapeDir, path.join(symlinkPlanCwd, 'link'), 'dir'); + fs.writeFileSync(symlinkPlanPath, JSON.stringify({ + id: 'task-symlink-openai-plan', + title: 'Symlink OpenAI Chat plan', + target: 'symlink-artifact-probe', + notes: 'The model will try to write link/index.html; this must be rejected because link is a symlink.', + cwd: symlinkPlanCwd, + threadId: 'thread-symlink-plan', + engine: 'openai-chat', + allowWrite: true, + dryRun: false, + concurrency: 1, + nodes: [ + { + id: 'symlink-openai-node', + title: 'Symlink OpenAI node', + kind: 'openai-chat', + prompt: 'symlink-artifact-probe: return link/index.html in an html fenced block.', + dependsOn: [] + } + ] + }, null, 2), 'utf-8'); + const symlinkPlanRunResult = runSync(node, [ + cliPath, + 'task', + 'run', + '--plan', + `@${symlinkPlanPath}`, + '--allow-write', + '--json' + ], { env }); + assert(symlinkPlanRunResult.status === 0, `OpenAI Chat symlink plan run failed: ${symlinkPlanRunResult.stderr || symlinkPlanRunResult.stdout}`); + const symlinkPlanRunPayload = parseJsonOutput(symlinkPlanRunResult.stdout); + assertOpenAiRunPayload(symlinkPlanRunPayload, 'OpenAI Chat symlink plan run'); + assert(!fs.existsSync(path.join(symlinkEscapeDir, 'index.html')), 'OpenAI Chat materialization must not follow symlinked parents outside cwd'); + const symlinkMaterializedFiles = symlinkPlanRunPayload.run.nodes.flatMap(item => item && item.output && Array.isArray(item.output.materializedFiles) ? item.output.materializedFiles : []); + assert.strictEqual(symlinkMaterializedFiles.length, 0, 'OpenAI Chat symlink materialization should not report escaped files'); + const symlinkLogs = JSON.stringify(symlinkPlanRunPayload.run.nodes.flatMap(item => Array.isArray(item && item.logs) ? item.logs : [])); + assert(symlinkLogs.includes('artifact parent is a symlink'), 'OpenAI Chat symlink rejection should be visible in run logs'); const openAiQueueAddResult = runSync(node, [ cliPath, diff --git a/tests/unit/task-codex-exec-windows.test.mjs b/tests/unit/task-codex-exec-windows.test.mjs index 727a623e..bd682fa0 100644 --- a/tests/unit/task-codex-exec-windows.test.mjs +++ b/tests/unit/task-codex-exec-windows.test.mjs @@ -113,6 +113,9 @@ test('runOpenAiChatTaskNode uses configured OpenAI Chat provider without spawnin truncateTaskText(text, limit) { return String(text || '').slice(0, limit || 1000); }, + redactTaskEndpointUrl(endpointUrl) { + return String(endpointUrl || '').replace(/sk-unit-secret/g, '***'); + }, toIsoTime() { return '2026-06-27T15:30:00.000Z'; }, diff --git a/tests/unit/task-orchestrator.test.mjs b/tests/unit/task-orchestrator.test.mjs index c83bcfa8..15f8b547 100644 --- a/tests/unit/task-orchestrator.test.mjs +++ b/tests/unit/task-orchestrator.test.mjs @@ -94,6 +94,16 @@ test('buildTaskPlan keeps workflowIds requests in workflow mode without explicit assert.deepStrictEqual(plan.nodes[0].input.followUps, ['总结风险']); }); +test('validateTaskPlan rejects external nodes with missing kind', () => { + const result = validateTaskPlan({ + engine: 'openai-chat', + nodes: [{ id: 'missing-kind', prompt: 'Do work' }] + }); + + assert.strictEqual(result.ok, false); + assert.ok(result.issues.some((issue) => issue.code === 'task-node-kind-invalid')); +}); + test('buildTaskPlan keeps workflow follow-ups inside the final workflow node payload', () => { const plan = buildTaskPlan({ target: '诊断并整理结果', diff --git a/tests/unit/web-ui-logic.test.mjs b/tests/unit/web-ui-logic.test.mjs index 42dc9cbe..2cf7e760 100644 --- a/tests/unit/web-ui-logic.test.mjs +++ b/tests/unit/web-ui-logic.test.mjs @@ -1286,6 +1286,8 @@ test('taskOrchestrationQueueStats counts queue statuses in one pass without chan { status: 'queued' }, { status: ' queued ' }, { status: 'running' }, + { runStatus: 'running' }, + { runStatus: 'queued' }, { status: 'FAILED' }, { status: 'ignored' } ] @@ -1295,8 +1297,8 @@ test('taskOrchestrationQueueStats counts queue statuses in one pass without chan const stats = computed.taskOrchestrationQueueStats.call(context); assert.deepStrictEqual(stats, { - queued: 2, - running: 1, + queued: 3, + running: 2, failed: 1 }); }); diff --git a/web-ui/modules/app.computed.main-tabs.mjs b/web-ui/modules/app.computed.main-tabs.mjs index eaac21fe..f279b6ab 100644 --- a/web-ui/modules/app.computed.main-tabs.mjs +++ b/web-ui/modules/app.computed.main-tabs.mjs @@ -206,7 +206,7 @@ export function createMainTabsComputed() { : []; const stats = { queued: 0, running: 0, failed: 0 }; for (const item of queue) { - const status = String(item && item.status || '').trim().toLowerCase(); + const status = String((item && (item.status || item.runStatus)) || '').trim().toLowerCase(); if (status === 'queued') stats.queued += 1; else if (status === 'running') stats.running += 1; else if (status === 'failed') stats.failed += 1; diff --git a/web-ui/modules/app.methods.task-orchestration.mjs b/web-ui/modules/app.methods.task-orchestration.mjs index 5a3d6049..8e0af2b8 100644 --- a/web-ui/modules/app.methods.task-orchestration.mjs +++ b/web-ui/modules/app.methods.task-orchestration.mjs @@ -71,6 +71,12 @@ function normalizeTaskRunMode(value) { return 'write'; } +function normalizeTaskSelectedEngine(value) { + const normalized = String(value || '').trim().toLowerCase().replace(/[\s_]+/g, '-'); + if (normalized === 'workflow') return 'workflow'; + return 'openai-chat'; +} + function buildRunModeFlags(runMode) { const normalized = normalizeTaskRunMode(runMode); return { @@ -94,6 +100,7 @@ export function createTaskOrchestrationMethods(options = {}) { } } current.runMode = normalizeTaskRunMode(current.runMode); + current.selectedEngine = normalizeTaskSelectedEngine(current.selectedEngine); return current; } this.taskOrchestration = createDefaultTaskOrchestrationState(); @@ -111,7 +118,7 @@ export function createTaskOrchestrationMethods(options = {}) { threadId: String(state.threadId || '').trim(), followUps: normalizeLines(state.followUpsText), workflowIds: normalizeLines(state.workflowIdsText), - engine: String(state.selectedEngine || 'openai-chat').trim().toLowerCase() === 'workflow' ? 'workflow' : 'openai-chat', + engine: normalizeTaskSelectedEngine(state.selectedEngine), allowWrite: flags.allowWrite, dryRun: flags.dryRun, concurrency: normalizePositiveInteger(state.concurrency, 2, 1, 8), diff --git a/web-ui/partials/index/panel-orchestration.html b/web-ui/partials/index/panel-orchestration.html index ad915278..4cfe9185 100644 --- a/web-ui/partials/index/panel-orchestration.html +++ b/web-ui/partials/index/panel-orchestration.html @@ -104,14 +104,14 @@
- {{ item.status }} - + {{ item.status || item.runStatus }} +
diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index 7b744690..4909130c 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -4171,8 +4171,7 @@ return function render(_ctx, _cache) { _createElementVNode("span", { class: "selector-label" }, _toDisplayString(_ctx.t('orchestration.fields.engine')), 1 /* TEXT */), _withDirectives(_createElementVNode("select", { "onUpdate:modelValue": $event => ((_ctx.taskOrchestration.selectedEngine) = $event), - class: "provider-fast-switch-select", - disabled: "" + class: "provider-fast-switch-select" }, [ _createElementVNode("option", { value: "openai-chat" }, _toDisplayString(_ctx.t('orchestration.engine.openaiChat')), 1 /* TEXT */), _createElementVNode("option", { value: "workflow" }, _toDisplayString(_ctx.t('orchestration.engine.workflow')), 1 /* TEXT */) @@ -4184,8 +4183,7 @@ return function render(_ctx, _cache) { _createElementVNode("span", { class: "selector-label" }, _toDisplayString(_ctx.t('orchestration.fields.runMode')), 1 /* TEXT */), _withDirectives(_createElementVNode("select", { "onUpdate:modelValue": $event => ((_ctx.taskOrchestration.runMode) = $event), - class: "provider-fast-switch-select", - disabled: "" + class: "provider-fast-switch-select" }, [ _createElementVNode("option", { value: "write" }, _toDisplayString(_ctx.t('orchestration.runMode.write')), 1 /* TEXT */), _createElementVNode("option", { value: "read" }, _toDisplayString(_ctx.t('orchestration.runMode.readOnly')), 1 /* TEXT */), @@ -4589,9 +4587,9 @@ return function render(_ctx, _cache) { ]), _createElementVNode("div", { class: "task-runtime-item-actions" }, [ _createElementVNode("span", { - class: _normalizeClass(['pill', _ctx.taskRunStatusTone(item.status)]) - }, _toDisplayString(item.status), 3 /* TEXT, CLASS */), - (_ctx.isTaskRunActive(item.status)) + class: _normalizeClass(['pill', _ctx.taskRunStatusTone(item.status || item.runStatus)]) + }, _toDisplayString(item.status || item.runStatus), 3 /* TEXT, CLASS */), + (_ctx.isTaskRunActive(item.status || item.runStatus)) ? (_openBlock(), _createElementBlock("button", { key: 0, type: "button", From 1c434acfcec520aecddce3024b3a6a0a1c8ea8fb Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sun, 28 Jun 2026 04:01:45 +0000 Subject: [PATCH 09/54] test(task-orchestration): align e2e assertions --- tests/e2e/test-task-orchestration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/test-task-orchestration.js b/tests/e2e/test-task-orchestration.js index fe88d3a5..a8d4746a 100644 --- a/tests/e2e/test-task-orchestration.js +++ b/tests/e2e/test-task-orchestration.js @@ -484,7 +484,7 @@ module.exports = async function testTaskOrchestration(ctx) { assertOpenAiRunPayload(symlinkPlanRunPayload, 'OpenAI Chat symlink plan run'); assert(!fs.existsSync(path.join(symlinkEscapeDir, 'index.html')), 'OpenAI Chat materialization must not follow symlinked parents outside cwd'); const symlinkMaterializedFiles = symlinkPlanRunPayload.run.nodes.flatMap(item => item && item.output && Array.isArray(item.output.materializedFiles) ? item.output.materializedFiles : []); - assert.strictEqual(symlinkMaterializedFiles.length, 0, 'OpenAI Chat symlink materialization should not report escaped files'); + assert(symlinkMaterializedFiles.length === 0, 'OpenAI Chat symlink materialization should not report escaped files'); const symlinkLogs = JSON.stringify(symlinkPlanRunPayload.run.nodes.flatMap(item => Array.isArray(item && item.logs) ? item.logs : [])); assert(symlinkLogs.includes('artifact parent is a symlink'), 'OpenAI Chat symlink rejection should be visible in run logs'); From d756bc5f7665d228d082096ba7d25264759456a7 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sun, 28 Jun 2026 04:35:43 +0000 Subject: [PATCH 10/54] feat(task-orchestration): make composer chat-first --- tests/unit/config-tabs-ui.test.mjs | 9 ++++++ tests/unit/i18n-locales.test.mjs | 24 +++++++++++++++ web-ui/modules/i18n/locales/en.mjs | 30 ++++++++++++------- web-ui/modules/i18n/locales/ja.mjs | 30 ++++++++++++------- web-ui/modules/i18n/locales/vi.mjs | 30 ++++++++++++------- web-ui/modules/i18n/locales/zh-tw.mjs | 30 ++++++++++++------- web-ui/modules/i18n/locales/zh.mjs | 30 ++++++++++++------- .../partials/index/panel-orchestration.html | 13 +++++--- web-ui/res/web-ui-render.precompiled.js | 17 ++++++++--- web-ui/styles/task-orchestration.css | 30 +++++++++++++++++++ 10 files changed, 180 insertions(+), 63 deletions(-) diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index 42850161..1b89fbe4 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -95,6 +95,13 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(orchestrationPanel, /class="selector-section task-hero-card"/); assert.match(orchestrationPanel, /class="[^"]*task-layout-grid task-layout-grid-primary[^"]*"/); assert.match(orchestrationPanel, /class="[^"]*task-quick-card[^"]*"/); + assert.match(orchestrationPanel, /class="[^"]*task-chat-composer[^"]*"/); + assert.match(orchestrationPanel, /t\('orchestration\.chat\.input\.label'\)/); + assert.match(orchestrationPanel, /t\('orchestration\.chat\.input\.placeholder'\)/); + assert.match(orchestrationPanel, /t\('orchestration\.chat\.context\.workspace\.auto'\)/); + assert.match(orchestrationPanel, /t\('orchestration\.chat\.context\.thread\.auto'\)/); + assert.match(orchestrationPanel, /class="task-chat-context-row" role="group" :aria-label="t\('orchestration\.chat\.context\.aria'\)"/); + assert.match(orchestrationPanel, /class="task-chat-context-chip"/); assert.match(orchestrationPanel, /class="task-quick-template-grid"/); assert.match(orchestrationPanel, /class="task-workflow-suggestions"/); assert.match(orchestrationPanel, /@click="appendTaskWorkflowId\(workflow.id \|\| workflow.name\)"/); @@ -122,6 +129,8 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(styles, /\.task-hero-card,[\s\S]*\.task-empty-state\s*\{[\s\S]*border:\s*1px solid/); assert.match(styles, /\.task-template-chip-group,[\s\S]*\.task-workflow-suggestions\s*\{[\s\S]*display:\s*flex;[\s\S]*flex-wrap:\s*wrap;/); assert.match(styles, /\.task-checklist-inline\s*\{[\s\S]*grid-template-columns:\s*repeat\(2, minmax\(0, 1fr\)\);/); + assert.match(styles, /\.task-chat-context-row\s*\{[\s\S]*display:\s*flex;[\s\S]*flex-wrap:\s*wrap;/); + assert.match(styles, /\.task-chat-context-chip\s*\{[\s\S]*border-radius:\s*999px;[\s\S]*text-overflow:\s*ellipsis;/); assert.match(styles, /\.task-stage-empty\s*\{[\s\S]*display:\s*flex;/); assert.match(styles, /\.task-workbench-tabs\s*\{[\s\S]*display:\s*flex;/); assert.match(styles, /\.task-action-row-right\s*\{[\s\S]*display:\s*flex;[\s\S]*flex-wrap:\s*wrap;/); diff --git a/tests/unit/i18n-locales.test.mjs b/tests/unit/i18n-locales.test.mjs index 28169856..1c4cfa11 100644 --- a/tests/unit/i18n-locales.test.mjs +++ b/tests/unit/i18n-locales.test.mjs @@ -181,6 +181,30 @@ test('provider cache and local Web preference settings are localized in every lo } }); +test('task orchestration chat composer copy is localized in every locale', () => { + const keys = [ + 'orchestration.chat.input.label', + 'orchestration.chat.input.placeholder', + 'orchestration.chat.input.hint', + 'orchestration.chat.context.aria', + 'orchestration.chat.context.workspace.auto', + 'orchestration.chat.context.workspace.value', + 'orchestration.chat.context.thread.auto', + 'orchestration.chat.context.thread.value' + ]; + for (const code of expectedLocales) { + for (const key of keys) { + assert.strictEqual(typeof DICT[code][key], 'string', `${code} should define ${key}`); + assert(DICT[code][key].trim(), `${code} ${key} should not be empty`); + assert.deepStrictEqual( + placeholders(DICT[code][key]), + placeholders(DICT.zh[key]), + `${code} placeholder mismatch for key: ${key}` + ); + } + } +}); + test('plugins catalog metadata is localized from i18n dictionaries', async () => { const { createPluginsComputed } = await import('../../plugins/prompt-templates/computed.mjs'); diff --git a/web-ui/modules/i18n/locales/en.mjs b/web-ui/modules/i18n/locales/en.mjs index 7a0dc708..a5580502 100644 --- a/web-ui/modules/i18n/locales/en.mjs +++ b/web-ui/modules/i18n/locales/en.mjs @@ -960,7 +960,7 @@ const en = Object.freeze({ 'orchestration.queueStats': 'Queue: {running} running · {queued} queued', 'orchestration.hero.kicker': 'Task orchestration', 'orchestration.hero.title': 'Turn goals into executable steps', - 'orchestration.hero.subtitle': 'Write a target, preview a plan, then run.', + 'orchestration.hero.subtitle': 'Send a task message, preview a plan, then run.', 'orchestration.quick.kicker': 'Quick task', 'orchestration.quick.title': 'Tell Codexmate what to finish', 'orchestration.quick.subtitle': 'Start with the outcome. Codexmate will preview a plan, run it, and keep advanced options out of the way until needed.', @@ -968,6 +968,14 @@ const en = Object.freeze({ 'orchestration.quick.templates.reviewFix.meta': 'Fix review feedback and validate regressions.', 'orchestration.quick.templates.planOnly.meta': 'Investigate first without writing files.', 'orchestration.quick.templates.workflowBatch.meta': 'Reuse local workflows for repeatable checks.', + 'orchestration.chat.input.label': 'Task message', + 'orchestration.chat.input.placeholder': 'Tell Codexmate what to finish, e.g. fix current PR review comments, add regression tests, and attach verification screenshots', + 'orchestration.chat.input.hint': 'Write it like a chat message; workspace, thread, and run mode are inferred by default and stay in Advanced when needed.', + 'orchestration.chat.context.aria': 'Task context', + 'orchestration.chat.context.workspace.auto': 'Workspace auto-detected', + 'orchestration.chat.context.workspace.value': 'Workspace {value}', + 'orchestration.chat.context.thread.auto': 'Thread auto-generated', + 'orchestration.chat.context.thread.value': 'Thread {value}', 'orchestration.quick.checklist.title': 'Ready check', 'orchestration.quick.checklist.subtitle': 'Only the blockers that affect execution stay visible.', 'orchestration.quick.status.title': 'Current draft', @@ -976,9 +984,9 @@ const en = Object.freeze({ 'orchestration.summary.running': 'Running', 'orchestration.summary.queued': 'Queued', 'orchestration.summary.runs': 'Runs', - 'orchestration.readiness.target.label': 'Target', - 'orchestration.readiness.target.done': 'Target written', - 'orchestration.readiness.target.missing': 'Target not written yet', + 'orchestration.readiness.target.label': 'Task message', + 'orchestration.readiness.target.done': 'Task message written', + 'orchestration.readiness.target.missing': 'Task message not written yet', 'orchestration.readiness.engine.label': 'Execution strategy', 'orchestration.readiness.engine.openaiChat': 'Use OpenAI Chat-compatible nodes', 'orchestration.readiness.workflow.done': '{count} workflow(s) selected', @@ -991,8 +999,8 @@ const en = Object.freeze({ 'orchestration.readiness.preview.missing': 'No plan generated yet', 'orchestration.readiness.preview.blocked': '{count} blocker(s)', 'orchestration.readiness.preview.ready': 'Plan ready, {count} node(s)', - 'orchestration.readiness.empty.title': 'Write a target first', - 'orchestration.readiness.empty.summary': 'Describe the outcome first, then let the orchestrator split it into nodes.', + 'orchestration.readiness.empty.title': 'Write a task message first', + 'orchestration.readiness.empty.summary': 'Describe the outcome like a chat message first, then let the orchestrator split it into nodes.', 'orchestration.readiness.workflow.title': 'Workflow missing', 'orchestration.readiness.workflow.summary': 'Workflow mode is selected, but no reusable workflow is specified yet.', 'orchestration.readiness.preview.title': 'Preview recommended', @@ -1020,9 +1028,9 @@ const en = Object.freeze({ 'orchestration.templates.workflowBatch.target': 'Run a fixed set of checks using workflows and summarize results', 'orchestration.templates.workflowBatch.workflowIds': 'diagnose-config\nsafe-provider-switch', 'orchestration.templates.workflowBatch.notes': 'Produce a unified conclusion; avoid repetitive narration', - 'orchestration.fields.target': 'Target', - 'orchestration.fields.target.placeholder': 'e.g. Address PR review comments and add regression tests; avoid unrelated modules', - 'orchestration.fields.target.hint': 'One sentence is enough: outcome, constraints, and acceptance criteria.', + 'orchestration.fields.target': 'Task message', + 'orchestration.fields.target.placeholder': 'e.g. Fix current PR review comments, add regression tests, and avoid unrelated modules', + 'orchestration.fields.target.hint': 'One chat-style message is enough: outcome, constraints, and acceptance criteria.', 'orchestration.engine.openaiChat': 'OpenAI Chat', 'orchestration.engine.workflow': 'Workflow', 'orchestration.runMode.write': 'Write', @@ -1068,8 +1076,8 @@ const en = Object.freeze({ 'orchestration.actions.queueAndStart': 'Queue & start', 'orchestration.actions.caption': '“Plan & run” refreshes the plan when needed; use “Queue & start” for batch runs.', 'orchestration.stage.title': 'Workspace opens when there is data', - 'orchestration.stage.subtitle': 'Write the target first, then preview the plan.', - 'orchestration.stage.pill.target': 'Write target', + 'orchestration.stage.subtitle': 'Send a task message first, then preview the plan.', + 'orchestration.stage.pill.target': 'Write message', 'orchestration.stage.pill.preview': 'Preview', 'orchestration.stage.pill.run': 'Run or queue', 'orchestration.plan.title': 'Plan preview', diff --git a/web-ui/modules/i18n/locales/ja.mjs b/web-ui/modules/i18n/locales/ja.mjs index 3bdd0b66..c1987e9e 100644 --- a/web-ui/modules/i18n/locales/ja.mjs +++ b/web-ui/modules/i18n/locales/ja.mjs @@ -949,7 +949,7 @@ const ja = Object.freeze({ 'orchestration.queueStats': 'キュー {running} 実行中 · {queued} 待機中', 'orchestration.hero.kicker': 'タスクオーケストレーション', 'orchestration.hero.title': '要件を実行可能なステップに分解', - 'orchestration.hero.subtitle': '最初に目標を書き、計画をプレビューし、実行します。', + 'orchestration.hero.subtitle': 'タスクメッセージを送り、計画をプレビューしてから実行します。', 'orchestration.quick.kicker': 'クイックタスク', 'orchestration.quick.title': 'Codexmate に何を完了させますか?', 'orchestration.quick.subtitle': 'まずゴールを書くだけで、計画をプレビューしてから実行します。詳細設定は必要になるまで隠します。', @@ -957,6 +957,14 @@ const ja = Object.freeze({ 'orchestration.quick.templates.reviewFix.meta': 'レビュー指摘を直し、回帰確認します。', 'orchestration.quick.templates.planOnly.meta': '書き込みなしで調査と計画だけ行います。', 'orchestration.quick.templates.workflowBatch.meta': 'ローカル Workflow で定型チェックします。', + 'orchestration.chat.input.label': 'タスクメッセージ', + 'orchestration.chat.input.placeholder': 'Codexmate に完了してほしいことを直接書いてください。例: 現在の PR レビューコメントを修正し、回帰テストを追加し、検証スクリーンショットを添付する', + 'orchestration.chat.input.hint': 'チャットのように要件を書くだけで、workspace・thread・run mode は既定で自動推定され、必要な時だけ Advanced に表示されます。', + 'orchestration.chat.context.aria': 'タスクコンテキスト', + 'orchestration.chat.context.workspace.auto': 'Workspace を自動検出', + 'orchestration.chat.context.workspace.value': 'Workspace {value}', + 'orchestration.chat.context.thread.auto': 'Thread を自動生成', + 'orchestration.chat.context.thread.value': 'Thread {value}', 'orchestration.quick.checklist.title': '実行前チェック', 'orchestration.quick.checklist.subtitle': '実行に影響するブロッカーだけを表示します。', 'orchestration.quick.status.title': '現在の下書き', @@ -965,9 +973,9 @@ const ja = Object.freeze({ 'orchestration.summary.running': '実行中', 'orchestration.summary.queued': '待機中', 'orchestration.summary.runs': '実行記録', - 'orchestration.readiness.target.label': 'ターゲット', - 'orchestration.readiness.target.done': 'ターゲット入力済み', - 'orchestration.readiness.target.missing': 'ターゲット未入力', + 'orchestration.readiness.target.label': 'タスクメッセージ', + 'orchestration.readiness.target.done': 'タスクメッセージを入力済み', + 'orchestration.readiness.target.missing': 'タスクメッセージが未入力', 'orchestration.readiness.engine.label': '実行方針', 'orchestration.readiness.engine.openaiChat': 'OpenAI Chat 互換ノードで計画', 'orchestration.readiness.workflow.done': '{count} 件の Workflow を選択済み', @@ -980,8 +988,8 @@ const ja = Object.freeze({ 'orchestration.readiness.preview.missing': '計画は未生成', 'orchestration.readiness.preview.blocked': '{count} 件のブロッカー', 'orchestration.readiness.preview.ready': '計画利用可能、{count} ノード', - 'orchestration.readiness.empty.title': 'まずターゲットを入力', - 'orchestration.readiness.empty.summary': '達成したい結果を明確にしてから、オーケストレーターにノード分解させます。', + 'orchestration.readiness.empty.title': 'まずタスクメッセージを書く', + 'orchestration.readiness.empty.summary': '達成したい結果をチャットのように明確にしてから、オーケストレーターにノード分解させます。', 'orchestration.readiness.workflow.title': 'Workflow が不足', 'orchestration.readiness.workflow.summary': 'Workflow モードが選択されていますが、再利用する Workflow がまだ指定されていません。', 'orchestration.readiness.preview.title': '先にプレビュー推奨', @@ -1009,9 +1017,9 @@ const ja = Object.freeze({ 'orchestration.templates.workflowBatch.target': 'Workflow 一括実行', 'orchestration.templates.workflowBatch.workflowIds': 'review-fix\nregression-test', 'orchestration.templates.workflowBatch.notes': 'Workflow で固定チェックを実行し、結果を整理する', - 'orchestration.fields.target': 'ターゲット', - 'orchestration.fields.target.placeholder': '例: /path/to/project', - 'orchestration.fields.target.hint': '作業ディレクトリまたはファイルパス。', + 'orchestration.fields.target': 'タスクメッセージ', + 'orchestration.fields.target.placeholder': '例: 現在の PR レビューコメントを修正し、回帰テストを追加し、関連しないモジュールは避ける', + 'orchestration.fields.target.hint': 'チャットのように、結果・制約・受け入れ条件を書けば十分です。', 'orchestration.engine.openaiChat': 'OpenAI Chat', 'orchestration.engine.workflow': 'Workflow', 'orchestration.runMode.write': '書き込み', @@ -1057,8 +1065,8 @@ const ja = Object.freeze({ 'orchestration.actions.queueAndStart': 'キューに入れて開始', 'orchestration.actions.caption': '「計画して実行」は必要に応じて計画を自動更新します。バッチタスクは「キューに入れて開始」を使用してください。', 'orchestration.stage.title': 'タスクステージ', - 'orchestration.stage.subtitle': '3ステップでタスクを完了:計画 → プレビュー → 実行。', - 'orchestration.stage.pill.target': 'ターゲット', + 'orchestration.stage.subtitle': 'まずタスクメッセージを書き、計画をプレビューします。', + 'orchestration.stage.pill.target': 'メッセージを書く', 'orchestration.stage.pill.preview': 'プレビュー', 'orchestration.stage.pill.run': '実行', 'orchestration.plan.title': '計画', diff --git a/web-ui/modules/i18n/locales/vi.mjs b/web-ui/modules/i18n/locales/vi.mjs index eb509b81..14fd4011 100644 --- a/web-ui/modules/i18n/locales/vi.mjs +++ b/web-ui/modules/i18n/locales/vi.mjs @@ -284,9 +284,9 @@ const vi = Object.freeze({ 'modal.providerCache.errorDetails': 'Chi tiết lỗi', 'modal.providerCache.loadFailed': 'Không tải được bản ghi cache', - 'orchestration.readiness.target.label': 'Mục tiêu', - 'orchestration.readiness.target.done': 'Đã viết mục tiêu', - 'orchestration.readiness.target.missing': 'Chưa viết mục tiêu', + 'orchestration.readiness.target.label': 'Tin nhắn tác vụ', + 'orchestration.readiness.target.done': 'Đã có tin nhắn tác vụ', + 'orchestration.readiness.target.missing': 'Chưa có tin nhắn tác vụ', 'orchestration.readiness.engine.label': 'Chiến lược chạy', 'orchestration.readiness.engine.openaiChat': 'Dùng node tương thích OpenAI Chat', 'orchestration.readiness.workflow.done': 'Đã chọn {count} Workflow', @@ -299,8 +299,8 @@ const vi = Object.freeze({ 'orchestration.readiness.preview.missing': 'Chưa tạo kế hoạch', 'orchestration.readiness.preview.blocked': 'Có {count} mục chặn', 'orchestration.readiness.preview.ready': 'Kế hoạch sẵn sàng, {count} node', - 'orchestration.readiness.empty.title': 'Viết mục tiêu trước', - 'orchestration.readiness.empty.summary': 'Mô tả rõ kết quả cần đạt, rồi để bộ điều phối tách thành các node.', + 'orchestration.readiness.empty.title': 'Viết tin nhắn tác vụ trước', + 'orchestration.readiness.empty.summary': 'Mô tả kết quả như một tin nhắn chat trước, rồi để bộ điều phối tách thành các node.', 'orchestration.readiness.workflow.title': 'Thiếu Workflow', 'orchestration.readiness.workflow.summary': 'Bạn đã chọn chế độ Workflow nhưng chưa chỉ định quy trình tái sử dụng.', 'orchestration.readiness.preview.title': 'Nên xem trước trước', @@ -1091,7 +1091,7 @@ const vi = Object.freeze({ 'orchestration.queueStats': 'Hàng đợi: {running} đang chạy · {queued} đang chờ', 'orchestration.hero.kicker': 'Điều phối tác vụ', 'orchestration.hero.title': 'Biến mục tiêu thành các bước thực thi', - 'orchestration.hero.subtitle': 'Viết mục tiêu, xem trước kế hoạch, rồi chạy.', + 'orchestration.hero.subtitle': 'Gửi một tin nhắn tác vụ, xem trước kế hoạch, rồi chạy.', 'orchestration.quick.kicker': 'Tác vụ nhanh', 'orchestration.quick.title': 'Bạn muốn Codexmate hoàn thành việc gì?', 'orchestration.quick.subtitle': 'Chỉ cần mô tả kết quả. Codexmate sẽ xem trước kế hoạch rồi chạy; thiết lập nâng cao được ẩn cho tới khi cần.', @@ -1099,6 +1099,14 @@ const vi = Object.freeze({ 'orchestration.quick.templates.reviewFix.meta': 'Sửa góp ý review và kiểm thử hồi quy.', 'orchestration.quick.templates.planOnly.meta': 'Chỉ điều tra và lập kế hoạch, không ghi file.', 'orchestration.quick.templates.workflowBatch.meta': 'Dùng Workflow cục bộ cho kiểm tra lặp lại.', + 'orchestration.chat.input.label': 'Tin nhắn tác vụ', + 'orchestration.chat.input.placeholder': 'Nói thẳng Codexmate cần hoàn thành gì, vd: sửa comment review PR hiện tại, thêm test regression và đính kèm ảnh xác minh', + 'orchestration.chat.input.hint': 'Viết như một tin nhắn chat; workspace, thread và chế độ chạy được tự suy luận mặc định, khi cần mới mở Nâng cao.', + 'orchestration.chat.context.aria': 'Ngữ cảnh tác vụ', + 'orchestration.chat.context.workspace.auto': 'Tự phát hiện workspace', + 'orchestration.chat.context.workspace.value': 'Workspace {value}', + 'orchestration.chat.context.thread.auto': 'Tự tạo thread', + 'orchestration.chat.context.thread.value': 'Thread {value}', 'orchestration.quick.checklist.title': 'Kiểm tra sẵn sàng', 'orchestration.quick.checklist.subtitle': 'Chỉ hiển thị blocker ảnh hưởng tới việc chạy.', 'orchestration.quick.status.title': 'Bản nháp hiện tại', @@ -1121,9 +1129,9 @@ const vi = Object.freeze({ 'orchestration.templates.workflowBatch.target': 'Chạy một bộ kiểm tra cố định bằng workflow và tóm tắt kết quả', 'orchestration.templates.workflowBatch.workflowIds': 'diagnose-config\\nsafe-provider-switch', 'orchestration.templates.workflowBatch.notes': 'Đưa ra kết luận thống nhất; tránh diễn giải lặp lại', - 'orchestration.fields.target': 'Mục tiêu', - 'orchestration.fields.target.placeholder': 'vd: Xử lý comment review PR và thêm test regression; tránh các module không liên quan', - 'orchestration.fields.target.hint': 'Một câu là đủ: kết quả, ràng buộc và tiêu chí chấp nhận.', + 'orchestration.fields.target': 'Tin nhắn tác vụ', + 'orchestration.fields.target.placeholder': 'vd: sửa comment review PR hiện tại, thêm test regression và tránh module không liên quan', + 'orchestration.fields.target.hint': 'Một tin nhắn kiểu chat là đủ: kết quả, ràng buộc và tiêu chí chấp nhận.', 'orchestration.engine.openaiChat': 'OpenAI Chat', 'orchestration.engine.workflow': 'Workflow', 'orchestration.runMode.write': 'Ghi', @@ -1169,8 +1177,8 @@ const vi = Object.freeze({ 'orchestration.actions.queueAndStart': 'Xếp hàng & bắt đầu', 'orchestration.actions.caption': '"Lập kế hoạch & chạy" làm mới kế hoạch khi cần; dùng "Xếp hàng & bắt đầu" cho chạy hàng loạt.', 'orchestration.stage.title': 'Không gian làm việc mở khi có dữ liệu', - 'orchestration.stage.subtitle': 'Viết mục tiêu trước, rồi xem trước kế hoạch.', - 'orchestration.stage.pill.target': 'Viết mục tiêu', + 'orchestration.stage.subtitle': 'Gửi tin nhắn tác vụ trước, rồi xem trước kế hoạch.', + 'orchestration.stage.pill.target': 'Viết tin nhắn', 'orchestration.stage.pill.preview': 'Xem trước', 'orchestration.stage.pill.run': 'Chạy hoặc xếp hàng', 'orchestration.plan.title': 'Xem trước kế hoạch', diff --git a/web-ui/modules/i18n/locales/zh-tw.mjs b/web-ui/modules/i18n/locales/zh-tw.mjs index 7669231e..07770a06 100644 --- a/web-ui/modules/i18n/locales/zh-tw.mjs +++ b/web-ui/modules/i18n/locales/zh-tw.mjs @@ -959,7 +959,7 @@ const zhTw = Object.freeze({ 'orchestration.queueStats': '隊列 {running} 運行中 · {queued} 等待中', 'orchestration.hero.kicker': '任務編排', 'orchestration.hero.title': '把需求拆成可執行步驟', - 'orchestration.hero.subtitle': '先寫目標,再預覽計劃,再執行。', + 'orchestration.hero.subtitle': '發一條任務訊息,預覽計畫,再執行。', 'orchestration.quick.kicker': '快速任務', 'orchestration.quick.title': '你想讓 Codexmate 完成什麼?', 'orchestration.quick.subtitle': '先寫結果,編排器會預覽計畫、再執行;進階設定預設收起,不把引擎艙塞到使用者面前。', @@ -967,6 +967,14 @@ const zhTw = Object.freeze({ 'orchestration.quick.templates.reviewFix.meta': '處理 review 回饋並補回歸驗證。', 'orchestration.quick.templates.planOnly.meta': '先排查規劃,不寫入檔案。', 'orchestration.quick.templates.workflowBatch.meta': '複用本地 Workflow 跑固定檢查。', + 'orchestration.chat.input.label': '任務訊息', + 'orchestration.chat.input.placeholder': '直接告訴 Codexmate 要完成什麼,例如:修復目前 PR review 評論,補回歸測試,並提供驗證截圖', + 'orchestration.chat.input.hint': '像發訊息一樣寫需求;目前 workspace、執行緒和運行方式預設自動帶入,需要時再展開進階設定。', + 'orchestration.chat.context.aria': '任務上下文', + 'orchestration.chat.context.workspace.auto': '工作區自動推斷', + 'orchestration.chat.context.workspace.value': '工作區 {value}', + 'orchestration.chat.context.thread.auto': '執行緒自動生成', + 'orchestration.chat.context.thread.value': '執行緒 {value}', 'orchestration.quick.checklist.title': '發布前檢查', 'orchestration.quick.checklist.subtitle': '只保留真正影響執行的阻塞項。', 'orchestration.quick.status.title': '目前草稿', @@ -975,9 +983,9 @@ const zhTw = Object.freeze({ 'orchestration.summary.running': '運行中', 'orchestration.summary.queued': '排隊中', 'orchestration.summary.runs': '運行記錄', - 'orchestration.readiness.target.label': '目標', - 'orchestration.readiness.target.done': '已寫目標', - 'orchestration.readiness.target.missing': '還沒寫目標', + 'orchestration.readiness.target.label': '任務訊息', + 'orchestration.readiness.target.done': '已寫任務訊息', + 'orchestration.readiness.target.missing': '還沒寫任務訊息', 'orchestration.readiness.engine.label': '執行策略', 'orchestration.readiness.engine.openaiChat': '使用 OpenAI Chat-compatible 節點', 'orchestration.readiness.workflow.done': '已選 {count} 個 Workflow', @@ -990,8 +998,8 @@ const zhTw = Object.freeze({ 'orchestration.readiness.preview.missing': '還沒生成計劃', 'orchestration.readiness.preview.blocked': '有 {count} 個阻塞項', 'orchestration.readiness.preview.ready': '計劃可用,{count} 個節點', - 'orchestration.readiness.empty.title': '先寫目標', - 'orchestration.readiness.empty.summary': '先把想完成的結果寫清楚,再讓編排器拆節點。', + 'orchestration.readiness.empty.title': '先寫任務訊息', + 'orchestration.readiness.empty.summary': '先像聊天一樣寫清楚想完成的結果,再讓編排器拆節點。', 'orchestration.readiness.workflow.title': '缺少 Workflow', 'orchestration.readiness.workflow.summary': '你已經選了 Workflow 模式,但還沒指定可複用流程。', 'orchestration.readiness.preview.title': '建議先預覽', @@ -1019,9 +1027,9 @@ const zhTw = Object.freeze({ 'orchestration.templates.workflowBatch.target': '用 Workflow 跑一組固定檢查並整理結果', 'orchestration.templates.workflowBatch.workflowIds': 'diagnose-config\nsafe-provider-switch', 'orchestration.templates.workflowBatch.notes': '輸出統一結論,避免重複描述', - 'orchestration.fields.target': '目標', - 'orchestration.fields.target.placeholder': '例如:修復目前 PR review 評論,並補對應迴歸測試;不要改無關模塊', - 'orchestration.fields.target.hint': '一句話寫清結果、邊界和驗收標準就夠了。', + 'orchestration.fields.target': '任務訊息', + 'orchestration.fields.target.placeholder': '例如:修復目前 PR review 評論,補對應迴歸測試,並避免改無關模塊', + 'orchestration.fields.target.hint': '像發訊息一樣寫清結果、邊界和驗收標準就夠了。', 'orchestration.engine.openaiChat': 'OpenAI Chat', 'orchestration.engine.workflow': 'Workflow', 'orchestration.runMode.write': '寫入', @@ -1067,8 +1075,8 @@ const zhTw = Object.freeze({ 'orchestration.actions.queueAndStart': '入隊並開始', 'orchestration.actions.caption': '“計劃並執行”會在必要時自動重新整理計劃;批量任務用“入隊並開始”。', 'orchestration.stage.title': '有內容時再展開工作區', - 'orchestration.stage.subtitle': '先寫目標,再預覽計劃。', - 'orchestration.stage.pill.target': '寫目標', + 'orchestration.stage.subtitle': '先發任務訊息,再預覽計畫。', + 'orchestration.stage.pill.target': '寫任務訊息', 'orchestration.stage.pill.preview': '預覽', 'orchestration.stage.pill.run': '執行或入隊', 'orchestration.plan.title': '計劃預覽', diff --git a/web-ui/modules/i18n/locales/zh.mjs b/web-ui/modules/i18n/locales/zh.mjs index 13463f3e..3c50ace5 100644 --- a/web-ui/modules/i18n/locales/zh.mjs +++ b/web-ui/modules/i18n/locales/zh.mjs @@ -959,7 +959,7 @@ const zh = Object.freeze({ 'orchestration.queueStats': '队列 {running} 运行中 · {queued} 等待中', 'orchestration.hero.kicker': '任务编排', 'orchestration.hero.title': '把需求拆成可执行步骤', - 'orchestration.hero.subtitle': '先写目标,再预览计划,再执行。', + 'orchestration.hero.subtitle': '发一条任务消息,预览计划,再执行。', 'orchestration.quick.kicker': '快速任务', 'orchestration.quick.title': '你想让 Codexmate 完成什么?', 'orchestration.quick.subtitle': '先写结果,编排器会预览计划、再执行;高级设置默认收起,不把发动机舱塞到用户脸上。', @@ -967,6 +967,14 @@ const zh = Object.freeze({ 'orchestration.quick.templates.reviewFix.meta': '处理 review 反馈并补回归验证。', 'orchestration.quick.templates.planOnly.meta': '先排查规划,不写入文件。', 'orchestration.quick.templates.workflowBatch.meta': '复用本地 Workflow 跑固定检查。', + 'orchestration.chat.input.label': '任务消息', + 'orchestration.chat.input.placeholder': '直接告诉 Codexmate 要完成什么,例如:修复当前 PR review 评论,补回归测试,并给出验证截图', + 'orchestration.chat.input.hint': '像发消息一样写需求;当前 workspace、线程和运行方式默认自动带入,需要时再展开高级设置。', + 'orchestration.chat.context.aria': '任务上下文', + 'orchestration.chat.context.workspace.auto': '工作区自动推断', + 'orchestration.chat.context.workspace.value': '工作区 {value}', + 'orchestration.chat.context.thread.auto': '线程自动生成', + 'orchestration.chat.context.thread.value': '线程 {value}', 'orchestration.quick.checklist.title': '发布前检查', 'orchestration.quick.checklist.subtitle': '只保留真正影响执行的阻塞项。', 'orchestration.quick.status.title': '当前草稿', @@ -975,9 +983,9 @@ const zh = Object.freeze({ 'orchestration.summary.running': '运行中', 'orchestration.summary.queued': '排队中', 'orchestration.summary.runs': '运行记录', - 'orchestration.readiness.target.label': '目标', - 'orchestration.readiness.target.done': '已写目标', - 'orchestration.readiness.target.missing': '还没写目标', + 'orchestration.readiness.target.label': '任务消息', + 'orchestration.readiness.target.done': '已写任务消息', + 'orchestration.readiness.target.missing': '还没写任务消息', 'orchestration.readiness.engine.label': '执行策略', 'orchestration.readiness.engine.openaiChat': '使用 OpenAI Chat-compatible 节点', 'orchestration.readiness.workflow.done': '已选 {count} 个 Workflow', @@ -990,8 +998,8 @@ const zh = Object.freeze({ 'orchestration.readiness.preview.missing': '还没生成计划', 'orchestration.readiness.preview.blocked': '有 {count} 个阻塞项', 'orchestration.readiness.preview.ready': '计划可用,{count} 个节点', - 'orchestration.readiness.empty.title': '先写目标', - 'orchestration.readiness.empty.summary': '先把想完成的结果写清楚,再让编排器拆节点。', + 'orchestration.readiness.empty.title': '先写任务消息', + 'orchestration.readiness.empty.summary': '先像聊天一样写清楚想完成的结果,再让编排器拆节点。', 'orchestration.readiness.workflow.title': '缺少 Workflow', 'orchestration.readiness.workflow.summary': '你已经选了 Workflow 模式,但还没指定可复用流程。', 'orchestration.readiness.preview.title': '建议先预览', @@ -1019,9 +1027,9 @@ const zh = Object.freeze({ 'orchestration.templates.workflowBatch.target': '用 Workflow 跑一组固定检查并整理结果', 'orchestration.templates.workflowBatch.workflowIds': 'diagnose-config\nsafe-provider-switch', 'orchestration.templates.workflowBatch.notes': '输出统一结论,避免重复描述', - 'orchestration.fields.target': '目标', - 'orchestration.fields.target.placeholder': '例如:修复当前 PR review 评论,并补对应回归测试;不要改无关模块', - 'orchestration.fields.target.hint': '一句话写清结果、边界和验收标准就够了。', + 'orchestration.fields.target': '任务消息', + 'orchestration.fields.target.placeholder': '例如:修复当前 PR review 评论,补对应回归测试,并避免改无关模块', + 'orchestration.fields.target.hint': '像发消息一样写清结果、边界和验收标准就够了。', 'orchestration.engine.openaiChat': 'OpenAI Chat', 'orchestration.engine.workflow': 'Workflow', 'orchestration.runMode.write': '写入', @@ -1067,8 +1075,8 @@ const zh = Object.freeze({ 'orchestration.actions.queueAndStart': '入队并开始', 'orchestration.actions.caption': '“计划并执行”会在必要时自动刷新计划;批量任务用“入队并开始”。', 'orchestration.stage.title': '有内容时再展开工作区', - 'orchestration.stage.subtitle': '先写目标,再预览计划。', - 'orchestration.stage.pill.target': '写目标', + 'orchestration.stage.subtitle': '先发任务消息,再预览计划。', + 'orchestration.stage.pill.target': '写任务消息', 'orchestration.stage.pill.preview': '预览', 'orchestration.stage.pill.run': '执行或入队', 'orchestration.plan.title': '计划预览', diff --git a/web-ui/partials/index/panel-orchestration.html b/web-ui/partials/index/panel-orchestration.html index 4cfe9185..0debce49 100644 --- a/web-ui/partials/index/panel-orchestration.html +++ b/web-ui/partials/index/panel-orchestration.html @@ -41,11 +41,16 @@
-
diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index 62dd586d..922bd8e3 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -4080,93 +4080,598 @@ return function render(_ctx, _cache) { : _createCommentVNode("v-if", true) ]) ], 2 /* CLASS */)) - }), 128 /* KEYED_FRAGMENT */)) + }), 128 /* KEYED_FRAGMENT */)), + (_ctx.taskOrchestration.plan || _ctx.taskOrchestration.planIssues.length || _ctx.taskOrchestration.planWarnings.length || _ctx.taskOrchestration.lastError) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-chat-bubble-row is-user task-thread-plan-request" + }, [ + _createElementVNode("div", { class: "task-chat-bubble" }, [ + _createElementVNode("div", { class: "task-chat-bubble-label" }, "You · latest request"), + _createElementVNode("div", { class: "task-chat-bubble-text" }, _toDisplayString(_ctx.taskOrchestration.target), 1 /* TEXT */), + (_ctx.taskOrchestrationDraftMetrics.followUpCount) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-chat-bubble-meta" + }, _toDisplayString(_ctx.t('orchestration.chat.input.sequenceHint', { count: _ctx.taskOrchestrationDraftMetrics.followUpCount + 1 })), 1 /* TEXT */)) + : _createCommentVNode("v-if", true) + ]) + ])) + : _createCommentVNode("v-if", true), + (_ctx.taskOrchestration.plan || _ctx.taskOrchestration.planIssues.length || _ctx.taskOrchestration.planWarnings.length || _ctx.taskOrchestration.lastError) + ? (_openBlock(), _createElementBlock("section", { + key: 1, + class: "selector-section task-plan-card task-thread-message-card task-thread-plan-card" + }, [ + _createElementVNode("div", { class: "task-thread-card-label" }, "AI · " + _toDisplayString(_ctx.t('orchestration.plan.title')), 1 /* TEXT */), + (_ctx.taskOrchestration.lastError) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-issue-item" + }, _toDisplayString(_ctx.taskOrchestration.lastError), 1 /* TEXT */)) + : _createCommentVNode("v-if", true), + _createElementVNode("div", { class: "selector-header task-section-header" }, [ + _createElementVNode("div", null, [ + _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('orchestration.plan.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "skills-panel-note" }, _toDisplayString(_ctx.t('orchestration.plan.subtitle')), 1 /* TEXT */) + ]) + ]), + (_ctx.taskOrchestration.planIssues.length) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-issues-list" + }, [ + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestration.planIssues, (issue) => { + return (_openBlock(), _createElementBlock("div", { + key: issue.code + issue.message, + class: "task-issue-item" + }, _toDisplayString(issue.message), 1 /* TEXT */)) + }), 128 /* KEYED_FRAGMENT */)) + ])) + : _createCommentVNode("v-if", true), + (_ctx.taskOrchestration.planWarnings.length) + ? (_openBlock(), _createElementBlock("div", { + key: 2, + class: "task-warning-list" + }, [ + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestration.planWarnings, (warning) => { + return (_openBlock(), _createElementBlock("div", { + key: warning, + class: "task-warning-item" + }, _toDisplayString(warning), 1 /* TEXT */)) + }), 128 /* KEYED_FRAGMENT */)) + ])) + : _createCommentVNode("v-if", true), + (_ctx.taskOrchestration.plan) + ? (_openBlock(), _createElementBlock(_Fragment, { key: 3 }, [ + _createElementVNode("div", { class: "task-plan-summary-strip" }, [ + _createElementVNode("div", { class: "task-plan-summary-item" }, [ + _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.nodes')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.plan.nodes.length), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-plan-summary-item" }, [ + _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.waves')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.plan.waves.length), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-plan-summary-item" }, [ + _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.engine')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.plan.engine), 1 /* TEXT */) + ]), + (_ctx.taskOrchestration.plan.threadId) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-plan-summary-item" + }, [ + _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.threadId')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.plan.threadId), 1 /* TEXT */) + ])) + : _createCommentVNode("v-if", true), + (_ctx.taskOrchestration.plan.cwd) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-plan-summary-item" + }, [ + _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.cwd')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.plan.cwd), 1 /* TEXT */) + ])) + : _createCommentVNode("v-if", true) + ]), + _createElementVNode("div", { class: "task-wave-list" }, [ + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestration.plan.waves, (wave) => { + return (_openBlock(), _createElementBlock("div", { + key: wave.label, + class: "task-wave-card" + }, [ + _createElementVNode("div", { class: "task-wave-title" }, _toDisplayString(wave.label), 1 /* TEXT */), + _createElementVNode("div", { class: "task-wave-nodes" }, _toDisplayString(wave.nodeIds.join(', ')), 1 /* TEXT */) + ])) + }), 128 /* KEYED_FRAGMENT */)) + ]), + _createElementVNode("div", { class: "task-node-list" }, [ + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestration.plan.nodes, (node) => { + return (_openBlock(), _createElementBlock("div", { + key: node.id, + class: "task-node-card" + }, [ + _createElementVNode("div", { class: "task-node-head" }, [ + _createElementVNode("div", null, [ + _createElementVNode("div", { class: "task-node-title" }, _toDisplayString(node.title || node.id), 1 /* TEXT */), + _createElementVNode("div", { class: "task-node-meta" }, [ + _createTextVNode(_toDisplayString(node.id) + " · " + _toDisplayString(node.kind), 1 /* TEXT */), + (node.workflowId) + ? (_openBlock(), _createElementBlock("span", { key: 0 }, " · " + _toDisplayString(node.workflowId), 1 /* TEXT */)) + : _createCommentVNode("v-if", true) + ]) + ]), + _createElementVNode("span", { + class: _normalizeClass(['pill', node.write ? 'configured' : 'empty']) + }, _toDisplayString(node.write ? _ctx.t('orchestration.plan.node.write') : _ctx.t('orchestration.plan.node.readOnly')), 3 /* TEXT, CLASS */) + ]), + _createElementVNode("div", { class: "task-node-deps" }, _toDisplayString(_ctx.t('orchestration.labels.dependencies')) + _toDisplayString(_ctx.formatTaskNodeDependencies(node)), 1 /* TEXT */) + ])) + }), 128 /* KEYED_FRAGMENT */)) + ]) + ], 64 /* STABLE_FRAGMENT */)) + : _createCommentVNode("v-if", true) + ])) + : _createCommentVNode("v-if", true) ], 8 /* PROPS */, ["aria-label"]), - _createElementVNode("label", { class: "task-quick-target-field task-chat-composer" }, [ - _createElementVNode("span", { class: "selector-label" }, _toDisplayString(_ctx.t('orchestration.chat.input.label')), 1 /* TEXT */), - _withDirectives(_createElementVNode("textarea", { - "onUpdate:modelValue": $event => ((_ctx.taskOrchestration.chatDraft) = $event), - class: "task-textarea task-textarea-goal task-quick-target", - rows: "3", - placeholder: _ctx.t('orchestration.chat.input.placeholder'), - onKeydown: _withKeys(_withModifiers($event => (_ctx.appendTaskChatMessage()), ["exact","prevent"]), ["enter"]) - }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "placeholder", "onKeydown"]), [ - [_vModelText, _ctx.taskOrchestration.chatDraft] + (_ctx.taskOrchestrationActiveQueue.length || _ctx.taskOrchestration.runs.length || _ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.selectedRunError) + ? (_openBlock(), _createElementBlock("section", { + key: 0, + class: "selector-section task-workbench-card task-thread-message-card task-thread-workbench-card" + }, [ + _createElementVNode("div", { class: "task-thread-card-label" }, "AI · " + _toDisplayString(_ctx.t('orchestration.workbench.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "selector-header task-section-header" }, [ + _createElementVNode("div", null, [ + _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('orchestration.workbench.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "skills-panel-note" }, _toDisplayString(_ctx.t('orchestration.workbench.subtitle')), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "settings-tab-actions task-header-actions" }, [ + _createElementVNode("button", { + type: "button", + class: "btn-tool btn-tool-compact", + onClick: $event => (_ctx.loadTaskOrchestrationOverview({ forceRefresh: true, includeDetail: true })), + disabled: _ctx.taskOrchestration.loading + }, _toDisplayString(_ctx.taskOrchestration.loading ? _ctx.t('common.refreshing') : _ctx.t('common.refresh')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), + (_ctx.taskOrchestrationActiveQueue.length) + ? (_openBlock(), _createElementBlock("button", { + key: 0, + type: "button", + class: "btn-tool btn-tool-compact", + onClick: $event => (_ctx.startTaskQueueRunner()), + disabled: _ctx.taskOrchestration.queueStarting + }, _toDisplayString(_ctx.taskOrchestration.queueStarting ? _ctx.t('orchestration.queue.starting') : _ctx.t('orchestration.queue.start')), 9 /* TEXT, PROPS */, ["onClick", "disabled"])) + : _createCommentVNode("v-if", true) + ]) + ]), + ((_ctx.taskOrchestrationActiveQueue.length ? 1 : 0) + (_ctx.taskOrchestration.runs.length ? 1 : 0) + ((_ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.selectedRunError) ? 1 : 0) > 1) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-workbench-tabs", + role: "group", + "aria-label": _ctx.t('orchestration.workbench.tabs.aria') + }, [ + (_ctx.taskOrchestrationActiveQueue.length) + ? (_openBlock(), _createElementBlock("button", { + key: 0, + type: "button", + class: _normalizeClass(["task-workbench-tab", { active: _ctx.taskOrchestration.workspaceTab === 'queue' }]), + onClick: $event => (_ctx.taskOrchestration.workspaceTab = 'queue') + }, _toDisplayString(_ctx.t('orchestration.workbench.tabs.queue', { count: _ctx.taskOrchestrationActiveQueue.length })), 11 /* TEXT, CLASS, PROPS */, ["onClick"])) + : _createCommentVNode("v-if", true), + (_ctx.taskOrchestration.runs.length) + ? (_openBlock(), _createElementBlock("button", { + key: 1, + type: "button", + class: _normalizeClass(["task-workbench-tab", { active: _ctx.taskOrchestration.workspaceTab === 'runs' }]), + onClick: $event => (_ctx.taskOrchestration.workspaceTab = 'runs') + }, _toDisplayString(_ctx.t('orchestration.workbench.tabs.runs', { count: _ctx.taskOrchestration.runs.length })), 11 /* TEXT, CLASS, PROPS */, ["onClick"])) + : _createCommentVNode("v-if", true), + (_ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.selectedRunError) + ? (_openBlock(), _createElementBlock("button", { + key: 2, + type: "button", + class: _normalizeClass(["task-workbench-tab", { active: _ctx.taskOrchestration.workspaceTab === 'detail' }]), + onClick: $event => (_ctx.taskOrchestration.workspaceTab = 'detail') + }, _toDisplayString(_ctx.t('orchestration.workbench.tabs.detail')), 11 /* TEXT, CLASS, PROPS */, ["onClick"])) + : _createCommentVNode("v-if", true) + ], 8 /* PROPS */, ["aria-label"])) + : _createCommentVNode("v-if", true), + ((_ctx.taskOrchestrationActiveQueue.length && _ctx.taskOrchestration.workspaceTab === 'queue') || (!_ctx.taskOrchestration.runs.length && !_ctx.taskOrchestration.selectedRunId && !_ctx.taskOrchestration.selectedRunError)) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-workbench-panel" + }, [ + (!_ctx.taskOrchestrationActiveQueue.length) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-empty-state" + }, [ + _createElementVNode("div", { class: "task-empty-title" }, _toDisplayString(_ctx.t('orchestration.queue.empty.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "task-empty-copy" }, _toDisplayString(_ctx.t('orchestration.queue.empty.subtitle')), 1 /* TEXT */) + ])) + : (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-runtime-list" + }, [ + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestrationActiveQueue, (item) => { + return (_openBlock(), _createElementBlock("div", { + key: item.taskId, + class: _normalizeClass(['task-runtime-item', { active: item.lastRunId && _ctx.taskOrchestration.selectedRunId === item.lastRunId, clickable: !!item.lastRunId }]), + role: item.lastRunId ? 'button' : null, + tabindex: item.lastRunId ? 0 : -1, + "aria-disabled": item.lastRunId ? null : 'true', + onClick: $event => (item.lastRunId ? (_ctx.taskOrchestration.workspaceTab = 'detail', _ctx.selectTaskRun(item.lastRunId)) : null), + onKeydown: [ + _withKeys(_withModifiers($event => (item.lastRunId ? (_ctx.taskOrchestration.workspaceTab = 'detail', _ctx.selectTaskRun(item.lastRunId)) : null), ["self","prevent"]), ["enter"]), + _withKeys(_withModifiers($event => (item.lastRunId ? (_ctx.taskOrchestration.workspaceTab = 'detail', _ctx.selectTaskRun(item.lastRunId)) : null), ["self","prevent"]), ["space"]) + ] + }, [ + _createElementVNode("div", { class: "task-runtime-item-main" }, [ + _createElementVNode("div", { class: "task-runtime-item-title" }, _toDisplayString(item.title || item.target || item.taskId), 1 /* TEXT */), + _createElementVNode("div", { class: "task-runtime-item-meta" }, _toDisplayString(item.taskId) + " · " + _toDisplayString(item.updatedAt || item.createdAt), 1 /* TEXT */), + (item.threadId || item.cwd) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-runtime-item-meta" + }, [ + (item.threadId) + ? (_openBlock(), _createElementBlock("span", { key: 0 }, _toDisplayString(_ctx.t('orchestration.plan.summary.threadId')) + ": " + _toDisplayString(item.threadId), 1 /* TEXT */)) + : _createCommentVNode("v-if", true), + (item.threadId && item.cwd) + ? (_openBlock(), _createElementBlock("span", { key: 1 }, " · ")) + : _createCommentVNode("v-if", true), + (item.cwd) + ? (_openBlock(), _createElementBlock("span", { key: 2 }, _toDisplayString(_ctx.t('orchestration.plan.summary.cwd')) + ": " + _toDisplayString(item.cwd), 1 /* TEXT */)) + : _createCommentVNode("v-if", true) + ])) + : _createCommentVNode("v-if", true), + (item.lastSummary) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-runtime-item-summary" + }, _toDisplayString(item.lastSummary), 1 /* TEXT */)) + : _createCommentVNode("v-if", true) + ]), + _createElementVNode("div", { class: "task-runtime-item-actions" }, [ + _createElementVNode("span", { + class: _normalizeClass(['pill', _ctx.taskRunStatusTone(item.status || item.runStatus)]) + }, _toDisplayString(item.status || item.runStatus), 3 /* TEXT, CLASS */), + (_ctx.isTaskRunActive(item.status || item.runStatus)) + ? (_openBlock(), _createElementBlock("button", { + key: 0, + type: "button", + class: "btn-mini", + onClick: _withModifiers($event => (_ctx.cancelTaskRunFromUi(item.taskId)), ["stop"]) + }, _toDisplayString(_ctx.t('common.cancel')), 9 /* TEXT, PROPS */, ["onClick"])) + : _createCommentVNode("v-if", true) + ]) + ], 42 /* CLASS, PROPS, NEED_HYDRATION */, ["role", "tabindex", "aria-disabled", "onClick", "onKeydown"])) + }), 128 /* KEYED_FRAGMENT */)) + ])) + ])) + : (_ctx.taskOrchestration.workspaceTab === 'runs' || (!_ctx.taskOrchestrationActiveQueue.length && _ctx.taskOrchestration.runs.length && !_ctx.taskOrchestration.selectedRunId && !_ctx.taskOrchestration.selectedRunError)) + ? (_openBlock(), _createElementBlock("div", { + key: 2, + class: "task-workbench-panel" + }, [ + (!_ctx.taskOrchestration.runs.length) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-empty-state" + }, [ + _createElementVNode("div", { class: "task-empty-title" }, _toDisplayString(_ctx.t('orchestration.runs.empty.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "task-empty-copy" }, _toDisplayString(_ctx.t('orchestration.runs.empty.subtitle')), 1 /* TEXT */) + ])) + : (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-runtime-list" + }, [ + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestration.runs, (item) => { + return (_openBlock(), _createElementBlock("button", { + key: item.runId, + type: "button", + class: _normalizeClass(['task-runtime-item', { active: _ctx.taskOrchestration.selectedRunId === item.runId }]), + onClick: $event => {_ctx.taskOrchestration.workspaceTab = 'detail'; _ctx.selectTaskRun(item.runId)} + }, [ + _createElementVNode("div", { class: "task-runtime-item-main" }, [ + _createElementVNode("div", { class: "task-runtime-item-title" }, _toDisplayString(item.title || item.taskId || item.runId), 1 /* TEXT */), + _createElementVNode("div", { class: "task-runtime-item-meta" }, _toDisplayString(item.runId) + " · " + _toDisplayString(item.durationMs || 0) + "ms", 1 /* TEXT */), + (item.threadId || item.cwd) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-runtime-item-meta" + }, [ + (item.threadId) + ? (_openBlock(), _createElementBlock("span", { key: 0 }, _toDisplayString(_ctx.t('orchestration.plan.summary.threadId')) + ": " + _toDisplayString(item.threadId), 1 /* TEXT */)) + : _createCommentVNode("v-if", true), + (item.threadId && item.cwd) + ? (_openBlock(), _createElementBlock("span", { key: 1 }, " · ")) + : _createCommentVNode("v-if", true), + (item.cwd) + ? (_openBlock(), _createElementBlock("span", { key: 2 }, _toDisplayString(_ctx.t('orchestration.plan.summary.cwd')) + ": " + _toDisplayString(item.cwd), 1 /* TEXT */)) + : _createCommentVNode("v-if", true) + ])) + : _createCommentVNode("v-if", true), + (item.summary) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-runtime-item-summary" + }, _toDisplayString(item.summary), 1 /* TEXT */)) + : _createCommentVNode("v-if", true) + ]), + _createElementVNode("div", { class: "task-runtime-item-actions" }, [ + _createElementVNode("span", { + class: _normalizeClass(['pill', _ctx.taskRunStatusTone(item.status)]) + }, _toDisplayString(item.status), 3 /* TEXT, CLASS */) + ]) + ], 10 /* CLASS, PROPS */, ["onClick"])) + }), 128 /* KEYED_FRAGMENT */)) + ])) + ])) + : (_openBlock(), _createElementBlock("div", { + key: 3, + class: "task-workbench-panel" + }, [ + _createElementVNode("div", { class: "task-detail-toolbar settings-tab-actions" }, [ + _createElementVNode("button", { + type: "button", + class: "btn-tool btn-tool-compact", + onClick: $event => (_ctx.taskOrchestration.selectedRunId ? _ctx.loadTaskRunDetail(_ctx.taskOrchestration.selectedRunId) : null), + disabled: !_ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.selectedRunLoading + }, _toDisplayString(_ctx.taskOrchestration.selectedRunLoading ? _ctx.t('common.refreshing') : _ctx.t('orchestration.detail.refresh')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), + _createElementVNode("button", { + type: "button", + class: "btn-tool btn-tool-compact", + onClick: $event => (_ctx.retryTaskRunFromUi(_ctx.taskOrchestration.selectedRunId)), + disabled: !_ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.retrying + }, _toDisplayString(_ctx.taskOrchestration.retrying ? _ctx.t('orchestration.detail.retrying') : _ctx.t('orchestration.detail.retry')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), + _createElementVNode("button", { + type: "button", + class: "btn-tool btn-tool-compact", + onClick: _ctx.continueTaskThreadFromUi, + disabled: !_ctx.taskOrchestrationSelectedRun + }, _toDisplayString(_ctx.t('orchestration.detail.continueThread')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), + (_ctx.taskOrchestrationSelectedRun && _ctx.taskOrchestrationSelectedRun.run && _ctx.isTaskRunActive(_ctx.taskOrchestrationSelectedRun.run.status)) + ? (_openBlock(), _createElementBlock("button", { + key: 0, + type: "button", + class: "btn-tool btn-tool-compact", + onClick: $event => (_ctx.cancelTaskRunFromUi(_ctx.taskOrchestration.selectedRunId)) + }, _toDisplayString(_ctx.t('common.cancel')), 9 /* TEXT, PROPS */, ["onClick"])) + : _createCommentVNode("v-if", true) + ]), + (_ctx.taskOrchestration.selectedRunError) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-issue-item" + }, _toDisplayString(_ctx.taskOrchestration.selectedRunError), 1 /* TEXT */)) + : _createCommentVNode("v-if", true), + (!_ctx.taskOrchestrationSelectedRun) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-empty-state" + }, [ + _createElementVNode("div", { class: "task-empty-title" }, _toDisplayString(_ctx.t('orchestration.detail.empty.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "task-empty-copy" }, _toDisplayString(_ctx.t('orchestration.detail.empty.subtitle')), 1 /* TEXT */) + ])) + : (_openBlock(), _createElementBlock(_Fragment, { key: 2 }, [ + _createElementVNode("div", { class: "task-detail-summary-strip" }, [ + _createElementVNode("div", { class: "task-plan-summary-item" }, [ + _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.detail.summary.status')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationSelectedRun.run.status), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-plan-summary-item" }, [ + _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.detail.summary.duration')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationSelectedRun.run.durationMs || 0) + "ms", 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-plan-summary-item" }, [ + _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.detail.summary.nodes')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationSelectedRunNodes.length), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-plan-summary-item" }, [ + _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.detail.summary.summary')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationSelectedRun.run.summary || _ctx.t('common.none')), 1 /* TEXT */) + ]), + (_ctx.taskOrchestrationSelectedRun.threadId) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-plan-summary-item" + }, [ + _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.threadId')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationSelectedRun.threadId), 1 /* TEXT */) + ])) + : _createCommentVNode("v-if", true), + (_ctx.taskOrchestrationSelectedRun.cwd) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-plan-summary-item" + }, [ + _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.cwd')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationSelectedRun.cwd), 1 /* TEXT */) + ])) + : _createCommentVNode("v-if", true) + ]), + (_ctx.taskOrchestrationSelectedRun.run.error) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-issue-item" + }, _toDisplayString(_ctx.taskOrchestrationSelectedRun.run.error), 1 /* TEXT */)) + : _createCommentVNode("v-if", true), + _createElementVNode("div", { class: "task-node-list" }, [ + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestrationSelectedRunNodes, (node) => { + return (_openBlock(), _createElementBlock("div", { + key: node.id, + class: "task-node-card task-node-card-detail" + }, [ + _createElementVNode("div", { class: "task-node-head" }, [ + _createElementVNode("div", null, [ + _createElementVNode("div", { class: "task-node-title" }, _toDisplayString(node.title || node.id), 1 /* TEXT */), + _createElementVNode("div", { class: "task-node-meta" }, _toDisplayString(_ctx.t('orchestration.detail.node.meta', { id: node.id, attempts: (node.attemptCount || 0), autoFix: (node.autoFixRounds || 0) })), 1 /* TEXT */) + ]), + _createElementVNode("span", { + class: _normalizeClass(['pill', _ctx.taskRunStatusTone(node.status)]) + }, _toDisplayString(node.status), 3 /* TEXT, CLASS */) + ]), + (node.summary) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-runtime-item-summary" + }, _toDisplayString(node.summary), 1 /* TEXT */)) + : _createCommentVNode("v-if", true), + (node.error && node.error !== node.summary) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-node-deps" + }, _toDisplayString(_ctx.t('orchestration.labels.error')) + _toDisplayString(node.error), 1 /* TEXT */)) + : _createCommentVNode("v-if", true), + _createElementVNode("div", { class: "task-node-deps" }, _toDisplayString(_ctx.t('orchestration.labels.dependencies')) + _toDisplayString(_ctx.formatTaskNodeDependencies(node)), 1 /* TEXT */), + (node.output && typeof node.output === 'object') + ? (_openBlock(), _createElementBlock("div", { + key: 2, + class: "task-node-output-card" + }, [ + _createElementVNode("div", { class: "task-node-output-head" }, [ + _createElementVNode("strong", null, _toDisplayString(_ctx.t('orchestration.detail.node.output')), 1 /* TEXT */), + (node.output.provider || node.output.model) + ? (_openBlock(), _createElementBlock("span", { + key: 0, + class: "task-node-output-meta" + }, [ + _createTextVNode(_toDisplayString(node.output.provider || ''), 1 /* TEXT */), + (node.output.provider && node.output.model) + ? (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, [ + _createTextVNode(" · ") + ], 64 /* STABLE_FRAGMENT */)) + : _createCommentVNode("v-if", true), + _createTextVNode(_toDisplayString(node.output.model || ''), 1 /* TEXT */) + ])) + : _createCommentVNode("v-if", true) + ]), + _createElementVNode("div", { class: "task-node-output-facts" }, [ + (node.output.endpoint) + ? (_openBlock(), _createElementBlock("span", { key: 0 }, _toDisplayString(_ctx.t('orchestration.detail.node.endpoint')) + _toDisplayString(node.output.endpoint), 1 /* TEXT */)) + : _createCommentVNode("v-if", true), + (node.output.status) + ? (_openBlock(), _createElementBlock("span", { key: 1 }, "HTTP " + _toDisplayString(node.output.status), 1 /* TEXT */)) + : _createCommentVNode("v-if", true), + (node.output.durationMs) + ? (_openBlock(), _createElementBlock("span", { key: 2 }, _toDisplayString(node.output.durationMs) + "ms", 1 /* TEXT */)) + : _createCommentVNode("v-if", true) + ]), + _createElementVNode("pre", { class: "task-log-block task-output-block" }, _toDisplayString(_ctx.formatTaskNodeOutputText(node)), 1 /* TEXT */), + (node.output.materializedFiles && node.output.materializedFiles.length) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-materialized-files" + }, [ + _createElementVNode("div", { class: "task-node-deps" }, _toDisplayString(_ctx.t('orchestration.detail.node.materializedFiles')), 1 /* TEXT */), + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(node.output.materializedFiles, (file) => { + return (_openBlock(), _createElementBlock("div", { + key: file.path || file.relativePath, + class: "task-materialized-file" + }, _toDisplayString(file.relativePath || file.path) + " · " + _toDisplayString(file.bytes || 0) + " bytes", 1 /* TEXT */)) + }), 128 /* KEYED_FRAGMENT */)) + ])) + : _createCommentVNode("v-if", true) + ])) + : _createCommentVNode("v-if", true), + _createElementVNode("pre", { class: "task-log-block" }, _toDisplayString(_ctx.formatTaskNodeLogs(node.logs)), 1 /* TEXT */) + ])) + }), 128 /* KEYED_FRAGMENT */)) + ]) + ], 64 /* STABLE_FRAGMENT */)) + ])) + ])) + : _createCommentVNode("v-if", true), + _createElementVNode("div", { class: "task-thread-composer" }, [ + _createElementVNode("label", { class: "task-quick-target-field task-chat-composer" }, [ + _createElementVNode("span", { class: "selector-label" }, _toDisplayString(_ctx.t('orchestration.chat.input.label')), 1 /* TEXT */), + _withDirectives(_createElementVNode("textarea", { + "onUpdate:modelValue": $event => ((_ctx.taskOrchestration.chatDraft) = $event), + class: "task-textarea task-textarea-goal task-quick-target", + rows: "3", + placeholder: _ctx.t('orchestration.chat.input.placeholder'), + onKeydown: _withKeys(_withModifiers($event => (_ctx.appendTaskChatMessage()), ["exact","prevent"]), ["enter"]) + }, null, 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "placeholder", "onKeydown"]), [ + [_vModelText, _ctx.taskOrchestration.chatDraft] + ]), + _createElementVNode("span", { class: "task-field-hint" }, _toDisplayString(_ctx.t('orchestration.chat.input.hint')), 1 /* TEXT */) ]), - _createElementVNode("span", { class: "task-field-hint" }, _toDisplayString(_ctx.t('orchestration.chat.input.hint')), 1 /* TEXT */) - ]), - _createElementVNode("div", { class: "task-chat-send-row" }, [ - _createElementVNode("button", { - type: "button", - class: "btn-tool btn-primary", - onClick: $event => (_ctx.appendTaskChatMessage()), - disabled: !_ctx.taskOrchestration.chatDraft.trim() - }, _toDisplayString(_ctx.t('orchestration.chat.input.send')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), - _createElementVNode("span", { class: "task-field-hint" }, _toDisplayString(_ctx.taskOrchestrationDraftMetrics.hasTarget ? _ctx.t('orchestration.chat.input.sequenceHint', { count: _ctx.taskOrchestrationDraftMetrics.followUpCount + 2 }) : _ctx.t('orchestration.chat.input.firstHint')), 1 /* TEXT */) - ]), - _createElementVNode("div", { - class: "task-chat-context-row", - role: "group", - "aria-label": _ctx.t('orchestration.chat.context.aria') - }, [ - _createElementVNode("span", { class: "task-chat-context-chip" }, _toDisplayString(_ctx.taskOrchestration.workspacePath.trim() ? _ctx.t('orchestration.chat.context.workspace.value', { value: _ctx.taskOrchestration.workspacePath.trim() }) : _ctx.t('orchestration.chat.context.workspace.auto')), 1 /* TEXT */), - _createElementVNode("span", { class: "task-chat-context-chip" }, _toDisplayString(_ctx.taskOrchestration.threadId.trim() ? _ctx.t('orchestration.chat.context.thread.value', { value: _ctx.taskOrchestration.threadId.trim() }) : _ctx.t('orchestration.chat.context.thread.auto')), 1 /* TEXT */), - _createElementVNode("span", { class: "task-chat-context-chip" }, _toDisplayString(_ctx.taskOrchestrationDraftMetrics.hasTarget ? _ctx.t('orchestration.chat.context.sequence.value', { count: _ctx.taskOrchestrationDraftMetrics.requestCount }) : _ctx.t('orchestration.chat.context.sequence.empty')), 1 /* TEXT */), - _createElementVNode("span", { class: "task-chat-context-chip" }, _toDisplayString(_ctx.taskOrchestration.runMode === 'dry-run' ? _ctx.t('orchestration.runMode.dryRun') : (_ctx.taskOrchestration.runMode === 'read' ? _ctx.t('orchestration.runMode.readOnly') : _ctx.t('orchestration.runMode.write') )), 1 /* TEXT */) - ], 8 /* PROPS */, ["aria-label"]), - _createElementVNode("div", { class: "task-draft-overview task-draft-inline task-quick-readiness" }, [ - _createElementVNode("div", { class: "task-draft-inline-head" }, [ - _createElementVNode("span", { - class: _normalizeClass(['pill', _ctx.taskOrchestrationDraftReadiness.tone]) - }, _toDisplayString(_ctx.taskOrchestrationDraftReadiness.title), 3 /* TEXT, CLASS */), - _createElementVNode("div", { class: "task-readiness-copy task-draft-inline-copy" }, _toDisplayString(_ctx.taskOrchestrationDraftReadiness.summary), 1 /* TEXT */) + _createElementVNode("div", { class: "task-chat-send-row" }, [ + _createElementVNode("button", { + type: "button", + class: "btn-tool btn-primary", + onClick: $event => (_ctx.appendTaskChatMessage()), + disabled: !_ctx.taskOrchestration.chatDraft.trim() + }, _toDisplayString(_ctx.t('orchestration.chat.input.send')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), + _createElementVNode("span", { class: "task-field-hint" }, _toDisplayString(_ctx.taskOrchestrationDraftMetrics.hasTarget ? _ctx.t('orchestration.chat.input.sequenceHint', { count: _ctx.taskOrchestrationDraftMetrics.followUpCount + 2 }) : _ctx.t('orchestration.chat.input.firstHint')), 1 /* TEXT */) ]), - _createElementVNode("div", { class: "task-config-strip" }, [ - _createElementVNode("div", { class: "task-config-pill" }, _toDisplayString(_ctx.taskOrchestration.selectedEngine === 'workflow' ? _ctx.t('orchestration.engine.workflow') : _ctx.t('orchestration.engine.openaiChat')), 1 /* TEXT */), - _createElementVNode("div", { class: "task-config-pill" }, _toDisplayString(_ctx.taskOrchestration.runMode === 'dry-run' ? _ctx.t('orchestration.runMode.dryRun') : (_ctx.taskOrchestration.runMode === 'read' ? _ctx.t('orchestration.runMode.readOnly') : _ctx.t('orchestration.runMode.write') )), 1 /* TEXT */), - (_ctx.taskOrchestrationDraftMetrics.hasTarget) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "task-config-pill" - }, _toDisplayString(_ctx.t('orchestration.pills.sequenceCount', { count: _ctx.taskOrchestrationDraftMetrics.requestCount })), 1 /* TEXT */)) - : _createCommentVNode("v-if", true), - (_ctx.taskOrchestration.title.trim()) - ? (_openBlock(), _createElementBlock("div", { - key: 1, - class: "task-config-pill" - }, _toDisplayString(_ctx.t('orchestration.pills.hasTitle')), 1 /* TEXT */)) - : _createCommentVNode("v-if", true), - (_ctx.taskOrchestration.selectedEngine === 'workflow' && _ctx.taskOrchestrationDraftMetrics.workflowCount > 0) - ? (_openBlock(), _createElementBlock("div", { - key: 2, - class: "task-config-pill" - }, _toDisplayString(_ctx.t('orchestration.pills.workflowCount', { count: _ctx.taskOrchestrationDraftMetrics.workflowCount })), 1 /* TEXT */)) - : _createCommentVNode("v-if", true), - (_ctx.taskOrchestration.plan) - ? (_openBlock(), _createElementBlock("div", { - key: 3, - class: "task-config-pill" - }, _toDisplayString(_ctx.t('orchestration.pills.planNodes', { count: _ctx.taskOrchestrationDraftMetrics.planNodeCount })), 1 /* TEXT */)) - : _createCommentVNode("v-if", true) - ]) - ]), - _createElementVNode("div", { class: "task-action-row task-action-row-prominent task-quick-actions" }, [ - _createElementVNode("button", { - type: "button", - class: "btn-tool task-action-preview", - onClick: $event => (_ctx.previewTaskPlan()), - disabled: _ctx.taskOrchestration.planning || _ctx.taskOrchestration.running || !_ctx.taskOrchestration.target.trim() - }, _toDisplayString(_ctx.taskOrchestration.planning ? _ctx.t('orchestration.actions.planning') : _ctx.t('orchestration.actions.previewOnly')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), - _createElementVNode("div", { class: "task-action-row-right task-action-row-right-prominent" }, [ + _createElementVNode("div", { + class: "task-chat-context-row", + role: "group", + "aria-label": _ctx.t('orchestration.chat.context.aria') + }, [ + _createElementVNode("span", { class: "task-chat-context-chip" }, _toDisplayString(_ctx.taskOrchestration.workspacePath.trim() ? _ctx.t('orchestration.chat.context.workspace.value', { value: _ctx.taskOrchestration.workspacePath.trim() }) : _ctx.t('orchestration.chat.context.workspace.auto')), 1 /* TEXT */), + _createElementVNode("span", { class: "task-chat-context-chip" }, _toDisplayString(_ctx.taskOrchestration.threadId.trim() ? _ctx.t('orchestration.chat.context.thread.value', { value: _ctx.taskOrchestration.threadId.trim() }) : _ctx.t('orchestration.chat.context.thread.auto')), 1 /* TEXT */), + _createElementVNode("span", { class: "task-chat-context-chip" }, _toDisplayString(_ctx.taskOrchestrationDraftMetrics.hasTarget ? _ctx.t('orchestration.chat.context.sequence.value', { count: _ctx.taskOrchestrationDraftMetrics.requestCount }) : _ctx.t('orchestration.chat.context.sequence.empty')), 1 /* TEXT */), + _createElementVNode("span", { class: "task-chat-context-chip" }, _toDisplayString(_ctx.taskOrchestration.runMode === 'dry-run' ? _ctx.t('orchestration.runMode.dryRun') : (_ctx.taskOrchestration.runMode === 'read' ? _ctx.t('orchestration.runMode.readOnly') : _ctx.t('orchestration.runMode.write') )), 1 /* TEXT */) + ], 8 /* PROPS */, ["aria-label"]), + _createElementVNode("div", { class: "task-draft-overview task-draft-inline task-quick-readiness" }, [ + _createElementVNode("div", { class: "task-draft-inline-head" }, [ + _createElementVNode("span", { + class: _normalizeClass(['pill', _ctx.taskOrchestrationDraftReadiness.tone]) + }, _toDisplayString(_ctx.taskOrchestrationDraftReadiness.title), 3 /* TEXT, CLASS */), + _createElementVNode("div", { class: "task-readiness-copy task-draft-inline-copy" }, _toDisplayString(_ctx.taskOrchestrationDraftReadiness.summary), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-config-strip" }, [ + _createElementVNode("div", { class: "task-config-pill" }, _toDisplayString(_ctx.taskOrchestration.selectedEngine === 'workflow' ? _ctx.t('orchestration.engine.workflow') : _ctx.t('orchestration.engine.openaiChat')), 1 /* TEXT */), + _createElementVNode("div", { class: "task-config-pill" }, _toDisplayString(_ctx.taskOrchestration.runMode === 'dry-run' ? _ctx.t('orchestration.runMode.dryRun') : (_ctx.taskOrchestration.runMode === 'read' ? _ctx.t('orchestration.runMode.readOnly') : _ctx.t('orchestration.runMode.write') )), 1 /* TEXT */), + (_ctx.taskOrchestrationDraftMetrics.hasTarget) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-config-pill" + }, _toDisplayString(_ctx.t('orchestration.pills.sequenceCount', { count: _ctx.taskOrchestrationDraftMetrics.requestCount })), 1 /* TEXT */)) + : _createCommentVNode("v-if", true), + (_ctx.taskOrchestration.title.trim()) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-config-pill" + }, _toDisplayString(_ctx.t('orchestration.pills.hasTitle')), 1 /* TEXT */)) + : _createCommentVNode("v-if", true), + (_ctx.taskOrchestration.selectedEngine === 'workflow' && _ctx.taskOrchestrationDraftMetrics.workflowCount > 0) + ? (_openBlock(), _createElementBlock("div", { + key: 2, + class: "task-config-pill" + }, _toDisplayString(_ctx.t('orchestration.pills.workflowCount', { count: _ctx.taskOrchestrationDraftMetrics.workflowCount })), 1 /* TEXT */)) + : _createCommentVNode("v-if", true), + (_ctx.taskOrchestration.plan) + ? (_openBlock(), _createElementBlock("div", { + key: 3, + class: "task-config-pill" + }, _toDisplayString(_ctx.t('orchestration.pills.planNodes', { count: _ctx.taskOrchestrationDraftMetrics.planNodeCount })), 1 /* TEXT */)) + : _createCommentVNode("v-if", true) + ]) + ]), + _createElementVNode("div", { class: "task-action-row task-action-row-prominent task-quick-actions" }, [ _createElementVNode("button", { type: "button", - class: "btn-tool btn-primary", - onClick: $event => (_ctx.planAndRunTaskOrchestration()), - disabled: _ctx.taskOrchestration.running || _ctx.taskOrchestration.planning || !_ctx.taskOrchestration.target.trim() - }, _toDisplayString((_ctx.taskOrchestration.running || _ctx.taskOrchestration.planning) ? _ctx.t('orchestration.actions.preparing') : (_ctx.taskOrchestration.runMode === 'dry-run' ? _ctx.t('orchestration.actions.generatePlan') : _ctx.t('orchestration.actions.planAndRun'))), 9 /* TEXT, PROPS */, ["onClick", "disabled"]) - ]) - ]), - _createElementVNode("div", { class: "task-action-caption" }, _toDisplayString(_ctx.t('orchestration.quick.caption')), 1 /* TEXT */) + class: "btn-tool task-action-preview", + onClick: $event => (_ctx.previewTaskPlan()), + disabled: _ctx.taskOrchestration.planning || _ctx.taskOrchestration.running || !_ctx.taskOrchestration.target.trim() + }, _toDisplayString(_ctx.taskOrchestration.planning ? _ctx.t('orchestration.actions.planning') : _ctx.t('orchestration.actions.previewOnly')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), + _createElementVNode("div", { class: "task-action-row-right task-action-row-right-prominent" }, [ + _createElementVNode("button", { + type: "button", + class: "btn-tool btn-primary", + onClick: $event => (_ctx.planAndRunTaskOrchestration()), + disabled: _ctx.taskOrchestration.running || _ctx.taskOrchestration.planning || !_ctx.taskOrchestration.target.trim() + }, _toDisplayString((_ctx.taskOrchestration.running || _ctx.taskOrchestration.planning) ? _ctx.t('orchestration.actions.preparing') : (_ctx.taskOrchestration.runMode === 'dry-run' ? _ctx.t('orchestration.actions.generatePlan') : _ctx.t('orchestration.actions.planAndRun'))), 9 /* TEXT, PROPS */, ["onClick", "disabled"]) + ]) + ]), + _createElementVNode("div", { class: "task-action-caption" }, _toDisplayString(_ctx.t('orchestration.quick.caption')), 1 /* TEXT */) + ]) ]), _createElementVNode("div", { class: "task-template-block task-quick-template-block" }, [ _createElementVNode("div", { class: "task-template-label" }, _toDisplayString(_ctx.t('orchestration.templates.title')), 1 /* TEXT */), @@ -4446,495 +4951,7 @@ return function render(_ctx, _cache) { ]) ]) ])) - : (_openBlock(), _createElementBlock("div", { - key: 1, - class: "task-layout-grid task-layout-grid-secondary" - }, [ - (_ctx.taskOrchestration.plan || _ctx.taskOrchestration.planIssues.length || _ctx.taskOrchestration.planWarnings.length || _ctx.taskOrchestration.lastError) - ? (_openBlock(), _createElementBlock("section", { - key: 0, - class: "selector-section task-plan-card" - }, [ - (_ctx.taskOrchestration.lastError) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "task-issue-item" - }, _toDisplayString(_ctx.taskOrchestration.lastError), 1 /* TEXT */)) - : _createCommentVNode("v-if", true), - _createElementVNode("div", { class: "selector-header task-section-header" }, [ - _createElementVNode("div", null, [ - _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('orchestration.plan.title')), 1 /* TEXT */), - _createElementVNode("div", { class: "skills-panel-note" }, _toDisplayString(_ctx.t('orchestration.plan.subtitle')), 1 /* TEXT */) - ]) - ]), - (_ctx.taskOrchestration.planIssues.length) - ? (_openBlock(), _createElementBlock("div", { - key: 1, - class: "task-issues-list" - }, [ - (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestration.planIssues, (issue) => { - return (_openBlock(), _createElementBlock("div", { - key: issue.code + issue.message, - class: "task-issue-item" - }, _toDisplayString(issue.message), 1 /* TEXT */)) - }), 128 /* KEYED_FRAGMENT */)) - ])) - : _createCommentVNode("v-if", true), - (_ctx.taskOrchestration.planWarnings.length) - ? (_openBlock(), _createElementBlock("div", { - key: 2, - class: "task-warning-list" - }, [ - (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestration.planWarnings, (warning) => { - return (_openBlock(), _createElementBlock("div", { - key: warning, - class: "task-warning-item" - }, _toDisplayString(warning), 1 /* TEXT */)) - }), 128 /* KEYED_FRAGMENT */)) - ])) - : _createCommentVNode("v-if", true), - (_ctx.taskOrchestration.plan) - ? (_openBlock(), _createElementBlock(_Fragment, { key: 3 }, [ - _createElementVNode("div", { class: "task-plan-summary-strip" }, [ - _createElementVNode("div", { class: "task-plan-summary-item" }, [ - _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.nodes')), 1 /* TEXT */), - _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.plan.nodes.length), 1 /* TEXT */) - ]), - _createElementVNode("div", { class: "task-plan-summary-item" }, [ - _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.waves')), 1 /* TEXT */), - _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.plan.waves.length), 1 /* TEXT */) - ]), - _createElementVNode("div", { class: "task-plan-summary-item" }, [ - _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.engine')), 1 /* TEXT */), - _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.plan.engine), 1 /* TEXT */) - ]), - (_ctx.taskOrchestration.plan.threadId) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "task-plan-summary-item" - }, [ - _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.threadId')), 1 /* TEXT */), - _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.plan.threadId), 1 /* TEXT */) - ])) - : _createCommentVNode("v-if", true), - (_ctx.taskOrchestration.plan.cwd) - ? (_openBlock(), _createElementBlock("div", { - key: 1, - class: "task-plan-summary-item" - }, [ - _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.cwd')), 1 /* TEXT */), - _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.plan.cwd), 1 /* TEXT */) - ])) - : _createCommentVNode("v-if", true) - ]), - _createElementVNode("div", { class: "task-wave-list" }, [ - (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestration.plan.waves, (wave) => { - return (_openBlock(), _createElementBlock("div", { - key: wave.label, - class: "task-wave-card" - }, [ - _createElementVNode("div", { class: "task-wave-title" }, _toDisplayString(wave.label), 1 /* TEXT */), - _createElementVNode("div", { class: "task-wave-nodes" }, _toDisplayString(wave.nodeIds.join(', ')), 1 /* TEXT */) - ])) - }), 128 /* KEYED_FRAGMENT */)) - ]), - _createElementVNode("div", { class: "task-node-list" }, [ - (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestration.plan.nodes, (node) => { - return (_openBlock(), _createElementBlock("div", { - key: node.id, - class: "task-node-card" - }, [ - _createElementVNode("div", { class: "task-node-head" }, [ - _createElementVNode("div", null, [ - _createElementVNode("div", { class: "task-node-title" }, _toDisplayString(node.title || node.id), 1 /* TEXT */), - _createElementVNode("div", { class: "task-node-meta" }, [ - _createTextVNode(_toDisplayString(node.id) + " · " + _toDisplayString(node.kind), 1 /* TEXT */), - (node.workflowId) - ? (_openBlock(), _createElementBlock("span", { key: 0 }, " · " + _toDisplayString(node.workflowId), 1 /* TEXT */)) - : _createCommentVNode("v-if", true) - ]) - ]), - _createElementVNode("span", { - class: _normalizeClass(['pill', node.write ? 'configured' : 'empty']) - }, _toDisplayString(node.write ? _ctx.t('orchestration.plan.node.write') : _ctx.t('orchestration.plan.node.readOnly')), 3 /* TEXT, CLASS */) - ]), - _createElementVNode("div", { class: "task-node-deps" }, _toDisplayString(_ctx.t('orchestration.labels.dependencies')) + _toDisplayString(_ctx.formatTaskNodeDependencies(node)), 1 /* TEXT */) - ])) - }), 128 /* KEYED_FRAGMENT */)) - ]) - ], 64 /* STABLE_FRAGMENT */)) - : _createCommentVNode("v-if", true) - ])) - : _createCommentVNode("v-if", true), - (_ctx.taskOrchestrationActiveQueue.length || _ctx.taskOrchestration.runs.length || _ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.selectedRunError) - ? (_openBlock(), _createElementBlock("section", { - key: 1, - class: "selector-section task-workbench-card" - }, [ - _createElementVNode("div", { class: "selector-header task-section-header" }, [ - _createElementVNode("div", null, [ - _createElementVNode("span", { class: "selector-title" }, _toDisplayString(_ctx.t('orchestration.workbench.title')), 1 /* TEXT */), - _createElementVNode("div", { class: "skills-panel-note" }, _toDisplayString(_ctx.t('orchestration.workbench.subtitle')), 1 /* TEXT */) - ]), - _createElementVNode("div", { class: "settings-tab-actions task-header-actions" }, [ - _createElementVNode("button", { - type: "button", - class: "btn-tool btn-tool-compact", - onClick: $event => (_ctx.loadTaskOrchestrationOverview({ forceRefresh: true, includeDetail: true })), - disabled: _ctx.taskOrchestration.loading - }, _toDisplayString(_ctx.taskOrchestration.loading ? _ctx.t('common.refreshing') : _ctx.t('common.refresh')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), - (_ctx.taskOrchestrationActiveQueue.length) - ? (_openBlock(), _createElementBlock("button", { - key: 0, - type: "button", - class: "btn-tool btn-tool-compact", - onClick: $event => (_ctx.startTaskQueueRunner()), - disabled: _ctx.taskOrchestration.queueStarting - }, _toDisplayString(_ctx.taskOrchestration.queueStarting ? _ctx.t('orchestration.queue.starting') : _ctx.t('orchestration.queue.start')), 9 /* TEXT, PROPS */, ["onClick", "disabled"])) - : _createCommentVNode("v-if", true) - ]) - ]), - ((_ctx.taskOrchestrationActiveQueue.length ? 1 : 0) + (_ctx.taskOrchestration.runs.length ? 1 : 0) + ((_ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.selectedRunError) ? 1 : 0) > 1) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "task-workbench-tabs", - role: "group", - "aria-label": _ctx.t('orchestration.workbench.tabs.aria') - }, [ - (_ctx.taskOrchestrationActiveQueue.length) - ? (_openBlock(), _createElementBlock("button", { - key: 0, - type: "button", - class: _normalizeClass(["task-workbench-tab", { active: _ctx.taskOrchestration.workspaceTab === 'queue' }]), - onClick: $event => (_ctx.taskOrchestration.workspaceTab = 'queue') - }, _toDisplayString(_ctx.t('orchestration.workbench.tabs.queue', { count: _ctx.taskOrchestrationActiveQueue.length })), 11 /* TEXT, CLASS, PROPS */, ["onClick"])) - : _createCommentVNode("v-if", true), - (_ctx.taskOrchestration.runs.length) - ? (_openBlock(), _createElementBlock("button", { - key: 1, - type: "button", - class: _normalizeClass(["task-workbench-tab", { active: _ctx.taskOrchestration.workspaceTab === 'runs' }]), - onClick: $event => (_ctx.taskOrchestration.workspaceTab = 'runs') - }, _toDisplayString(_ctx.t('orchestration.workbench.tabs.runs', { count: _ctx.taskOrchestration.runs.length })), 11 /* TEXT, CLASS, PROPS */, ["onClick"])) - : _createCommentVNode("v-if", true), - (_ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.selectedRunError) - ? (_openBlock(), _createElementBlock("button", { - key: 2, - type: "button", - class: _normalizeClass(["task-workbench-tab", { active: _ctx.taskOrchestration.workspaceTab === 'detail' }]), - onClick: $event => (_ctx.taskOrchestration.workspaceTab = 'detail') - }, _toDisplayString(_ctx.t('orchestration.workbench.tabs.detail')), 11 /* TEXT, CLASS, PROPS */, ["onClick"])) - : _createCommentVNode("v-if", true) - ], 8 /* PROPS */, ["aria-label"])) - : _createCommentVNode("v-if", true), - ((_ctx.taskOrchestrationActiveQueue.length && _ctx.taskOrchestration.workspaceTab === 'queue') || (!_ctx.taskOrchestration.runs.length && !_ctx.taskOrchestration.selectedRunId && !_ctx.taskOrchestration.selectedRunError)) - ? (_openBlock(), _createElementBlock("div", { - key: 1, - class: "task-workbench-panel" - }, [ - (!_ctx.taskOrchestrationActiveQueue.length) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "task-empty-state" - }, [ - _createElementVNode("div", { class: "task-empty-title" }, _toDisplayString(_ctx.t('orchestration.queue.empty.title')), 1 /* TEXT */), - _createElementVNode("div", { class: "task-empty-copy" }, _toDisplayString(_ctx.t('orchestration.queue.empty.subtitle')), 1 /* TEXT */) - ])) - : (_openBlock(), _createElementBlock("div", { - key: 1, - class: "task-runtime-list" - }, [ - (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestrationActiveQueue, (item) => { - return (_openBlock(), _createElementBlock("div", { - key: item.taskId, - class: _normalizeClass(['task-runtime-item', { active: item.lastRunId && _ctx.taskOrchestration.selectedRunId === item.lastRunId, clickable: !!item.lastRunId }]), - role: item.lastRunId ? 'button' : null, - tabindex: item.lastRunId ? 0 : -1, - "aria-disabled": item.lastRunId ? null : 'true', - onClick: $event => (item.lastRunId ? (_ctx.taskOrchestration.workspaceTab = 'detail', _ctx.selectTaskRun(item.lastRunId)) : null), - onKeydown: [ - _withKeys(_withModifiers($event => (item.lastRunId ? (_ctx.taskOrchestration.workspaceTab = 'detail', _ctx.selectTaskRun(item.lastRunId)) : null), ["self","prevent"]), ["enter"]), - _withKeys(_withModifiers($event => (item.lastRunId ? (_ctx.taskOrchestration.workspaceTab = 'detail', _ctx.selectTaskRun(item.lastRunId)) : null), ["self","prevent"]), ["space"]) - ] - }, [ - _createElementVNode("div", { class: "task-runtime-item-main" }, [ - _createElementVNode("div", { class: "task-runtime-item-title" }, _toDisplayString(item.title || item.target || item.taskId), 1 /* TEXT */), - _createElementVNode("div", { class: "task-runtime-item-meta" }, _toDisplayString(item.taskId) + " · " + _toDisplayString(item.updatedAt || item.createdAt), 1 /* TEXT */), - (item.threadId || item.cwd) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "task-runtime-item-meta" - }, [ - (item.threadId) - ? (_openBlock(), _createElementBlock("span", { key: 0 }, _toDisplayString(_ctx.t('orchestration.plan.summary.threadId')) + ": " + _toDisplayString(item.threadId), 1 /* TEXT */)) - : _createCommentVNode("v-if", true), - (item.threadId && item.cwd) - ? (_openBlock(), _createElementBlock("span", { key: 1 }, " · ")) - : _createCommentVNode("v-if", true), - (item.cwd) - ? (_openBlock(), _createElementBlock("span", { key: 2 }, _toDisplayString(_ctx.t('orchestration.plan.summary.cwd')) + ": " + _toDisplayString(item.cwd), 1 /* TEXT */)) - : _createCommentVNode("v-if", true) - ])) - : _createCommentVNode("v-if", true), - (item.lastSummary) - ? (_openBlock(), _createElementBlock("div", { - key: 1, - class: "task-runtime-item-summary" - }, _toDisplayString(item.lastSummary), 1 /* TEXT */)) - : _createCommentVNode("v-if", true) - ]), - _createElementVNode("div", { class: "task-runtime-item-actions" }, [ - _createElementVNode("span", { - class: _normalizeClass(['pill', _ctx.taskRunStatusTone(item.status || item.runStatus)]) - }, _toDisplayString(item.status || item.runStatus), 3 /* TEXT, CLASS */), - (_ctx.isTaskRunActive(item.status || item.runStatus)) - ? (_openBlock(), _createElementBlock("button", { - key: 0, - type: "button", - class: "btn-mini", - onClick: _withModifiers($event => (_ctx.cancelTaskRunFromUi(item.taskId)), ["stop"]) - }, _toDisplayString(_ctx.t('common.cancel')), 9 /* TEXT, PROPS */, ["onClick"])) - : _createCommentVNode("v-if", true) - ]) - ], 42 /* CLASS, PROPS, NEED_HYDRATION */, ["role", "tabindex", "aria-disabled", "onClick", "onKeydown"])) - }), 128 /* KEYED_FRAGMENT */)) - ])) - ])) - : (_ctx.taskOrchestration.workspaceTab === 'runs' || (!_ctx.taskOrchestrationActiveQueue.length && _ctx.taskOrchestration.runs.length && !_ctx.taskOrchestration.selectedRunId && !_ctx.taskOrchestration.selectedRunError)) - ? (_openBlock(), _createElementBlock("div", { - key: 2, - class: "task-workbench-panel" - }, [ - (!_ctx.taskOrchestration.runs.length) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "task-empty-state" - }, [ - _createElementVNode("div", { class: "task-empty-title" }, _toDisplayString(_ctx.t('orchestration.runs.empty.title')), 1 /* TEXT */), - _createElementVNode("div", { class: "task-empty-copy" }, _toDisplayString(_ctx.t('orchestration.runs.empty.subtitle')), 1 /* TEXT */) - ])) - : (_openBlock(), _createElementBlock("div", { - key: 1, - class: "task-runtime-list" - }, [ - (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestration.runs, (item) => { - return (_openBlock(), _createElementBlock("button", { - key: item.runId, - type: "button", - class: _normalizeClass(['task-runtime-item', { active: _ctx.taskOrchestration.selectedRunId === item.runId }]), - onClick: $event => {_ctx.taskOrchestration.workspaceTab = 'detail'; _ctx.selectTaskRun(item.runId)} - }, [ - _createElementVNode("div", { class: "task-runtime-item-main" }, [ - _createElementVNode("div", { class: "task-runtime-item-title" }, _toDisplayString(item.title || item.taskId || item.runId), 1 /* TEXT */), - _createElementVNode("div", { class: "task-runtime-item-meta" }, _toDisplayString(item.runId) + " · " + _toDisplayString(item.durationMs || 0) + "ms", 1 /* TEXT */), - (item.threadId || item.cwd) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "task-runtime-item-meta" - }, [ - (item.threadId) - ? (_openBlock(), _createElementBlock("span", { key: 0 }, _toDisplayString(_ctx.t('orchestration.plan.summary.threadId')) + ": " + _toDisplayString(item.threadId), 1 /* TEXT */)) - : _createCommentVNode("v-if", true), - (item.threadId && item.cwd) - ? (_openBlock(), _createElementBlock("span", { key: 1 }, " · ")) - : _createCommentVNode("v-if", true), - (item.cwd) - ? (_openBlock(), _createElementBlock("span", { key: 2 }, _toDisplayString(_ctx.t('orchestration.plan.summary.cwd')) + ": " + _toDisplayString(item.cwd), 1 /* TEXT */)) - : _createCommentVNode("v-if", true) - ])) - : _createCommentVNode("v-if", true), - (item.summary) - ? (_openBlock(), _createElementBlock("div", { - key: 1, - class: "task-runtime-item-summary" - }, _toDisplayString(item.summary), 1 /* TEXT */)) - : _createCommentVNode("v-if", true) - ]), - _createElementVNode("div", { class: "task-runtime-item-actions" }, [ - _createElementVNode("span", { - class: _normalizeClass(['pill', _ctx.taskRunStatusTone(item.status)]) - }, _toDisplayString(item.status), 3 /* TEXT, CLASS */) - ]) - ], 10 /* CLASS, PROPS */, ["onClick"])) - }), 128 /* KEYED_FRAGMENT */)) - ])) - ])) - : (_openBlock(), _createElementBlock("div", { - key: 3, - class: "task-workbench-panel" - }, [ - _createElementVNode("div", { class: "task-detail-toolbar settings-tab-actions" }, [ - _createElementVNode("button", { - type: "button", - class: "btn-tool btn-tool-compact", - onClick: $event => (_ctx.taskOrchestration.selectedRunId ? _ctx.loadTaskRunDetail(_ctx.taskOrchestration.selectedRunId) : null), - disabled: !_ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.selectedRunLoading - }, _toDisplayString(_ctx.taskOrchestration.selectedRunLoading ? _ctx.t('common.refreshing') : _ctx.t('orchestration.detail.refresh')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), - _createElementVNode("button", { - type: "button", - class: "btn-tool btn-tool-compact", - onClick: $event => (_ctx.retryTaskRunFromUi(_ctx.taskOrchestration.selectedRunId)), - disabled: !_ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.retrying - }, _toDisplayString(_ctx.taskOrchestration.retrying ? _ctx.t('orchestration.detail.retrying') : _ctx.t('orchestration.detail.retry')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), - _createElementVNode("button", { - type: "button", - class: "btn-tool btn-tool-compact", - onClick: _ctx.continueTaskThreadFromUi, - disabled: !_ctx.taskOrchestrationSelectedRun - }, _toDisplayString(_ctx.t('orchestration.detail.continueThread')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]), - (_ctx.taskOrchestrationSelectedRun && _ctx.taskOrchestrationSelectedRun.run && _ctx.isTaskRunActive(_ctx.taskOrchestrationSelectedRun.run.status)) - ? (_openBlock(), _createElementBlock("button", { - key: 0, - type: "button", - class: "btn-tool btn-tool-compact", - onClick: $event => (_ctx.cancelTaskRunFromUi(_ctx.taskOrchestration.selectedRunId)) - }, _toDisplayString(_ctx.t('common.cancel')), 9 /* TEXT, PROPS */, ["onClick"])) - : _createCommentVNode("v-if", true) - ]), - (_ctx.taskOrchestration.selectedRunError) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "task-issue-item" - }, _toDisplayString(_ctx.taskOrchestration.selectedRunError), 1 /* TEXT */)) - : _createCommentVNode("v-if", true), - (!_ctx.taskOrchestrationSelectedRun) - ? (_openBlock(), _createElementBlock("div", { - key: 1, - class: "task-empty-state" - }, [ - _createElementVNode("div", { class: "task-empty-title" }, _toDisplayString(_ctx.t('orchestration.detail.empty.title')), 1 /* TEXT */), - _createElementVNode("div", { class: "task-empty-copy" }, _toDisplayString(_ctx.t('orchestration.detail.empty.subtitle')), 1 /* TEXT */) - ])) - : (_openBlock(), _createElementBlock(_Fragment, { key: 2 }, [ - _createElementVNode("div", { class: "task-detail-summary-strip" }, [ - _createElementVNode("div", { class: "task-plan-summary-item" }, [ - _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.detail.summary.status')), 1 /* TEXT */), - _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationSelectedRun.run.status), 1 /* TEXT */) - ]), - _createElementVNode("div", { class: "task-plan-summary-item" }, [ - _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.detail.summary.duration')), 1 /* TEXT */), - _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationSelectedRun.run.durationMs || 0) + "ms", 1 /* TEXT */) - ]), - _createElementVNode("div", { class: "task-plan-summary-item" }, [ - _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.detail.summary.nodes')), 1 /* TEXT */), - _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationSelectedRunNodes.length), 1 /* TEXT */) - ]), - _createElementVNode("div", { class: "task-plan-summary-item" }, [ - _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.detail.summary.summary')), 1 /* TEXT */), - _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationSelectedRun.run.summary || _ctx.t('common.none')), 1 /* TEXT */) - ]), - (_ctx.taskOrchestrationSelectedRun.threadId) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "task-plan-summary-item" - }, [ - _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.threadId')), 1 /* TEXT */), - _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationSelectedRun.threadId), 1 /* TEXT */) - ])) - : _createCommentVNode("v-if", true), - (_ctx.taskOrchestrationSelectedRun.cwd) - ? (_openBlock(), _createElementBlock("div", { - key: 1, - class: "task-plan-summary-item" - }, [ - _createElementVNode("span", { class: "task-plan-summary-label" }, _toDisplayString(_ctx.t('orchestration.plan.summary.cwd')), 1 /* TEXT */), - _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationSelectedRun.cwd), 1 /* TEXT */) - ])) - : _createCommentVNode("v-if", true) - ]), - (_ctx.taskOrchestrationSelectedRun.run.error) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "task-issue-item" - }, _toDisplayString(_ctx.taskOrchestrationSelectedRun.run.error), 1 /* TEXT */)) - : _createCommentVNode("v-if", true), - _createElementVNode("div", { class: "task-node-list" }, [ - (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestrationSelectedRunNodes, (node) => { - return (_openBlock(), _createElementBlock("div", { - key: node.id, - class: "task-node-card task-node-card-detail" - }, [ - _createElementVNode("div", { class: "task-node-head" }, [ - _createElementVNode("div", null, [ - _createElementVNode("div", { class: "task-node-title" }, _toDisplayString(node.title || node.id), 1 /* TEXT */), - _createElementVNode("div", { class: "task-node-meta" }, _toDisplayString(_ctx.t('orchestration.detail.node.meta', { id: node.id, attempts: (node.attemptCount || 0), autoFix: (node.autoFixRounds || 0) })), 1 /* TEXT */) - ]), - _createElementVNode("span", { - class: _normalizeClass(['pill', _ctx.taskRunStatusTone(node.status)]) - }, _toDisplayString(node.status), 3 /* TEXT, CLASS */) - ]), - (node.summary) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "task-runtime-item-summary" - }, _toDisplayString(node.summary), 1 /* TEXT */)) - : _createCommentVNode("v-if", true), - (node.error && node.error !== node.summary) - ? (_openBlock(), _createElementBlock("div", { - key: 1, - class: "task-node-deps" - }, _toDisplayString(_ctx.t('orchestration.labels.error')) + _toDisplayString(node.error), 1 /* TEXT */)) - : _createCommentVNode("v-if", true), - _createElementVNode("div", { class: "task-node-deps" }, _toDisplayString(_ctx.t('orchestration.labels.dependencies')) + _toDisplayString(_ctx.formatTaskNodeDependencies(node)), 1 /* TEXT */), - (node.output && typeof node.output === 'object') - ? (_openBlock(), _createElementBlock("div", { - key: 2, - class: "task-node-output-card" - }, [ - _createElementVNode("div", { class: "task-node-output-head" }, [ - _createElementVNode("strong", null, _toDisplayString(_ctx.t('orchestration.detail.node.output')), 1 /* TEXT */), - (node.output.provider || node.output.model) - ? (_openBlock(), _createElementBlock("span", { - key: 0, - class: "task-node-output-meta" - }, [ - _createTextVNode(_toDisplayString(node.output.provider || ''), 1 /* TEXT */), - (node.output.provider && node.output.model) - ? (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, [ - _createTextVNode(" · ") - ], 64 /* STABLE_FRAGMENT */)) - : _createCommentVNode("v-if", true), - _createTextVNode(_toDisplayString(node.output.model || ''), 1 /* TEXT */) - ])) - : _createCommentVNode("v-if", true) - ]), - _createElementVNode("div", { class: "task-node-output-facts" }, [ - (node.output.endpoint) - ? (_openBlock(), _createElementBlock("span", { key: 0 }, _toDisplayString(_ctx.t('orchestration.detail.node.endpoint')) + _toDisplayString(node.output.endpoint), 1 /* TEXT */)) - : _createCommentVNode("v-if", true), - (node.output.status) - ? (_openBlock(), _createElementBlock("span", { key: 1 }, "HTTP " + _toDisplayString(node.output.status), 1 /* TEXT */)) - : _createCommentVNode("v-if", true), - (node.output.durationMs) - ? (_openBlock(), _createElementBlock("span", { key: 2 }, _toDisplayString(node.output.durationMs) + "ms", 1 /* TEXT */)) - : _createCommentVNode("v-if", true) - ]), - _createElementVNode("pre", { class: "task-log-block task-output-block" }, _toDisplayString(_ctx.formatTaskNodeOutputText(node)), 1 /* TEXT */), - (node.output.materializedFiles && node.output.materializedFiles.length) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "task-materialized-files" - }, [ - _createElementVNode("div", { class: "task-node-deps" }, _toDisplayString(_ctx.t('orchestration.detail.node.materializedFiles')), 1 /* TEXT */), - (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(node.output.materializedFiles, (file) => { - return (_openBlock(), _createElementBlock("div", { - key: file.path || file.relativePath, - class: "task-materialized-file" - }, _toDisplayString(file.relativePath || file.path) + " · " + _toDisplayString(file.bytes || 0) + " bytes", 1 /* TEXT */)) - }), 128 /* KEYED_FRAGMENT */)) - ])) - : _createCommentVNode("v-if", true) - ])) - : _createCommentVNode("v-if", true), - _createElementVNode("pre", { class: "task-log-block" }, _toDisplayString(_ctx.formatTaskNodeLogs(node.logs)), 1 /* TEXT */) - ])) - }), 128 /* KEYED_FRAGMENT */)) - ]) - ], 64 /* STABLE_FRAGMENT */)) - ])) - ])) - : _createCommentVNode("v-if", true) - ])) + : _createCommentVNode("v-if", true) ], 8 /* PROPS */, ["data-active"])), [ [_vShow, _ctx.mainTab === 'orchestration'] ]) diff --git a/web-ui/styles/task-orchestration.css b/web-ui/styles/task-orchestration.css index 108a4d03..3b3e4961 100644 --- a/web-ui/styles/task-orchestration.css +++ b/web-ui/styles/task-orchestration.css @@ -1307,94 +1307,198 @@ min-width: 0; } +#panel-orchestration { + min-height: calc(100vh - 96px); +} + +#panel-orchestration .task-hero-card { + padding: 10px 14px; + border-radius: 16px; + background: rgba(255, 255, 255, 0.62); + box-shadow: none; +} + +#panel-orchestration .task-hero-main { + align-items: center; + gap: 12px; +} + +#panel-orchestration .task-hero-card .selector-title { + font-size: 1rem; + line-height: 1.25; +} + +#panel-orchestration .task-hero-card .task-hero-copy, +#panel-orchestration .task-hero-card .task-hero-kicker { + display: none; +} + +#panel-orchestration .task-quick-card { + display: grid; + grid-template-columns: minmax(0, 1fr) var(--task-orchestration-context-width); + gap: 14px; + align-items: start; + padding: 0; + border: 0; + background: transparent; + box-shadow: none; +} + +#panel-orchestration .task-quick-main { + display: flex; + flex-direction: column; + gap: 10px; + min-width: 0; +} + #panel-orchestration .task-chat-panel { + display: flex; + flex-direction: column; + gap: 12px; + height: min(780px, calc(100vh - 170px)); + min-height: 620px; margin: 0; - padding: 14px; - border: 1px solid var(--color-border-soft); - border-radius: 18px; - background: var(--color-surface-raised, var(--color-surface)); - box-shadow: none; + padding: 12px; + overflow: hidden; + border: 1px solid rgba(199, 116, 98, 0.16); + border-radius: 22px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(255, 252, 248, 0.7)), + rgba(255, 255, 255, 0.6); + box-shadow: 0 18px 52px rgba(86, 58, 39, 0.08); } #panel-orchestration .task-chat-thread { - max-height: 280px; - margin-bottom: 10px; - padding: 0; + flex: 1 1 auto; + min-height: 0; + max-height: none; + margin: 0; + padding: 10px 8px 16px; + gap: 12px; + overflow-y: auto; border: 0; background: transparent; } -#panel-orchestration .task-chat-composer .task-textarea-goal { - min-height: 116px; +#panel-orchestration .task-thread-message-card { + position: relative; + align-self: flex-start; + width: min(100%, 760px); + margin: 2px 0 4px 44px; + padding: 14px; + border: 1px solid rgba(199, 116, 98, 0.18); + border-radius: 20px; + background: rgba(255, 255, 255, 0.84); + box-shadow: 0 12px 30px rgba(86, 58, 39, 0.09); +} + +#panel-orchestration .task-thread-message-card::before { + content: 'AI'; + position: absolute; + left: -40px; + top: 12px; + display: grid; + width: 30px; + height: 30px; + place-items: center; + border-radius: 999px; + background: var(--color-brand-light); + color: var(--color-brand-dark); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.04em; +} + +#panel-orchestration .task-thread-message-card .selector-header { + margin-bottom: 10px; +} + +#panel-orchestration .task-thread-message-card .selector-title { + font-size: 0.98rem; +} + +#panel-orchestration .task-thread-message-card .skills-panel-note { + font-size: 0.82rem; +} + +#panel-orchestration .task-thread-composer { + flex: 0 0 auto; + position: sticky; + bottom: 0; + z-index: 2; + width: min(100%, 800px); + align-self: center; + margin-top: 0; + padding: 10px; + border: 1px solid rgba(199, 116, 98, 0.18); + border-radius: 18px 18px 20px 20px; + background: rgba(255, 255, 255, 0.96); + box-shadow: 0 -10px 34px rgba(86, 58, 39, 0.13); +} + +#panel-orchestration .task-thread-composer .selector-label { + font-size: 0.78rem; +} + +#panel-orchestration .task-thread-composer .task-textarea-goal { + min-height: 76px; resize: vertical; border: 0; - border-radius: 0; - background: transparent; - padding: 0; + border-radius: 12px; + background: rgba(255, 248, 241, 0.48); + font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace); font-size: 15px; line-height: 1.55; box-shadow: none; } -#panel-orchestration .task-chat-composer .task-textarea-goal:focus { +#panel-orchestration .task-thread-composer .task-textarea-goal:focus { box-shadow: none; } -#panel-orchestration .task-chat-send-row, -#panel-orchestration .task-chat-context-row, -#panel-orchestration .task-draft-inline, -#panel-orchestration .task-action-row-prominent { - margin-top: 10px; +#panel-orchestration .task-thread-composer .task-chat-send-row, +#panel-orchestration .task-thread-composer .task-chat-context-row, +#panel-orchestration .task-thread-composer .task-draft-inline, +#panel-orchestration .task-thread-composer .task-action-row-prominent { + margin-top: 8px; } -#panel-orchestration .task-chat-context-row, -#panel-orchestration .task-config-strip { - opacity: 0.82; +#panel-orchestration .task-thread-composer .task-draft-inline { + padding: 8px 10px; + border-radius: 14px; + background: rgba(255, 248, 241, 0.58); } -#panel-orchestration .task-action-row-prominent { - padding-top: 10px; - border-top: 1px solid var(--color-border-soft); +#panel-orchestration .task-thread-composer .task-action-row-prominent { + padding-top: 0; + border-top: 1px solid rgba(199, 116, 98, 0.12); } -#panel-orchestration .task-action-row-prominent .task-action-row-right { +#panel-orchestration .task-thread-composer .task-action-row-right { margin-left: auto; } -#panel-orchestration .task-codex-queue-action { - justify-content: flex-start; - margin: 0 0 12px; +#panel-orchestration .task-thread-composer .task-field-hint { + font-size: 0.75rem; } -#panel-orchestration .task-quick-advanced { - margin-top: 8px; - border: 0; - background: transparent; - box-shadow: none; +#panel-orchestration .task-plan-summary-strip, +#panel-orchestration .task-wave-list, +#panel-orchestration .task-node-list, +#panel-orchestration .task-runtime-list { + gap: 8px; } -#panel-orchestration .task-plan-card, -#panel-orchestration .task-workbench-card { - border-radius: 20px; +#panel-orchestration .task-quick-advanced { + margin-top: 8px; + border: 1px solid rgba(148, 163, 184, 0.14); + background: rgba(255, 255, 255, 0.46); box-shadow: none; } -@media (max-width: 760px) { - #panel-orchestration .task-hero-main, - #panel-orchestration .task-action-row-prominent, - #panel-orchestration .task-action-row-prominent .task-action-row-right { - align-items: stretch; - flex-direction: column; - } - - #panel-orchestration .task-action-row-prominent .btn-tool, - #panel-orchestration .task-action-row-prominent .task-action-row-right { - width: 100%; - } -} - -#panel-orchestration { - min-height: calc(100vh - 96px); +#panel-orchestration .task-codex-queue-action { + justify-content: flex-start; + margin: 0 0 12px; } #panel-orchestration .task-quick-side-card { @@ -1402,7 +1506,16 @@ top: 14px; padding: 12px; border-radius: 18px; - background: rgba(255, 255, 255, 0.58); + background: rgba(255, 255, 255, 0.42); + opacity: 0.82; + box-shadow: none; +} + +#panel-orchestration .task-provider-status-card, +#panel-orchestration .task-quick-status-card, +#panel-orchestration .task-quick-side-card .task-readiness-item { + background: rgba(255, 255, 255, 0.48); + border-color: rgba(148, 163, 184, 0.16); box-shadow: none; } @@ -1411,7 +1524,6 @@ margin-bottom: 10px; padding: 10px; border-radius: 14px; - background: rgba(255, 255, 255, 0.72); } #panel-orchestration .task-provider-status-grid { @@ -1422,7 +1534,7 @@ #panel-orchestration .task-provider-status-row { padding: 7px 8px; border-radius: 10px; - background: rgba(248, 250, 252, 0.78); + background: rgba(248, 250, 252, 0.62); } #panel-orchestration .task-provider-status-card .task-readiness-copy, @@ -1450,6 +1562,15 @@ grid-template-columns: minmax(0, 1fr); } + #panel-orchestration .task-thread-message-card { + width: 100%; + margin-left: 0; + } + + #panel-orchestration .task-thread-message-card::before { + display: none; + } + #panel-orchestration .task-quick-side-card { position: static; display: grid; @@ -1464,7 +1585,423 @@ } @media (max-width: 760px) { - #panel-orchestration .task-quick-side-card { + #panel-orchestration .task-hero-main, + #panel-orchestration .task-action-row-prominent, + #panel-orchestration .task-action-row-prominent .task-action-row-right { + align-items: stretch; + flex-direction: column; + } + + #panel-orchestration .task-action-row-prominent .btn-tool, + #panel-orchestration .task-action-row-prominent .task-action-row-right { + width: 100%; + } + + #panel-orchestration .task-quick-side-card, + #panel-orchestration .task-template-block, + #panel-orchestration .task-quick-copy, + #panel-orchestration .task-stage-card { display: none; } + + #panel-orchestration .task-chat-panel { + height: calc(100vh - 126px); + min-height: 620px; + padding: 10px; + border-radius: 18px; + } + + #panel-orchestration .task-chat-thread { + padding: 10px 6px 14px; + } + + #panel-orchestration .task-thread-message-card { + padding: 12px; + border-radius: 18px; + } + + #panel-orchestration .task-thread-composer { + width: 100%; + padding: 8px; + border-radius: 16px; + box-shadow: 0 -8px 26px rgba(86, 58, 39, 0.12); + } + + #panel-orchestration .task-thread-composer .task-textarea-goal { + min-height: 56px; + max-height: 120px; + } + + #panel-orchestration .task-thread-composer .task-chat-context-row, + #panel-orchestration .task-thread-composer .task-draft-inline, + #panel-orchestration .task-thread-composer .task-action-caption, + #panel-orchestration .task-thread-composer .task-field-hint { + display: none; + } + + #panel-orchestration .task-thread-composer .task-chat-send-row, + #panel-orchestration .task-thread-composer .task-action-row-prominent { + margin-top: 6px; + } + + #panel-orchestration .task-thread-composer .btn-tool { + min-height: 34px; + padding: 0 12px; + font-size: 0.78rem; + } +} + +/* Codex-style final pass: keep the transcript primary and collapse dashboard chrome. */ +#panel-orchestration .task-layout-grid-primary, +#panel-orchestration .task-layout-grid-secondary { + max-width: 980px; +} + +#panel-orchestration .task-hero-card { + max-width: 980px; + margin-bottom: 8px; + padding: 8px 12px; + border-color: rgba(148, 163, 184, 0.12); + background: rgba(255, 255, 255, 0.38); +} + +#panel-orchestration .task-hero-actions .btn-tool { + min-height: 30px; + padding: 0 10px; + font-size: 0.76rem; +} + +#panel-orchestration .task-quick-card { + grid-template-columns: minmax(0, 1fr); + max-width: 980px; +} + +#panel-orchestration .task-quick-side-card { + display: none; +} + +#panel-orchestration .task-chat-panel { + height: min(790px, calc(100vh - 136px)); + min-height: 660px; +} + +#panel-orchestration .task-thread-composer { + position: relative; + bottom: auto; + width: min(100%, 760px); + padding: 8px; + border-radius: 18px; + box-shadow: 0 -4px 18px rgba(86, 58, 39, 0.08); +} + +#panel-orchestration .task-thread-composer .selector-label, +#panel-orchestration .task-thread-composer .task-chat-context-row, +#panel-orchestration .task-thread-composer .task-draft-inline, +#panel-orchestration .task-thread-composer .task-action-caption, +#panel-orchestration .task-thread-composer .task-field-hint { + display: none; +} + +#panel-orchestration .task-thread-composer .task-textarea-goal { + min-height: 64px; + border-radius: 14px; +} + +#panel-orchestration .task-thread-composer .task-chat-send-row, +#panel-orchestration .task-thread-composer .task-action-row-prominent { + margin-top: 6px; +} + +#panel-orchestration .task-thread-composer .task-action-row-prominent { + border-top: 0; +} + +#panel-orchestration .task-thread-message-card { + width: min(100%, 720px); +} + +@media (max-width: 760px) { + #panel-orchestration .task-layout-grid-primary, + #panel-orchestration .task-layout-grid-secondary, + #panel-orchestration .task-hero-card, + #panel-orchestration .task-quick-card { + max-width: 100%; + } + + #panel-orchestration .task-hero-card { + display: none; + } + + #panel-orchestration .task-chat-panel { + height: calc(100vh - 82px); + min-height: 620px; + } + + #panel-orchestration .task-thread-message-card { + width: 100%; + } +} + +/* Transcript-first hardening: remove console chrome from the default path. */ +#panel-orchestration .task-hero-card, +#panel-orchestration .task-quick-advanced { + display: none; +} + +#panel-orchestration .task-chat-panel { + height: min(820px, calc(100vh - 72px)); + min-height: 680px; +} + +#panel-orchestration .task-chat-thread { + padding-bottom: 28px; +} + +#panel-orchestration .task-plan-summary-item, +#panel-orchestration .task-wave-card, +#panel-orchestration .task-node-card { + background: rgba(255, 255, 255, 0.46); + border-color: rgba(148, 163, 184, 0.14); + box-shadow: none; +} + +@media (max-width: 760px) { + #panel-orchestration .task-chat-panel { + height: calc(100vh - 58px); + min-height: 640px; + } + + #panel-orchestration .task-chat-thread { + padding-bottom: 32px; + } +} + +#panel-orchestration .task-thread-card-label { + margin-bottom: 8px; + color: var(--color-text-secondary); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +#panel-orchestration .task-thread-message-card { + border-radius: 18px 18px 18px 6px; +} + +#panel-orchestration .task-thread-message-card .selector-header { + padding-bottom: 8px; + border-bottom: 1px solid rgba(148, 163, 184, 0.12); +} + +#panel-orchestration .task-thread-message-card .task-plan-summary-strip { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +#panel-orchestration .task-thread-message-card .task-plan-summary-item { + min-width: 104px; + padding: 8px 10px; + border-radius: 12px; +} + +#panel-orchestration .task-thread-message-card .task-wave-card, +#panel-orchestration .task-thread-message-card .task-node-card, +#panel-orchestration .task-thread-message-card .task-runtime-item { + padding: 9px 10px; + border-radius: 12px; + background: rgba(248, 250, 252, 0.54); +} + +#panel-orchestration .task-thread-message-card .task-node-title, +#panel-orchestration .task-thread-message-card .task-wave-title, +#panel-orchestration .task-thread-message-card .task-runtime-item-title { + font-size: 0.86rem; +} + +#panel-orchestration .task-thread-message-card .task-node-meta, +#panel-orchestration .task-thread-message-card .task-node-deps, +#panel-orchestration .task-thread-message-card .task-wave-nodes, +#panel-orchestration .task-thread-message-card .task-runtime-item-meta { + font-size: 0.74rem; + line-height: 1.45; +} + +@media (max-width: 760px) { + #panel-orchestration .task-thread-message-card .task-plan-summary-item { + min-width: calc(50% - 4px); + } +} + +#panel-orchestration .task-thread-plan-request { + margin-top: 4px; +} + +#panel-orchestration .task-thread-plan-request .task-chat-bubble { + max-width: min(78%, 640px); + border-radius: 18px 18px 6px 18px; +} + +@media (max-width: 760px) { + #panel-orchestration .task-thread-plan-request .task-chat-bubble { + max-width: 92%; + } +} + +/* Composer must reserve layout space, never overlay transcript content. */ +#panel-orchestration .task-thread-composer { + position: relative; + bottom: auto; + z-index: 0; + box-shadow: 0 10px 32px rgba(86, 58, 39, 0.08); +} + +#panel-orchestration .task-chat-thread { + scroll-padding-bottom: 32px; + padding-bottom: 36px; +} + +@media (max-width: 760px) { + #panel-orchestration .task-chat-thread { + scroll-padding-bottom: 36px; + padding-bottom: 40px; + } +} + +/* In the chat transcript, plan details are attachments, not the whole conversation. */ +#panel-orchestration .task-thread-plan-card .task-plan-summary-strip { + margin-top: 6px; +} + +#panel-orchestration .task-thread-plan-card .task-wave-list, +#panel-orchestration .task-thread-plan-card .task-node-list { + gap: 5px; + margin-top: 8px; +} + +#panel-orchestration .task-thread-plan-card .task-wave-card, +#panel-orchestration .task-thread-plan-card .task-node-card { + padding: 6px 8px; + border-radius: 10px; +} + +#panel-orchestration .task-thread-plan-card .task-wave-nodes, +#panel-orchestration .task-thread-plan-card .task-node-meta, +#panel-orchestration .task-thread-plan-card .task-node-deps { + display: none; +} + +#panel-orchestration .task-thread-plan-card .task-node-head { + align-items: center; +} + +#panel-orchestration .task-thread-plan-card .pill { + padding: 3px 7px; + font-size: 10px; +} + +#panel-orchestration .task-thread-plan-card .task-node-title, +#panel-orchestration .task-thread-plan-card .task-wave-title { + font-size: 0.82rem; + font-weight: 650; +} + +#panel-orchestration .task-thread-plan-card .task-plan-summary-item { + min-width: 88px; + padding: 6px 8px; +} + +#panel-orchestration .task-thread-plan-card .task-plan-summary-label { + font-size: 9px; +} + +@media (max-width: 760px) { + #panel-orchestration .task-thread-plan-card .task-plan-summary-item { + min-width: calc(50% - 4px); + } +} + +/* Codex-like composer: compact on desktop too, so it cannot eat the transcript. */ +#panel-orchestration .task-thread-composer .selector-label, +#panel-orchestration .task-thread-composer .task-chat-context-row, +#panel-orchestration .task-thread-composer .task-draft-inline, +#panel-orchestration .task-thread-composer .task-action-caption, +#panel-orchestration .task-thread-composer .task-field-hint { + display: none; +} + +#panel-orchestration .task-thread-composer { + display: grid; + gap: 8px; + padding: 8px 10px; +} + +#panel-orchestration .task-thread-composer .task-textarea-goal { + min-height: 54px; + max-height: 96px; + padding: 8px 10px; + font-size: 14px; + line-height: 1.35; +} + +#panel-orchestration .task-thread-composer .task-chat-send-row, +#panel-orchestration .task-thread-composer .task-action-row-prominent { + margin-top: 0; +} + +#panel-orchestration .task-thread-composer .task-chat-send-row { + display: flex; + justify-content: flex-end; +} + +#panel-orchestration .task-thread-composer .task-action-row-prominent { + display: flex; + align-items: center; + gap: 8px; + padding-top: 0; + border-top: 0; +} + +#panel-orchestration .task-thread-composer .btn-tool { + padding: 6px 10px; +} + +#panel-orchestration .task-thread-composer { + grid-template-columns: minmax(0, 1fr) auto; + grid-template-rows: auto auto; + align-items: stretch; +} + +#panel-orchestration .task-thread-composer .task-chat-composer { + grid-column: 1; + grid-row: 1 / span 2; + display: block; +} + +#panel-orchestration .task-thread-composer .task-chat-send-row { + grid-column: 2; + grid-row: 1; + align-items: center; + justify-content: flex-end; +} + +#panel-orchestration .task-thread-composer .task-action-row-prominent { + grid-column: 2; + grid-row: 2; + justify-content: flex-end; +} + +@media (max-width: 760px) { + #panel-orchestration .task-thread-composer { + grid-template-columns: 1fr; + grid-template-rows: auto auto auto; + } + + #panel-orchestration .task-thread-composer .task-chat-composer, + #panel-orchestration .task-thread-composer .task-chat-send-row, + #panel-orchestration .task-thread-composer .task-action-row-prominent { + grid-column: 1; + grid-row: auto; + } } From e37ac3ffdcd4e1c426ce17f4b46a0440e110c00e Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sun, 28 Jun 2026 10:59:52 +0000 Subject: [PATCH 22/54] fix(task-orchestration): clear stale quick plans --- tests/unit/config-tabs-ui.test.mjs | 2 ++ tests/unit/web-ui-logic.test.mjs | 21 +++++++++++++++++++ .../app.methods.task-orchestration.mjs | 2 +- .../partials/index/panel-orchestration.html | 6 +++--- web-ui/res/web-ui-render.precompiled.js | 6 +++--- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index 799b3907..29ffc99a 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -116,6 +116,8 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(orchestrationPanel, /class="task-chat-context-chip"/); assert.match(orchestrationPanel, /@change="taskOrchestration\.selectedEngine === 'workflow' \? null : taskOrchestration\.workflowIdsText = ''"/); assert.match(orchestrationPanel, /class="task-quick-template-grid"/); + assert.match(orchestrationPanel, /taskOrchestration\.planFingerprint = ''/); + assert.match(orchestrationPanel, /taskOrchestration\.lastError = ''/); assert.match(orchestrationPanel, /class="task-workflow-suggestions"/); assert.match(orchestrationPanel, /@click="appendTaskWorkflowId\(workflow.id \|\| workflow.name\)"/); assert.match(orchestrationPanel, /class="[^"]*task-draft-overview[^"]*"/); diff --git a/tests/unit/web-ui-logic.test.mjs b/tests/unit/web-ui-logic.test.mjs index 722b254e..77cc4b9a 100644 --- a/tests/unit/web-ui-logic.test.mjs +++ b/tests/unit/web-ui-logic.test.mjs @@ -1579,6 +1579,27 @@ test('task orchestration detail helpers surface AI output and open OpenAI Chat c ['switchMainTab', 'config'], ['openEditModal', 'mock-openai'] ]); + + const fallbackCalls = []; + methods.openTaskOpenAiChatConfig.call({ + mainTab: 'orchestration', + configMode: 'openclaw', + providersList: [{ name: 'different-provider', hasKey: true }], + taskOrchestration: { + openAiChatStatus: { providerName: 'missing-provider' } + }, + switchMainTab(tab) { + fallbackCalls.push(['switchMainTab', tab]); + this.mainTab = tab; + }, + openAddProviderModal() { + fallbackCalls.push(['openAddProviderModal']); + } + }); + assert.deepStrictEqual(fallbackCalls, [ + ['switchMainTab', 'config'], + ['openAddProviderModal'] + ]); }); test('selectTaskRun switches workbench to detail and keeps latest detail response only', async () => { diff --git a/web-ui/modules/app.methods.task-orchestration.mjs b/web-ui/modules/app.methods.task-orchestration.mjs index a5a8f577..01ade140 100644 --- a/web-ui/modules/app.methods.task-orchestration.mjs +++ b/web-ui/modules/app.methods.task-orchestration.mjs @@ -214,7 +214,7 @@ export function createTaskOrchestrationMethods(options = {}) { : null; if (provider && typeof this.openEditModal === 'function') { this.openEditModal(provider); - } else if (!providerName && typeof this.openAddProviderModal === 'function') { + } else if (typeof this.openAddProviderModal === 'function') { this.openAddProviderModal(); } }, diff --git a/web-ui/partials/index/panel-orchestration.html b/web-ui/partials/index/panel-orchestration.html index 96302cf3..0421369e 100644 --- a/web-ui/partials/index/panel-orchestration.html +++ b/web-ui/partials/index/panel-orchestration.html @@ -358,15 +358,15 @@
{{ t('orchestration.templates.title') }}
- - - diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index 922bd8e3..f7a4fe83 100644 --- a/web-ui/res/web-ui-render.precompiled.js +++ b/web-ui/res/web-ui-render.precompiled.js @@ -4679,7 +4679,7 @@ return function render(_ctx, _cache) { _createElementVNode("button", { type: "button", class: "task-quick-template-card", - onClick: $event => {_ctx.taskOrchestration.target = _ctx.t('orchestration.templates.reviewFix.target'); _ctx.taskOrchestration.selectedEngine = 'openai-chat'; _ctx.taskOrchestration.workflowIdsText = ''; _ctx.taskOrchestration.notes = _ctx.t('orchestration.templates.reviewFix.notes'); _ctx.taskOrchestration.followUpsText = _ctx.t('orchestration.templates.reviewFix.followUps')} + onClick: $event => {_ctx.taskOrchestration.target = _ctx.t('orchestration.templates.reviewFix.target'); _ctx.taskOrchestration.selectedEngine = 'openai-chat'; _ctx.taskOrchestration.workflowIdsText = ''; _ctx.taskOrchestration.notes = _ctx.t('orchestration.templates.reviewFix.notes'); _ctx.taskOrchestration.followUpsText = _ctx.t('orchestration.templates.reviewFix.followUps'); _ctx.taskOrchestration.plan = null; _ctx.taskOrchestration.planIssues = []; _ctx.taskOrchestration.planWarnings = []; _ctx.taskOrchestration.planFingerprint = ''; _ctx.taskOrchestration.lastError = ''} }, [ _createElementVNode("strong", null, _toDisplayString(_ctx.t('orchestration.templates.reviewFix.label')), 1 /* TEXT */), _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.quick.templates.reviewFix.meta')), 1 /* TEXT */) @@ -4687,7 +4687,7 @@ return function render(_ctx, _cache) { _createElementVNode("button", { type: "button", class: "task-quick-template-card", - onClick: $event => {_ctx.taskOrchestration.target = _ctx.t('orchestration.templates.planOnly.target'); _ctx.taskOrchestration.selectedEngine = 'openai-chat'; _ctx.taskOrchestration.workflowIdsText = ''; _ctx.taskOrchestration.notes = _ctx.t('orchestration.templates.planOnly.notes'); _ctx.taskOrchestration.followUpsText = ''} + onClick: $event => {_ctx.taskOrchestration.target = _ctx.t('orchestration.templates.planOnly.target'); _ctx.taskOrchestration.selectedEngine = 'openai-chat'; _ctx.taskOrchestration.workflowIdsText = ''; _ctx.taskOrchestration.notes = _ctx.t('orchestration.templates.planOnly.notes'); _ctx.taskOrchestration.followUpsText = ''; _ctx.taskOrchestration.plan = null; _ctx.taskOrchestration.planIssues = []; _ctx.taskOrchestration.planWarnings = []; _ctx.taskOrchestration.planFingerprint = ''; _ctx.taskOrchestration.lastError = ''} }, [ _createElementVNode("strong", null, _toDisplayString(_ctx.t('orchestration.templates.planOnly.label')), 1 /* TEXT */), _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.quick.templates.planOnly.meta')), 1 /* TEXT */) @@ -4695,7 +4695,7 @@ return function render(_ctx, _cache) { _createElementVNode("button", { type: "button", class: "task-quick-template-card", - onClick: $event => {_ctx.taskOrchestration.target = _ctx.t('orchestration.templates.workflowBatch.target'); _ctx.taskOrchestration.selectedEngine = 'workflow'; _ctx.taskOrchestration.workflowIdsText = _ctx.t('orchestration.templates.workflowBatch.workflowIds'); _ctx.taskOrchestration.notes = _ctx.t('orchestration.templates.workflowBatch.notes'); _ctx.taskOrchestration.followUpsText = ''} + onClick: $event => {_ctx.taskOrchestration.target = _ctx.t('orchestration.templates.workflowBatch.target'); _ctx.taskOrchestration.selectedEngine = 'workflow'; _ctx.taskOrchestration.workflowIdsText = _ctx.t('orchestration.templates.workflowBatch.workflowIds'); _ctx.taskOrchestration.notes = _ctx.t('orchestration.templates.workflowBatch.notes'); _ctx.taskOrchestration.followUpsText = ''; _ctx.taskOrchestration.plan = null; _ctx.taskOrchestration.planIssues = []; _ctx.taskOrchestration.planWarnings = []; _ctx.taskOrchestration.planFingerprint = ''; _ctx.taskOrchestration.lastError = ''} }, [ _createElementVNode("strong", null, _toDisplayString(_ctx.t('orchestration.templates.workflowBatch.label')), 1 /* TEXT */), _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.quick.templates.workflowBatch.meta')), 1 /* TEXT */) From 38bf86465b2019969224bc72f445038934eab8b8 Mon Sep 17 00:00:00 2001 From: awsl233777 Date: Sun, 28 Jun 2026 11:06:27 +0000 Subject: [PATCH 23/54] fix(task-orchestration): remove preview-only action --- tests/unit/config-tabs-ui.test.mjs | 5 +++-- web-ui/partials/index/panel-orchestration.html | 3 --- web-ui/res/web-ui-render.precompiled.js | 6 ------ web-ui/styles/task-orchestration.css | 3 --- 4 files changed, 3 insertions(+), 14 deletions(-) diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index 29ffc99a..5b73380c 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -87,7 +87,7 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(html, /v-show="mainTab === 'orchestration'"/); assert.match(orchestrationPanel, /t\('orchestration\.hero\.kicker'\)/); assert.match(orchestrationPanel, /t\('orchestration\.hero\.title'\)/); - assert.match(orchestrationPanel, /@click="previewTaskPlan\(\)"/); + assert.doesNotMatch(orchestrationPanel, /@click="previewTaskPlan\(\)"/); assert.match(orchestrationPanel, /@click="planAndRunTaskOrchestration\(\)"/); assert.match(orchestrationPanel, /@click="queueTaskOrchestrationAndStart\(\)"/); assert.match(orchestrationPanel, /@click="startTaskQueueRunner\(\)"/); @@ -132,7 +132,8 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(orchestrationPanel, /taskOrchestration\.plan \|\| taskOrchestration\.planIssues\.length \|\| taskOrchestration\.planWarnings\.length \|\| taskOrchestration\.lastError/); assert.match(orchestrationPanel, /class="selector-section task-stage-card"/); assert.match(orchestrationPanel, /t\('orchestration\.stage\.title'\)/); - assert.match(orchestrationPanel, /class="btn-tool task-action-preview" @click="previewTaskPlan\(\)"/); + assert.doesNotMatch(orchestrationPanel, /class="btn-tool task-action-preview" @click="previewTaskPlan\(\)"/); + assert.doesNotMatch(orchestrationPanel, /orchestration\.actions\.previewOnly/); assert.match(orchestrationPanel, /class="task-action-row-right task-action-row-right-prominent"/); assert.match(orchestrationPanel, /class="task-action-caption"/); assert.match(orchestrationPanel, /class="task-empty-state"/); diff --git a/web-ui/partials/index/panel-orchestration.html b/web-ui/partials/index/panel-orchestration.html index 0421369e..4eb46571 100644 --- a/web-ui/partials/index/panel-orchestration.html +++ b/web-ui/partials/index/panel-orchestration.html @@ -342,9 +342,6 @@
-
+
+
AI · suggestions
+
{{ t('orchestration.templates.title') }}
+
+ + + +
+
+ +
+
AI · next step
+
+
+ {{ taskOrchestrationDraftReadiness.title }} +
{{ taskOrchestrationDraftReadiness.summary }}
+
+
+
{{ taskOrchestration.selectedEngine === 'workflow' ? t('orchestration.engine.workflow') : t('orchestration.engine.openaiChat') }}
+
{{ taskOrchestration.runMode === 'dry-run' ? t('orchestration.runMode.dryRun') : (taskOrchestration.runMode === 'read' ? t('orchestration.runMode.readOnly') : t('orchestration.runMode.write') ) }}
+
{{ t('orchestration.pills.sequenceCount', { count: taskOrchestrationDraftMetrics.requestCount }) }}
+
{{ t('orchestration.pills.hasTitle') }}
+
{{ t('orchestration.pills.workflowCount', { count: taskOrchestrationDraftMetrics.workflowCount }) }}
+
+
+
+
+ {{ item.label }} + {{ item.detail }} +
+
+
+ +
+ AI · {{ t('orchestration.advanced.title') }} +
+ +
+
+ + + + + + + + + + +
+
+
@@ -126,7 +245,6 @@
-
@@ -145,6 +263,18 @@ +
+ {{ taskOrchestrationSelectedRun.run.status }} + {{ taskOrchestration.runs[0].status }} + {{ taskOrchestrationActiveQueue.length }} queued + Ready + {{ taskOrchestrationSelectedRun.run.summary || taskOrchestrationSelectedRun.run.runId || taskOrchestration.selectedRunId }} + {{ taskOrchestration.runs[0].summary || taskOrchestration.runs[0].runId }} + {{ taskOrchestrationActiveQueue[0].title || taskOrchestrationActiveQueue[0].target || taskOrchestrationActiveQueue[0].taskId }} + {{ t('orchestration.workbench.subtitle') }} +
+
+ Execution details
+
+
-
-
- {{ taskOrchestrationDraftReadiness.title }} -
{{ taskOrchestrationDraftReadiness.summary }}
-
-
-
{{ taskOrchestration.selectedEngine === 'workflow' ? t('orchestration.engine.workflow') : t('orchestration.engine.openaiChat') }}
-
{{ taskOrchestration.runMode === 'dry-run' ? t('orchestration.runMode.dryRun') : (taskOrchestration.runMode === 'read' ? t('orchestration.runMode.readOnly') : t('orchestration.runMode.write') ) }}
-
{{ t('orchestration.pills.sequenceCount', { count: taskOrchestrationDraftMetrics.requestCount }) }}
-
{{ t('orchestration.pills.hasTitle') }}
-
{{ t('orchestration.pills.workflowCount', { count: taskOrchestrationDraftMetrics.workflowCount }) }}
-
{{ t('orchestration.pills.planNodes', { count: taskOrchestrationDraftMetrics.planNodeCount }) }}
-
-
-
- -
-
{{ t('orchestration.templates.title') }}
-
- - - -
-
- -
- {{ t('orchestration.advanced.title') }} -
- -
-
- - - - - - - - - - -
-