diff --git a/crates/ironclaw_gateway/static/i18n/en.js b/crates/ironclaw_gateway/static/i18n/en.js index f99e4e1f74..f8b59f0c23 100644 --- a/crates/ironclaw_gateway/static/i18n/en.js +++ b/crates/ironclaw_gateway/static/i18n/en.js @@ -879,6 +879,27 @@ I18n.register('en', { 'missions.pauseFailed': 'Pause failed: {message}', 'missions.resumed': 'Mission resumed', 'missions.resumeFailed': 'Resume failed: {message}', + 'missions.successCriteria': 'Success Criteria', + 'missions.currentFocus': 'Current Focus', + 'missions.approachHistory': 'Approach History', + 'missions.prompt': 'Mission Prompt', + 'missions.missionBrief': 'Mission Brief', + 'missions.runLabel': 'Run {number}', + 'missions.latestRun': 'Latest Run', + 'missions.goalAchieved': 'Goal Achieved', + 'missions.openLoop': 'Open Loop', + 'missions.expectedLabel': 'Expected', + 'missions.observedLabel': 'Observed', + 'missions.fixAppliedLabel': 'Fix Applied', + 'missions.nextFocusLabel': 'Next Focus', + 'missions.outcomeLabel': 'Outcome', + 'missions.recentActivity': 'Recent Activity', + 'missions.spawnedThreads': 'Spawned Threads', + 'missions.cooldown': 'Cooldown', + 'missions.project': 'Project', + 'missions.retry': 'Retry', + 'missions.fireOnce': 'Fire Once', + 'missions.lastFire': 'Last Fire', // TEE (dynamic) 'tee.reportCopied': 'Attestation report copied', diff --git a/crates/ironclaw_gateway/static/index.html b/crates/ironclaw_gateway/static/index.html index 007090aaf4..182319f8be 100644 --- a/crates/ironclaw_gateway/static/index.html +++ b/crates/ironclaw_gateway/static/index.html @@ -366,20 +366,10 @@
- - - - - - - - - - - - - -
NameGoalCadenceThreadsStatusProgressActions
+
+
+
+
diff --git a/crates/ironclaw_gateway/static/js/core/ui-helpers.js b/crates/ironclaw_gateway/static/js/core/ui-helpers.js index f92b05e990..20294ed68b 100644 --- a/crates/ironclaw_gateway/static/js/core/ui-helpers.js +++ b/crates/ironclaw_gateway/static/js/core/ui-helpers.js @@ -333,7 +333,7 @@ document.addEventListener('click', function(e) { crBackToOverview(); break; case 'cr-close-detail': - document.getElementById('cr-detail').style.display = 'none'; + closeCrDetail(); break; case 'cr-att-click': if (el.dataset.project) drillIntoProject(el.dataset.project); @@ -341,12 +341,15 @@ document.addEventListener('click', function(e) { case 'cr-new-project': crNewProject(); break; + case 'open-project-mission': + openMissionFromProjects(el.dataset.id); + break; case 'open-mission': openMissionDetail(el.dataset.id); break; case 'close-mission-detail': if (crCurrentProjectId) { - document.getElementById('cr-detail').style.display = 'none'; + closeCrDetail(); } else { closeMissionDetail(); } @@ -368,7 +371,7 @@ document.addEventListener('click', function(e) { break; case 'back-to-mission': if (currentMissionId) openMissionDetail(currentMissionId); - else document.getElementById('cr-detail').style.display = 'none'; + else closeCrDetail(); break; case 'open-active-work': if (el.dataset.kind === 'job') { diff --git a/crates/ironclaw_gateway/static/js/surfaces/projects.js b/crates/ironclaw_gateway/static/js/surfaces/projects.js index d889c9acd1..e639e7d32e 100644 --- a/crates/ironclaw_gateway/static/js/surfaces/projects.js +++ b/crates/ironclaw_gateway/static/js/surfaces/projects.js @@ -97,7 +97,7 @@ function drillIntoProject(projectId) { document.getElementById('cr-cards').style.display = 'none'; var drill = document.getElementById('cr-drill'); drill.style.display = ''; - document.getElementById('cr-detail').style.display = 'none'; + closeCrDetail(); // Find project from cached overview. var proj = crOverview && crOverview.projects @@ -145,10 +145,30 @@ function crBackToOverview() { crCurrentProjectId = null; destroyProjectWidgets(); document.getElementById('cr-drill').style.display = 'none'; - document.getElementById('cr-detail').style.display = 'none'; + closeCrDetail(); document.getElementById('cr-cards').style.display = ''; } +function setCrDetailOpen(isOpen) { + var shell = document.getElementById('cr-shell'); + var detail = document.getElementById('cr-detail'); + if (shell) shell.classList.toggle('cr-shell-detail-open', !!isOpen); + if (!detail) return; + detail.style.display = isOpen ? 'block' : 'none'; + if (!isOpen) detail.innerHTML = ''; +} + +function closeCrDetail() { + setCrDetailOpen(false); +} + +function openMissionFromProjects(missionId) { + if (!missionId) return; + closeCrDetail(); + switchTab('missions'); + openMissionDetail(missionId); +} + function renderCrDrillMissions(missions) { var el = document.getElementById('cr-drill-missions'); if (!el) return; @@ -162,7 +182,7 @@ function renderCrDrillMissions(missions) { var statusClass = m.status === 'Active' ? 'in_progress' : m.status === 'Completed' ? 'completed' : m.status === 'Paused' ? 'pending' : 'failed'; - html += '' - + '

Thread: ' + escapeHtml(t.goal) + '

' - + '' + escapeHtml(t.state) + '
'; - html += '
' - + metaItem('Type', t.thread_type) + metaItem('Steps', t.step_count) - + metaItem('Tokens', t.total_tokens.toLocaleString()) - + metaItem('Cost', t.total_cost_usd > 0 ? '$' + t.total_cost_usd.toFixed(4) : '\u2014') - + metaItem('Created', formatDate(t.created_at)) - + metaItem('Completed', t.completed_at ? formatDate(t.completed_at) : '\u2014') - + '
'; - if (t.messages && t.messages.length) { - html += '

Messages (' + t.messages.length + ')

'; - t.messages.forEach(function(msg) { - var roleClass = msg.role === 'Assistant' ? 'assistant' : msg.role === 'User' ? 'user' : 'system'; - html += '
' - + '
' + escapeHtml(msg.role) + '
' - + '
' + renderMarkdown(msg.content) + '
'; - }); - html += '
'; + var presentation = getCrThreadPresentation(t); + var html = '
' + + '
' + + '' + + '
' + + '

' + escapeHtml(presentation.title) + '

' + + (presentation.subtitle ? '
' + escapeHtml(presentation.subtitle) + '
' : '') + + '
' + + '' + escapeHtml(t.state) + '' + + '
' + + renderCrThreadSummary(t, presentation); + + if (presentation.brief) { + html += '
' + + '
Mission brief
' + + '
' + renderMarkdown(presentation.brief) + '
' + + '
'; } + + html += '
' + + crThreadMetaItem('Type', t.thread_type || 'mission_run') + + crThreadMetaItem('Steps', String(t.step_count || 0)) + + crThreadMetaItem('Tokens', (t.total_tokens || 0).toLocaleString()) + + crThreadMetaItem('Cost', t.total_cost_usd > 0 ? '$' + t.total_cost_usd.toFixed(4) : '\u2014') + + crThreadMetaItem('Created', t.created_at ? formatDate(t.created_at) : '\u2014') + + crThreadMetaItem('Completed', t.completed_at ? formatDate(t.completed_at) : '\u2014') + + '
' + + '
'; + + if (t.messages && t.messages.length) { + t.messages.forEach(function(msg) { + html += renderCrThreadMessage(msg); + }); + } else { + html += '
No messages captured for this thread yet.
'; + } + + html += '
'; crShowDetail(html); }).catch(function(err) { console.error('[projects] Failed to load thread:', err); @@ -570,63 +693,164 @@ function loadMissions() { currentMissionId = null; currentMissionData = null; currentEngineThreadDetail = null; - const detail = document.getElementById('mission-detail'); + var detail = document.getElementById('mission-detail'); if (detail) detail.style.display = 'none'; - const table = document.getElementById('missions-table'); - if (table) table.style.display = ''; + var body = document.getElementById('missions-body'); + if (body) body.style.display = ''; Promise.all([ apiFetch('/api/engine/missions/summary'), apiFetch('/api/engine/missions'), - ]).then(([summary, listData]) => { + apiFetch('/api/engine/threads').catch(function() { return { threads: [] }; }), + ]).then(function(results) { + var summary = results[0]; + var listData = results[1]; + var threadData = results[2]; currentMissionList = listData.missions || []; activeWorkStore.rememberMissions(currentMissionList); renderMissionsSummary(summary); renderMissionsList(currentMissionList); + renderMissionsActivity(threadData.threads || []); enrichMissionProgress(currentMissionList); - }).catch(() => {}); + }).catch(function() {}); } function renderMissionsSummary(s) { - document.getElementById('missions-summary').innerHTML = '' - + summaryCard(I18n.t('missions.summary.total'), s.total, '') - + summaryCard(I18n.t('missions.summary.active'), s.active, 'active') - + summaryCard(I18n.t('missions.summary.paused'), s.paused, '') - + summaryCard(I18n.t('missions.summary.completed'), s.completed, 'completed') - + summaryCard(I18n.t('missions.summary.failed'), s.failed, 'failed'); + document.getElementById('missions-summary').innerHTML = + '
' + escapeHtml(I18n.t('missions.summary.total')) + '' + s.total + '
' + + '
' + escapeHtml(I18n.t('missions.summary.active')) + '' + s.active + '
' + + '
' + escapeHtml(I18n.t('missions.summary.paused')) + '' + s.paused + '
' + + '
' + escapeHtml(I18n.t('missions.summary.completed')) + '' + s.completed + '
' + + '
' + escapeHtml(I18n.t('missions.summary.failed')) + '' + s.failed + '
'; } function renderMissionsList(missions) { - const tbody = document.getElementById('missions-tbody'); - const empty = document.getElementById('missions-empty'); + var col = document.getElementById('missions-list-col'); + var empty = document.getElementById('missions-empty'); + var body = document.getElementById('missions-body'); if (!missions || missions.length === 0) { - tbody.innerHTML = ''; + if (col) col.innerHTML = ''; + if (body) body.style.display = 'none'; empty.style.display = 'block'; return; } empty.style.display = 'none'; - tbody.innerHTML = missions.map((m) => { - const statusClass = m.status === 'Active' ? 'in_progress' - : m.status === 'Completed' ? 'completed' - : m.status === 'Paused' ? 'pending' - : 'failed'; + if (body) body.style.display = ''; - return '' - + '' + escapeHtml(m.name) + '' - + '' + escapeHtml(m.goal) + '' - + '' + escapeHtml(m.cadence_description || m.cadence_type) + '' - + '' + m.thread_count + '' - + '' + escapeHtml(m.status) + '' - + '' + renderMissionProgressCell(m.id) + '' - + '' - + (m.status === 'Active' ? ' ' : '') - + (m.status === 'Paused' ? ' ' : '') - + '' - + '' - + ''; - }).join(''); + var groups = { Active: [], Paused: [], Completed: [], Failed: [] }; + missions.forEach(function(m) { + if (groups[m.status]) groups[m.status].push(m); + else groups.Active.push(m); + }); + + var html = ''; + var order = ['Active', 'Paused', 'Completed', 'Failed']; + var labels = { + Active: I18n.t('missions.summary.active'), + Paused: I18n.t('missions.summary.paused'), + Completed: I18n.t('missions.summary.completed'), + Failed: I18n.t('missions.summary.failed') + }; + + order.forEach(function(status) { + var list = groups[status]; + if (!list.length) return; + + html += '
' + escapeHtml(labels[status]) + '
'; + list.forEach(function(m) { + var badgeClass = m.status === 'Active' ? 'in_progress' + : m.status === 'Completed' ? 'completed' + : m.status === 'Paused' ? 'pending' : 'failed'; + var progress = activeWorkStore.getMissionProgress(m.id); + var liveHtml = progress + ? ' Running' + : ''; + + html += '
' + + '
' + + '
' + + '' + escapeHtml(m.name) + '' + + '' + escapeHtml(m.status) + '' + + '
' + + '
' + escapeHtml(m.goal) + '
' + + '
' + + '' + escapeHtml(m.cadence_description || m.cadence_type || 'manual') + '' + + '' + m.thread_count + ' threads' + + '
' + + '
' + + '
' + + liveHtml + + '
' + m.thread_count + '
' + + '
threads
' + + '
' + + '
'; + }); + }); + + col.innerHTML = html; +} + +function renderMissionsActivity(threads) { + var col = document.getElementById('missions-activity-col'); + if (!col) return; + if (!threads || !threads.length) { + col.innerHTML = '
' + escapeHtml(I18n.t('missions.recentActivity')) + '
' + + '
No recent activity.
'; + return; + } + + var sorted = threads.slice().sort(function(a, b) { + return new Date(b.updated_at || b.created_at) - new Date(a.updated_at || a.created_at); + }); + + var html = '
' + escapeHtml(I18n.t('missions.recentActivity')) + '
'; + var lastDay = ''; + + sorted.slice(0, 20).forEach(function(t) { + var d = new Date(t.updated_at || t.created_at); + var now = new Date(); + var dayLabel = ''; + if (d.toDateString() === now.toDateString()) dayLabel = 'Today'; + else { + var yesterday = new Date(now); + yesterday.setDate(yesterday.getDate() - 1); + if (d.toDateString() === yesterday.toDateString()) dayLabel = 'Yesterday'; + else dayLabel = d.toLocaleDateString(); + } + if (dayLabel !== lastDay) { + html += '
' + escapeHtml(dayLabel) + '
'; + lastDay = dayLabel; + } + + var dotClass = (t.state === 'Running') ? 'running' + : (t.state === 'Done' || t.state === 'Completed') ? 'done' + : (t.state === 'Failed') ? 'failed' : 'done'; + var label = t.title || t.goal || ('Thread ' + (t.id || '').slice(0, 8)); + var costStr = t.total_cost_usd > 0 ? '$' + t.total_cost_usd.toFixed(2) : ''; + var durationStr = ''; + if (t.completed_at && t.created_at) { + var secs = Math.round((new Date(t.completed_at) - new Date(t.created_at)) / 1000); + if (secs < 60) durationStr = secs + 's'; + else durationStr = Math.floor(secs / 60) + 'm ' + (secs % 60) + 's'; + } + + html += '
' + + '
' + + '
' + + '
' + escapeHtml(label) + '
' + + (t.state === 'Running' ? '
In progress
' : '') + + (durationStr || costStr ? '
' + + (durationStr ? '' + escapeHtml(durationStr) + '' : '') + + (costStr ? '' + escapeHtml(costStr) + '' : '') + + '
' : '') + + '
' + + '' + escapeHtml(formatRelativeTime(t.updated_at || t.created_at)) + '' + + '
'; + }); + + col.innerHTML = html; } function openMissionDetail(id) { @@ -653,97 +877,406 @@ function closeMissionDetail() { loadMissions(); } +function renderMissionRichBlock(text, extraClass) { + var classes = 'ms-rich'; + if (extraClass) classes += ' ' + extraClass; + return '
' + renderMarkdown(text || '') + '
'; +} + +function isLikelyMissionHeading(lines, index) { + var line = (lines[index] || '').trim(); + if (!line || line.length > 48) return false; + if (/^[-*+]\s/.test(line) || /^\d+[.)]\s/.test(line) || /[:.]$/.test(line)) return false; + + var known = [ + 'input', 'inputs', 'investigation process', 'process', 'root cause categories', + 'classification', 'hard rules', 'rules', 'fix policy', 'success criteria', 'output' + ]; + if (known.indexOf(line.toLowerCase()) !== -1) return true; + + var prev = index > 0 ? (lines[index - 1] || '').trim() : ''; + var next = index < lines.length - 1 ? (lines[index + 1] || '').trim() : ''; + if (next === '' || (prev && prev !== '---')) return false; + return /^[A-Za-z][A-Za-z0-9 /&()_\-]+$/.test(line); +} + +function splitMissionDocument(text) { + var lines = String(text || '').replace(/\r\n/g, '\n').split('\n'); + var intro = []; + var sections = []; + var current = null; + + lines.forEach(function(line, index) { + var trimmed = line.trim(); + var markdownHeading = trimmed.match(/^#{1,6}\s+(.+?)\s*#*$/); + var plainHeading = !markdownHeading && isLikelyMissionHeading(lines, index) ? trimmed : null; + + if (markdownHeading || plainHeading) { + current = { + title: (markdownHeading ? markdownHeading[1] : plainHeading).trim(), + lines: [] + }; + sections.push(current); + return; + } + + if (current) current.lines.push(line); + else intro.push(line); + }); + + return { + intro: intro.join('\n').trim(), + sections: sections + .map(function(section) { + return { + title: section.title, + body: section.lines.join('\n').trim() + }; + }) + .filter(function(section) { + return section.title || section.body; + }) + }; +} + +function inferMissionBriefKind(title) { + var lower = String(title || '').toLowerCase(); + if (lower.indexOf('input') !== -1) return 'inputs'; + if (lower.indexOf('process') !== -1 || lower.indexOf('steps') !== -1) return 'process'; + if (lower.indexOf('rule') !== -1 || lower.indexOf('policy') !== -1) return 'rules'; + if (lower.indexOf('classification') !== -1 || lower.indexOf('root cause') !== -1) return 'classification'; + return 'generic'; +} + +function parseMissionDefinitions(text) { + var lines = String(text || '').replace(/\r\n/g, '\n').split('\n'); + var items = []; + var notes = []; + var raw = []; + + lines.forEach(function(line) { + var trimmed = line.trim(); + if (!trimmed) return; + + var match = trimmed.match(/^(?:[-*+]\s+)?`?([A-Za-z0-9_."\[\]()\/-]+)`?\s*(?:—|–|-|:)\s*(.+)$/); + if (match) { + items.push({ + key: match[1], + text: match[2] + }); + return; + } + + if (/contains:$/i.test(trimmed) || /includes:$/i.test(trimmed)) { + notes.push(trimmed); + return; + } + + raw.push(line); + }); + + return { + items: items, + note: notes.join('\n').trim(), + raw: raw.join('\n').trim() + }; +} + +function parseMissionListItems(text) { + var items = []; + String(text || '').replace(/\r\n/g, '\n').split('\n').forEach(function(line) { + var match = line.match(/^\s*(?:\d+[.)]|[-*+])\s+(.+)$/); + if (match) items.push(match[1]); + }); + return items; +} + +function renderMissionBriefSection(section) { + var kind = inferMissionBriefKind(section.title); + var html = '
' + + '
' + + '

' + escapeHtml(section.title) + '

' + + '
'; + + if (kind === 'inputs') { + var defs = parseMissionDefinitions(section.body); + if (defs.note) { + html += '
' + renderMissionRichBlock(defs.note, 'ms-brief-note-copy') + '
'; + } + if (defs.items.length > 0) { + html += '
'; + defs.items.forEach(function(item) { + html += '
' + + '
' + escapeHtml(item.key) + '
' + + '
' + escapeHtml(item.text) + '
' + + '
'; + }); + html += '
'; + } + if (defs.raw) html += renderMissionRichBlock(defs.raw, 'ms-brief-copy'); + } else if (kind === 'process') { + var steps = parseMissionListItems(section.body); + if (steps.length > 0) { + html += '
'; + steps.forEach(function(step, index) { + html += '
' + + '
' + (index + 1) + '
' + + '
' + escapeHtml(step) + '
' + + '
'; + }); + html += '
'; + } else { + html += renderMissionRichBlock(section.body, 'ms-brief-copy'); + } + } else if (kind === 'rules') { + var rules = parseMissionListItems(section.body); + if (rules.length > 0) { + html += '
'; + rules.forEach(function(rule) { + html += '
' + + '
!
' + + '
' + escapeHtml(rule) + '
' + + '
'; + }); + html += '
'; + } else { + html += renderMissionRichBlock(section.body, 'ms-brief-copy'); + } + } else if (kind === 'classification') { + var categories = parseMissionDefinitions(section.body); + if (categories.items.length > 0) { + html += '
'; + categories.items.forEach(function(item) { + html += '
' + + '
' + escapeHtml(item.key) + '
' + + '
' + escapeHtml(item.text) + '
' + + '
'; + }); + html += '
'; + } + if (categories.raw) html += renderMissionRichBlock(categories.raw, 'ms-brief-copy'); + } else { + html += renderMissionRichBlock(section.body, 'ms-brief-copy'); + } + + html += '
'; + return html; +} + +function renderMissionBrief(text) { + var parsed = splitMissionDocument(text); + if (!parsed.sections.length) { + return '
' + renderMissionRichBlock(text, 'ms-brief-copy') + '
'; + } + + var html = '
'; + if (parsed.intro) { + html += '
' + + '
' + escapeHtml(I18n.t('missions.missionBrief')) + '
' + + renderMissionRichBlock(parsed.intro, 'ms-brief-intro-copy') + + '
'; + } + + parsed.sections.forEach(function(section) { + html += renderMissionBriefSection(section); + }); + + html += '
'; + return html; +} + +function normalizeApproachField(label) { + var lower = String(label || '').toLowerCase().replace(/[^a-z]+/g, ' ').trim(); + if (lower.indexOf('expected') !== -1) return 'expected'; + if (lower.indexOf('what happened') !== -1 || lower.indexOf('observed') !== -1 || lower.indexOf('actual') !== -1) return 'observed'; + if (lower.indexOf('root cause') !== -1 || lower.indexOf('classification') !== -1) return 'classification'; + if (lower.indexOf('fix applied') !== -1 || lower === 'fix' || lower.indexOf('applied') !== -1) return 'fix'; + if (lower.indexOf('next focus') !== -1 || lower === 'next') return 'next'; + if (lower.indexOf('goal achieved') !== -1 || lower.indexOf('outcome') !== -1 || lower.indexOf('result') !== -1) return 'outcome'; + return ''; +} + +function parseApproachHistoryRecord(text) { + var lines = String(text || '').replace(/\r\n/g, '\n').split('\n'); + var record = { + lead: [], + fields: {} + }; + var currentField = ''; + + lines.forEach(function(line) { + var trimmed = line.trim(); + if (/^run\s+\d+$/i.test(trimmed)) return; + + var match = trimmed.match(/^(?:[-*+]\s+)?([A-Za-z][A-Za-z ]{1,40}):\s*(.*)$/); + var normalized = match ? normalizeApproachField(match[1]) : ''; + if (normalized) { + currentField = normalized; + if (!record.fields[currentField]) record.fields[currentField] = []; + if (match[2]) record.fields[currentField].push(match[2]); + return; + } + + if (currentField) { + if (!record.fields[currentField]) record.fields[currentField] = []; + record.fields[currentField].push(line); + } else { + record.lead.push(line); + } + }); + + Object.keys(record.fields).forEach(function(key) { + record.fields[key] = record.fields[key].join('\n').trim(); + }); + record.lead = record.lead.join('\n').trim(); + return record; +} + +function renderApproachField(label, value, className) { + if (!value) return ''; + var classes = 'ms-approach-field'; + if (className) classes += ' ' + className; + return '
' + + '
' + escapeHtml(label) + '
' + + renderMissionRichBlock(value, 'ms-approach-value') + + '
'; +} + +function renderApproachHistoryCard(entryText, index, isLatest) { + var parsed = parseApproachHistoryRecord(entryText); + var classification = parsed.fields.classification || ''; + var outcome = parsed.fields.outcome || ''; + var achieved = /\b(yes|resolved|fixed|done|completed|achieved)\b/i.test(outcome) && !/\b(no|not yet|pending|blocked)\b/i.test(outcome); + + var html = '
' + + '
' + + '
' + escapeHtml(I18n.t('missions.runLabel', { number: index + 1 })) + '
' + + '
'; + + if (classification) { + html += '' + escapeHtml(classification) + ''; + } + if (isLatest) { + html += '' + escapeHtml(I18n.t('missions.latestRun')) + ''; + } + if (outcome) { + html += '' + escapeHtml(achieved ? I18n.t('missions.goalAchieved') : I18n.t('missions.openLoop')) + ''; + } + + html += '
'; + + if (parsed.lead) { + html += '
' + renderMissionRichBlock(parsed.lead, 'ms-approach-summary-copy') + '
'; + } + + var fieldsHtml = ''; + fieldsHtml += renderApproachField(I18n.t('missions.expectedLabel'), parsed.fields.expected); + fieldsHtml += renderApproachField(I18n.t('missions.observedLabel'), parsed.fields.observed); + fieldsHtml += renderApproachField(I18n.t('missions.fixAppliedLabel'), parsed.fields.fix, 'full'); + fieldsHtml += renderApproachField(I18n.t('missions.nextFocusLabel'), parsed.fields.next); + fieldsHtml += renderApproachField(I18n.t('missions.outcomeLabel'), parsed.fields.outcome); + + if (fieldsHtml) { + html += '
' + fieldsHtml + '
'; + } else { + html += '
' + renderMarkdown(entryText) + '
'; + } + + html += '
'; + return html; +} + function renderMissionDetail(m) { - const table = document.getElementById('missions-table'); - if (table) table.style.display = 'none'; + var body = document.getElementById('missions-body'); + if (body) body.style.display = 'none'; document.getElementById('missions-empty').style.display = 'none'; - const detail = document.getElementById('mission-detail'); + var detail = document.getElementById('mission-detail'); detail.style.display = 'block'; - const statusClass = m.status === 'Active' ? 'in_progress' + var badgeClass = m.status === 'Active' ? 'in_progress' : m.status === 'Completed' ? 'completed' - : m.status === 'Paused' ? 'pending' - : 'failed'; + : m.status === 'Paused' ? 'pending' : 'failed'; + var progress = activeWorkStore.getMissionProgress(m.id); - let html = '
' - + '' - + '

' + escapeHtml(m.name) + '

' - + '' + escapeHtml(m.status) + '' + var html = ''; + + html += '
' + + '
' + + '
' + + '' + escapeHtml(m.name) + '' + + '' + escapeHtml(m.status) + '' + + (progress ? ' Running' : '') + + '
' + + '
' + + '
'; + + if (m.status === 'Active') { + html += ''; + html += ''; + } else if (m.status === 'Paused') { + html += ''; + html += ''; + } else if (m.status === 'Failed') { + html += ''; + } + html += '
'; + + html += '
' + + '
' + escapeHtml(I18n.t('missions.cadence')) + '
' + escapeHtml(m.cadence_description || m.cadence_type || 'manual') + '
' + + '
' + escapeHtml(I18n.t('missions.threadsToday')) + '
' + (m.threads_today || 0) + ' / ' + (m.max_threads_per_day || '\u221E') + '
' + + '
' + escapeHtml(I18n.t('missions.totalThreads')) + '
' + m.thread_count + '
' + + '
' + escapeHtml(I18n.t('missions.nextFire')) + '
' + (m.next_fire_at ? formatDate(m.next_fire_at) : (m.status === 'Paused' ? '\u2014 paused' : '\u2014')) + '
' + + '
' + escapeHtml(I18n.t('missions.created')) + '
' + formatDate(m.created_at) + '
' + '
'; - // Goal — full-width markdown block - html += '

Goal

' - + '
' + renderMarkdown(m.goal) + '
'; - - html += '
' - + metaItem(I18n.t('missions.cadence'), m.cadence_description || m.cadence_type) - + metaItem(I18n.t('missions.status'), m.status) - + metaItem(I18n.t('missions.threadsToday'), m.threads_today + ' / ' + (m.max_threads_per_day || '\u221E')) - + metaItem(I18n.t('missions.totalThreads'), m.thread_count) - + metaItem(I18n.t('missions.created'), formatDate(m.created_at)) - + metaItem(I18n.t('missions.nextFire'), m.next_fire_at ? formatDate(m.next_fire_at) : I18n.t('common.noData')) - + '
'; + if (m.goal) { + html += '
' + escapeHtml(I18n.t('missions.prompt')) + '
' + + '
' + renderMissionBrief(m.goal) + '
'; + } if (m.current_focus) { - html += '

Current Focus

' - + '
' + renderMarkdown(m.current_focus) + '
'; + html += '
' + escapeHtml(I18n.t('missions.currentFocus')) + '
' + + '
' + renderMissionRichBlock(m.current_focus) + '
'; } if (m.success_criteria) { - html += '

Success Criteria

' - + '
' + renderMarkdown(m.success_criteria) + '
'; + html += '
' + escapeHtml(I18n.t('missions.successCriteria')) + '
' + + '
' + renderMissionRichBlock(m.success_criteria) + '
'; } if (m.notify_channels && m.notify_channels.length > 0) { - html += '

Notify Channels

' - + '
' + m.notify_channels.map(escapeHtml).join(', ') + '
'; + html += '
Notify Channels
' + + '
' + renderMissionRichBlock(m.notify_channels.map(escapeHtml).join(', ')) + '
'; } if (m.approach_history && m.approach_history.length > 0) { - html += '

Approach History

'; - m.approach_history.forEach((a, i) => { - html += '
' - + 'Run ' + (i + 1) + '
' - + renderMarkdown(a) + '
'; + html += '
' + escapeHtml(I18n.t('missions.approachHistory')) + '
' + + '
'; + m.approach_history.forEach(function(a, i) { + html += renderApproachHistoryCard(a, i, i === m.approach_history.length - 1); }); html += '
'; } if (m.threads && m.threads.length > 0) { - html += '

Spawned Threads

' - + '' - + '' - + ''; - m.threads.forEach((t) => { - var tState = t.state === 'Done' || t.state === 'Completed' ? 'completed' + html += '
' + escapeHtml(I18n.t('missions.spawnedThreads')) + '
' + + '
'; + m.threads.forEach(function(t) { + var tState = (t.state === 'Done' || t.state === 'Completed') ? 'done' : t.state === 'Failed' ? 'failed' - : t.state === 'Running' ? 'in_progress' - : 'pending'; - html += '
' - + '' - + '' - + '' - + '' - + '' - + '' - + '' - + ''; + : t.state === 'Running' ? 'running' : 'pending'; + var costStr = t.total_cost_usd > 0 ? '$' + t.total_cost_usd.toFixed(2) : ''; + html += '
' + + '' + escapeHtml(t.state) + '' + + '' + escapeHtml(t.goal) + '' + + '' + escapeHtml(costStr) + '' + + '' + formatRelativeTime(t.created_at) + '' + + '
'; }); - html += '
GoalTypeState' + escapeHtml(I18n.t('missions.progress')) + 'StepsTokensCreated
' + escapeHtml(t.goal) + '' + escapeHtml(t.thread_type) + '' + escapeHtml(t.state) + '' + renderMissionThreadProgress(t.id) + '' + t.step_count + '' + t.total_tokens.toLocaleString() + '' + formatDate(t.created_at) + '
'; + html += '
'; } - // Action buttons - html += '
'; - if (m.status === 'Active') { - html += ' '; - } - if (m.status === 'Paused') { - html += ' '; - } - html += ''; - html += '
'; - detail.innerHTML = html; } @@ -756,30 +1289,31 @@ function renderEngineThreadDetail(t) { : 'pending'; var progress = activeWorkStore.getThreadProgress(t.id); - var html = '
' - + '' - + '

Thread: ' + escapeHtml(t.goal) + '

' + var html = ''; + + html += '
' + + '
' + + '
' + + '' + escapeHtml(t.goal) + '' + '' + escapeHtml(t.state) + '' - + '
'; + + '
'; - html += '

Current Progress

' - + '
' + escapeHtml(progress || '') + '
'; + if (progress) { + html += '
' + + '

' + escapeHtml(progress) + '

'; + } - html += '
' - + metaItem(I18n.t('missions.threadId'), t.id) - + metaItem(I18n.t('missions.type'), t.thread_type) - + metaItem(I18n.t('missions.steps'), t.step_count) - + metaItem(I18n.t('missions.tokens'), t.total_tokens.toLocaleString()) - + metaItem(I18n.t('missions.cost'), t.total_cost_usd > 0 ? '$' + t.total_cost_usd.toFixed(4) : '-') - + metaItem(I18n.t('missions.maxIterations'), t.max_iterations) - + metaItem(I18n.t('missions.created'), formatDate(t.created_at)) - + metaItem(I18n.t('jobs.completedLabel'), t.completed_at ? formatDate(t.completed_at) : '-') + html += '
' + + '
' + escapeHtml(I18n.t('missions.type')) + '
' + escapeHtml(t.thread_type || '-') + '
' + + '
' + escapeHtml(I18n.t('missions.steps')) + '
' + t.step_count + '
' + + '
' + escapeHtml(I18n.t('missions.tokens')) + '
' + t.total_tokens.toLocaleString() + '
' + + '
' + escapeHtml(I18n.t('missions.cost')) + '
' + (t.total_cost_usd > 0 ? '$' + t.total_cost_usd.toFixed(4) : '-') + '
' + + '
' + escapeHtml(I18n.t('missions.created')) + '
' + formatDate(t.created_at) + '
' + + '
' + escapeHtml(I18n.t('jobs.completedLabel')) + '
' + (t.completed_at ? formatDate(t.completed_at) : '-') + '
' + '
'; if (t.messages && t.messages.length > 0) { - html += '

Messages (' + t.messages.length + ')

'; + html += '
Messages (' + t.messages.length + ')
'; t.messages.forEach(function(msg) { var roleClass = msg.role === 'Assistant' ? 'assistant' : msg.role === 'User' ? 'user' : 'system'; html += '
' @@ -787,7 +1321,6 @@ function renderEngineThreadDetail(t) { + '
' + renderMarkdown(msg.content) + '
' + '
'; }); - html += '
'; } detail.innerHTML = html; diff --git a/crates/ironclaw_gateway/static/styles/surfaces/missions.css b/crates/ironclaw_gateway/static/styles/surfaces/missions.css index 53ea0e8c05..f0873ee179 100644 --- a/crates/ironclaw_gateway/static/styles/surfaces/missions.css +++ b/crates/ironclaw_gateway/static/styles/surfaces/missions.css @@ -1,65 +1,727 @@ -/* Routines Tab */ /* ── Missions ──────────────────────────────────── */ .missions-container { flex: 1; overflow-y: auto; - padding: var(--space-4); + padding: var(--space-8) 40px 64px; } +/* ── Summary cards ── */ .missions-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: var(--space-3); - margin-bottom: 20px; + margin-bottom: var(--space-6); } - -.missions-table { - width: 100%; - border-collapse: collapse; +.ms-summary-card { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 14px; + padding: 18px 20px; + display: flex; + flex-direction: column; + gap: 4px; + transition: background var(--transition-fast), border-color var(--transition-fast); } - -.missions-table th, -.missions-table td { - padding: 10px 12px; - text-align: left; - border-bottom: 1px solid var(--border); - font-size: var(--text-sm); +.ms-summary-card:hover { + background: var(--bg-tertiary); + border-color: var(--border-hover); } - -.missions-table th { - color: var(--text-secondary); - font-weight: 500; - text-transform: uppercase; +.ms-summary-label { font-size: var(--text-xs); - letter-spacing: 0.5px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.06em; + font-weight: 500; +} +.ms-summary-value { + font-size: var(--text-2xl); + font-weight: 600; + line-height: 1; + letter-spacing: -0.02em; +} +.ms-summary-value.green { color: var(--accent); } +.ms-summary-value.amber { color: var(--warning); } +.ms-summary-value.red { color: var(--danger); } +.ms-summary-value.blue { color: var(--info); } +.ms-summary-sub { + font-size: var(--text-xs); + color: var(--text-dimmed); } -.missions-table tr:hover td { - background: var(--hover-surface); +/* ── Two-column layout ── */ +.missions-body { + display: grid; + grid-template-columns: 1fr 380px; + gap: var(--space-8); + align-items: start; } -.mission-row { +/* ── Section titles ── */ +.ms-section-title { + font-size: var(--text-xs); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); + margin-bottom: 10px; + margin-top: var(--space-6); +} +.ms-section-title:first-child { margin-top: 0; } + +/* ── Mission cards ── */ +.ms-card { + display: grid; + grid-template-columns: 1fr auto; + gap: var(--space-4); + padding: 18px 22px; + border-radius: 14px; + background: var(--bg-secondary); + border: 1px solid var(--border); + box-shadow: var(--shadow-sm); cursor: pointer; + color: var(--text); + margin-bottom: 8px; + transition: background 120ms ease-out, border-color 120ms ease-out, box-shadow 120ms ease-out; +} +.ms-card:hover { + background: var(--bg-tertiary); + border-color: var(--border-hover); + box-shadow: var(--shadow-md); +} +.ms-card-body { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} +.ms-card-head { + display: flex; + align-items: center; + gap: 10px; +} +.ms-card-name { + font-size: 15px; + font-weight: 600; + letter-spacing: -0.01em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.ms-card-goal { + font-size: var(--text-sm); + color: var(--text-secondary); + line-height: 1.45; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} +.ms-card-meta { + display: flex; + gap: 16px; + font-size: var(--text-xs); + color: var(--text-dimmed); + margin-top: 2px; +} +.ms-card-right { + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: space-between; + gap: 8px; + min-width: 72px; +} +.ms-card-threads-num { + font-size: var(--text-xl); + font-weight: 600; + line-height: 1; + letter-spacing: -0.02em; +} +.ms-card-threads-label { + font-size: var(--text-xs); + color: var(--text-dimmed); } -.mission-detail { - padding: 16px 0; +/* ── Live indicator ── */ +.ms-live-tag { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: var(--text-xs); + color: var(--accent); + font-weight: 500; +} +.ms-live-dot { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--accent); + animation: ms-livePulse 2s ease-in-out infinite; +} +@keyframes ms-livePulse { + 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(52,211,153,0.4); } + 50% { opacity: 0.4; box-shadow: 0 0 0 4px rgba(52,211,153,0); } } +/* ── Activity feed ── */ +.ms-act-row { + display: flex; + align-items: flex-start; + gap: 10px; + width: 100%; + padding: 12px 14px; + border-radius: var(--radius); + cursor: pointer; + transition: background var(--transition-fast); +} +.ms-act-row:hover { background: var(--hover-surface); } +.ms-act-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex: 0 0 auto; + margin-top: 5px; +} +.ms-act-dot.running { background: var(--accent); animation: ms-livePulse 2s ease-in-out infinite; } +.ms-act-dot.done { background: var(--info); } +.ms-act-dot.failed { background: var(--danger); } +.ms-act-content { + flex: 1; + min-width: 0; +} +.ms-act-label { + font-size: var(--text-sm); + color: var(--text); + margin-bottom: 2px; + line-height: 1.35; +} +.ms-act-sub { + font-size: var(--text-xs); + color: var(--text-dimmed); + line-height: 1.4; +} +.ms-act-tag { + display: inline-block; + font-size: 10px; + font-weight: 500; + color: var(--accent); + background: var(--accent-subtle); + padding: 2px 7px; + border-radius: 4px; + margin-top: 6px; +} +.ms-act-tag.danger { + color: var(--danger); + background: var(--danger-subtle); +} +.ms-act-metrics { + display: flex; + gap: 10px; + margin-top: 4px; + font-size: 10px; + font-family: var(--font-mono); + color: var(--text-dimmed); +} +.ms-act-time { + font-size: var(--text-xs); + color: var(--text-dimmed); + flex: 0 0 auto; + white-space: nowrap; + margin-top: 3px; +} +.ms-day-divider { + font-size: var(--text-xs); + color: var(--text-dimmed); + text-transform: uppercase; + letter-spacing: 0.06em; + font-weight: 500; + padding: 14px 14px 4px; +} + +/* ── Detail view ── */ +.mission-detail { padding: 0; } + +.ms-detail-back { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: var(--text-sm); + color: var(--accent); + background: none; + border: none; + cursor: pointer; + padding: 0; + margin-bottom: var(--space-4); +} +.ms-detail-back:hover { text-decoration: underline; } + +.ms-detail-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-4); + margin-bottom: var(--space-6); +} +.ms-detail-header-left { + flex: 1; + min-width: 0; +} +.ms-detail-title-row { + display: flex; + align-items: center; + gap: var(--space-3); + margin-bottom: 8px; + flex-wrap: wrap; +} +.ms-detail-title { + font-size: var(--text-xl); + font-weight: 600; + letter-spacing: -0.02em; +} +.ms-detail-goal { + width: min(100%, 960px); +} +.ms-detail-actions { + display: flex; + gap: 8px; + flex: 0 0 auto; +} + +/* ── Buttons ── */ +.ms-btn { + font-family: inherit; + font-size: var(--text-sm); + font-weight: 500; + padding: 8px 18px; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--bg-secondary); + color: var(--text); + cursor: pointer; + transition: all var(--transition-fast); +} +.ms-btn:hover { + background: var(--bg-tertiary); + border-color: var(--border-hover); +} +.ms-btn.primary { + background: var(--accent); + border-color: var(--accent); + color: var(--text-on-accent); + font-weight: 600; +} +.ms-btn.primary:hover { background: var(--accent-hover); } +.ms-btn.danger { + border-color: var(--danger-border-subtle); + color: var(--danger); +} +.ms-btn.danger:hover { background: var(--danger-subtle); } + +/* ── Meta grid ── */ +.ms-meta-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 1px; + background: var(--border); + border-radius: var(--radius-lg); + overflow: hidden; + margin-bottom: var(--space-6); +} +.ms-meta-cell { + background: var(--bg-secondary); + padding: 16px 20px; +} +.ms-meta-label { + font-size: var(--text-xs); + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.06em; + font-weight: 500; + margin-bottom: 4px; +} +.ms-meta-value { + font-size: var(--text-base); + font-weight: 500; + color: var(--text); +} +.ms-meta-value.mono { font-family: var(--font-mono); } + +/* ── Content blocks ── */ +.ms-content-block { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 18px; + padding: 18px 22px; + margin-bottom: var(--space-3); +} +.ms-content-block--focus { + border-color: rgba(96, 165, 250, 0.24); +} +.ms-content-block--success { + border-color: rgba(52, 211, 153, 0.26); + box-shadow: inset 0 0 0 1px rgba(52, 211, 153, 0.06); +} + +/* ── Rich markdown ── */ +.ms-rich { + font-size: var(--text-sm); + line-height: 1.65; + color: var(--text); +} +.ms-rich > *:first-child { margin-top: 0; } +.ms-rich > *:last-child { margin-bottom: 0; } +.ms-rich p { + margin: 0 0 12px; + color: var(--text-secondary); +} +.ms-rich h1, +.ms-rich h2, +.ms-rich h3, +.ms-rich h4 { + color: var(--text); + font-weight: 600; + letter-spacing: -0.02em; + margin: 0 0 12px; +} +.ms-rich h1 { font-size: 22px; } +.ms-rich h2 { font-size: 18px; } +.ms-rich h3, +.ms-rich h4 { font-size: 15px; } +.ms-rich ul, +.ms-rich ol { + margin: 0 0 14px 18px; + padding: 0; +} +.ms-rich li { + margin-bottom: 6px; + color: var(--text-secondary); +} +.ms-rich strong { + color: var(--text); + font-weight: 600; +} +.ms-rich code { + font-family: var(--font-mono); + font-size: 12px; + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: 6px; + padding: 1px 6px; + color: var(--text); +} +.ms-rich pre { + margin: 0 0 14px; + padding: 14px 16px; + border-radius: 14px; + background: var(--bg-tertiary); + border: 1px solid var(--border); + overflow-x: auto; +} +.ms-rich blockquote { + margin: 0 0 14px; + padding: 12px 14px; + border-left: 3px solid var(--accent); + background: var(--bg-tertiary); + border-radius: 0 12px 12px 0; +} + +/* ── Mission brief ── */ +.ms-brief { + display: flex; + flex-direction: column; + gap: 14px; +} +.ms-brief-intro, +.ms-brief-section { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 18px; + padding: 18px 20px; + box-shadow: var(--shadow-sm); +} +.ms-brief-intro { + background-image: linear-gradient(180deg, rgba(52, 211, 153, 0.08), rgba(52, 211, 153, 0)); +} +.ms-brief-kicker, +.ms-approach-label { + display: inline-flex; + align-items: center; + gap: 8px; + margin-bottom: 10px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-dimmed); +} +.ms-brief-section--inputs { border-color: rgba(52, 211, 153, 0.18); } +.ms-brief-section--process { border-color: rgba(96, 165, 250, 0.18); } +.ms-brief-section--classification { border-color: rgba(168, 85, 247, 0.18); } +.ms-brief-section--rules { border-color: rgba(245, 158, 11, 0.26); } +.ms-brief-section-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} +.ms-brief-section-title { + margin: 0; + font-size: 18px; + font-weight: 600; + letter-spacing: -0.02em; + color: var(--text); +} +.ms-brief-note { + margin-bottom: 12px; + padding: 12px 14px; + border-radius: 14px; + background: var(--bg-tertiary); + border: 1px solid var(--border); +} +.ms-schema-list, +.ms-category-list, +.ms-step-list, +.ms-callout-list { + display: grid; + gap: 10px; +} +.ms-schema-item, +.ms-category-item, +.ms-step-item, +.ms-callout-item { + display: grid; + align-items: start; + gap: 12px; + padding: 12px 14px; + border-radius: 14px; + background: var(--bg-tertiary); + border: 1px solid var(--border); +} +.ms-schema-item, +.ms-category-item { + grid-template-columns: minmax(0, 200px) 1fr; +} +.ms-step-item, +.ms-callout-item { + grid-template-columns: auto 1fr; +} +.ms-schema-key, +.ms-category-key { + font-family: var(--font-mono); + font-size: 12px; + color: var(--text); +} +.ms-schema-key { color: var(--accent); } +.ms-category-key { + color: var(--info); + text-transform: uppercase; + letter-spacing: 0.06em; + font-weight: 600; +} +.ms-schema-text, +.ms-category-text, +.ms-step-copy, +.ms-callout-copy { + font-size: var(--text-sm); + line-height: 1.55; + color: var(--text-secondary); +} +.ms-step-index { + width: 24px; + height: 24px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-on-accent); + background: var(--accent); + font-weight: 700; +} +.ms-callout-item { + border-color: rgba(245, 158, 11, 0.18); + background: rgba(245, 158, 11, 0.08); +} +.ms-callout-icon { + width: 24px; + height: 24px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + background: rgba(245, 158, 11, 0.16); + color: var(--warning); + font-weight: 700; + font-size: 12px; +} + +/* ── Approach history ── */ +.ms-approach-list { + display: flex; + flex-direction: column; + gap: 14px; +} +.ms-approach-entry { + background: linear-gradient(180deg, rgba(255,255,255,0.015), rgba(255,255,255,0)), var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 18px; + padding: 18px 20px; + border-left: 3px solid rgba(255,255,255,0.06); + box-shadow: var(--shadow-sm); +} +.ms-approach-entry.latest { + border-left-color: var(--accent); + box-shadow: 0 0 0 1px rgba(52, 211, 153, 0.08), var(--shadow-sm); +} +.ms-approach-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 14px; + flex-wrap: wrap; +} +.ms-approach-run { + font-family: var(--font-mono); + font-size: 10px; + color: var(--text-dimmed); + text-transform: uppercase; + letter-spacing: 0.08em; +} +.ms-approach-badges { + display: flex; + gap: 8px; + flex-wrap: wrap; +} +.ms-approach-chip { + display: inline-flex; + align-items: center; + padding: 4px 9px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + border: 1px solid transparent; +} +.ms-approach-chip.classification { + color: var(--info); + background: rgba(96, 165, 250, 0.12); + border-color: rgba(96, 165, 250, 0.16); +} +.ms-approach-chip.latest { + color: var(--accent); + background: rgba(52, 211, 153, 0.12); + border-color: rgba(52, 211, 153, 0.16); +} +.ms-approach-chip.success { + color: var(--accent); + background: rgba(52, 211, 153, 0.1); + border-color: rgba(52, 211, 153, 0.16); +} +.ms-approach-chip.open { + color: var(--warning); + background: rgba(245, 158, 11, 0.1); + border-color: rgba(245, 158, 11, 0.16); +} +.ms-approach-summary { + margin-bottom: 14px; + padding: 12px 14px; + border-radius: 14px; + background: var(--bg-tertiary); + border: 1px solid var(--border); +} +.ms-approach-summary .ms-rich p { + color: var(--text); + margin-bottom: 0; +} +.ms-approach-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} +.ms-approach-field { + min-width: 0; + padding: 14px 16px; + border-radius: 14px; + background: var(--bg-tertiary); + border: 1px solid var(--border); +} +.ms-approach-field.full { + grid-column: 1 / -1; +} +.ms-approach-value p { margin-bottom: 10px; } +.ms-approach-value ul, +.ms-approach-value ol { margin-left: 18px; } +.ms-approach-body { + font-size: var(--text-sm); + color: var(--text-secondary); + line-height: 1.6; +} + +/* ── Thread list ── */ +.ms-thread-list { + display: flex; + flex-direction: column; + gap: 2px; +} +.ms-thread-row { + display: grid; + grid-template-columns: auto 1fr auto auto; + gap: var(--space-3); + align-items: center; + padding: 12px 14px; + border-radius: var(--radius); + cursor: pointer; + transition: background var(--transition-fast); +} +.ms-thread-row:hover { background: var(--hover-surface); } +.ms-thread-state { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + padding: 3px 8px; + border-radius: 4px; + min-width: 64px; + text-align: center; +} +.ms-thread-state.running { color: var(--accent); background: var(--accent-subtle); } +.ms-thread-state.done { color: var(--info); background: rgba(96, 165, 250, 0.12); } +.ms-thread-state.failed { color: var(--danger); background: var(--danger-subtle); } +.ms-thread-state.pending { color: var(--text-muted); background: var(--hover-subtle); } +.ms-thread-label { + font-size: var(--text-sm); + color: var(--text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.ms-thread-cost { + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--text-dimmed); +} +.ms-thread-time { + font-size: var(--text-xs); + color: var(--text-dimmed); + white-space: nowrap; +} + +/* ── Progress (kept for activeWorkStore integration) ── */ .mission-progress-live { color: var(--accent); font-weight: 500; } - .mission-progress-idle { color: var(--text-muted); } -.mission-thread-progress { - border-color: var(--accent); -} - +/* ── Thread messages (used by renderEngineThreadDetail) ── */ .thread-message { border-left: 3px solid var(--border); padding: 8px 12px; @@ -67,7 +729,6 @@ border-radius: 0 4px 4px 0; font-size: var(--text-sm); } - .thread-msg-role { font-weight: 600; font-size: var(--text-xs); @@ -75,28 +736,39 @@ color: var(--text-secondary); margin-bottom: 4px; } +.thread-msg-content { overflow-x: auto; } +.thread-msg-assistant { border-left-color: var(--accent); } +.thread-msg-user { border-left-color: var(--success); } +.thread-msg-system { border-left-color: var(--text-secondary); opacity: 0.7; } -.thread-msg-content { - overflow-x: auto; +/* ── Responsive ── */ +@media (max-width: 1100px) { + .missions-body { grid-template-columns: 1fr; } + .ms-approach-grid, + .ms-schema-item, + .ms-category-item { + grid-template-columns: 1fr; + } } - -.thread-msg-assistant { - border-left-color: var(--accent); +@media (max-width: 900px) { + .ms-detail-header { + flex-direction: column; + align-items: stretch; + } + .ms-detail-actions { + width: 100%; + flex-wrap: wrap; + } } - -.thread-msg-user { - border-left-color: var(--success); +@media (max-width: 700px) { + .missions-container { padding: var(--space-4); } + .ms-brief-intro, + .ms-brief-section, + .ms-approach-entry, + .ms-content-block { + padding: 16px; + } + .ms-meta-cell { + padding: 14px 16px; + } } - -.thread-msg-system { - border-left-color: var(--text-secondary); - opacity: 0.7; -} - -.truncate { - max-width: 300px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - diff --git a/crates/ironclaw_gateway/static/styles/surfaces/projects.css b/crates/ironclaw_gateway/static/styles/surfaces/projects.css index f21ebd4f42..5df893ddfb 100644 --- a/crates/ironclaw_gateway/static/styles/surfaces/projects.css +++ b/crates/ironclaw_gateway/static/styles/surfaces/projects.css @@ -11,6 +11,25 @@ overflow: hidden; } +.cr-shell.cr-shell-detail-open { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(420px, 560px); + grid-template-rows: auto minmax(0, 1fr); +} +.cr-shell.cr-shell-detail-open .cr-attention { + grid-column: 1 / -1; +} +.cr-shell.cr-shell-detail-open .cr-drill { + grid-column: 1; + grid-row: 2; + min-width: 0; +} +.cr-shell.cr-shell-detail-open .cr-detail { + grid-column: 2; + grid-row: 2; + min-width: 0; +} + /* Attention bar */ .cr-attention { background: var(--danger-error-bg); @@ -263,22 +282,182 @@ /* Detail panel */ .cr-detail { - flex: 1; + display: none; overflow-y: auto; - padding: 24px 40px 48px; + padding: 24px 24px 32px; + border-left: 1px solid var(--border); + background: linear-gradient(180deg, rgba(255,255,255,0.01), rgba(255,255,255,0)), var(--bg-primary); } .cr-detail-header { display: flex; - align-items: center; + align-items: flex-start; gap: 12px; flex-wrap: wrap; - margin-bottom: 24px; + margin-bottom: 20px; } .cr-detail-header h2 { - font-size: 18px; + font-size: 20px; font-weight: 600; margin: 0; flex: 1; + letter-spacing: -0.02em; +} + +.cr-thread-inspector { + max-width: 720px; +} +.cr-thread-heading { + flex: 1; + min-width: 0; +} +.cr-thread-title { + max-width: 18ch; +} +.cr-thread-subtitle { + margin-top: 6px; + font-size: 12px; + color: var(--text-dimmed); + letter-spacing: 0.02em; +} +.cr-thread-kicker { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-dimmed); + margin-bottom: 8px; +} +.cr-thread-summary { + margin-bottom: 18px; + padding: 16px 18px; + border-radius: 16px; + border: 1px solid var(--border); + background: var(--bg-secondary); +} +.cr-thread-summary p { + margin: 0; + font-size: 13px; + line-height: 1.55; + color: var(--text-secondary); +} +.cr-thread-brief { + margin-bottom: 18px; + padding: 16px 18px; + border-radius: 16px; + border: 1px solid rgba(52, 211, 153, 0.18); + background: linear-gradient(180deg, rgba(52,211,153,0.08), rgba(52,211,153,0)), var(--bg-secondary); +} +.cr-thread-brief-copy { + max-width: 68ch; + font-size: 13px; + line-height: 1.65; + color: var(--text-secondary); +} +.cr-thread-brief-copy > *:first-child { margin-top: 0; } +.cr-thread-brief-copy > *:last-child { margin-bottom: 0; } +.cr-thread-brief-copy p { margin: 0 0 12px; } +.cr-thread-brief-copy ul, +.cr-thread-brief-copy ol { margin: 0 0 12px 18px; padding: 0; } +.cr-thread-brief-copy li { margin-bottom: 6px; } +.cr-thread-meta-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + margin-bottom: 18px; +} +.cr-thread-meta-card { + padding: 12px 14px; + border-radius: 14px; + background: var(--bg-secondary); + border: 1px solid var(--border); +} +.cr-thread-meta-label { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-dimmed); + margin-bottom: 6px; +} +.cr-thread-meta-value { + font-size: 13px; + color: var(--text); + line-height: 1.4; +} +.cr-thread-timeline { + display: flex; + flex-direction: column; + gap: 12px; +} +.cr-thread-message { + padding: 16px 18px; + border-radius: 16px; + border: 1px solid var(--border); + background: var(--bg-secondary); + box-shadow: var(--shadow-sm); +} +.cr-thread-message-assistant { border-left: 3px solid var(--accent); } +.cr-thread-message-user { border-left: 3px solid var(--success); } +.cr-thread-message-system { border-left: 3px solid var(--info); } +.cr-thread-message-role { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-dimmed); + margin-bottom: 10px; +} +.cr-thread-message-body { + max-width: 68ch; + font-size: 13px; + line-height: 1.65; + color: var(--text-secondary); +} +.cr-thread-message-body > *:first-child { margin-top: 0; } +.cr-thread-message-body > *:last-child { margin-bottom: 0; } +.cr-thread-message-body p { + margin: 0 0 12px; +} +.cr-thread-message-body h1, +.cr-thread-message-body h2, +.cr-thread-message-body h3, +.cr-thread-message-body h4 { + margin: 0 0 10px; + color: var(--text); + font-size: 15px; + letter-spacing: -0.02em; +} +.cr-thread-message-body ul, +.cr-thread-message-body ol { + margin: 0 0 12px 18px; + padding: 0; +} +.cr-thread-message-body li { + margin-bottom: 6px; +} +.cr-thread-message-body code { + font-family: var(--font-mono); + font-size: 12px; + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: 6px; + padding: 1px 6px; + color: var(--text); +} +.cr-thread-message-body pre { + margin: 0 0 12px; + padding: 14px 16px; + border-radius: 14px; + background: var(--bg-tertiary); + border: 1px solid var(--border); + overflow-x: auto; +} +.cr-thread-empty { + padding: 18px; + border-radius: 16px; + border: 1px dashed var(--border); + color: var(--text-muted); + font-size: 13px; } /* General (default project) section */ @@ -321,7 +500,25 @@ } /* Responsive */ +@media (max-width: 1100px) { + .cr-shell.cr-shell-detail-open { + grid-template-columns: 1fr; + grid-template-rows: auto minmax(0, 1fr) auto; + } + .cr-shell.cr-shell-detail-open .cr-drill { + grid-row: 2; + } + .cr-shell.cr-shell-detail-open .cr-detail { + grid-column: 1; + grid-row: 3; + border-left: none; + border-top: 1px solid var(--border); + } +} @media (max-width: 700px) { .cr-cards { padding: 16px; } .cr-drill, .cr-detail { padding: 16px; } + .cr-thread-meta-grid { + grid-template-columns: 1fr; + } } diff --git a/docs/drafts/assets/screenshots/pr-2894-missions-dossier.png b/docs/drafts/assets/screenshots/pr-2894-missions-dossier.png new file mode 100644 index 0000000000..53c797d48f Binary files /dev/null and b/docs/drafts/assets/screenshots/pr-2894-missions-dossier.png differ diff --git a/docs/drafts/assets/screenshots/pr-2894-projects-control-room.png b/docs/drafts/assets/screenshots/pr-2894-projects-control-room.png new file mode 100644 index 0000000000..89271a994d Binary files /dev/null and b/docs/drafts/assets/screenshots/pr-2894-projects-control-room.png differ diff --git a/docs/drafts/assets/screenshots/pr-2894-projects-thread-inspector.png b/docs/drafts/assets/screenshots/pr-2894-projects-thread-inspector.png new file mode 100644 index 0000000000..51a55eb69e Binary files /dev/null and b/docs/drafts/assets/screenshots/pr-2894-projects-thread-inspector.png differ diff --git a/docs/superpowers/specs/2026-04-23-projects-tab-control-room-design.md b/docs/superpowers/specs/2026-04-23-projects-tab-control-room-design.md new file mode 100644 index 0000000000..d9a34facbc --- /dev/null +++ b/docs/superpowers/specs/2026-04-23-projects-tab-control-room-design.md @@ -0,0 +1,234 @@ +# Projects tab control-room redesign + +Date: 2026-04-23 +Branch: `feat/missions-ui-redesign` +Status: Approved design, pending implementation + +## Goal + +Bring the Projects tab up to the same product quality as the redesigned Missions tab without duplicating the Missions detail experience. + +The current Projects drill-in mixes two different roles: +- a useful control-room overview for project missions and recent activity +- a legacy detail renderer that expands raw markdown / large thread dumps at the bottom of the page + +That second role is the main problem. It creates a jarring quality drop compared with the dossier-style Missions tab. + +## Approved direction + +Use **Option 2**: +- Clicking a **mission card** in Projects should **switch to the Missions tab** and open the matching mission in the canonical Missions detail view. +- Clicking a **Recent Activity** item should **stay in Projects** and open a **polished thread detail** surface within Projects. +- The old Projects-specific mission-detail renderer and raw bottom-of-page dump behavior should be removed. + +## Product model + +After this change, each surface has a clear responsibility: + +### Projects tab +The Projects tab is the **control room**. +It should help the user: +- scan project health +- see what missions exist in a project +- inspect recent execution activity +- triage threads quickly without leaving project context unless they explicitly open a mission + +### Missions tab +The Missions tab is the **canonical mission workspace**. +It owns: +- mission prompt / brief rendering +- mission metadata +- mission progress +- approach history +- mission thread drill-in +- mission actions + +This avoids maintaining two competing mission-detail UIs. + +## Problems in the current Projects tab + +### 1. Mission clicks use the wrong detail model +Projects currently opens a custom mission detail panel in `cr-detail` using older rendering patterns (`renderMissionDetailInCr`). +This causes a lower-quality experience than the main Missions tab and creates duplicated maintenance. + +### 2. Recent Activity opens oversized raw detail at the bottom +Recent activity rows currently expand a legacy thread detail renderer in the bottom detail panel. +The output is technically accurate but visually uncurated: +- full-width markdown walls +- very large raw prompt bodies +- low hierarchy between summary and detail +- message dumps without enough visual grouping + +### 3. Drill-in layout loses focus when detail opens +The current drill-in becomes a stacked page where the detail appears below the missions and activity lists. +This makes detail inspection feel like an accidental page append instead of a deliberate inspection state. + +## UX requirements + +### Mission cards in Projects +When the user clicks a mission card in the project drill-in: +1. Switch to the `missions` tab +2. Load/open the exact mission by id +3. Show the mission in the canonical dossier-style mission detail view +4. Preserve the expected back behavior inside Missions + +This interaction should feel like “open this mission in its real workspace”. + +### Recent Activity in Projects +When the user clicks an activity row in the project drill-in: +1. Stay in the Projects tab +2. Open a polished thread detail view for that thread +3. Keep the project context visible and understandable +4. Make it easy to close the detail and return to the project drill-in + +This interaction should feel like “inspect this execution without leaving the control room”. + +## Intended interaction design + +### Projects drill-in default state +The project drill-in should show: +- project header +- project widget area (existing) +- mission list column +- recent activity column +- no expanded raw detail dump by default + +### Projects drill-in thread inspection state +When a recent activity item is opened: +- the thread detail should appear as a **designed inspection surface**, not as a raw appended dump +- the surrounding drill-in should still feel like the current context +- the detail must have strong hierarchy and constrained reading width + +Implementation may use one of these layouts: +- replace the lower detail region with a polished inspector panel, or +- show the detail as a side/contained panel within the drill-in + +For this implementation, the important requirement is not the exact geometry but the behavior: +- it must feel intentional +- it must not look like a markdown dump appended to the page +- it must not compete with the Missions tab’s role + +## Thread detail design requirements + +The Projects thread detail should be optimized for inspection, not archival completeness. + +### Required sections + +#### Header +- back action to return to the project drill-in +- thread goal/title +- state badge +- optional relationship hint if tied to a mission + +#### Meta strip +Compact metadata cards or chips for: +- thread type +- steps +- tokens +- cost +- created time +- completed time + +#### Summary block +A lightweight summary or lead section should appear before message content when possible. +If no derived summary exists, use the goal/title and metadata to establish orientation. + +#### Message timeline +Messages should render as a readable timeline/log, not a wall: +- role labels remain visible +- assistant / user / system visually differentiated +- large prompt bodies constrained in width +- markdown typography improved +- code blocks and inline code styled clearly +- long content broken into digestible sections with spacing + +### Content handling rules +- Keep full fidelity of message content, but improve presentation. +- Do not expose raw JSON-looking blobs unless they are genuinely the content. +- Long markdown bodies should use readable containers with max width and vertical rhythm. +- The visual emphasis should be on understanding the thread, not dumping every token equally. + +## Architecture / implementation plan shape + +### Behavior changes + +### Remove Projects mission detail renderer as a destination +`renderMissionDetailInCr()` should no longer be the default path for mission-card clicks in Projects. +Mission-card clicks should route to the Missions tab and reuse the existing mission-detail behavior. + +### Keep Projects-specific thread detail renderer +`crOpenEngineThread()` should remain a Projects-owned interaction, but its rendering should be redesigned. +It should become a control-room thread inspector rather than a raw bottom dump. + +### Navigation changes +A new helper should exist for “open mission in Missions tab by id” from non-Missions surfaces. +That helper should: +- switch tabs +- ensure Missions state is loaded +- open the matching mission detail + +The control-room should call that helper instead of using its own mission renderer. + +### Visual/style changes +Projects-specific styles should be added or updated so the thread detail feels aligned with the new Missions quality bar: +- clearer container hierarchy +- stronger section titles +- better spacing +- narrower prose width for long content +- better timeline/message treatment +- improved empty/loading/back states + +## Out of scope +- Reworking the top-level project overview cards +- Changing the project widget system +- Redesigning the Missions tab again +- Changing engine API payload shape unless implementation reveals a true blocker +- Moving Recent Activity out of Projects into another tab + +## Acceptance criteria + +### Mission navigation +- From Projects drill-in, clicking a mission opens the same mission in the Missions tab +- The Projects tab no longer renders a separate mission detail surface for that click path + +### Thread inspection quality +- From Projects drill-in, clicking a recent activity row opens a polished thread detail in Projects +- The detail no longer reads like a raw bottom-of-page dump +- Long markdown / prompt content is readable and visually structured + +### Role clarity +- Projects clearly feels like a control room +- Missions clearly feels like the canonical mission workspace +- There is no duplicated mission-detail experience with conflicting quality levels + +## Testing / verification expectations + +Implementation should verify at least: +- mission click from Projects routes to Missions and opens the intended mission +- recent activity click stays in Projects and opens thread detail +- back action from thread detail returns cleanly to the project drill-in +- no broken hash/navigation behavior +- JS syntax check passes +- diff is whitespace-clean + +## Files likely involved + +Primary: +- `crates/ironclaw_gateway/static/js/surfaces/projects.js` +- `crates/ironclaw_gateway/static/styles/surfaces/projects.css` (if present) +- `crates/ironclaw_gateway/static/styles/surfaces/missions.css` only if shared patterns are intentionally reused +- `crates/ironclaw_gateway/static/index.html` if layout hooks need adjustment + +Possible supporting files: +- routing/navigation helpers under `crates/ironclaw_gateway/static/js/core/` +- shared markdown rendering styles if thread-inspector typography should be reused elsewhere + +## Implementation recommendation + +Implement this as a focused Projects-surface cleanup: +1. add mission deep-link helper to Missions +2. replace Projects mission click behavior +3. redesign Projects thread detail +4. verify navigation and readability + +That keeps the diff scoped and aligned with the approved product direction. diff --git a/tests/e2e/helpers.py b/tests/e2e/helpers.py index c76643fbcb..eb75820720 100644 --- a/tests/e2e/helpers.py +++ b/tests/e2e/helpers.py @@ -181,6 +181,25 @@ SEL = { "activity_thinking_text": ".activity-thinking-text", # Thread processing indicator "thread_processing": ".thread-processing", + # Projects control-room + "projects_cards": "#cr-cards", + "projects_card": ".cr-card", + "projects_card_by_id": '.cr-card[data-id="{id}"]', + "projects_drill": "#cr-drill", + "projects_drill_name": ".cr-drill-name", + "projects_detail": "#cr-detail", + "projects_mission_card": ".cr-mission-card", + "projects_activity_row": ".cr-activity-row", + "projects_activity_row_by_id": '.cr-activity-row[data-id="{id}"]', + "projects_thread_title": ".cr-thread-title", + "projects_thread_subtitle": ".cr-thread-subtitle", + "projects_thread_brief": ".cr-thread-brief", + "projects_thread_meta": ".cr-thread-meta-grid", + "projects_thread_timeline": ".cr-thread-timeline", + "projects_thread_message": ".cr-thread-message", + # Canonical Missions detail surface + "missions_detail": "#mission-detail", + "missions_detail_title": ".ms-detail-title", } TABS = ["chat", "memory", "jobs", "routines", "settings"] diff --git a/tests/e2e/scenarios/test_project_detail.py b/tests/e2e/scenarios/test_project_detail.py index 428655469a..a6c782c328 100644 --- a/tests/e2e/scenarios/test_project_detail.py +++ b/tests/e2e/scenarios/test_project_detail.py @@ -1,16 +1,30 @@ -"""Screenshot test for the project detail (drill-in) page. +"""Tests for the project detail (drill-in) page. Seeds mock data via page.route() API interception, navigates to the -projects tab, drills into a project, and captures a screenshot for -PR documentation. +projects tab, drills into a project, and asserts control-room behavior. """ import json +from helpers import SEL +from playwright.async_api import expect -# ── Mock data ─────────────────────────────────────────────────── MOCK_PROJECT_ID = "068f67da-49b6-4f6c-9463-8d243c2cff6c" +FIRST_MISSION_ID = "m-001" +FIRST_MISSION_NAME = "Daily AI Paper Monitoring" +THREAD_DETAIL_ID = "t-002" +THREAD_DETAIL_TITLE = "Daily Work Digest" +THREAD_DETAIL_GOAL = "Analyze weekly research trends" +MISSION_RUN_GOAL = ( + "# Mission: Daily Work Digest Goal: Create and send a daily digest that reviews " + "my Google Calendar, Gmail, Notion, and GitHub to identify what I need to do that day. " + "Each run should: 1) look at today's Google Calendar events and summarize schedule and likely priorities; " + "2) review Gmail for recent unread or important messages that imply actions, deadlines, or follow-ups; " + "3) review Notion for tasks, meeting notes, pages, or items relevant to today, including due/urgent/open work when available; " + "4) review GitHub for my open PRs, issues assigned to me, and PRs requesting my review; " + "5) synthesize everything into one concise actionable morning briefing; 6) send the digest back to me in this conversation channel." +) MOCK_OVERVIEW = { "projects": [ @@ -69,8 +83,8 @@ MOCK_OVERVIEW = { MOCK_MISSIONS = { "missions": [ { - "id": "m-001", - "name": "Daily AI Paper Monitoring", + "id": FIRST_MISSION_ID, + "name": FIRST_MISSION_NAME, "status": "Active", "cadence_type": "daily", "cadence_description": "Every day at 9:00 AM", @@ -147,70 +161,145 @@ MOCK_THREADS = { ], } +MOCK_MISSION_DETAIL = { + "mission": { + "id": FIRST_MISSION_ID, + "name": FIRST_MISSION_NAME, + "status": "Active", + "goal": "# Input\n- `query` — papers from the last 24h\n\n# Investigation Process\n1. Fetch papers\n2. Rank them\n3. Summarize notable work", + "cadence_type": "daily", + "cadence_description": "Every day at 9:00 AM", + "thread_count": 42, + "threads_today": 2, + "max_threads_per_day": 3, + "created_at": "2026-04-12T08:45:00Z", + "next_fire_at": "2026-04-13T09:00:00Z", + "current_focus": "Tighten filtering for papers with real-world impact.", + "success_criteria": "Return a concise digest with 3-5 papers and clear takeaways.", + "approach_history": [ + "Expected: produce a daily digest\nObserved: arXiv query is still broad\nFix applied: narrow to ai + cs.LG\nNext focus: improve ranking" + ], + "threads": [], + } +} -# ── Test ──────────────────────────────────────────────────────── +MOCK_THREAD_DETAIL = { + "thread": { + "id": THREAD_DETAIL_ID, + "goal": MISSION_RUN_GOAL, + "title": "", + "state": "Done", + "thread_type": "mission_run", + "step_count": 6, + "total_tokens": 18234, + "total_cost_usd": 0.42, + "created_at": "2026-04-07T10:00:00Z", + "completed_at": "2026-04-07T10:45:00Z", + "messages": [ + {"role": "System", "content": "# Mission\nInvestigate weekly research themes."}, + {"role": "Assistant", "content": "## Findings\n- Agentic workflows are trending\n- Benchmarks remain noisy"}, + ], + } +} -async def test_project_detail_screenshot(page): +def _json_route(body): + async def handler(route): + await route.fulfill( + status=200, + content_type="application/json", + body=json.dumps(body), + ) + + return handler + + +async def _open_project_detail(page): + await page.evaluate( + "() => {" + " if (window.bootstrap) window.bootstrap.engineV2Enabled = true;" + " engineV2Enabled = true;" + " applyEngineModeToTabs();" + "}" + ) + await page.locator(SEL["tab_button"].format(tab="projects")).click() + await page.locator(SEL["projects_cards"]).wait_for(state="visible", timeout=5000) + await page.locator(SEL["projects_card"]).first.wait_for(state="visible", timeout=5000) + await page.locator(SEL["projects_card_by_id"].format(id=MOCK_PROJECT_ID)).click() + await page.locator(SEL["projects_drill"]).wait_for(state="visible", timeout=5000) + await page.locator(SEL["projects_drill_name"]).wait_for(state="visible", timeout=5000) + + +async def _route_project_detail_fixtures(page): + """Register all mock API routes needed for project detail tests.""" + + await page.route("**/api/engine/projects/overview", _json_route(MOCK_OVERVIEW)) + # Register specific detail routes before generic list routes because Playwright + # resolves overlapping routes in registration order. + await page.route( + f"**/api/engine/missions/{FIRST_MISSION_ID}", + _json_route(MOCK_MISSION_DETAIL), + ) + await page.route("**/api/engine/missions*", _json_route(MOCK_MISSIONS)) + await page.route( + f"**/api/engine/threads/{THREAD_DETAIL_ID}", + _json_route(MOCK_THREAD_DETAIL), + ) + await page.route("**/api/engine/threads*", _json_route(MOCK_THREADS)) + await page.route("**/api/engine/projects/*/widgets", _json_route([])) + + +async def test_project_detail_screenshot(page, tmp_path): """Navigate to projects tab, drill into a project, capture screenshot.""" - # Intercept API calls to return mock data. - async def handle_overview(route): - await route.fulfill( - status=200, - content_type="application/json", - body=json.dumps(MOCK_OVERVIEW), - ) + await _route_project_detail_fixtures(page) + await _open_project_detail(page) - async def handle_missions(route): - await route.fulfill( - status=200, - content_type="application/json", - body=json.dumps(MOCK_MISSIONS), - ) + await expect(page.locator(SEL["projects_drill_name"])).to_have_text( + "AI Research Intelligence" + ) + await expect(page.locator(SEL["projects_mission_card"]).first).to_be_visible() + await expect(page.locator(SEL["projects_activity_row"]).first).to_be_visible() - async def handle_threads(route): - await route.fulfill( - status=200, - content_type="application/json", - body=json.dumps(MOCK_THREADS), - ) + await page.screenshot(path=str(tmp_path / "project-detail.png")) - async def handle_widgets(route): - await route.fulfill( - status=200, - content_type="application/json", - body=json.dumps([]), - ) - await page.route("**/api/engine/projects/overview", handle_overview) - await page.route("**/api/engine/missions*", handle_missions) - await page.route("**/api/engine/threads*", handle_threads) - await page.route("**/api/engine/projects/*/widgets", handle_widgets) +async def test_project_mission_card_opens_canonical_missions_view(page): + """Mission card in Projects should switch to the Missions tab and open the mission dossier.""" + await _route_project_detail_fixtures(page) + await _open_project_detail(page) + await page.locator(SEL["projects_mission_card"]).first.click() - # Enable engine v2 mode so the Projects tab is visible. - await page.evaluate("engineV2Enabled = true; applyEngineModeToTabs();") - - # Click the Projects tab. - await page.locator('.tab-bar button[data-tab="projects"]').click() - await page.locator("#cr-cards").wait_for(state="visible", timeout=5000) - - # Wait for project cards to render. - await page.locator(".cr-card").first.wait_for(state="visible", timeout=5000) - - # Drill into the AI Research Intelligence project. - await page.locator( - f'.cr-card[data-id="{MOCK_PROJECT_ID}"]' - ).click() - - # Wait for drill-in view to render. - await page.locator("#cr-drill").wait_for(state="visible", timeout=5000) - await page.locator(".cr-drill-name").wait_for(state="visible", timeout=5000) - - # Wait for missions to render. - await page.locator(".cr-mission-card").first.wait_for( - state="visible", timeout=5000 + await expect(page.locator(SEL["tab_button"].format(tab="missions"))).to_have_attribute( + "aria-selected", "true" + ) + await expect(page.locator(SEL["tab_panel"].format(tab="projects"))).not_to_be_visible() + await expect(page.locator(SEL["tab_panel"].format(tab="missions"))).to_be_visible() + await expect(page.locator(SEL["missions_detail"])).to_be_visible() + await expect(page.locator(SEL["missions_detail_title"])).to_have_text( + FIRST_MISSION_NAME ) - # Take the screenshot. - await page.screenshot(path="project-detail.png") + +async def test_project_activity_row_opens_polished_thread_inspector(page): + """Activity row in Projects should open the thread inspector inside Projects.""" + await _route_project_detail_fixtures(page) + await _open_project_detail(page) + await page.locator(SEL["projects_activity_row_by_id"].format(id=THREAD_DETAIL_ID)).click() + + await expect(page.locator(SEL["tab_button"].format(tab="projects"))).to_have_attribute( + "aria-selected", "true" + ) + await expect(page.locator(SEL["projects_detail"])).to_be_visible() + await expect(page.locator(SEL["projects_thread_title"])).to_have_text( + THREAD_DETAIL_TITLE + ) + await expect(page.locator(SEL["projects_thread_subtitle"])).to_have_text( + "Mission run" + ) + await expect(page.locator(SEL["projects_thread_brief"])).to_contain_text( + "Create and send a daily digest" + ) + await expect(page.locator(SEL["projects_thread_meta"])).to_be_visible() + await expect(page.locator(SEL["projects_thread_timeline"])).to_be_visible() + await expect(page.locator(SEL["projects_thread_message"])).to_have_count(2)