diff --git a/cli.js b/cli.js index 78808257..742b49aa 100644 --- a/cli.js +++ b/cli.js @@ -73,8 +73,15 @@ const { truncateText: truncateTaskText, buildTaskPlan, validateTaskPlan, - executeTaskPlan + executeTaskPlan, + computePlanWaves } = require('./lib/task-orchestrator'); +const { + buildWorkspaceChatContext, + loadWorkspaceChatThread, + appendWorkspaceChatThread, + applyWorkspaceFileOperations +} = require('./lib/task-workspace-chat'); const { readAutomationConfig, matchAutomationRule, @@ -258,6 +265,9 @@ 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_WORKSPACE_CHAT_THREADS_DIR = path.join(CONFIG_DIR, 'codexmate-task-chat-threads'); +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; @@ -13015,6 +13025,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser detached: true, taskId, runId, + threadId: plan.threadId || '', + cwd: plan.cwd || '', warnings: validation.warnings || [] }; } else { @@ -13859,7 +13871,11 @@ function printTaskHelp() { console.log(' --allow-write 允许写入工作区'); console.log(' --dry-run 仅计划/预演,不执行写入'); console.log(' --plan-only 仅输出计划,不执行'); - console.log(' --engine 选择编排引擎'); + console.log(' --engine 选择编排引擎'); + console.log(' --cwd <路径> 指定任务工作区路径'); + console.log(' --thread-id 指定任务线程 ID'); + console.log(' --conversation-id 指定任务线程 ID(兼容别名)'); + console.log(' --session-id 指定任务线程 ID(兼容别名)'); console.log(' --concurrency 并发度'); console.log(' --auto-fix-rounds 自动修复回合数'); console.log(' --limit runs/queue list 数量'); @@ -13882,7 +13898,7 @@ function parseTaskCliOptions(args = []) { allowWrite: false, dryRun: false, planOnly: false, - engine: 'codex', + engine: 'openai-chat', concurrency: 2, autoFixRounds: 1, limit: 20, @@ -14029,6 +14045,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; @@ -14086,9 +14134,11 @@ 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.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) { @@ -14102,7 +14152,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}`); @@ -15556,6 +15606,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) { @@ -15567,9 +15621,19 @@ 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() : ''; - 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 = []) { @@ -15610,6 +15674,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() : '', @@ -15619,6 +15684,7 @@ function normalizeTaskPlanRequest(params = {}) { dryRun: source.dryRun === true, concurrency: Number.isFinite(source.concurrency) ? source.concurrency : parseInt(source.concurrency, 10), autoFixRounds: Number.isFinite(source.autoFixRounds) ? source.autoFixRounds : parseInt(source.autoFixRounds, 10), + previewOnly: source.previewOnly === true, workflowIds: rawWorkflowIds, followUps: normalizeTaskFollowUps(rawFollowUps) }; @@ -15627,12 +15693,18 @@ 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', 'previewOnly', '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.cwd = typeof plan.cwd === 'string' && plan.cwd.trim() ? plan.cwd.trim() : process.cwd(); + 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 || []); @@ -15647,6 +15719,7 @@ function coerceTaskPlanPayload(params = {}) { }); return { ...plan, + threadId: request.threadId || createTaskThreadId(), engine: normalizeTaskEngine(request.engine || plan.engine) }; } @@ -15669,8 +15742,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(), ''), @@ -15871,8 +15946,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, @@ -15996,6 +16073,7 @@ function buildTaskOverviewPayload(options = {}) { } return { workflows: workflowCatalog.workflows, + openAiChatStatus: buildTaskOpenAiChatStatus(), warnings, queue, runs, @@ -16058,20 +16136,434 @@ 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 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' + ? 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 pickTaskProviderTemperature(provider) { + const raw = provider && Object.prototype.hasOwnProperty.call(provider, 'temperature') + ? provider.temperature + : undefined; + if (raw === undefined || raw === null || String(raw).trim() === '') { + return 0.2; + } + const value = Number(raw); + if (!Number.isFinite(value) || value < 0 || value > 2) { + return 0.2; + } + return value; +} + +function pickTaskOpenAiChatProviderName(config) { + const taskProvider = typeof config.task_openai_chat_provider === 'string' + ? config.task_openai_chat_provider.trim() + : ''; + if (taskProvider) { + return taskProvider; + } + return typeof config.model_provider === 'string' ? config.model_provider.trim() : ''; +} + +function resolveTaskOpenAiChatConfig() { + const configResult = readConfigOrVirtualDefault(); + const config = configResult && configResult.config && typeof configResult.config === 'object' ? configResult.config : {}; + const providerName = pickTaskOpenAiChatProviderName(config); + 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, + temperature: pickTaskProviderTemperature(provider) + }; +} + +function buildTaskOpenAiChatStatus() { + const requestConfig = resolveTaskOpenAiChatConfig(); + if (requestConfig && requestConfig.error) { + return { + ok: false, + ready: false, + error: requestConfig.error, + providerName: '', + model: '', + endpoint: '', + hasApiKey: false, + hasExtraHeaders: false + }; + } + const hasApiKey = !!(requestConfig && typeof requestConfig.apiKey === 'string' && requestConfig.apiKey.trim()); + const hasExtraHeaders = !!(requestConfig + && requestConfig.extraHeaders + && typeof requestConfig.extraHeaders === 'object' + && !Array.isArray(requestConfig.extraHeaders) + && Object.keys(requestConfig.extraHeaders).length > 0); + const hasAuthMaterial = hasApiKey || hasExtraHeaders; + return { + ok: true, + ready: hasAuthMaterial, + error: hasAuthMaterial ? '' : `OpenAI Chat 提供商 ${requestConfig.providerName || ''} 缺少 API key 或额外 headers`, + providerName: requestConfig.providerName || '', + model: requestConfig.model || '', + endpoint: redactTaskEndpointUrl(requestConfig.endpointUrl || ''), + hasApiKey, + hasExtraHeaders + }; +} + +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) => { + if (settled) return; + receivedBytes += chunk.length || 0; + if (receivedBytes > TASK_OPENAI_CHAT_MAX_RESPONSE_BYTES) { + 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; + 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(); + }); +} + +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 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 = [ + /(?: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(); + 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)); + try { + if (fs.lstatSync(normalized.path).isSymbolicLink()) { + warnings.push(`artifact target is a symlink: ${normalized.relativePath}`); + continue; + } + } catch (error) { + if (!error || error.code !== 'ENOENT') { + throw error; + } + } + 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) { 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 hasApiKey = typeof requestConfig.apiKey === 'string' && requestConfig.apiKey.trim(); + const hasExtraHeaders = requestConfig.extraHeaders + && typeof requestConfig.extraHeaders === 'object' + && !Array.isArray(requestConfig.extraHeaders) + && Object.keys(requestConfig.extraHeaders).length > 0; + if (!hasApiKey && !hasExtraHeaders) { + const error = `OpenAI Chat 提供商 ${requestConfig.providerName || ''} 缺少 API key 或额外 headers`; + return { + success: false, + error, + summary: error, + output: { + provider: requestConfig.providerName || '', + model: requestConfig.model || '', + endpoint: redactTaskEndpointUrl(requestConfig.endpointUrl || ''), + status: 0, + text: '', + response: null, + durationMs: 0, + materializedFiles: [] + }, + logs: [{ at: toIsoTime(Date.now()), level: 'error', message: error }] + }; + } + const allowWrite = context.allowWrite === true && node.write !== false && context.dryRun !== true; const dependencyResults = Array.isArray(context.dependencyResults) ? context.dependencyResults : []; const dependencyLines = dependencyResults .map((item) => { @@ -16091,124 +16583,95 @@ 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 rawThreadId = String(context.threadId || (context.plan && context.plan.threadId) || '').trim(); + const threadId = /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(rawThreadId) ? rawThreadId : ''; + const threadStoreDir = typeof TASK_WORKSPACE_CHAT_THREADS_DIR === 'string' ? TASK_WORKSPACE_CHAT_THREADS_DIR : ''; + const canLoadWorkspaceThread = typeof loadWorkspaceChatThread === 'function' && !!threadStoreDir; + const thread = threadId && canLoadWorkspaceThread ? loadWorkspaceChatThread(threadStoreDir, threadId) : { messages: [] }; + const historyMessages = Array.isArray(thread.messages) ? thread.messages.slice(-12) : []; + const workspaceContext = typeof buildWorkspaceChatContext === 'function' + ? buildWorkspaceChatContext(context.cwd || process.cwd(), finalPrompt, historyMessages) + : ''; + const systemPrompt = [ + '你是 codexmate 任务编排中的 OpenAI Chat-compatible 执行节点。', + '基于用户给出的目标、前置节点摘要、同一 thread 历史和当前工作区快照,输出可执行、可验证、事实谨慎的结果。', + allowWrite + ? '当前任务允许写入;如果需要创建或更新文件,优先使用 fenced code block:```codexmate-file action="write" path="相对路径"。代码块内容会写入任务工作目录内对应文本文件。删除文件请单独写一行 CODEXMATE_DELETE_FILE: 相对路径。只允许安全相对路径。' + : '当前任务是只读/验证节点,不要声称修改了文件。', + '查询/读取文件时,先依据用户要求和工作区快照回答;如果快照不足,说明缺口,不要编造。', + '回答使用中文,避免空泛总结。' + ].join('\n'); + const currentUserMessage = [ + finalPrompt, + workspaceContext ? '\n--- codexmate workspace snapshot ---' : '', + workspaceContext + ].filter(Boolean).join('\n'); + const startedAt = Date.now(); + const body = { + model: requestConfig.model, + messages: [ + { role: 'system', content: systemPrompt }, + ...historyMessages.map((message) => ({ + role: message.role === 'assistant' ? 'assistant' : 'user', + content: String(message.content || '') + })), + { role: 'user', content: currentUserMessage } + ], + temperature: Number.isFinite(requestConfig.temperature) ? requestConfig.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 = redactTaskEndpointUrl(requestConfig.endpointUrl); + 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: [] }; + const workspaceOperations = success && typeof applyWorkspaceFileOperations === 'function' + ? applyWorkspaceFileOperations(text, context.cwd || process.cwd(), { allowWrite }) + : { files: [], warnings: [] }; + const threadAppend = threadId && typeof appendWorkspaceChatThread === 'function' && !!threadStoreDir + ? appendWorkspaceChatThread(threadStoreDir, threadId, [ + { role: 'user', content: finalPrompt }, + { role: 'assistant', content: text || errorMessage } + ], { cwd: context.cwd || process.cwd() }) + : { ok: false, error: 'thread id is empty' }; 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, + messageCount: body.messages.length, + threadId, + threadStore: threadAppend && threadAppend.file ? threadAppend.file : '', + materializedFiles: materialized.files, + workspaceFiles: workspaceOperations.files }, 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}` }, + ...(threadId ? [{ at: toIsoTime(Date.now()), level: 'info', message: `workspace chat thread=${threadId} messages=${body.messages.length}` }] : []), + ...(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)` })), + ...workspaceOperations.files.map((file) => ({ at: toIsoTime(Date.now()), level: 'info', message: `workspace ${file.operation} ${file.relativePath} (${file.bytes} bytes)` })), + ...materialized.warnings.map((warning) => ({ at: toIsoTime(Date.now()), level: 'warn', message: warning })), + ...workspaceOperations.warnings.map((warning) => ({ at: toIsoTime(Date.now()), level: 'warn', message: warning })), + ...(threadAppend && threadAppend.error ? [{ at: toIsoTime(Date.now()), level: 'warn', message: threadAppend.error }] : []) ] }; } @@ -16246,7 +16709,7 @@ async function executeTaskNodeAdapter(node, context = {}) { : [] }; } - return runCodexExecTaskNode(node, context); + return runOpenAiChatTaskNode(node, context); } async function runTaskPlanInternal(plan, options = {}) { @@ -16260,13 +16723,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, @@ -16302,6 +16770,8 @@ async function runTaskPlanInternal(plan, options = {}) { const queued = upsertTaskQueueItem({ ...options.queueItem, taskId, + threadId, + cwd, status: 'running', runStatus: 'running', lastRunId: runId, @@ -16320,9 +16790,10 @@ async function runTaskPlanInternal(plan, options = {}) { plan, taskId, runId, + threadId, allowWrite: plan.allowWrite === true, dryRun: plan.dryRun === true, - cwd: plan.cwd || process.cwd() + cwd }), onUpdate: async (snapshot) => { const nextDetail = { @@ -16336,6 +16807,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')), @@ -16365,6 +16838,8 @@ async function runTaskPlanInternal(plan, options = {}) { const queued = upsertTaskQueueItem({ ...options.queueItem, taskId, + threadId, + cwd, status: run.status === 'success' ? 'completed' : (run.status === 'cancelled' ? 'cancelled' : 'failed'), @@ -16395,8 +16870,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 64a69734..43ab85f5 100644 --- a/lib/task-orchestrator.js +++ b/lib/task-orchestrator.js @@ -1,3 +1,5 @@ +const OPENAI_CHAT_TRANSIENT_RETRY_LIMIT = 2; + function isPlainObject(value) { return !!value && typeof value === 'object' && !Array.isArray(value); } @@ -100,6 +102,23 @@ 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, ''); + 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'; + return normalized; +} + function normalizeDependencyIds(value = []) { return uniqueStringList(value) .map((depId) => normalizeId(depId, '')) @@ -193,11 +212,15 @@ function detectDependencyCycle(nodes = []) { return false; } -function buildCodexNodePrompt(kind, context = {}) { +const PLAN_DISCUSSION_PREFIX = '我们先探讨方案,在我让你生成之前不要生成'; + +function buildOpenAiChatNodePrompt(kind, context = {}) { const title = normalizeText(context.title, 160); const target = normalizeText(context.target, 4000); const item = normalizeText(context.item, 1200); const followUp = normalizeText(context.followUp, 1200); + const followUps = uniqueStringList(context.followUps || []).slice(0, 8); + const cwd = normalizeText(context.cwd, 1200); const dependencySummaries = uniqueStringList(context.dependencySummaries || []).slice(0, 6); const allowWrite = context.allowWrite === true; const dependencyBlock = dependencySummaries.length > 0 @@ -206,10 +229,12 @@ function buildCodexNodePrompt(kind, context = {}) { const writeRule = allowWrite ? '允许直接修改本地工作区,但必须控制变更范围并完成最小必要验证。' : '只允许只读调查,不要修改任何文件,不要执行写入型操作。'; + const cwdLine = cwd ? `工作区路径: ${cwd}` : ''; if (kind === 'analysis') { return [ `任务标题: ${title || '任务分析'}`, + cwdLine, `任务目标:\n${target}`, writeRule, '请先调查当前仓库与上下文,给出简洁的现状判断、主要风险、建议执行顺序,以及你认为最小可行的验证方案。', @@ -217,9 +242,25 @@ function buildCodexNodePrompt(kind, context = {}) { ].join('\n\n'); } + if (kind === 'plan') { + const userDemand = [PLAN_DISCUSSION_PREFIX, target].filter(Boolean).join('\n\n'); + const followUpBlock = followUps.length > 0 + ? `\n补充需求:\n${followUps.map((entry, index) => `${index + 1}. ${entry}`).join('\n')}` + : ''; + return [ + `任务标题: ${title || '方案草拟'}`, + cwdLine, + `任务需求:\n${userDemand}`, + followUpBlock, + '请只输出可讨论、可确认的执行方案:目标拆解、建议步骤、风险点、需要用户确认的问题。', + '不要修改文件、不要启动实际执行,也不要把“等待用户确认”拆成依赖执行链。' + ].filter(Boolean).join('\n\n'); + } + if (kind === 'work') { return [ `任务标题: ${title || '执行子任务'}`, + cwdLine, `总目标:\n${target}`, `当前子任务:\n${item || title || target}`, dependencyBlock, @@ -231,6 +272,7 @@ function buildCodexNodePrompt(kind, context = {}) { if (kind === 'verify') { return [ `任务标题: ${title || '验证与总结'}`, + cwdLine, `总目标:\n${target}`, dependencyBlock, writeRule, @@ -240,6 +282,7 @@ function buildCodexNodePrompt(kind, context = {}) { return [ `任务标题: ${title || '后续任务'}`, + cwdLine, `总目标:\n${target}`, dependencyBlock, writeRule, @@ -267,7 +310,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 requestedEngine = normalizeTaskEngine(request.engine); const allowWrite = request.allowWrite === true; const dryRun = request.dryRun === true; const concurrency = normalizePositiveInteger(request.concurrency, 2, 1, 8); @@ -278,12 +321,33 @@ 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')}`; - const shouldBuildWorkflowNodes = requestedWorkflowIds.length > 0 || engine === 'workflow'; + const shouldBuildWorkflowNodes = requestedWorkflowIds.length > 0 || requestedEngine === 'workflow'; + const engine = shouldBuildWorkflowNodes ? 'workflow' : requestedEngine; + const previewOnly = request.previewOnly === true && !shouldBuildWorkflowNodes; - if (shouldBuildWorkflowNodes) { + if (previewOnly) { + const previewId = nextNodeId('plan'); + nodes.push({ + id: previewId, + title: '方案草拟', + kind: 'openai-chat', + prompt: buildOpenAiChatNodePrompt('plan', { + title, + target, + followUps, + cwd, + allowWrite: false + }), + dependsOn: [], + write: false, + retryLimit: 0, + autoFixRounds: 0 + }); + } else if (shouldBuildWorkflowNodes) { let previousId = ''; for (const workflowId of requestedWorkflowIds) { const meta = workflowMap.get(workflowId); @@ -313,15 +377,16 @@ function buildTaskPlan(request = {}, options = {}) { nodes.push({ id: analysisId, title: '现状分析', - kind: 'codex', - prompt: buildCodexNodePrompt('analysis', { + kind: 'openai-chat', + prompt: buildOpenAiChatNodePrompt('analysis', { title, target, + cwd, allowWrite: false }), dependsOn: [], write: false, - retryLimit: 0, + retryLimit: OPENAI_CHAT_TRANSIENT_RETRY_LIMIT, autoFixRounds: 0 }); @@ -333,16 +398,17 @@ 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, + cwd, allowWrite }), dependsOn: [analysisId], write: allowWrite, - retryLimit: 0, + retryLimit: OPENAI_CHAT_TRANSIENT_RETRY_LIMIT, autoFixRounds }); } @@ -351,15 +417,16 @@ function buildTaskPlan(request = {}, options = {}) { nodes.push({ id: verifyId, title: '验证与总结', - kind: 'codex', - prompt: buildCodexNodePrompt('verify', { + kind: 'openai-chat', + prompt: buildOpenAiChatNodePrompt('verify', { title, target, + cwd, allowWrite: false }), dependsOn: executionNodeIds.slice(), write: false, - retryLimit: 0, + retryLimit: OPENAI_CHAT_TRANSIENT_RETRY_LIMIT, autoFixRounds: 0 }); } @@ -371,16 +438,17 @@ 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, + cwd, allowWrite, followUp }), dependsOn: followUpDependsOn, write: allowWrite, - retryLimit: 0, + retryLimit: OPENAI_CHAT_TRANSIENT_RETRY_LIMIT, autoFixRounds }); followUpDependsOn = [nodeId]; @@ -389,6 +457,7 @@ function buildTaskPlan(request = {}, options = {}) { const plan = { id: normalizeId(request.id, ''), + threadId, title, target, notes, @@ -417,7 +486,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 +514,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 +526,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 +561,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/lib/task-workspace-chat.js b/lib/task-workspace-chat.js new file mode 100644 index 00000000..5979a987 --- /dev/null +++ b/lib/task-workspace-chat.js @@ -0,0 +1,292 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const TEXT_FILE_EXTENSIONS = new Set(['.html', '.htm', '.css', '.js', '.mjs', '.json', '.md', '.txt', '.svg', '.ts', '.tsx', '.jsx', '.yml', '.yaml']); +const MAX_FILE_BYTES = 512 * 1024; +const MAX_CONTEXT_FILE_BYTES = 16 * 1024; +const MAX_CONTEXT_TOTAL_BYTES = 64 * 1024; +const MAX_HISTORY_MESSAGES = 20; +const SENSITIVE_CONTEXT_NAME_RE = /(^|[._\-/])(credential|credentials|secret|secrets|token|tokens|apikey|api-key|api_key|password|passwd|private-key|private_key|\.env)([._\-/]|$)/i; + +function ensureDir(dir) { + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); +} + +function sanitizeThreadId(value) { + const text = typeof value === 'string' ? value.trim() : ''; + if (!text) return ''; + return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(text) ? text : ''; +} + +function normalizeWorkspacePath(rawPath, cwd) { + const baseDir = path.resolve(typeof cwd === 'string' && cwd.trim() ? cwd.trim() : process.cwd()); + const text = typeof rawPath === 'string' ? rawPath.trim().replace(/^['"]|['"]$/g, '') : ''; + if (!text || text.length > 240) { + return { error: 'workspace file path is empty or too long' }; + } + if (path.isAbsolute(text) || text.includes('\\')) { + return { error: `workspace file 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: `workspace file path escapes cwd: ${text}` }; + } + const ext = path.extname(normalized).toLowerCase(); + if (!TEXT_FILE_EXTENSIONS.has(ext)) { + return { error: `workspace file 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: `workspace file path escapes cwd: ${text}` }; + } + return { path: targetPath, relativePath: normalized, baseDir }; +} + +function validateParentPath(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: 'workspace parent escapes cwd' }; + if (fs.existsSync(current)) { + const stats = fs.lstatSync(current); + if (stats.isSymbolicLink()) return { ok: false, error: `workspace 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: 'workspace 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: 'workspace parent escapes cwd' }; + } + return { ok: true }; + } + pending.push(current); + current = path.dirname(current); + } +} + +function parseAttributes(info) { + const attrs = {}; + const text = typeof info === 'string' ? info : ''; + const attrRe = /([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*("([^"]*)"|'([^']*)'|([^\s]+))/g; + let match; + while ((match = attrRe.exec(text)) !== null) { + attrs[match[1]] = match[3] || match[4] || match[5] || ''; + } + return attrs; +} + +function parseWorkspaceFileOperations(text) { + const content = typeof text === 'string' ? text : ''; + const operations = []; + const fenceRe = /```([^\n`]*)\n([\s\S]*?)```/g; + let match; + while ((match = fenceRe.exec(content)) !== null) { + const info = String(match[1] || '').trim(); + if (!/^codexmate-file\b/i.test(info)) continue; + const attrs = parseAttributes(info); + const action = String(attrs.action || attrs.op || 'write').trim().toLowerCase(); + const filePath = String(attrs.path || attrs.file || attrs.filename || '').trim(); + operations.push({ action: action === 'update' ? 'write' : action, path: filePath, content: String(match[2] || '') }); + } + + const deleteRe = /^\s*CODEXMATE_(?:DELETE|REMOVE)_FILE\s*:\s*(.+?)\s*$/gmi; + while ((match = deleteRe.exec(content)) !== null) { + operations.push({ action: 'delete', path: String(match[1] || '').trim(), content: '' }); + } + return operations; +} + +function isSensitiveWorkspaceContextFile(relativePath) { + const normalized = String(relativePath || '').replace(/\\+/g, '/').toLowerCase(); + return SENSITIVE_CONTEXT_NAME_RE.test(normalized); +} + +function isWorkspaceFileMentioned(relativePath, promptText) { + const normalizedPath = String(relativePath || '').toLowerCase(); + const baseName = path.basename(normalizedPath); + return !!normalizedPath && (promptText.includes(normalizedPath) || (!!baseName && promptText.includes(baseName))); +} + +function applyWorkspaceFileOperations(text, cwd, options = {}) { + const allowWrite = options.allowWrite === true; + const operations = parseWorkspaceFileOperations(text); + const files = []; + const warnings = []; + if (!allowWrite && operations.length > 0) { + return { files, warnings: ['workspace file operations skipped because allowWrite is false'] }; + } + for (const operation of operations) { + const action = operation.action === 'delete' ? 'delete' : 'write'; + const normalized = normalizeWorkspacePath(operation.path, cwd); + if (normalized.error) { + warnings.push(normalized.error); + continue; + } + try { + if (action === 'delete') { + if (!fs.existsSync(normalized.path)) { + files.push({ path: normalized.path, relativePath: normalized.relativePath, bytes: 0, operation: 'delete', existed: false }); + continue; + } + const stats = fs.lstatSync(normalized.path); + if (stats.isSymbolicLink()) { + warnings.push(`workspace target is a symlink: ${normalized.relativePath}`); + continue; + } + if (!stats.isFile()) { + warnings.push(`workspace target is not a file: ${normalized.relativePath}`); + continue; + } + fs.unlinkSync(normalized.path); + files.push({ path: normalized.path, relativePath: normalized.relativePath, bytes: 0, operation: 'delete', existed: true }); + continue; + } + + const body = String(operation.content || ''); + const bytes = Buffer.byteLength(body, 'utf-8'); + if (bytes > MAX_FILE_BYTES) { + warnings.push(`workspace artifact too large and skipped: ${normalized.relativePath}`); + continue; + } + const parentValidation = validateParentPath(normalized.path, normalized.baseDir); + if (!parentValidation.ok) { + warnings.push(parentValidation.error || `workspace parent is unsafe: ${normalized.relativePath}`); + continue; + } + ensureDir(path.dirname(normalized.path)); + try { + if (fs.lstatSync(normalized.path).isSymbolicLink()) { + warnings.push(`workspace target is a symlink: ${normalized.relativePath}`); + continue; + } + } catch (error) { + if (!error || error.code !== 'ENOENT') throw error; + } + fs.writeFileSync(normalized.path, body, { encoding: 'utf-8', mode: 0o600 }); + files.push({ path: normalized.path, relativePath: normalized.relativePath, bytes, operation: 'write' }); + } catch (error) { + warnings.push(`workspace operation failed for ${normalized.relativePath}: ${error && error.message ? error.message : String(error)}`); + } + } + return { files, warnings }; +} + +function walkWorkspace(cwd, options = {}) { + const baseDir = path.resolve(typeof cwd === 'string' && cwd.trim() ? cwd.trim() : process.cwd()); + const maxFiles = Number.isFinite(options.maxFiles) ? options.maxFiles : 120; + const files = []; + const skipNames = new Set(['.git', 'node_modules', '.codexmate', '.DS_Store']); + function walk(dir) { + if (files.length >= maxFiles) return; + let entries = []; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch (_) { + return; + } + entries.sort((a, b) => a.name.localeCompare(b.name)); + for (const entry of entries) { + if (files.length >= maxFiles) break; + if (skipNames.has(entry.name)) continue; + const fullPath = path.join(dir, entry.name); + let stats; + try { + stats = fs.lstatSync(fullPath); + } catch (_) { + continue; + } + if (stats.isSymbolicLink()) continue; + const relativePath = path.relative(baseDir, fullPath); + if (stats.isDirectory()) { + walk(fullPath); + } else if (stats.isFile()) { + files.push({ path: fullPath, relativePath, bytes: stats.size, ext: path.extname(relativePath).toLowerCase() }); + } + } + } + if (fs.existsSync(baseDir)) walk(baseDir); + return files; +} + +function buildWorkspaceChatContext(cwd, prompt = '', historyMessages = []) { + const files = walkWorkspace(cwd); + const promptText = [prompt, ...historyMessages.map((item) => item && item.content ? item.content : '')].join('\n').toLowerCase(); + const treeLines = files.map((file) => `- ${file.relativePath} (${file.bytes} bytes)`); + const contentBlocks = []; + let total = 0; + for (const file of files) { + if (!TEXT_FILE_EXTENSIONS.has(file.ext)) continue; + if (file.bytes > MAX_CONTEXT_FILE_BYTES) continue; + const mentioned = isWorkspaceFileMentioned(file.relativePath, promptText); + if (!mentioned) continue; + if (isSensitiveWorkspaceContextFile(file.relativePath)) continue; + if (total + file.bytes > MAX_CONTEXT_TOTAL_BYTES) break; + try { + const body = fs.readFileSync(file.path, 'utf-8'); + total += Buffer.byteLength(body, 'utf-8'); + contentBlocks.push(`### ${file.relativePath}\n\`\`\`\n${body}\n\`\`\``); + } catch (_) {} + } + return [ + '当前工作区文件列表:', + treeLines.length ? treeLines.join('\n') : '(empty workspace)', + contentBlocks.length ? '\n当前工作区可读文本文件内容快照:\n' + contentBlocks.join('\n\n') : '\n当前没有可注入的文本文件内容快照。' + ].join('\n'); +} + +function getThreadFile(storeDir, threadId) { + const safe = sanitizeThreadId(threadId); + if (!safe) return ''; + return path.join(storeDir, `${safe}.json`); +} + +function loadWorkspaceChatThread(storeDir, threadId) { + const file = getThreadFile(storeDir, threadId); + if (!file || !fs.existsSync(file)) return { threadId: sanitizeThreadId(threadId), messages: [] }; + try { + const parsed = JSON.parse(fs.readFileSync(file, 'utf-8')); + const messages = Array.isArray(parsed.messages) ? parsed.messages.filter((item) => item && (item.role === 'user' || item.role === 'assistant') && typeof item.content === 'string') : []; + return { ...parsed, threadId: sanitizeThreadId(parsed.threadId || threadId), messages: messages.slice(-MAX_HISTORY_MESSAGES) }; + } catch (_) { + return { threadId: sanitizeThreadId(threadId), messages: [] }; + } +} + +function appendWorkspaceChatThread(storeDir, threadId, messages, metadata = {}) { + const safe = sanitizeThreadId(threadId); + if (!safe) return { ok: false, error: 'invalid thread id' }; + ensureDir(storeDir); + const existing = loadWorkspaceChatThread(storeDir, safe); + const now = new Date().toISOString(); + const nextMessages = existing.messages.concat((Array.isArray(messages) ? messages : []).map((item) => ({ + role: item.role === 'assistant' ? 'assistant' : 'user', + content: String(item.content || ''), + at: item.at || now + }))).slice(-MAX_HISTORY_MESSAGES); + const payload = { + threadId: safe, + cwd: metadata.cwd || existing.cwd || '', + updatedAt: now, + messages: nextMessages + }; + const file = getThreadFile(storeDir, safe); + fs.writeFileSync(file, JSON.stringify(payload, null, 2), { encoding: 'utf-8', mode: 0o600 }); + return { ok: true, file, messageCount: nextMessages.length }; +} + +module.exports = { + TEXT_FILE_EXTENSIONS, + sanitizeThreadId, + normalizeWorkspacePath, + parseWorkspaceFileOperations, + applyWorkspaceFileOperations, + walkWorkspace, + buildWorkspaceChatContext, + loadWorkspaceChatThread, + appendWorkspaceChatThread +}; 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 313f412c..ef6e3513 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.34 + 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..bec7ad8e 100644 --- a/tests/e2e/test-task-orchestration.js +++ b/tests/e2e/test-task-orchestration.js @@ -1,5 +1,175 @@ +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: 'glm-5.2', object: 'model' }, + { id: 'glm-5.2-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 requestText = JSON.stringify(parsedBody || {}); + const content = requestText.includes('target-symlink-artifact-probe') + ? '输出文件:index.html\\n\`\`\`html\\n

target symlink escape

\\n\`\`\`' + : 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({ + id: 'chatcmpl-task-e2e-' + requestCount, + object: 'chat.completion', + 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') }); + 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 = "gpt-5.3-codex"', + 'model_provider = "local"', + 'task_openai_chat_provider = "new-api-chat"', + '', + '[model_providers.local]', + 'name = "Local Codex"', + 'base_url = "http://127.0.0.1:9/v1"', + 'wire_api = "responses"', + 'preferred_auth_method = "sk-codex-tab-secret"', + 'models = ["gpt-5.3-codex"]', + '', + '[model_providers.new-api-chat]', + 'name = "New API Chat"', + `base_url = "${baseUrl}/v1"`, + 'wire_api = "chat_completions"', + 'preferred_auth_method = "sk-task-e2e-secret"', + 'temperature = 0.7', + 'models = ["glm-5.2", "glm-5.2-flash"]', + '' + ].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 === 'new-api-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}`); + assert(!JSON.stringify(chatRequests).includes('sk-codex-tab-secret'), `${label} must not use the Codex tab provider key`); + 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 === 'glm-5.2', `${label} should pass selected model`); + assert(item.body.temperature === 0.7, `${label} should pass configured temperature`); + 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 +205,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 +343,336 @@ 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://codex-user:codex-secret@127.0.0.1:${openAiMock.port}`); + + const openAiPlanResult = runSync(node, [ + cliPath, + 'task', + 'plan', + '--target', + 'OpenAI Chat provider 端到端模拟', + '--cwd', + path.join(tmpHome, 'task-plan-workspace'), + '--thread-id', + 'thread-cli-plan', + '--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.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', + 'run', + '--target', + 'OpenAI Chat provider CLI 运行链路', + '--follow-up', + '输出验证摘要', + '--engine', + 'openai-chat', + '--cwd', + openAiRunCwd, + '--thread-id', + 'thread-cli-run', + '--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'); + 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, + '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=new-api-chat'), 'OpenAI Chat logs should include provider request'); + + const rawPlanDefaultCwdPath = path.join(tmpHome, 'task-raw-plan-default-cwd.json'); + const rawPlanDefaultCwd = path.join(tmpHome, 'task-raw-plan-default-cwd-workspace'); + fs.mkdirSync(rawPlanDefaultCwd, { recursive: true }); + fs.writeFileSync(rawPlanDefaultCwdPath, JSON.stringify({ + id: 'task-raw-plan-default-cwd', + title: 'Raw plan default cwd', + target: 'Keep the implicit cwd stable', + engine: 'openai-chat', + nodes: [ + { + id: 'raw-default-cwd-node', + title: 'Raw default cwd node', + kind: 'openai-chat', + prompt: 'No artifact write needed.', + dependsOn: [] + } + ] + }, null, 2), 'utf-8'); + const rawPlanDefaultCwdResult = runSync(node, [ + cliPath, + 'task', + 'plan', + '--plan', + `@${rawPlanDefaultCwdPath}`, + '--json' + ], { env, cwd: rawPlanDefaultCwd }); + assert(rawPlanDefaultCwdResult.status === 0, `OpenAI Chat raw plan default cwd failed: ${rawPlanDefaultCwdResult.stderr || rawPlanDefaultCwdResult.stdout}`); + const rawPlanDefaultCwdPayload = parseJsonOutput(rawPlanDefaultCwdResult.stdout); + assert(rawPlanDefaultCwdPayload.plan && rawPlanDefaultCwdPayload.plan.cwd === rawPlanDefaultCwd, 'OpenAI Chat raw plans should default cwd to the invoking process cwd'); + + 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, + threadId: 'thread-direct-plan', + 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'); + 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'); + 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'), process.platform === 'win32' ? 'junction' : '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(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'); + + if (process.platform !== 'win32') { + const targetSymlinkPlanPath = path.join(tmpHome, 'task-target-symlink-openai-plan.json'); + const targetSymlinkCwd = path.join(tmpHome, 'task-target-symlink-workspace'); + const targetSymlinkEscapeDir = path.join(tmpHome, 'task-target-symlink-escape-target'); + fs.mkdirSync(targetSymlinkCwd, { recursive: true }); + fs.mkdirSync(targetSymlinkEscapeDir, { recursive: true }); + fs.symlinkSync(path.join(targetSymlinkEscapeDir, 'index.html'), path.join(targetSymlinkCwd, 'index.html')); + fs.writeFileSync(targetSymlinkPlanPath, JSON.stringify({ + id: 'task-target-symlink-openai-plan', + title: 'Target symlink OpenAI Chat plan', + target: 'target-symlink-artifact-probe', + notes: 'The model will try to write index.html; this must be rejected because index.html is a symlink.', + cwd: targetSymlinkCwd, + threadId: 'thread-target-symlink-plan', + engine: 'openai-chat', + allowWrite: true, + dryRun: false, + concurrency: 1, + nodes: [ + { + id: 'target-symlink-openai-node', + title: 'Target symlink OpenAI node', + kind: 'openai-chat', + prompt: 'target-symlink-artifact-probe: return index.html in an html fenced block.', + dependsOn: [] + } + ] + }, null, 2), 'utf-8'); + const targetSymlinkRunResult = runSync(node, [ + cliPath, + 'task', + 'run', + '--json', + '--allow-write', + '--plan', + `@${targetSymlinkPlanPath}` + ], { env }); + assert(targetSymlinkRunResult.status === 0, `OpenAI Chat target symlink plan run failed: ${targetSymlinkRunResult.stderr || targetSymlinkRunResult.stdout}`); + const targetSymlinkPayload = parseJsonOutput(targetSymlinkRunResult.stdout); + assertOpenAiRunPayload(targetSymlinkPayload, 'OpenAI Chat target symlink plan run'); + assert(!fs.existsSync(path.join(targetSymlinkEscapeDir, 'index.html')), 'OpenAI Chat materialization must not follow final target symlinks outside cwd'); + const targetSymlinkFiles = targetSymlinkPayload.run.nodes.flatMap(item => item && item.output && Array.isArray(item.output.materializedFiles) ? item.output.materializedFiles : []); + assert(targetSymlinkFiles.length === 0, 'OpenAI Chat target symlink materialization should not report escaped files'); + const targetSymlinkLogs = JSON.stringify(targetSymlinkPayload.run.nodes.flatMap(item => Array.isArray(item && item.logs) ? item.logs : [])); + assert(targetSymlinkLogs.includes('artifact target is a symlink'), 'OpenAI Chat target symlink rejection should be visible in run logs'); + } + + const openAiQueueAddResult = runSync(node, [ + cliPath, + 'task', + 'queue', + 'add', + '--target', + '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, + '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'); + 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', + 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'); + 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/cli-help.test.mjs b/tests/unit/cli-help.test.mjs index 30d7d5ff..646dbffb 100644 --- a/tests/unit/cli-help.test.mjs +++ b/tests/unit/cli-help.test.mjs @@ -23,3 +23,13 @@ test('top-level help flags print usage and exit successfully', () => { assert.doesNotMatch(result.stderr, /error|exception/i); } }); + +test('task help documents workspace and thread flags', () => { + const result = runCli(['task', '--help']); + + assert.strictEqual(result.status, 0, `stderr: ${result.stderr}`); + assert.match(result.stdout, /--cwd <路径>/); + assert.match(result.stdout, /--thread-id /); + assert.match(result.stdout, /--conversation-id /); + assert.match(result.stdout, /--session-id /); +}); diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index 6055425b..96152d4f 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -76,6 +76,12 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(html, /v-if="taskOrchestrationTabEnabled"[\s\S]*id="panel-orchestration"/); assert.match(html, /taskOrchestrationTabEnabled && mainTab === 'orchestration'/); assert.match(bundledScript, /taskOrchestrationTabEnabled:\s*true/); + assert.match(bundledScript, /codexmateTaskOrchestrationTabEnabled/); + assert.match(bundledScript, /taskOrchestration/); + const webUiRedirectBlock = bundledScript.match(/pathname === '\/web-ui'[\s\S]*?window\.location\.replace\(url\.toString\(\)\);/)?.[0] || ''; + assert.match(webUiRedirectBlock, /url\.pathname\s*=\s*'\/'/); + assert.doesNotMatch(webUiRedirectBlock, /url\.search\s*=/); + assert.doesNotMatch(webUiRedirectBlock, /url\.hash\s*=/); assert.match(html, /id="side-tab-orchestration"/); assert.match(html, /id="tab-orchestration"/); assert.match(html, /data-main-tab="orchestration"/); @@ -87,44 +93,175 @@ 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.match(orchestrationPanel, /@click="planAndRunTaskOrchestration\(\)"/); + assert.doesNotMatch(orchestrationPanel, /@click="previewTaskPlan\(\)"/); + assert.doesNotMatch(orchestrationPanel, /@click="planAndRunTaskOrchestration\(\)"/); + assert.doesNotMatch(orchestrationPanel, /orchestration\.actions\.generatePlan/); assert.match(orchestrationPanel, /@click="queueTaskOrchestrationAndStart\(\)"/); 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.doesNotMatch(orchestrationPanel, /class="task-panel-toolbar"/); + assert.doesNotMatch(orchestrationPanel, /class="task-chat-status-strip"/); + assert.doesNotMatch(orchestrationPanel, /class="task-quick-input-card task-chat-panel"[\s\S]*配置 \{\{ taskOrchestrationEngineLabel \}\}/); + assert.doesNotMatch(orchestrationPanel, /class="task-quick-input-card task-chat-panel"[\s\S]*运行中 \{\{ taskOrchestrationQueueStats\.running \}\}/); + assert.doesNotMatch(orchestrationPanel, /class="task-quick-input-card task-chat-panel"[\s\S]*历史 RUNS \{\{ taskOrchestration\.runs\.length \}\}/); + assert.match(orchestrationPanel, /class="task-quick-input-card task-chat-panel"\s*>\s*
/); + assert.doesNotMatch(orchestrationPanel, /class="task-project-list" role="listbox"/); + assert.match(orchestrationPanel, /v-for="workspace in taskOrchestrationWorkspaceItems"/); + assert.match(orchestrationPanel, /:aria-selected="workspace\.active \? 'true' : 'false'"/); + assert.match(orchestrationPanel, /@click="selectTaskWorkspace\(workspace\.path\)"/); + assert.match(orchestrationPanel, /@click="startNewTaskWorkspaceSession\(\)"/); + assert.match(orchestrationPanel, /v-for="session in taskOrchestrationWorkspaceSessions\.slice\(0, 8\)"/); + assert.match(orchestrationPanel, /@click="continueTaskWorkspaceSession\(session\)"/); + assert.match(orchestrationPanel, /
-
-
{{ t('orchestration.summary.running') }} {{ taskOrchestrationQueueStats.running }}
-
{{ t('orchestration.summary.queued') }} {{ taskOrchestrationQueueStats.queued }}
-
{{ t('orchestration.summary.runs') }} {{ taskOrchestration.runs.length }}
-
-
-
-
-
-
1
+
+
+ +
+
+
{{ t('orchestration.quick.kicker') }}
+
{{ t('orchestration.quick.title') }}
+
{{ t('orchestration.quick.subtitle') }}
-
- {{ t('orchestration.templates.title') }} -
-
- - - +
+
+
+
{{ t('orchestration.agent.kicker') }}
+
{{ t('orchestration.agent.title') }}
+
{{ t('orchestration.agent.subtitle') }}
+ + {{ taskOrchestration.running ? t('orchestration.agent.state.running') : (taskOrchestration.planning ? t('orchestration.agent.state.planning') : (taskOrchestrationSelectedRun && taskOrchestrationSelectedRun.run ? taskOrchestrationSelectedRun.run.status : t('orchestration.agent.state.ready'))) }} +
-
+
+
+ {{ t('orchestration.agent.surface.workspace') }} + {{ taskOrchestrationWorkspacePath ? t('orchestration.privacy.workspace.selected') : t('orchestration.chat.context.workspace.auto') }} +
+
+ {{ t('orchestration.agent.surface.session') }} + {{ (taskOrchestration.threadId.trim() || (taskOrchestrationSelectedRun && taskOrchestrationSelectedRun.threadId)) ? t('orchestration.privacy.thread.selected') : t('orchestration.chat.context.thread.auto') }} +
+
+ {{ t('orchestration.agent.surface.mode') }} + {{ 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.agent.surface.trace') }} + {{ taskOrchestrationWorkspaceRuns.length }} {{ t('orchestration.agent.surface.runs') }} · {{ taskOrchestrationWorkspaceQueue.length }} {{ t('orchestration.agent.surface.queue') }} +
+
+
-
- -
+
+
+
+
+
{{ message.label }}
+
{{ message.text }}
+
{{ message.meta }}
+
+
+
+
+
You · /plan
+
/plan {{ taskOrchestration.target }}
+
{{ t('orchestration.chat.input.sequenceHint', { count: taskOrchestrationDraftMetrics.followUpCount + 1 }) }}
+
+
+
+
AI · {{ t('orchestration.plan.title') }}
+
{{ taskOrchestration.lastError }}
+
+ /plan + {{ t('orchestration.chat.assistant.planSummary', { nodes: taskOrchestration.plan.nodes.length, waves: taskOrchestration.plan.waves.length }) }} +
+
+
+ {{ issue.message }} +
+
+
+
+ {{ warning }} +
+
+ +
-
-
- {{ taskOrchestrationDraftReadiness.title }} -
{{ taskOrchestrationDraftReadiness.summary }}
-
-
{{ taskOrchestration.selectedEngine === 'workflow' ? t('orchestration.engine.workflow') : t('orchestration.engine.codex') }}
-
{{ 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 }) }}
-
{{ t('orchestration.pills.planNodes', { count: taskOrchestrationDraftMetrics.planNodeCount }) }}
+
+ +
+
+ +
+ {{ taskOrchestrationDraftMetrics.hasTarget ? t('orchestration.chat.input.sequenceHint', { count: taskOrchestrationDraftMetrics.followUpCount + 2 }) : t('orchestration.chat.input.workHint') }} +
+
+ {{ t('orchestration.chat.input.workCaption') }} +
+
+ + {{ t('orchestration.plan.summary.cwd') }} + {{ (taskOrchestrationWorkspacePath || taskOrchestration.workspacePath.trim()) ? t('orchestration.privacy.workspace.selected') : t('orchestration.chat.context.workspace.auto') }} + + + {{ t('orchestration.plan.summary.threadId') }} + {{ (taskOrchestration.threadId.trim() || (taskOrchestrationSelectedRun && taskOrchestrationSelectedRun.threadId)) ? t('orchestration.privacy.thread.selected') : t('orchestration.chat.context.thread.auto') }} + + {{ taskOrchestrationDraftMetrics.hasTarget ? t('orchestration.chat.context.sequence.value', { count: taskOrchestrationDraftMetrics.requestCount }) : t('orchestration.chat.context.sequence.empty') }} + {{ taskOrchestration.runMode === 'dry-run' ? t('orchestration.runMode.dryRun') : (taskOrchestration.runMode === 'read' ? t('orchestration.runMode.readOnly') : t('orchestration.runMode.write') ) }} + +
+ +
{{ t('orchestration.quick.caption') }}
-
-
-
2
-
-
{{ t('orchestration.step2.title') }}
-
{{ t('orchestration.step2.subtitle') }}
-
-
+
- -
-
-
3
+
+
{{ t('orchestration.openai.status.title') }}
+
{{ t('orchestration.openai.status.subtitle') }}
+
+
+ {{ t('orchestration.openai.status.provider') }} + {{ taskOrchestration.openAiChatStatus.providerName ? t('orchestration.openai.status.configured') : t('orchestration.openai.status.notSet') }} +
+
+ {{ t('orchestration.openai.status.model') }} + {{ taskOrchestration.openAiChatStatus.model ? t('orchestration.openai.status.configured') : t('orchestration.openai.status.notSet') }} +
+
+ {{ t('orchestration.openai.status.endpoint') }} + {{ taskOrchestration.openAiChatStatus.endpoint ? t('orchestration.openai.status.configured') : t('orchestration.openai.status.notSet') }} +
+
+ {{ t('orchestration.openai.status.apiKey') }} + {{ taskOrchestration.openAiChatStatus.hasApiKey ? t('orchestration.openai.status.configured') : t('orchestration.openai.status.missing') }} +
+
+ {{ t('orchestration.openai.status.headers') }} + {{ taskOrchestration.openAiChatStatus.hasExtraHeaders ? t('orchestration.openai.status.configured') : t('orchestration.openai.status.notSet') }} +
+
+
{{ t('orchestration.openai.status.notLoaded') }}
+
{{ taskOrchestration.openAiChatStatus.error }}
+ +
+
-
{{ t('orchestration.step3.title') }}
-
{{ t('orchestration.step3.subtitle') }}
+
{{ t('orchestration.quick.checklist.title') }}
+
{{ t('orchestration.quick.checklist.subtitle') }}
-
- -
- - +
+
+ {{ item.label }} + {{ item.detail }}
-
{{ t('orchestration.actions.caption') }}
-
+
+
{{ t('orchestration.quick.status.title') }}
+ {{ taskOrchestrationDraftReadiness.title }} + {{ taskOrchestrationDraftReadiness.summary }} +
+
@@ -191,201 +595,4 @@
-
-
-
{{ taskOrchestration.lastError }}
-
-
- {{ t('orchestration.plan.title') }} -
{{ t('orchestration.plan.subtitle') }}
-
-
-
-
- {{ issue.message }} -
-
-
-
- {{ warning }} -
-
- -
- -
-
-
- {{ t('orchestration.workbench.title') }} -
{{ t('orchestration.workbench.subtitle') }}
-
-
- - -
-
- -
- - - -
- -
-
-
{{ t('orchestration.queue.empty.title') }}
-
{{ t('orchestration.queue.empty.subtitle') }}
-
-
-
-
-
{{ item.title || item.target || item.taskId }}
-
{{ item.taskId }} · {{ item.updatedAt || item.createdAt }}
-
{{ item.lastSummary }}
-
-
- {{ item.status }} - -
-
-
-
- -
-
-
{{ t('orchestration.runs.empty.title') }}
-
{{ t('orchestration.runs.empty.subtitle') }}
-
-
- -
-
- -
-
- - - -
- -
{{ taskOrchestration.selectedRunError }}
-
-
{{ t('orchestration.detail.empty.title') }}
-
{{ t('orchestration.detail.empty.subtitle') }}
-
- -
-
-
diff --git a/web-ui/res/web-ui-render.precompiled.js b/web-ui/res/web-ui-render.precompiled.js index 7960a8f5..c084e1d7 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", @@ -3997,6 +3997,7 @@ return function render(_ctx, _cache) { key: 0, class: "mode-content", id: "panel-orchestration", + "data-active": _ctx.mainTab === 'orchestration' ? 'true' : 'false', role: "tabpanel", "aria-labelledby": "tab-orchestration" }, [ @@ -4027,149 +4028,792 @@ return function render(_ctx, _cache) { disabled: _ctx.loading || !!_ctx.initError }, _toDisplayString(_ctx.t('dashboard.doctor.title')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]) ]) - ]), - (_ctx.taskOrchestrationQueueStats.running || _ctx.taskOrchestrationQueueStats.queued || _ctx.taskOrchestration.runs.length) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - class: "task-hero-meta-strip", - "aria-label": _ctx.t('orchestration.summary.aria') - }, [ - _createElementVNode("div", { class: "task-hero-meta" }, [ - _createTextVNode(_toDisplayString(_ctx.t('orchestration.summary.running')) + " ", 1 /* TEXT */), - _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationQueueStats.running), 1 /* TEXT */) - ]), - _createElementVNode("div", { class: "task-hero-meta" }, [ - _createTextVNode(_toDisplayString(_ctx.t('orchestration.summary.queued')) + " ", 1 /* TEXT */), - _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationQueueStats.queued), 1 /* TEXT */) - ]), - _createElementVNode("div", { class: "task-hero-meta" }, [ - _createTextVNode(_toDisplayString(_ctx.t('orchestration.summary.runs')) + " ", 1 /* TEXT */), - _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.runs.length), 1 /* TEXT */) - ]) - ], 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", { class: "task-layout-grid task-layout-grid-primary task-quick-layout" }, [ + _createElementVNode("section", { class: "selector-section task-compose-flow-card task-quick-card" }, [ + _createElementVNode("aside", { + class: "task-project-sidebar", + "aria-label": _ctx.t('orchestration.workspace.aria') + }, [ + _createElementVNode("div", { class: "task-project-sidebar-head" }, [ _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-thread-card-label" }, _toDisplayString(_ctx.t('orchestration.workspace.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "skills-panel-note" }, _toDisplayString(_ctx.t('orchestration.workspace.subtitle')), 1 /* TEXT */) + ]), + _createElementVNode("button", { + type: "button", + class: "btn-mini", + 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.target.trim()) - ? (_openBlock(), _createElementBlock("details", { - key: 0, - class: "task-template-panel" + _createElementVNode("div", { + class: "task-project-list", + "aria-label": _ctx.t('orchestration.workspace.selector') + }, [ + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestrationWorkspaceItems, (workspace) => { + return (_openBlock(), _createElementBlock("button", { + key: workspace.key, + type: "button", + class: _normalizeClass(['task-project-item', { active: workspace.active }]), + "aria-selected": workspace.active ? 'true' : 'false', + title: workspace.path ? _ctx.t('orchestration.workspace.pathHidden') : workspace.meta, + onClick: $event => (_ctx.selectTaskWorkspace(workspace.path)) }, [ - _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 = 'codex'; _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 = ''} - }, _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("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", - 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("span", { class: "task-project-item-title" }, _toDisplayString(workspace.label), 1 /* TEXT */), + _createElementVNode("span", { class: "task-project-item-meta" }, _toDisplayString(workspace.path ? _ctx.t('orchestration.workspace.pathHidden') : workspace.meta), 1 /* TEXT */), + _createElementVNode("span", { class: "task-project-item-stats" }, _toDisplayString(_ctx.t('orchestration.workspace.counts', { runs: workspace.runCount, queue: workspace.queueCount })), 1 /* TEXT */) + ], 10 /* CLASS, PROPS */, ["aria-selected", "title", "onClick"])) + }), 128 /* KEYED_FRAGMENT */)) + ], 8 /* PROPS */, ["aria-label"]), + _createElementVNode("button", { + type: "button", + class: "btn-tool btn-primary task-project-new-session", + onClick: $event => (_ctx.startNewTaskWorkspaceSession()) + }, _toDisplayString(_ctx.t('orchestration.workspace.newSession')), 9 /* TEXT, PROPS */, ["onClick"]), + _createElementVNode("div", { class: "task-session-inbox" }, [ + _createElementVNode("div", { class: "task-session-inbox-head" }, [ + _createElementVNode("span", { class: "task-readiness-title" }, _toDisplayString(_ctx.t('orchestration.workspace.sessions.title')), 1 /* TEXT */), + _createElementVNode("span", { class: "pill neutral" }, _toDisplayString(_ctx.taskOrchestrationWorkspaceSessions.length), 1 /* TEXT */) + ]), + (!_ctx.taskOrchestrationWorkspaceSessions.length) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-empty-state task-session-empty" + }, [ + _createElementVNode("div", { class: "task-empty-title" }, _toDisplayString(_ctx.t('orchestration.workspace.sessions.empty.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "task-empty-copy" }, _toDisplayString(_ctx.t('orchestration.workspace.sessions.empty.subtitle')), 1 /* TEXT */) + ])) + : _createCommentVNode("v-if", true), + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestrationWorkspaceSessions.slice(0, 8), (session) => { + return (_openBlock(), _createElementBlock("button", { + key: session.id, + type: "button", + class: "task-session-inbox-item", + onClick: $event => (_ctx.continueTaskWorkspaceSession(session)) + }, [ + _createElementVNode("span", { class: "task-session-inbox-main" }, [ + _createElementVNode("span", { class: "task-session-inbox-title" }, _toDisplayString(session.type === 'queue' ? _ctx.t('orchestration.workspace.sessions.queueTitle') : _ctx.t('orchestration.workspace.sessions.runTitle')), 1 /* TEXT */), + _createElementVNode("span", { class: "task-session-inbox-meta" }, _toDisplayString(_ctx.t('orchestration.workspace.sessions.detailsHidden')), 1 /* TEXT */) + ]), + _createElementVNode("span", { + class: _normalizeClass(['pill', _ctx.taskRunStatusTone(session.status)]) + }, _toDisplayString(session.status), 3 /* TEXT, CLASS */) + ], 8 /* PROPS */, ["onClick"])) + }), 128 /* KEYED_FRAGMENT */)) + ]) + ], 8 /* PROPS */, ["aria-label"]), + _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 */) ]), - _createElementVNode("div", { class: "task-draft-overview task-draft-inline" }, [ - _createElementVNode("div", { class: "task-draft-inline-head" }, [ + _createElementVNode("section", { + class: "task-agent-cockpit", + "aria-label": _ctx.t('orchestration.agent.aria') + }, [ + _createElementVNode("div", { class: "task-agent-cockpit-head" }, [ + _createElementVNode("div", null, [ + _createElementVNode("div", { class: "task-hero-kicker" }, _toDisplayString(_ctx.t('orchestration.agent.kicker')), 1 /* TEXT */), + _createElementVNode("div", { class: "selector-title task-agent-title" }, _toDisplayString(_ctx.t('orchestration.agent.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "skills-panel-note task-agent-copy" }, _toDisplayString(_ctx.t('orchestration.agent.subtitle')), 1 /* TEXT */) + ]), _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 */) + class: _normalizeClass(['pill', _ctx.taskOrchestration.running || _ctx.taskOrchestration.planning ? 'warn' : (_ctx.taskOrchestrationSelectedRun && _ctx.taskOrchestrationSelectedRun.run ? _ctx.taskRunStatusTone(_ctx.taskOrchestrationSelectedRun.run.status) : 'neutral')]) + }, _toDisplayString(_ctx.taskOrchestration.running ? _ctx.t('orchestration.agent.state.running') : (_ctx.taskOrchestration.planning ? _ctx.t('orchestration.agent.state.planning') : (_ctx.taskOrchestrationSelectedRun && _ctx.taskOrchestrationSelectedRun.run ? _ctx.taskOrchestrationSelectedRun.run.status : _ctx.t('orchestration.agent.state.ready')))), 3 /* TEXT, CLASS */) ]), - _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.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()) + _createElementVNode("div", { class: "task-agent-surface-grid" }, [ + _createElementVNode("div", { class: "task-agent-surface-card" }, [ + _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.agent.surface.workspace')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationWorkspacePath ? _ctx.t('orchestration.privacy.workspace.selected') : _ctx.t('orchestration.chat.context.workspace.auto')), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-agent-surface-card" }, [ + _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.agent.surface.session')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString((_ctx.taskOrchestration.threadId.trim() || (_ctx.taskOrchestrationSelectedRun && _ctx.taskOrchestrationSelectedRun.threadId)) ? _ctx.t('orchestration.privacy.thread.selected') : _ctx.t('orchestration.chat.context.thread.auto')), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-agent-surface-card" }, [ + _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.agent.surface.mode')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.selectedEngine === 'workflow' ? _ctx.t('orchestration.engine.workflow') : _ctx.t('orchestration.engine.openaiChat')) + " · " + _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 */) + ]), + _createElementVNode("div", { class: "task-agent-surface-card" }, [ + _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.agent.surface.trace')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationWorkspaceRuns.length) + " " + _toDisplayString(_ctx.t('orchestration.agent.surface.runs')) + " · " + _toDisplayString(_ctx.taskOrchestrationWorkspaceQueue.length) + " " + _toDisplayString(_ctx.t('orchestration.agent.surface.queue')), 1 /* TEXT */) + ]) + ]) + ], 8 /* PROPS */, ["aria-label"]), + _createElementVNode("div", { class: "task-quick-input-card task-chat-panel" }, [ + _createElementVNode("div", { + class: "task-chat-thread", + role: "log", + "aria-label": _ctx.t('orchestration.chat.thread.aria') + }, [ + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.taskOrchestrationConversationMessages, (message) => { + return (_openBlock(), _createElementBlock("div", { + key: message.id, + class: _normalizeClass(['task-chat-bubble-row', message.role === 'user' ? 'is-user' : 'is-assistant']) + }, [ + _createElementVNode("div", { class: "task-chat-bubble" }, [ + _createElementVNode("div", { class: "task-chat-bubble-label" }, _toDisplayString(message.label), 1 /* TEXT */), + _createElementVNode("div", { class: "task-chat-bubble-text" }, _toDisplayString(message.text), 1 /* TEXT */), + (message.meta) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-chat-bubble-meta" + }, _toDisplayString(message.meta), 1 /* TEXT */)) + : _createCommentVNode("v-if", true) + ]) + ], 2 /* CLASS */)) + }), 128 /* KEYED_FRAGMENT */)), + (_ctx.taskOrchestration.plan || _ctx.taskOrchestration.planIssues.length || _ctx.taskOrchestration.planWarnings.length || _ctx.taskOrchestration.lastError) ? (_openBlock(), _createElementBlock("div", { key: 0, - class: "task-config-pill" - }, _toDisplayString(_ctx.t('orchestration.pills.hasTitle')), 1 /* TEXT */)) + 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 · /plan"), + _createElementVNode("div", { class: "task-chat-bubble-text" }, "/plan " + _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.selectedEngine === 'workflow' && _ctx.taskOrchestrationDraftMetrics.workflowCount > 0) - ? (_openBlock(), _createElementBlock("div", { + (_ctx.taskOrchestration.plan || _ctx.taskOrchestration.planIssues.length || _ctx.taskOrchestration.planWarnings.length || _ctx.taskOrchestration.lastError) + ? (_openBlock(), _createElementBlock("section", { 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 */)) + 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), + (_ctx.taskOrchestration.plan) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-thread-run-summary task-thread-plan-summary" + }, [ + _createElementVNode("span", { class: "pill configured" }, "/plan"), + _createElementVNode("span", { class: "task-thread-run-summary-copy" }, _toDisplayString(_ctx.t('orchestration.chat.assistant.planSummary', { nodes: _ctx.taskOrchestration.plan.nodes.length, waves: _ctx.taskOrchestration.plan.waves.length })), 1 /* TEXT */) + ])) + : _createCommentVNode("v-if", true), + (_ctx.taskOrchestration.planIssues.length) + ? (_openBlock(), _createElementBlock("div", { + key: 2, + 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: 3, + 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("details", { + key: 4, + class: "task-thread-run-details task-thread-plan-details" + }, [ + _createElementVNode("summary", null, "Plan details"), + _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 */)) + ]) + ])) + : _createCommentVNode("v-if", true) + ])) : _createCommentVNode("v-if", true) + ], 8 /* PROPS */, ["aria-label"]), + _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 */), + _createElementVNode("span", { + class: "task-composer-prompt-glyph", + "aria-hidden": "true" + }, "›"), + _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.submitTaskOrchestrationChatMessage()), ["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("div", { class: "task-chat-send-row" }, [ + _createElementVNode("div", { class: "task-chat-action-buttons task-chat-primary-action" }, [ + _createElementVNode("button", { + type: "button", + class: "btn-tool btn-primary task-chat-primary-button", + onClick: $event => (_ctx.planAndRunTaskOrchestrationFromChat()), + disabled: _ctx.taskOrchestration.running || _ctx.taskOrchestration.planning || (!_ctx.taskOrchestration.target.trim() && !_ctx.taskOrchestration.chatDraft.trim()) + }, _toDisplayString(_ctx.taskOrchestration.running ? _ctx.t('orchestration.actions.processing') : (_ctx.taskOrchestration.planning ? _ctx.t('orchestration.actions.planning') : _ctx.t('orchestration.chat.input.work'))), 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.workHint')), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-chat-execute-caption" }, _toDisplayString(_ctx.t('orchestration.chat.input.workCaption')), 1 /* TEXT */), + _createElementVNode("div", { + class: "task-chat-context-row task-chat-context-row-primary", + role: "group", + "aria-label": _ctx.t('orchestration.chat.context.aria') + }, [ + _createElementVNode("span", { class: "task-chat-context-chip task-chat-context-chip-strong" }, [ + _createElementVNode("small", null, _toDisplayString(_ctx.t('orchestration.plan.summary.cwd')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString((_ctx.taskOrchestrationWorkspacePath || _ctx.taskOrchestration.workspacePath.trim()) ? _ctx.t('orchestration.privacy.workspace.selected') : _ctx.t('orchestration.chat.context.workspace.auto')), 1 /* TEXT */) + ]), + _createElementVNode("span", { class: "task-chat-context-chip task-chat-context-chip-strong" }, [ + _createElementVNode("small", null, _toDisplayString(_ctx.t('orchestration.plan.summary.threadId')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString((_ctx.taskOrchestration.threadId.trim() || (_ctx.taskOrchestrationSelectedRun && _ctx.taskOrchestrationSelectedRun.threadId)) ? _ctx.t('orchestration.privacy.thread.selected') : _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 */), + _createElementVNode("button", { + type: "button", + class: "task-chat-context-chip task-chat-context-action", + onClick: $event => (_ctx.taskOrchestration.settingsOpen = true) + }, _toDisplayString(_ctx.t('orchestration.advanced.open')), 9 /* TEXT, PROPS */, ["onClick"]) + ], 8 /* PROPS */, ["aria-label"]), + _createElementVNode("div", { class: "task-action-caption" }, _toDisplayString(_ctx.t('orchestration.quick.caption')), 1 /* TEXT */) ]) ]) ]), - _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: "codex" }, _toDisplayString(_ctx.t('orchestration.engine.codex')), 1 /* TEXT */), - _createElementVNode("option", { value: "workflow" }, _toDisplayString(_ctx.t('orchestration.engine.workflow')), 1 /* TEXT */) - ], 8 /* PROPS */, ["onUpdate:modelValue"]), [ - [_vModelSelect, _ctx.taskOrchestration.selectedEngine] - ]) + _createElementVNode("aside", { class: "task-quick-side-card" }, [ + _createElementVNode("section", { class: "selector-section task-workbench-card task-side-workbench-card" }, [ + _createElementVNode("div", { class: "task-thread-card-label" }, _toDisplayString(_ctx.t('orchestration.workbench.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "task-thread-run-summary" }, [ + (_ctx.taskOrchestrationSelectedRun && _ctx.taskOrchestrationSelectedRun.run) + ? (_openBlock(), _createElementBlock("span", { + key: 0, + class: _normalizeClass(['pill', _ctx.taskRunStatusTone(_ctx.taskOrchestrationSelectedRun.run.status)]) + }, _toDisplayString(_ctx.taskOrchestrationSelectedRun.run.status), 3 /* TEXT, CLASS */)) + : (_ctx.taskOrchestrationWorkspaceRuns.length) + ? (_openBlock(), _createElementBlock("span", { + key: 1, + class: _normalizeClass(['pill', _ctx.taskRunStatusTone(_ctx.taskOrchestrationWorkspaceRuns[0].status)]) + }, _toDisplayString(_ctx.taskOrchestrationWorkspaceRuns[0].status), 3 /* TEXT, CLASS */)) + : (_ctx.taskOrchestrationWorkspaceQueue.length) + ? (_openBlock(), _createElementBlock("span", { + key: 2, + class: "pill neutral" + }, _toDisplayString(_ctx.t('orchestration.workbench.queueCount', { count: _ctx.taskOrchestrationWorkspaceQueue.length })), 1 /* TEXT */)) + : (_openBlock(), _createElementBlock("span", { + key: 3, + class: "pill empty" + }, _toDisplayString(_ctx.t('orchestration.workbench.ready')), 1 /* TEXT */)), + (_ctx.taskOrchestrationSelectedRun && _ctx.taskOrchestrationSelectedRun.run) + ? (_openBlock(), _createElementBlock("span", { + key: 4, + class: "task-thread-run-summary-copy" + }, _toDisplayString(_ctx.taskOrchestrationSelectedRun.run.summary || _ctx.taskOrchestrationSelectedRun.run.runId || _ctx.taskOrchestration.selectedRunId), 1 /* TEXT */)) + : (_ctx.taskOrchestrationWorkspaceRuns.length) + ? (_openBlock(), _createElementBlock("span", { + key: 5, + class: "task-thread-run-summary-copy" + }, _toDisplayString(_ctx.taskOrchestrationWorkspaceRuns[0].summary || _ctx.taskOrchestrationWorkspaceRuns[0].runId), 1 /* TEXT */)) + : (_ctx.taskOrchestrationWorkspaceQueue.length) + ? (_openBlock(), _createElementBlock("span", { + key: 6, + class: "task-thread-run-summary-copy" + }, _toDisplayString(_ctx.taskOrchestrationWorkspaceQueue[0].title || _ctx.taskOrchestrationWorkspaceQueue[0].target || _ctx.taskOrchestrationWorkspaceQueue[0].taskId), 1 /* TEXT */)) + : (_openBlock(), _createElementBlock("span", { + key: 7, + class: "task-thread-run-summary-copy" + }, _toDisplayString(_ctx.t('orchestration.workbench.subtitle')), 1 /* TEXT */)) ]), - _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("details", { + class: "task-thread-run-details", + open: "" + }, [ + _createElementVNode("summary", null, _toDisplayString(_ctx.t('orchestration.agent.trace.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "settings-tab-actions task-header-actions task-thread-detail-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.taskOrchestrationWorkspaceQueue.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) + ]), + _createElementVNode("div", { class: "task-agent-trace-grid" }, [ + _createElementVNode("div", { class: "task-agent-trace-card" }, [ + _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.agent.trace.plan')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.plan ? _ctx.taskOrchestration.plan.nodes.length : 0), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-agent-trace-card" }, [ + _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.agent.trace.queue')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationWorkspaceQueue.length), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-agent-trace-card" }, [ + _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.agent.trace.runs')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationWorkspaceRuns.length), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-agent-trace-card" }, [ + _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.agent.trace.nodes')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestrationSelectedRunNodes.length), 1 /* TEXT */) + ]) + ]), + ((_ctx.taskOrchestrationWorkspaceQueue.length ? 1 : 0) + (_ctx.taskOrchestrationWorkspaceRuns.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.taskOrchestrationWorkspaceQueue.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.taskOrchestrationWorkspaceQueue.length })), 11 /* TEXT, CLASS, PROPS */, ["onClick"])) + : _createCommentVNode("v-if", true), + (_ctx.taskOrchestrationWorkspaceRuns.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.taskOrchestrationWorkspaceRuns.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.taskOrchestrationWorkspaceQueue.length && _ctx.taskOrchestration.workspaceTab === 'queue') || (!_ctx.taskOrchestrationWorkspaceRuns.length && !_ctx.taskOrchestration.selectedRunId && !_ctx.taskOrchestration.selectedRunError)) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-workbench-panel" + }, [ + (!_ctx.taskOrchestrationWorkspaceQueue.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.taskOrchestrationWorkspaceQueue, (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.taskOrchestrationWorkspaceQueue.length && _ctx.taskOrchestrationWorkspaceRuns.length && !_ctx.taskOrchestration.selectedRunId && !_ctx.taskOrchestration.selectedRunError)) + ? (_openBlock(), _createElementBlock("div", { + key: 2, + class: "task-workbench-panel" + }, [ + (!_ctx.taskOrchestrationWorkspaceRuns.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.taskOrchestrationWorkspaceRuns, (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), + (node.output.workspaceFiles && node.output.workspaceFiles.length) + ? (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-materialized-files task-workspace-files" + }, [ + _createElementVNode("div", { class: "task-node-deps" }, _toDisplayString(_ctx.t('orchestration.detail.node.workspaceFiles')), 1 /* TEXT */), + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(node.output.workspaceFiles, (file) => { + return (_openBlock(), _createElementBlock("div", { + key: file.path || file.relativePath, + class: "task-materialized-file" + }, _toDisplayString([String(file.operation || '').trim().toUpperCase(), String(file.relativePath || file.path || '').trim(), Number.isFinite(Number(file.bytes)) ? (Number(file.bytes) + ' bytes') : '', typeof file.existed === 'boolean' ? (file.existed ? 'existed' : 'new') : ''].filter(Boolean).join(' · ')), 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 */)) + ])) ]) ]), - _createElementVNode("details", { class: "task-advanced-panel" }, [ + _createElementVNode("details", { + class: "selector-section task-side-settings-card task-advanced-panel task-quick-advanced", + open: _ctx.taskOrchestration.settingsOpen, + onToggle: $event => (_ctx.taskOrchestration.settingsOpen = $event.target.open) + }, [ _createElementVNode("summary", { class: "task-advanced-summary" }, _toDisplayString(_ctx.t('orchestration.advanced.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "task-action-row-right task-action-row-right-prominent task-codex-queue-action" }, [ + _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: "selector-grid task-composer-grid task-composer-grid-secondary" }, [ _createElementVNode("label", { class: "selector-field task-field-wide" }, [ _createElementVNode("span", { class: "selector-label" }, _toDisplayString(_ctx.t('orchestration.fields.title')), 1 /* TEXT */), @@ -4182,6 +4826,32 @@ 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", + onChange: $event => (_ctx.taskOrchestration.selectedEngine === 'workflow' ? null : _ctx.taskOrchestration.workflowIdsText = '') + }, [ + _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 */) + ], 40 /* PROPS, NEED_HYDRATION */, ["onUpdate:modelValue", "onChange"]), [ + [_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" + }, [ + _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", { @@ -4194,6 +4864,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", { @@ -4269,43 +4963,78 @@ return function render(_ctx, _cache) { ])) : _createCommentVNode("v-if", true) ]) - ]) - ]), - _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("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-action-row task-action-row-prominent" }, [ + ], 40 /* PROPS, NEED_HYDRATION */, ["open", "onToggle"]), + _createElementVNode("div", { class: "task-provider-status-card" }, [ + _createElementVNode("div", { class: "task-readiness-title" }, _toDisplayString(_ctx.t('orchestration.openai.status.title')), 1 /* TEXT */), + _createElementVNode("div", { class: "task-readiness-copy" }, _toDisplayString(_ctx.t('orchestration.openai.status.subtitle')), 1 /* TEXT */), + (_ctx.taskOrchestration.openAiChatStatus) + ? (_openBlock(), _createElementBlock("div", { + key: 0, + class: "task-provider-status-grid" + }, [ + _createElementVNode("div", { class: "task-provider-status-row" }, [ + _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.openai.status.provider')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.openAiChatStatus.providerName ? _ctx.t('orchestration.openai.status.configured') : _ctx.t('orchestration.openai.status.notSet')), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-provider-status-row" }, [ + _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.openai.status.model')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.openAiChatStatus.model ? _ctx.t('orchestration.openai.status.configured') : _ctx.t('orchestration.openai.status.notSet')), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-provider-status-row task-provider-status-row-wide" }, [ + _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.openai.status.endpoint')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.openAiChatStatus.endpoint ? _ctx.t('orchestration.openai.status.configured') : _ctx.t('orchestration.openai.status.notSet')), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-provider-status-row" }, [ + _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.openai.status.apiKey')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.openAiChatStatus.hasApiKey ? _ctx.t('orchestration.openai.status.configured') : _ctx.t('orchestration.openai.status.missing')), 1 /* TEXT */) + ]), + _createElementVNode("div", { class: "task-provider-status-row" }, [ + _createElementVNode("span", null, _toDisplayString(_ctx.t('orchestration.openai.status.headers')), 1 /* TEXT */), + _createElementVNode("strong", null, _toDisplayString(_ctx.taskOrchestration.openAiChatStatus.hasExtraHeaders ? _ctx.t('orchestration.openai.status.configured') : _ctx.t('orchestration.openai.status.notSet')), 1 /* TEXT */) + ]) + ])) + : (_openBlock(), _createElementBlock("div", { + key: 1, + class: "task-readiness-copy" + }, _toDisplayString(_ctx.t('orchestration.openai.status.notLoaded')), 1 /* TEXT */)), + (_ctx.taskOrchestration.openAiChatStatus && _ctx.taskOrchestration.openAiChatStatus.error) + ? (_openBlock(), _createElementBlock("div", { + key: 2, + class: "task-issue-item task-provider-status-error" + }, _toDisplayString(_ctx.taskOrchestration.openAiChatStatus.error), 1 /* TEXT */)) + : _createCommentVNode("v-if", true), _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"]) + class: "btn-tool btn-tool-compact task-provider-config-button", + onClick: $event => (_ctx.openTaskOpenAiChatConfig()) + }, _toDisplayString(_ctx.t('orchestration.openai.status.configure')), 9 /* TEXT, PROPS */, ["onClick"]) + ]), + _createElementVNode("div", { class: "task-readiness-head" }, [ + _createElementVNode("div", null, [ + _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-caption" }, _toDisplayString(_ctx.t('orchestration.actions.caption')), 1 /* TEXT */) + _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-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 */) + ]) ]) ]) ]), - (!(_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.taskOrchestrationWorkspaceQueue.length || _ctx.taskOrchestrationWorkspaceRuns.length || _ctx.taskOrchestration.selectedRunId || _ctx.taskOrchestration.selectedRunError)) ? (_openBlock(), _createElementBlock("section", { key: 0, class: "selector-section task-stage-card" @@ -4322,368 +5051,8 @@ 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 */) - ]) - ]), - _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.taskOrchestration.queue.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.taskOrchestration.queue.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.taskOrchestration.queue.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) - ? (_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"])) - : _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.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) - ? (_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.taskOrchestration.queue, (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.lastSummary) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - 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)]) - }, _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"]) - ]) - ], 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)) - ? (_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.summary) - ? (_openBlock(), _createElementBlock("div", { - key: 0, - 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: $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.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.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 */), - _createElementVNode("pre", { class: "task-log-block" }, _toDisplayString(_ctx.formatTaskNodeLogs(node.logs)), 1 /* TEXT */) - ])) - }), 128 /* KEYED_FRAGMENT */)) - ]) - ], 64 /* STABLE_FRAGMENT */)) - ])) - ])) - : _createCommentVNode("v-if", true) - ])) - ], 512 /* NEED_PATCH */)), [ + : _createCommentVNode("v-if", true) + ], 8 /* PROPS */, ["data-active"])), [ [_vShow, _ctx.mainTab === 'orchestration'] ]) : _createCommentVNode("v-if", true), diff --git a/web-ui/styles/task-orchestration.css b/web-ui/styles/task-orchestration.css index 62c4d194..7b6c7f7f 100644 --- a/web-ui/styles/task-orchestration.css +++ b/web-ui/styles/task-orchestration.css @@ -528,9 +528,6 @@ box-sizing: border-box; } -.task-action-preview { - min-width: 104px; -} .task-action-row-right { display: flex; @@ -779,9 +776,71 @@ line-height: 1.55; } +.task-node-output-card { + margin-top: 12px; + padding: 12px; + border-radius: 16px; + border: 1px solid rgba(100, 116, 139, 0.16); + background: #F8FAFC; +} + +.task-node-output-head, +.task-node-output-facts { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + flex-wrap: wrap; +} + +.task-node-output-head strong { + font-size: 13px; + color: var(--color-text-primary); +} + +.task-node-output-meta, +.task-node-output-facts, +.task-materialized-file { + font-size: 12px; + line-height: 1.55; + color: var(--color-text-secondary); +} + +.task-node-output-facts { + justify-content: flex-start; + margin-top: 6px; +} + +.task-output-block { + max-height: 260px; + min-height: 96px; + overflow: auto; + background: #FFFFFF; +} + +.task-materialized-files { + margin-top: 10px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.task-workspace-files { + padding: 10px; + border: 1px solid rgba(199, 116, 98, 0.2); + border-radius: 14px; + background: #FFFDFC; +} + +.task-workspace-files .task-node-deps { + color: var(--color-brand-dark); + font-weight: 800; +} + @media (max-width: 1200px) { .task-layout-grid-primary, - .task-layout-grid-secondary { + .task-layout-grid-secondary, + .task-chat-context-row-primary { grid-template-columns: 1fr; } } @@ -820,3 +879,2101 @@ 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 var(--color-border-soft); + background: #F8FAFC; + box-shadow: var(--shadow-subtle); +} + +.task-quick-target-field { + display: block; +} + +.task-chat-composer .selector-label { + font-size: 12px; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.task-quick-target { + min-height: 88px; + border-radius: 18px; + background: var(--color-surface); + font-size: 15px; +} + + +.task-chat-panel { + display: flex; + flex-direction: column; + gap: 14px; +} + +.task-chat-thread { + display: flex; + flex-direction: column; + gap: 10px; + max-height: 420px; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-width: thin; + scrollbar-color: transparent transparent; + padding: 12px; + border-radius: 20px; + border: 1px solid var(--color-border-soft); + background: rgba(255, 255, 255, 0.74); + transition: scrollbar-color 0.16s ease; +} + +.task-chat-thread:hover, +.task-chat-thread:focus-within { + scrollbar-color: rgba(100, 116, 139, 0.28) transparent; +} + +.task-chat-thread::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.task-chat-thread::-webkit-scrollbar-track { + background: transparent; +} + +.task-chat-thread::-webkit-scrollbar-thumb { + border-radius: 999px; + background: transparent; +} + +.task-chat-thread:hover::-webkit-scrollbar-thumb, +.task-chat-thread:focus-within::-webkit-scrollbar-thumb { + background: rgba(100, 116, 139, 0.28); +} + +.task-chat-thread::-webkit-scrollbar-thumb:hover { + background: rgba(100, 116, 139, 0.42); +} + +.task-chat-bubble-row { + display: flex; + width: 100%; +} + +.task-chat-bubble-row.is-assistant { + justify-content: flex-start; +} + +.task-chat-bubble-row.is-user { + justify-content: flex-end; +} + +.task-chat-bubble { + max-width: min(76%, 680px); + padding: 11px 13px; + border-radius: 18px; + border: 1px solid var(--color-border-soft); + background: var(--color-surface); + box-shadow: 0 8px 24px rgba(36, 24, 20, 0.06); +} + +.task-chat-bubble-row.is-user .task-chat-bubble { + border-color: rgba(199, 116, 98, 0.28); + background: var(--color-brand-light); + color: var(--color-brand-dark); + border-bottom-right-radius: 6px; +} + +.task-chat-bubble-row.is-assistant .task-chat-bubble { + border-bottom-left-radius: 6px; +} + +.task-chat-bubble-label { + font-size: 11px; + font-weight: 800; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--color-text-muted); + margin-bottom: 5px; +} + +.task-chat-bubble-text { + font-size: 14px; + line-height: 1.55; + color: var(--color-text-primary); + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.task-chat-bubble-row.is-user .task-chat-bubble-text, +.task-chat-bubble-row.is-user .task-chat-bubble-label, +.task-chat-bubble-row.is-user .task-chat-bubble-meta { + color: var(--color-brand-dark); +} + +.task-chat-bubble-meta { + margin-top: 6px; + font-size: 11px; + line-height: 1.45; + color: var(--color-text-secondary); +} + +.task-chat-send-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +.task-chat-action-buttons { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.task-chat-primary-action { + flex: 1 1 180px; + display: grid; + place-items: center; + align-self: stretch; +} + +.task-chat-primary-button { + min-width: 168px; + min-height: 44px; + justify-content: center; + font-weight: 800; +} + +.task-chat-execute-caption { + margin-top: 8px; + color: var(--color-text-secondary); + font-size: 12px; + line-height: 1.45; +} + +.task-chat-context-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} + +.task-chat-context-row-primary { + display: grid; + grid-template-columns: minmax(260px, 1.2fr) minmax(220px, 0.8fr) repeat(3, minmax(96px, auto)); + align-items: stretch; + padding: 10px; + border: 1px solid rgba(199, 116, 98, 0.18); + border-radius: 18px; + background: #FFFFFF; +} + +.task-chat-context-chip { + display: inline-flex; + align-items: center; + min-height: 30px; + max-width: 100%; + padding: 0 10px; + border-radius: 999px; + border: 1px solid rgba(199, 116, 98, 0.18); + background: rgba(255, 255, 255, 0.68); + color: var(--color-text-secondary); + font-size: 12px; + font-weight: 600; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.task-chat-context-chip-strong { + display: flex; + align-items: flex-start; + flex-direction: column; + justify-content: center; + gap: 2px; + min-height: 42px; + border-radius: 14px; + background: #FFFDFC; + color: var(--color-text-primary); + white-space: normal; +} + +.task-chat-context-chip-strong small { + color: var(--color-text-muted); + font-size: 10px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.task-chat-context-chip-strong strong { + max-width: 100%; + overflow-wrap: anywhere; + word-break: break-word; + font-size: 12px; + line-height: 1.35; +} + +.task-chat-context-action { + cursor: pointer; + font: inherit; +} + +.task-chat-context-action:hover, +.task-chat-context-action:focus-visible { + border-color: rgba(199, 116, 98, 0.38); + color: var(--color-brand-dark); + outline: none; +} + +.task-quick-readiness { + margin-top: 14px; + background: rgba(255, 255, 255, 0.58); +} + +.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-provider-status-card { + margin-bottom: 14px; + padding: 14px; + border-radius: 16px; + border: 1px solid rgba(100, 116, 139, 0.16); + background: #FFFFFF; + display: flex; + flex-direction: column; + gap: 10px; +} + +.task-provider-status-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.task-provider-status-row { + min-width: 0; + padding: 9px 10px; + border-radius: 12px; + background: #F8FAFC; + border: 1px solid rgba(100, 116, 139, 0.10); +} + +.task-provider-status-row-wide { + grid-column: 1 / -1; +} + +.task-provider-status-row span, +.task-provider-status-row strong { + display: block; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.task-provider-status-row span { + font-size: 11px; + color: var(--color-text-muted); +} + +.task-provider-status-row strong { + margin-top: 3px; + font-size: 12px; + color: var(--color-text-primary); +} + +.task-provider-status-error { + margin: 0; +} + +.task-provider-config-button { + align-self: flex-start; +} + +.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; + } +} + +/* Codex-style orchestration simplification: keep the chat thread as the main surface. */ +#panel-orchestration { + --task-orchestration-main-width: 1200px; + --task-orchestration-context-width: 260px; +} + +#panel-orchestration .task-hero-card { + max-width: var(--task-orchestration-main-width); + margin: -4px auto 14px; + padding: 16px 18px; + box-shadow: none; +} + +#panel-orchestration .task-hero-main { + align-items: center; +} + +#panel-orchestration .task-hero-copy, +#panel-orchestration .task-hero-meta-strip, +#panel-orchestration .task-quick-copy, +#panel-orchestration .task-template-block, +#panel-orchestration .task-stage-card { + display: none; +} + +#panel-orchestration .task-layout-grid-primary, +#panel-orchestration .task-layout-grid-secondary { + max-width: var(--task-orchestration-main-width); + margin-left: auto; + margin-right: auto; +} + +#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: block; + 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: 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 { + 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-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: 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-thread-composer .task-textarea-goal:focus { + box-shadow: none; +} + +#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-thread-composer .task-draft-inline { + padding: 8px 10px; + border-radius: 14px; + background: rgba(255, 248, 241, 0.58); +} + +#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-thread-composer .task-action-row-right { + margin-left: auto; +} + +#panel-orchestration .task-thread-composer .task-field-hint { + font-size: 0.75rem; +} + +#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-quick-advanced { + margin-top: 8px; + border: 1px solid rgba(148, 163, 184, 0.14); + background: rgba(255, 255, 255, 0.46); + box-shadow: none; +} + +#panel-orchestration .task-codex-queue-action { + justify-content: flex-start; + margin: 0 0 12px; +} + +#panel-orchestration .task-quick-side-card { + position: sticky; + top: 14px; + padding: 12px; + border-radius: 18px; + 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; +} + +#panel-orchestration .task-provider-status-card, +#panel-orchestration .task-quick-status-card { + margin-bottom: 10px; + padding: 10px; + border-radius: 14px; +} + +#panel-orchestration .task-provider-status-grid { + grid-template-columns: 1fr; + gap: 6px; +} + +#panel-orchestration .task-provider-status-row { + padding: 7px 8px; + border-radius: 10px; + background: rgba(248, 250, 252, 0.62); +} + +#panel-orchestration .task-provider-status-card .task-readiness-copy, +#panel-orchestration .task-readiness-head .task-readiness-copy { + display: none; +} + +#panel-orchestration .task-readiness-grid.task-quick-checklist { + grid-template-columns: 1fr; + gap: 7px; + margin-top: 8px; +} + +#panel-orchestration .task-readiness-item { + padding: 9px 10px; + border-radius: 12px; +} + +#panel-orchestration .task-quick-status-card { + margin-top: 10px; +} + +@media (max-width: 1180px) { + #panel-orchestration .task-quick-card { + 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; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + } + + #panel-orchestration .task-provider-status-card, + #panel-orchestration .task-quick-status-card { + margin-bottom: 0; + } +} + +@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 .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 transcript final pass: the task flow is a chat, not a dashboard. */ +#panel-orchestration { + --task-orchestration-main-width: 920px; + --task-orchestration-context-width: 0px; + + min-height: calc(100vh - 72px); + padding-bottom: 188px; +} + +#panel-orchestration .task-hero-card, +#panel-orchestration .task-quick-copy, +#panel-orchestration .task-stage-card { + display: none; +} + +#panel-orchestration .task-layout-grid, +#panel-orchestration .task-layout-grid-primary, +#panel-orchestration .task-layout-grid-secondary, +#panel-orchestration .task-quick-card { + max-width: var(--task-orchestration-main-width); + width: min(100%, var(--task-orchestration-main-width)); + margin-left: auto; + margin-right: auto; +} + +#panel-orchestration .task-layout-grid { + gap: 0; + margin-bottom: 0; +} + +#panel-orchestration .task-quick-card { + display: block; + padding: 0; + border: 0; + background: transparent; + box-shadow: none; +} + +#panel-orchestration .task-quick-main { + display: block; + min-width: 0; +} + +#panel-orchestration .task-template-block { + display: block; +} + +#panel-orchestration .task-chat-panel { + display: flex; + flex-direction: column; + gap: 8px; + height: auto; + min-height: 0; + margin: 0 auto; + padding: 0; + overflow: visible; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; +} + +#panel-orchestration .task-chat-thread { + flex: 0 0 auto; + min-height: 0; + max-height: none; + margin: 0; + padding: 8px 8px 188px; + gap: 7px; + overflow: visible; + border: 0; + background: transparent; + scroll-padding-bottom: 188px; +} + +#panel-orchestration .task-chat-bubble-row { + position: relative; + justify-content: flex-start; + gap: 8px; + margin: 0; + padding: 1px 0 6px 28px; +} + +#panel-orchestration .task-chat-bubble-row::before { + content: '•'; + position: absolute; + top: 4px; + left: 8px; + color: #64748B; + font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace); + font-size: 16px; + font-weight: 800; + line-height: 1; +} + +#panel-orchestration .task-chat-bubble-row.is-user::before { + content: '›'; + top: 2px; + color: #2563EB; + font-size: 20px; +} + +#panel-orchestration .task-chat-bubble { + max-width: min(100%, 760px); + padding: 0 0 5px; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; +} + +#panel-orchestration .task-chat-bubble-row.is-user .task-chat-bubble, +#panel-orchestration .task-chat-bubble-row.is-assistant .task-chat-bubble { + border-radius: 0; + background: transparent; +} + +#panel-orchestration .task-chat-bubble-label { + margin-bottom: 3px; + color: #64748B; + font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace); + font-size: 10px; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +#panel-orchestration .task-chat-bubble-text { + color: #0F172A; + font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace); + font-size: 14px; + line-height: 1.5; +} + +#panel-orchestration .task-chat-bubble-meta { + margin-top: 5px; + color: #64748B; + font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace); +} + +#panel-orchestration .task-thread-plan-request { + margin-top: 2px; +} + +#panel-orchestration .task-thread-plan-request .task-chat-bubble { + max-width: min(100%, 760px); +} + +#panel-orchestration .task-thread-message-card { + position: relative; + align-self: flex-start; + width: min(100%, 660px); + margin: 0 0 2px 34px; + padding: 8px 10px; + border: 1px solid rgba(148, 163, 184, 0.18); + border-left: 3px solid rgba(199, 116, 98, 0.32); + border-radius: 14px 14px 14px 5px; + background: rgba(255, 255, 255, 0.50); + box-shadow: none; +} + +#panel-orchestration .task-thread-message-card::before { + content: 'AI'; + position: absolute; + left: -34px; + top: 8px; + display: grid; + width: 24px; + height: 24px; + place-items: center; + border-radius: 999px; + background: var(--color-brand-light); + color: var(--color-brand-dark); + font-size: 9px; + font-weight: 800; + letter-spacing: 0.04em; +} + +#panel-orchestration .task-thread-card-label { + margin-bottom: 6px; + color: var(--color-text-secondary); + font-size: 10px; + font-weight: 800; + letter-spacing: 0.07em; + text-transform: uppercase; +} + +#panel-orchestration .task-thread-message-card .selector-header { + margin-bottom: 6px; + padding-bottom: 0; + border-bottom: 0; +} + +#panel-orchestration .task-thread-message-card .selector-title { + font-size: 0.9rem; + line-height: 1.2; +} + +#panel-orchestration .task-side-settings-card .task-advanced-summary { + margin: 0 0 8px; + color: var(--color-text-primary); + font-size: 0.86rem; + font-weight: 750; +} + +#panel-orchestration .task-side-settings-card:not([open]) { + padding: 8px 10px; +} + +#panel-orchestration .task-side-settings-card .selector-grid { + margin-top: 8px; +} + +#panel-orchestration .task-side-settings-card .task-codex-queue-action { + justify-content: flex-start; + margin: 8px 0 0; +} + +#panel-orchestration .task-thread-message-card .skills-panel-note { + font-size: 0.76rem; + line-height: 1.45; +} + +#panel-orchestration .task-thread-message-card .task-plan-summary-strip { + display: flex; + flex-wrap: wrap; + gap: 5px; + margin-top: 5px; +} + +#panel-orchestration .task-thread-message-card .task-plan-summary-item { + min-width: 0; + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; +} + +#panel-orchestration .task-thread-message-card .task-plan-summary-label { + font-size: 9px; +} + +#panel-orchestration .task-thread-plan-card .task-wave-list, +#panel-orchestration .task-runtime-list { + gap: 5px; + margin-top: 7px; +} + +#panel-orchestration .task-thread-plan-card .task-wave-list { + display: none; +} + +#panel-orchestration .task-thread-plan-card .task-node-list { + display: none; +} + +#panel-orchestration .task-thread-plan-card .task-plan-summary-item:nth-child(n+3) { + display: none; +} + +#panel-orchestration .task-thread-plan-card .task-wave-card, +#panel-orchestration .task-thread-plan-card .task-node-card, +#panel-orchestration .task-thread-message-card .task-runtime-item { + padding: 4px 0; + border: 0; + border-radius: 0; + background: transparent; + box-shadow: none; +} + +#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, +#panel-orchestration .task-thread-message-card .task-runtime-item-title { + font-size: 0.8rem; + font-weight: 650; +} + +#panel-orchestration .task-thread-message-card .task-runtime-item-meta { + font-size: 0.72rem; + line-height: 1.4; +} + +#panel-orchestration .task-side-workbench-card, +#panel-orchestration .task-side-settings-card { + max-height: min(560px, calc(100vh - 220px)); + margin-bottom: 8px; + overflow: auto; +} + +#panel-orchestration .task-side-workbench-card .task-node-list { + max-height: 320px; + overflow: auto; + padding-right: 4px; +} + +#panel-orchestration .task-quick-side-card { + width: min(100%, 660px); + margin: 8px auto 0; + padding: 10px 12px; + border: 1px solid rgba(148, 163, 184, 0.18); + border-left: 3px solid rgba(100, 116, 139, 0.24); + border-radius: 14px; + background: rgba(255, 255, 255, 0.42); + box-shadow: none; +} + +#panel-orchestration .task-quick-side-card .task-provider-status-card { + margin-bottom: 8px; + padding: 0; + border: 0; + background: transparent; +} + +#panel-orchestration .task-quick-side-card .task-readiness-head, +#panel-orchestration .task-quick-side-card .task-readiness-grid, +#panel-orchestration .task-quick-side-card .task-quick-status-card { + display: none; +} + +#panel-orchestration .task-quick-side-card .task-provider-status-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +#panel-orchestration .task-thread-composer { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + grid-template-rows: auto auto; + align-items: stretch; + gap: 6px; + flex: 0 0 auto; + position: fixed; + right: 50%; + bottom: 16px; + z-index: 20; + width: min(872px, calc(100vw - 48px)); + transform: translateX(50%); + margin-top: 0; + padding: 6px 8px; + border: 1px solid rgba(148, 163, 184, 0.20); + border-radius: 14px; + background: rgba(255, 255, 255, 0.92); + box-shadow: 0 14px 36px rgba(15, 23, 42, 0.13); +} + +#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-chat-composer { + grid-column: 1; + grid-row: 1 / span 2; + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: start; + gap: 8px; +} + +#panel-orchestration .task-thread-composer .task-composer-prompt-glyph { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 18px; + padding-top: 7px; + color: #2563EB; + font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace); + font-size: 22px; + font-weight: 800; + line-height: 1; +} + +#panel-orchestration .task-thread-composer .task-textarea-goal { + min-height: 48px; + max-height: 92px; + padding: 8px 10px; + resize: vertical; + border: 0; + border-radius: 12px; + background: rgba(248, 250, 252, 0.66); + font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace); + font-size: 14px; + line-height: 1.35; + box-shadow: none; +} + +#panel-orchestration .task-thread-composer .task-textarea-goal:focus { + box-shadow: none; +} + +#panel-orchestration .task-thread-composer .task-chat-send-row { + grid-column: 2; + grid-row: 1 / span 2; + display: flex; + align-items: center; + justify-content: center; + align-self: center; + margin-top: 0; +} + +#panel-orchestration .task-thread-composer .task-chat-execute-caption { + grid-column: 1 / -1; + grid-row: 3; + margin: -1px 2px 0 28px; + color: #64748B; + font-family: var(--font-family-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace); + font-size: 11px; + line-height: 1.35; +} + +#panel-orchestration .task-thread-composer .task-action-row-prominent { + grid-column: 2; + grid-row: 2; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 7px; + margin-top: 0; + padding-top: 0; + border-top: 0; +} + +#panel-orchestration .task-thread-composer .task-action-row-right { + margin-left: 0; +} + +#panel-orchestration .task-thread-composer .btn-tool { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 32px; + padding: 5px 10px; + font-size: 0.78rem; +} + +@media (max-width: 760px) { + #panel-orchestration { + --task-orchestration-main-width: 100%; + + min-height: calc(100vh - 52px); + padding-bottom: 208px; + } + + #panel-orchestration .task-layout-grid, + #panel-orchestration .task-layout-grid-primary, + #panel-orchestration .task-layout-grid-secondary, + #panel-orchestration .task-quick-card { + width: 100%; + max-width: 100%; + } + + #panel-orchestration .task-template-block, + #panel-orchestration .task-quick-advanced, + #panel-orchestration .task-quick-side-card { + display: block; + } + + #panel-orchestration .task-chat-panel { + height: auto; + min-height: 0; + } + + #panel-orchestration .task-chat-thread { + padding: 8px 4px 16px; + gap: 7px; + scroll-padding-bottom: 28px; + } + + #panel-orchestration .task-chat-bubble, + #panel-orchestration .task-thread-plan-request .task-chat-bubble { + max-width: 92%; + } + + #panel-orchestration .task-thread-message-card { + width: 100%; + margin-left: 0; + padding: 9px 10px; + border-radius: 15px; + } + + #panel-orchestration .task-thread-message-card::before { + display: none; + } + + #panel-orchestration .task-thread-message-card .task-plan-summary-item { + min-width: calc(50% - 4px); + } + + #panel-orchestration .task-side-workbench-card { + max-height: calc(100vh - 190px); + } + + #panel-orchestration .task-side-workbench-card .task-node-list { + max-height: 260px; + } + + #panel-orchestration .task-chat-thread { + padding-bottom: 208px; + scroll-padding-bottom: 208px; + } + + #panel-orchestration .task-thread-composer { + grid-template-columns: minmax(0, 1fr) auto; + grid-template-rows: auto; + right: 10px; + bottom: calc(env(safe-area-inset-bottom, 0px) + 8px); + left: 10px; + width: auto; + transform: none; + padding: 7px; + border-radius: 14px; + box-shadow: 0 14px 34px rgba(15, 23, 42, 0.16); + } + + #panel-orchestration .task-thread-composer .task-chat-composer { + grid-column: 1; + grid-row: 1; + } + + #panel-orchestration .task-thread-composer .task-chat-send-row { + grid-column: 2; + grid-row: 1; + align-self: center; + } + + #panel-orchestration .task-thread-composer .task-action-row-prominent, + #panel-orchestration .task-thread-composer .task-action-row-prominent .task-action-row-right, + #panel-orchestration .task-thread-composer .task-action-row-prominent .btn-tool { + width: 100%; + } + +#panel-orchestration .task-thread-composer .task-textarea-goal { + min-height: 52px; + max-height: 118px; + } +} + +/* Chat-alignment correction: keep the transcript clean while preserving a + separate side rail for settings, history, and run details. */ +#panel-orchestration { + padding-bottom: 240px; +} + +#panel-orchestration .task-chat-thread { + padding-bottom: 240px; + scroll-padding-bottom: 240px; +} + +#panel-orchestration .task-thread-message-card { + width: min(100%, 620px); + padding: 10px 12px; + border-color: rgba(148, 163, 184, 0.14); + border-left-color: rgba(199, 116, 98, 0.26); + background: rgba(255, 255, 255, 0.62); +} + +#panel-orchestration .task-thread-plan-card, +#panel-orchestration .task-side-workbench-card { + overflow: visible; + max-height: none; +} + +#panel-orchestration .task-thread-message-card .selector-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 8px; +} + +#panel-orchestration .task-thread-message-card .selector-title { + font-size: 0.86rem; +} + +#panel-orchestration .task-thread-plan-card .skills-panel-note, +#panel-orchestration .task-side-workbench-card .skills-panel-note { + display: none; +} + +#panel-orchestration .task-thread-plan-card .task-plan-summary-strip, +#panel-orchestration .task-side-workbench-card .task-detail-summary-strip { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin: 6px 0 0; +} + +#panel-orchestration .task-thread-plan-card .task-plan-summary-item, +#panel-orchestration .task-side-workbench-card .task-plan-summary-item { + min-width: auto; + padding: 4px 7px; + border: 1px solid rgba(148, 163, 184, 0.12); + border-radius: 999px; + background: rgba(248, 250, 252, 0.60); +} + +#panel-orchestration .task-thread-plan-card .task-plan-summary-label, +#panel-orchestration .task-side-workbench-card .task-plan-summary-label { + display: inline; + margin-right: 4px; + color: var(--color-text-muted); + font-size: 9px; +} + +#panel-orchestration .task-thread-plan-card .task-plan-summary-item strong, +#panel-orchestration .task-side-workbench-card .task-plan-summary-item strong { + display: inline; + color: var(--color-text-primary); + font-size: 11px; + font-weight: 700; +} + +#panel-orchestration .task-side-workbench-card .task-header-actions, +#panel-orchestration .task-side-workbench-card .task-detail-toolbar, +#panel-orchestration .task-side-workbench-card .task-workbench-tabs { + margin: 0; + padding: 0; + border: 0; + background: transparent; + box-shadow: none; +} + +#panel-orchestration .task-side-workbench-card .task-header-actions .btn-tool, +#panel-orchestration .task-side-workbench-card .task-detail-toolbar .btn-tool, +#panel-orchestration .task-side-workbench-card .task-workbench-tab { + min-height: 26px; + padding: 3px 7px; + border-radius: 999px; + font-size: 0.7rem; +} + +#panel-orchestration .task-side-workbench-card .task-workbench-panel { + margin-top: 8px; + padding: 0; + border: 0; + background: transparent; + box-shadow: none; +} + +#panel-orchestration .task-thread-run-summary { + display: flex; + align-items: center; + gap: 8px; + margin-top: 4px; + color: var(--color-text-secondary); + font-size: 0.82rem; + line-height: 1.45; +} + +#panel-orchestration .task-thread-run-summary-copy { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#panel-orchestration .task-thread-run-details { + margin-top: 8px; + color: var(--color-text-secondary); +} + +#panel-orchestration .task-thread-run-details > summary { + width: fit-content; + cursor: pointer; + border-radius: 999px; + padding: 4px 9px; + background: rgba(248, 250, 252, 0.70); + color: var(--color-text-muted); + font-size: 0.72rem; + font-weight: 700; +} + +#panel-orchestration .task-thread-run-details:not([open]) { + margin-bottom: 0; +} + +#panel-orchestration .task-thread-run-details[open] > summary { + margin-bottom: 8px; +} + +#panel-orchestration .task-thread-detail-actions { + justify-content: flex-start; + margin-bottom: 8px; +} + +#panel-orchestration .task-thread-plan-details .task-plan-summary-strip { + margin-top: 0; +} + +#panel-orchestration .task-side-workbench-card .task-runtime-list, +#panel-orchestration .task-side-workbench-card .task-node-list { + max-height: none; + gap: 6px; + padding-right: 0; + overflow: visible; +} + +#panel-orchestration .task-side-workbench-card .task-node-card, +#panel-orchestration .task-side-workbench-card .task-runtime-item, +#panel-orchestration .task-side-workbench-card .task-node-output-card { + padding: 7px 0; + border-width: 1px 0 0; + border-radius: 0; + background: transparent; + box-shadow: none; +} + +#panel-orchestration .task-side-workbench-card .task-node-output-card, +#panel-orchestration .task-side-workbench-card .task-log-block { + max-height: 120px; + overflow: auto; + border-radius: 10px; + background: rgba(15, 23, 42, 0.04); +} + +#panel-orchestration .task-thread-composer { + bottom: 20px; + background: rgba(255, 255, 255, 0.97); + box-shadow: 0 8px 26px rgba(15, 23, 42, 0.13); +} + +@media (max-width: 760px) { + #panel-orchestration { + padding-bottom: 72px; + } + + #panel-orchestration .task-chat-thread { + padding-bottom: 16px; + scroll-padding-bottom: 130px; + } + + #panel-orchestration .task-thread-message-card { + width: calc(100% - 10px); + margin-left: 0; + } + + #panel-orchestration .task-side-workbench-card .task-node-output-card, + #panel-orchestration .task-side-workbench-card .task-log-block { + max-height: 96px; + } + + #panel-orchestration .task-thread-composer { + bottom: calc(env(safe-area-inset-bottom, 0px) + 10px); + } +} + + +/* Workspace selector + records rail: project context belongs beside the chat, not inside it. */ +#panel-orchestration { + --task-orchestration-main-width: 1360px; + --task-orchestration-project-width: 260px; + --task-orchestration-records-width: 320px; +} + +#panel-orchestration .task-layout-grid, +#panel-orchestration .task-layout-grid-primary, +#panel-orchestration .task-layout-grid-secondary, +#panel-orchestration .task-quick-card { + max-width: var(--task-orchestration-main-width); + width: min(100%, var(--task-orchestration-main-width)); +} + +#panel-orchestration .task-quick-card { + display: grid; + grid-template-columns: var(--task-orchestration-project-width) minmax(0, 1fr) var(--task-orchestration-records-width); + align-items: start; + gap: 16px; +} + +#panel-orchestration .task-project-sidebar, +#panel-orchestration .task-quick-side-card { + position: sticky; + top: 84px; + align-self: start; + max-height: calc(100vh - 112px); + overflow: auto; + scrollbar-width: thin; +} + +#panel-orchestration .task-project-sidebar, +#panel-orchestration .task-quick-side-card > .selector-section, +#panel-orchestration .task-quick-side-card > details, +#panel-orchestration .task-provider-status-card { + border: 1px solid rgba(148, 163, 184, 0.16); + border-radius: 18px; + background: rgba(255, 255, 255, 0.62); + box-shadow: none; +} + +#panel-orchestration .task-project-sidebar { + display: flex; + flex-direction: column; + gap: 12px; + padding: 12px; +} + +#panel-orchestration .task-project-sidebar-head, +#panel-orchestration .task-session-inbox-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; +} + +#panel-orchestration .task-project-sidebar .skills-panel-note { + margin-top: 2px; + font-size: 0.76rem; + line-height: 1.4; +} + +#panel-orchestration .task-project-list, +#panel-orchestration .task-session-inbox { + display: flex; + flex-direction: column; + gap: 7px; +} + +#panel-orchestration .task-project-item, +#panel-orchestration .task-session-inbox-item { + width: 100%; + min-width: 0; + border: 1px solid rgba(148, 163, 184, 0.14); + border-radius: 14px; + background: rgba(248, 250, 252, 0.58); + color: var(--color-text-primary); + text-align: left; + cursor: pointer; + transition: border-color 0.16s ease, background 0.16s ease, transform 0.16s ease; +} + +#panel-orchestration .task-project-item { + display: grid; + gap: 3px; + padding: 9px 10px; +} + +#panel-orchestration .task-project-item:hover, +#panel-orchestration .task-session-inbox-item:hover, +#panel-orchestration .task-project-item.active { + border-color: rgba(199, 116, 98, 0.34); + background: rgba(255, 255, 255, 0.86); +} + +#panel-orchestration .task-project-item.active { + box-shadow: inset 3px 0 0 rgba(199, 116, 98, 0.56); +} + +#panel-orchestration .task-project-item-title, +#panel-orchestration .task-session-inbox-title { + display: block; + min-width: 0; + overflow: hidden; + color: var(--color-text-primary); + font-size: 0.86rem; + font-weight: 800; + text-overflow: ellipsis; + white-space: nowrap; +} + +#panel-orchestration .task-project-item-meta, +#panel-orchestration .task-session-inbox-meta, +#panel-orchestration .task-project-item-stats { + display: block; + min-width: 0; + overflow: hidden; + color: var(--color-text-muted); + font-size: 0.72rem; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; +} + +#panel-orchestration .task-project-new-session { + width: 100%; + justify-content: center; +} + +#panel-orchestration .task-session-inbox { + padding-top: 2px; +} + +#panel-orchestration .task-session-inbox-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 8px 9px; +} + +#panel-orchestration .task-session-inbox-main { + min-width: 0; +} + +#panel-orchestration .task-session-empty { + padding: 10px; + border-radius: 14px; + background: rgba(248, 250, 252, 0.52); +} + +#panel-orchestration .task-quick-main { + min-width: 0; +} + +#panel-orchestration .task-quick-side-card { + display: flex; + flex-direction: column; + gap: 10px; + width: auto; + margin: 0; + padding: 0; + border: 0; + background: transparent; +} + +#panel-orchestration .task-quick-side-card > .selector-section, +#panel-orchestration .task-quick-side-card > details, +#panel-orchestration .task-provider-status-card { + margin: 0; + padding: 12px; +} + +#panel-orchestration .task-side-workbench-card, +#panel-orchestration .task-side-settings-card { + max-height: none; +} + +#panel-orchestration .task-side-workbench-card .task-runtime-list, +#panel-orchestration .task-side-workbench-card .task-node-list { + max-height: 340px; + overflow: auto; +} + +#panel-orchestration .task-thread-composer { + width: min(700px, calc(100vw - 680px)); + min-width: 520px; +} + +@media (max-width: 1180px) { + #panel-orchestration { + --task-orchestration-main-width: 100%; + } + + #panel-orchestration .task-quick-card { + grid-template-columns: minmax(220px, 0.34fr) minmax(0, 0.66fr); + } + + #panel-orchestration .task-quick-side-card { + grid-column: 1 / -1; + position: static; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + #panel-orchestration .task-project-sidebar { + position: sticky; + } + + #panel-orchestration .task-thread-composer { + width: min(680px, calc(100vw - 360px)); + min-width: 420px; + } +} + +@media (max-width: 760px) { + #panel-orchestration .task-quick-card, + #panel-orchestration .task-quick-side-card { + display: flex; + flex-direction: column; + } + + #panel-orchestration .task-project-sidebar, + #panel-orchestration .task-quick-side-card { + position: static; + max-height: none; + } + + #panel-orchestration .task-thread-composer { + right: auto; + bottom: 10px; + left: auto; + width: 100%; + min-width: 0; + transform: none; + } +} + +/* Web Agent cockpit pass: the orchestration tab is an agent workbench, not a hidden task form. */ +#panel-orchestration { + --task-orchestration-main-width: 1440px; + --task-orchestration-project-width: 270px; + --task-orchestration-records-width: 340px; + + padding-bottom: 40px; +} + +#panel-orchestration .task-hero-card { + display: block; + max-width: var(--task-orchestration-main-width); + margin: -2px auto 14px; + border-color: rgba(59, 130, 246, 0.12); + background: + radial-gradient(circle at 12% 10%, rgba(59, 130, 246, 0.08), transparent 26%), + linear-gradient(135deg, rgba(255, 255, 255, 0.92), rgba(248, 250, 252, 0.76)); +} + +#panel-orchestration .task-hero-card .task-hero-kicker, +#panel-orchestration .task-hero-card .task-hero-copy { + display: block; +} + +#panel-orchestration .task-quick-card { + grid-template-columns: var(--task-orchestration-project-width) minmax(0, 1fr) var(--task-orchestration-records-width); + gap: 18px; +} + +#panel-orchestration .task-project-sidebar, +#panel-orchestration .task-quick-side-card { + top: 92px; + max-height: calc(100vh - 120px); +} + +#panel-orchestration .task-agent-cockpit { + margin-bottom: 12px; + padding: 14px; + border: 1px solid rgba(59, 130, 246, 0.14); + border-radius: 20px; + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.92), rgba(239, 246, 255, 0.54)), + var(--color-surface); + box-shadow: 0 14px 38px rgba(15, 23, 42, 0.07); +} + +#panel-orchestration .task-agent-cockpit-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +#panel-orchestration .task-agent-title { + font-size: clamp(18px, 1.45vw, 24px); + line-height: 1.18; +} + +#panel-orchestration .task-agent-copy { + display: block; + margin-top: 6px; + max-width: 760px; + line-height: 1.5; +} + +#panel-orchestration .task-agent-surface-grid, +#panel-orchestration .task-agent-trace-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; + margin-top: 12px; +} + +#panel-orchestration .task-agent-surface-card, +#panel-orchestration .task-agent-trace-card { + min-width: 0; + padding: 10px; + border: 1px solid rgba(148, 163, 184, 0.16); + border-radius: 14px; + background: rgba(255, 255, 255, 0.72); +} + +#panel-orchestration .task-agent-surface-card span, +#panel-orchestration .task-agent-trace-card span { + display: block; + color: var(--color-text-muted); + font-size: 10px; + font-weight: 800; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +#panel-orchestration .task-agent-surface-card strong, +#panel-orchestration .task-agent-trace-card strong { + display: block; + min-width: 0; + margin-top: 5px; + overflow: hidden; + color: var(--color-text-primary); + font-size: 12px; + font-weight: 800; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; +} + +#panel-orchestration .task-agent-trace-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin: 10px 0 2px; +} + +#panel-orchestration .task-agent-trace-card { + padding: 8px 9px; + background: rgba(248, 250, 252, 0.72); +} + +#panel-orchestration .task-agent-trace-card strong { + font-size: 18px; +} + +#panel-orchestration .task-chat-panel { + min-height: calc(100vh - 330px); + border-radius: 22px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(248, 250, 252, 0.66)), + var(--color-surface); +} + +#panel-orchestration .task-chat-thread { + padding-bottom: 18px; + scroll-padding-bottom: 120px; +} + +#panel-orchestration .task-thread-composer { + grid-template-columns: minmax(0, 1fr) auto; + right: auto; + bottom: 14px; + left: auto; + width: min(100%, 760px); + min-width: 0; + margin-top: 10px; + transform: none; + align-self: center; + border-color: rgba(59, 130, 246, 0.16); + border-radius: 18px; +} + +#panel-orchestration .task-chat-action-buttons { + justify-content: center; +} + +#panel-orchestration .task-chat-action-buttons .btn-tool { + min-width: 168px; +} + +#panel-orchestration .task-chat-action-buttons .btn-primary { + min-width: 168px; +} + +#panel-orchestration .task-chat-primary-button { + border-color: #1D4ED8; + background: #2563EB; + color: #FFFFFF; + box-shadow: 0 10px 22px rgba(37, 99, 235, 0.24); +} + +#panel-orchestration .task-chat-primary-button:hover:not(:disabled), +#panel-orchestration .task-chat-primary-button:focus-visible:not(:disabled) { + border-color: #1E40AF; + background: #1D4ED8; + color: #FFFFFF; +} + +#panel-orchestration .task-side-workbench-card .task-thread-run-details { + margin-top: 10px; +} + +#panel-orchestration .task-side-workbench-card .task-thread-run-details > summary { + background: rgba(239, 246, 255, 0.76); + color: #1D4ED8; +} + +#panel-orchestration .task-side-workbench-card .task-workbench-panel, +#panel-orchestration .task-side-workbench-card .task-runtime-list, +#panel-orchestration .task-side-workbench-card .task-node-list { + max-height: 360px; + overflow: auto; +} + +#panel-orchestration .task-side-settings-card { + border-style: dashed; +} + +@media (max-width: 1180px) { + #panel-orchestration .task-quick-card { + grid-template-columns: minmax(220px, 0.35fr) minmax(0, 0.65fr); + } + + #panel-orchestration .task-agent-surface-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + #panel-orchestration .task-thread-composer { + width: min(100%, 720px); + min-width: 0; + } +} + +@media (max-width: 760px) { + #panel-orchestration { + padding-bottom: 72px; + } + + #panel-orchestration .task-hero-card { + margin-bottom: 10px; + padding: 12px; + } + + #panel-orchestration .task-agent-cockpit { + padding: 12px; + border-radius: 18px; + } + + #panel-orchestration .task-agent-cockpit-head { + flex-direction: column; + align-items: stretch; + } + + #panel-orchestration .task-agent-surface-grid, + #panel-orchestration .task-agent-trace-grid { + grid-template-columns: 1fr; + } + + #panel-orchestration .task-chat-thread { + padding-bottom: 16px; + scroll-padding-bottom: 130px; + } + + #panel-orchestration .task-thread-composer { + right: auto; + bottom: 10px; + left: auto; + width: 100%; + min-width: 0; + transform: none; + } + + #panel-orchestration .task-chat-action-buttons { + flex-direction: column; + gap: 6px; + } + + #panel-orchestration .task-chat-action-buttons .btn-tool { + width: 100%; + min-width: 0; + } +}