feat(missions): redesign missions overview surface (#2894)

This commit is contained in:
firat.sertgoz
2026-04-24 04:24:42 +03:00
committed by GitHub
parent 49f3e8d566
commit 2d4b35daa9
12 changed files with 2032 additions and 274 deletions

View File

@@ -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',

View File

@@ -366,20 +366,10 @@
<div class="tab-panel" id="tab-missions">
<div class="missions-container">
<div class="missions-summary" id="missions-summary"></div>
<table class="missions-table" id="missions-table">
<thead>
<tr>
<th data-i18n="missions.name">Name</th>
<th data-i18n="missions.goal">Goal</th>
<th data-i18n="missions.cadence">Cadence</th>
<th data-i18n="missions.threads">Threads</th>
<th data-i18n="missions.status">Status</th>
<th data-i18n="missions.progress">Progress</th>
<th data-i18n="missions.actions">Actions</th>
</tr>
</thead>
<tbody id="missions-tbody"></tbody>
</table>
<div class="missions-body" id="missions-body">
<div class="missions-list-col" id="missions-list-col"></div>
<div class="missions-activity-col" id="missions-activity-col"></div>
</div>
<div class="empty-state" id="missions-empty" style="display:none">
<span data-i18n="missions.noConfigured">No missions found. Ask the assistant to create one.</span>
</div>

View File

@@ -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') {

View File

@@ -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 += '<button class="cr-mission-card" data-action="open-mission" data-id="' + escapeHtml(m.id) + '">'
html += '<button class="cr-mission-card" data-action="open-project-mission" data-id="' + escapeHtml(m.id) + '">'
+ '<div class="cr-mc-head">'
+ '<span class="cr-mc-name">' + escapeHtml(m.name) + '</span>'
+ '<span class="badge ' + statusClass + '">' + escapeHtml(m.status) + '</span></div>'
@@ -175,6 +195,52 @@ function renderCrDrillMissions(missions) {
el.innerHTML = html;
}
function parseMissionRunGoal(goal) {
var text = String(goal || '').trim();
if (!text) return null;
var markdownMatch = text.match(/^#\s*Mission:\s*(.+?)\s+Goal:\s*([\s\S]+)$/i);
if (markdownMatch) {
return {
missionName: markdownMatch[1].trim(),
missionBrief: markdownMatch[2].trim(),
};
}
var plainMatch = text.match(/^Mission:\s*(.+?)\s+Goal:\s*([\s\S]+)$/i);
if (plainMatch) {
return {
missionName: plainMatch[1].trim(),
missionBrief: plainMatch[2].trim(),
};
}
return null;
}
function getCrThreadPresentation(t) {
var parsedMission = parseMissionRunGoal(t && t.goal);
var title = '';
var subtitle = '';
var brief = '';
if (parsedMission) {
title = parsedMission.missionName;
subtitle = 'Mission run';
brief = parsedMission.missionBrief;
} else {
title = (t && t.title) || (t && t.goal) || ('Thread ' + ((t && t.id) || '').slice(0, 8));
subtitle = (t && t.thread_type) ? t.thread_type.replace(/_/g, ' ') : 'Thread';
brief = (t && t.title && t.goal && t.title !== t.goal) ? t.goal : '';
}
return {
title: title,
subtitle: subtitle,
brief: brief,
};
}
function renderCrDrillActivity(threads, missions) {
var el = document.getElementById('cr-drill-activity');
if (!el) return;
@@ -192,7 +258,8 @@ function renderCrDrillActivity(threads, missions) {
var stateClass = (t.state === 'Done' || t.state === 'Completed') ? 'completed'
: t.state === 'Failed' ? 'failed'
: t.state === 'Running' ? 'in_progress' : 'pending';
var label = t.title || t.goal || ('Thread ' + (t.id || '').slice(0, 8));
var presentation = getCrThreadPresentation(t);
var label = presentation.title;
var time = formatRelativeTime(t.updated_at);
html += '<button class="cr-activity-row" data-action="open-engine-thread" data-id="' + escapeHtml(t.id) + '">'
+ '<span class="badge ' + stateClass + '">' + escapeHtml(t.state) + '</span>'
@@ -205,8 +272,47 @@ function renderCrDrillActivity(threads, missions) {
function crShowDetail(html) {
var detail = document.getElementById('cr-detail');
detail.style.display = '';
detail.innerHTML = html;
setCrDetailOpen(true);
}
function crThreadMetaItem(label, value) {
return '<div class="cr-thread-meta-card">'
+ '<div class="cr-thread-meta-label">' + escapeHtml(label) + '</div>'
+ '<div class="cr-thread-meta-value">' + escapeHtml(value || '\u2014') + '</div>'
+ '</div>';
}
function renderCrThreadSummary(t, presentation) {
var parts = [];
if (presentation.subtitle) {
parts.push('Context: ' + presentation.subtitle + '.');
}
if (t.title && t.goal && t.title !== t.goal) {
parts.push('Run label: ' + t.title + '.');
}
if (t.step_count > 0) {
parts.push(t.step_count + ' steps recorded.');
}
if (t.completed_at) {
parts.push('Completed ' + formatRelativeTime(t.completed_at) + '.');
} else {
parts.push('Still active in the control room.');
}
return '<section class="cr-thread-summary">'
+ '<div class="cr-thread-kicker">Thread Summary</div>'
+ '<p>' + escapeHtml(parts.join(' ')) + '</p>'
+ '</section>';
}
function renderCrThreadMessage(msg) {
var role = msg && msg.role ? msg.role : 'System';
var roleClass = role === 'Assistant' ? 'assistant'
: role === 'User' ? 'user' : 'system';
return '<article class="cr-thread-message cr-thread-message-' + roleClass + '">'
+ '<div class="cr-thread-message-role">' + escapeHtml(role) + '</div>'
+ '<div class="cr-thread-message-body">' + renderMarkdown((msg && msg.content) || '') + '</div>'
+ '</article>';
}
// CR-specific mission detail: renders into the control-room cr-detail panel.
@@ -276,27 +382,44 @@ function crOpenEngineThread(threadId) {
var t = data.thread;
var stateClass = (t.state === 'Done' || t.state === 'Completed') ? 'completed'
: t.state === 'Failed' ? 'failed' : t.state === 'Running' ? 'in_progress' : 'pending';
var html = '<div class="cr-detail-header">'
+ '<button class="cr-back" data-action="cr-close-detail">&larr; Back</button>'
+ '<h2>Thread: ' + escapeHtml(t.goal) + '</h2>'
+ '<span class="badge ' + stateClass + '">' + escapeHtml(t.state) + '</span></div>';
html += '<div class="job-meta-grid">'
+ 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')
+ '</div>';
if (t.messages && t.messages.length) {
html += '<div class="job-description"><h3>Messages (' + t.messages.length + ')</h3>';
t.messages.forEach(function(msg) {
var roleClass = msg.role === 'Assistant' ? 'assistant' : msg.role === 'User' ? 'user' : 'system';
html += '<div class="thread-message thread-msg-' + roleClass + '">'
+ '<div class="thread-msg-role">' + escapeHtml(msg.role) + '</div>'
+ '<div class="thread-msg-content">' + renderMarkdown(msg.content) + '</div></div>';
});
html += '</div>';
var presentation = getCrThreadPresentation(t);
var html = '<div class="cr-thread-inspector">'
+ '<div class="cr-detail-header">'
+ '<button class="cr-back" data-action="cr-close-detail">&larr; Back to project</button>'
+ '<div class="cr-thread-heading">'
+ '<h2 class="cr-thread-title">' + escapeHtml(presentation.title) + '</h2>'
+ (presentation.subtitle ? '<div class="cr-thread-subtitle">' + escapeHtml(presentation.subtitle) + '</div>' : '')
+ '</div>'
+ '<span class="badge ' + stateClass + '">' + escapeHtml(t.state) + '</span>'
+ '</div>'
+ renderCrThreadSummary(t, presentation);
if (presentation.brief) {
html += '<section class="cr-thread-brief">'
+ '<div class="cr-thread-kicker">Mission brief</div>'
+ '<div class="cr-thread-brief-copy">' + renderMarkdown(presentation.brief) + '</div>'
+ '</section>';
}
html += '<div class="cr-thread-meta-grid">'
+ 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')
+ '</div>'
+ '<div class="cr-thread-timeline">';
if (t.messages && t.messages.length) {
t.messages.forEach(function(msg) {
html += renderCrThreadMessage(msg);
});
} else {
html += '<div class="cr-thread-empty">No messages captured for this thread yet.</div>';
}
html += '</div></div>';
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 =
'<div class="ms-summary-card"><span class="ms-summary-label">' + escapeHtml(I18n.t('missions.summary.total')) + '</span><span class="ms-summary-value">' + s.total + '</span></div>'
+ '<div class="ms-summary-card"><span class="ms-summary-label">' + escapeHtml(I18n.t('missions.summary.active')) + '</span><span class="ms-summary-value green">' + s.active + '</span></div>'
+ '<div class="ms-summary-card"><span class="ms-summary-label">' + escapeHtml(I18n.t('missions.summary.paused')) + '</span><span class="ms-summary-value amber">' + s.paused + '</span></div>'
+ '<div class="ms-summary-card"><span class="ms-summary-label">' + escapeHtml(I18n.t('missions.summary.completed')) + '</span><span class="ms-summary-value blue">' + s.completed + '</span></div>'
+ '<div class="ms-summary-card"><span class="ms-summary-label">' + escapeHtml(I18n.t('missions.summary.failed')) + '</span><span class="ms-summary-value red">' + s.failed + '</span></div>';
}
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 '<tr class="mission-row" data-action="open-mission" data-id="' + escapeHtml(m.id) + '">'
+ '<td>' + escapeHtml(m.name) + '</td>'
+ '<td class="truncate">' + escapeHtml(m.goal) + '</td>'
+ '<td>' + escapeHtml(m.cadence_description || m.cadence_type) + '</td>'
+ '<td>' + m.thread_count + '</td>'
+ '<td><span class="badge ' + statusClass + '">' + escapeHtml(m.status) + '</span></td>'
+ '<td>' + renderMissionProgressCell(m.id) + '</td>'
+ '<td>'
+ (m.status === 'Active' ? '<button class="btn-cancel" data-action="pause-mission" data-id="' + escapeHtml(m.id) + '">' + escapeHtml(I18n.t('missions.pause')) + '</button> ' : '')
+ (m.status === 'Paused' ? '<button class="btn-restart" data-action="resume-mission" data-id="' + escapeHtml(m.id) + '">' + escapeHtml(I18n.t('missions.resume')) + '</button> ' : '')
+ '<button class="btn-restart" data-action="fire-mission" data-id="' + escapeHtml(m.id) + '">' + escapeHtml(I18n.t('missions.fire')) + '</button>'
+ '</td>'
+ '</tr>';
}).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 += '<div class="ms-section-title">' + escapeHtml(labels[status]) + '</div>';
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
? '<span class="ms-live-tag"><span class="ms-live-dot"></span> Running</span>'
: '';
html += '<div class="ms-card" data-action="open-mission" data-id="' + escapeHtml(m.id) + '">'
+ '<div class="ms-card-body">'
+ '<div class="ms-card-head">'
+ '<span class="ms-card-name">' + escapeHtml(m.name) + '</span>'
+ '<span class="badge ' + badgeClass + '">' + escapeHtml(m.status) + '</span>'
+ '</div>'
+ '<div class="ms-card-goal">' + escapeHtml(m.goal) + '</div>'
+ '<div class="ms-card-meta">'
+ '<span>' + escapeHtml(m.cadence_description || m.cadence_type || 'manual') + '</span>'
+ '<span>' + m.thread_count + ' threads</span>'
+ '</div>'
+ '</div>'
+ '<div class="ms-card-right">'
+ liveHtml
+ '<div><div class="ms-card-threads-num">' + m.thread_count + '</div>'
+ '<div class="ms-card-threads-label">threads</div></div>'
+ '</div>'
+ '</div>';
});
});
col.innerHTML = html;
}
function renderMissionsActivity(threads) {
var col = document.getElementById('missions-activity-col');
if (!col) return;
if (!threads || !threads.length) {
col.innerHTML = '<div class="ms-section-title">' + escapeHtml(I18n.t('missions.recentActivity')) + '</div>'
+ '<div style="color:var(--text-dimmed);font-size:var(--text-sm);padding:12px 14px;">No recent activity.</div>';
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 = '<div class="ms-section-title">' + escapeHtml(I18n.t('missions.recentActivity')) + '</div>';
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 += '<div class="ms-day-divider">' + escapeHtml(dayLabel) + '</div>';
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 += '<div class="ms-act-row" data-action="open-engine-thread" data-id="' + escapeHtml(t.id) + '">'
+ '<div class="ms-act-dot ' + dotClass + '"></div>'
+ '<div class="ms-act-content">'
+ '<div class="ms-act-label">' + escapeHtml(label) + '</div>'
+ (t.state === 'Running' ? '<div class="ms-act-sub">In progress</div>' : '')
+ (durationStr || costStr ? '<div class="ms-act-metrics">'
+ (durationStr ? '<span>' + escapeHtml(durationStr) + '</span>' : '')
+ (costStr ? '<span>' + escapeHtml(costStr) + '</span>' : '')
+ '</div>' : '')
+ '</div>'
+ '<span class="ms-act-time">' + escapeHtml(formatRelativeTime(t.updated_at || t.created_at)) + '</span>'
+ '</div>';
});
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 '<div class="' + classes + '">' + renderMarkdown(text || '') + '</div>';
}
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 = '<section class="ms-brief-section ms-brief-section--' + kind + '">'
+ '<div class="ms-brief-section-head">'
+ '<h3 class="ms-brief-section-title">' + escapeHtml(section.title) + '</h3>'
+ '</div>';
if (kind === 'inputs') {
var defs = parseMissionDefinitions(section.body);
if (defs.note) {
html += '<div class="ms-brief-note">' + renderMissionRichBlock(defs.note, 'ms-brief-note-copy') + '</div>';
}
if (defs.items.length > 0) {
html += '<div class="ms-schema-list">';
defs.items.forEach(function(item) {
html += '<div class="ms-schema-item">'
+ '<div class="ms-schema-key">' + escapeHtml(item.key) + '</div>'
+ '<div class="ms-schema-text">' + escapeHtml(item.text) + '</div>'
+ '</div>';
});
html += '</div>';
}
if (defs.raw) html += renderMissionRichBlock(defs.raw, 'ms-brief-copy');
} else if (kind === 'process') {
var steps = parseMissionListItems(section.body);
if (steps.length > 0) {
html += '<div class="ms-step-list">';
steps.forEach(function(step, index) {
html += '<div class="ms-step-item">'
+ '<div class="ms-step-index">' + (index + 1) + '</div>'
+ '<div class="ms-step-copy">' + escapeHtml(step) + '</div>'
+ '</div>';
});
html += '</div>';
} else {
html += renderMissionRichBlock(section.body, 'ms-brief-copy');
}
} else if (kind === 'rules') {
var rules = parseMissionListItems(section.body);
if (rules.length > 0) {
html += '<div class="ms-callout-list">';
rules.forEach(function(rule) {
html += '<div class="ms-callout-item">'
+ '<div class="ms-callout-icon">!</div>'
+ '<div class="ms-callout-copy">' + escapeHtml(rule) + '</div>'
+ '</div>';
});
html += '</div>';
} else {
html += renderMissionRichBlock(section.body, 'ms-brief-copy');
}
} else if (kind === 'classification') {
var categories = parseMissionDefinitions(section.body);
if (categories.items.length > 0) {
html += '<div class="ms-category-list">';
categories.items.forEach(function(item) {
html += '<div class="ms-category-item">'
+ '<div class="ms-category-key">' + escapeHtml(item.key) + '</div>'
+ '<div class="ms-category-text">' + escapeHtml(item.text) + '</div>'
+ '</div>';
});
html += '</div>';
}
if (categories.raw) html += renderMissionRichBlock(categories.raw, 'ms-brief-copy');
} else {
html += renderMissionRichBlock(section.body, 'ms-brief-copy');
}
html += '</section>';
return html;
}
function renderMissionBrief(text) {
var parsed = splitMissionDocument(text);
if (!parsed.sections.length) {
return '<div class="ms-brief ms-brief-fallback">' + renderMissionRichBlock(text, 'ms-brief-copy') + '</div>';
}
var html = '<div class="ms-brief">';
if (parsed.intro) {
html += '<section class="ms-brief-intro">'
+ '<div class="ms-brief-kicker">' + escapeHtml(I18n.t('missions.missionBrief')) + '</div>'
+ renderMissionRichBlock(parsed.intro, 'ms-brief-intro-copy')
+ '</section>';
}
parsed.sections.forEach(function(section) {
html += renderMissionBriefSection(section);
});
html += '</div>';
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 '<div class="' + classes + '">'
+ '<div class="ms-approach-label">' + escapeHtml(label) + '</div>'
+ renderMissionRichBlock(value, 'ms-approach-value')
+ '</div>';
}
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 = '<article class="ms-approach-entry' + (isLatest ? ' latest' : '') + '">'
+ '<div class="ms-approach-head">'
+ '<div class="ms-approach-run"><span>' + escapeHtml(I18n.t('missions.runLabel', { number: index + 1 })) + '</span></div>'
+ '<div class="ms-approach-badges">';
if (classification) {
html += '<span class="ms-approach-chip classification">' + escapeHtml(classification) + '</span>';
}
if (isLatest) {
html += '<span class="ms-approach-chip latest">' + escapeHtml(I18n.t('missions.latestRun')) + '</span>';
}
if (outcome) {
html += '<span class="ms-approach-chip ' + (achieved ? 'success' : 'open') + '">' + escapeHtml(achieved ? I18n.t('missions.goalAchieved') : I18n.t('missions.openLoop')) + '</span>';
}
html += '</div></div>';
if (parsed.lead) {
html += '<div class="ms-approach-summary">' + renderMissionRichBlock(parsed.lead, 'ms-approach-summary-copy') + '</div>';
}
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 += '<div class="ms-approach-grid">' + fieldsHtml + '</div>';
} else {
html += '<div class="ms-approach-body">' + renderMarkdown(entryText) + '</div>';
}
html += '</article>';
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 = '<div class="job-detail-header">'
+ '<button class="btn-back" data-action="close-mission-detail">' + escapeHtml(I18n.t('common.back')) + '</button>'
+ '<h2>' + escapeHtml(m.name) + '</h2>'
+ '<span class="badge ' + statusClass + '">' + escapeHtml(m.status) + '</span>'
var html = '<button class="ms-detail-back" data-action="close-mission-detail">&larr; ' + escapeHtml(I18n.t('common.back')) + '</button>';
html += '<div class="ms-detail-header">'
+ '<div class="ms-detail-header-left">'
+ '<div class="ms-detail-title-row">'
+ '<span class="ms-detail-title">' + escapeHtml(m.name) + '</span>'
+ '<span class="badge ' + badgeClass + '">' + escapeHtml(m.status) + '</span>'
+ (progress ? '<span class="ms-live-tag"><span class="ms-live-dot"></span> Running</span>' : '')
+ '</div>'
+ '</div>'
+ '<div class="ms-detail-actions">';
if (m.status === 'Active') {
html += '<button class="ms-btn primary" data-action="fire-mission" data-id="' + escapeHtml(m.id) + '">' + escapeHtml(I18n.t('missions.fireNow')) + '</button>';
html += '<button class="ms-btn danger" data-action="pause-mission" data-id="' + escapeHtml(m.id) + '">' + escapeHtml(I18n.t('missions.pause')) + '</button>';
} else if (m.status === 'Paused') {
html += '<button class="ms-btn primary" data-action="resume-mission" data-id="' + escapeHtml(m.id) + '">' + escapeHtml(I18n.t('missions.resume')) + '</button>';
html += '<button class="ms-btn" data-action="fire-mission" data-id="' + escapeHtml(m.id) + '">' + escapeHtml(I18n.t('missions.fireOnce')) + '</button>';
} else if (m.status === 'Failed') {
html += '<button class="ms-btn primary" data-action="fire-mission" data-id="' + escapeHtml(m.id) + '">' + escapeHtml(I18n.t('missions.retry')) + '</button>';
}
html += '</div></div>';
html += '<div class="ms-meta-grid">'
+ '<div class="ms-meta-cell"><div class="ms-meta-label">' + escapeHtml(I18n.t('missions.cadence')) + '</div><div class="ms-meta-value">' + escapeHtml(m.cadence_description || m.cadence_type || 'manual') + '</div></div>'
+ '<div class="ms-meta-cell"><div class="ms-meta-label">' + escapeHtml(I18n.t('missions.threadsToday')) + '</div><div class="ms-meta-value mono">' + (m.threads_today || 0) + ' / ' + (m.max_threads_per_day || '\u221E') + '</div></div>'
+ '<div class="ms-meta-cell"><div class="ms-meta-label">' + escapeHtml(I18n.t('missions.totalThreads')) + '</div><div class="ms-meta-value mono">' + m.thread_count + '</div></div>'
+ '<div class="ms-meta-cell"><div class="ms-meta-label">' + escapeHtml(I18n.t('missions.nextFire')) + '</div><div class="ms-meta-value">' + (m.next_fire_at ? formatDate(m.next_fire_at) : (m.status === 'Paused' ? '\u2014 paused' : '\u2014')) + '</div></div>'
+ '<div class="ms-meta-cell"><div class="ms-meta-label">' + escapeHtml(I18n.t('missions.created')) + '</div><div class="ms-meta-value">' + formatDate(m.created_at) + '</div></div>'
+ '</div>';
// Goal — full-width markdown block
html += '<div class="job-description"><h3>Goal</h3>'
+ '<div class="job-description-body">' + renderMarkdown(m.goal) + '</div></div>';
html += '<div class="job-meta-grid">'
+ 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'))
+ '</div>';
if (m.goal) {
html += '<div class="ms-section-title">' + escapeHtml(I18n.t('missions.prompt')) + '</div>'
+ '<div class="ms-detail-goal">' + renderMissionBrief(m.goal) + '</div>';
}
if (m.current_focus) {
html += '<div class="job-description"><h3>Current Focus</h3>'
+ '<div class="job-description-body">' + renderMarkdown(m.current_focus) + '</div></div>';
html += '<div class="ms-section-title">' + escapeHtml(I18n.t('missions.currentFocus')) + '</div>'
+ '<div class="ms-content-block ms-content-block--focus">' + renderMissionRichBlock(m.current_focus) + '</div>';
}
if (m.success_criteria) {
html += '<div class="job-description"><h3>Success Criteria</h3>'
+ '<div class="job-description-body">' + renderMarkdown(m.success_criteria) + '</div></div>';
html += '<div class="ms-section-title">' + escapeHtml(I18n.t('missions.successCriteria')) + '</div>'
+ '<div class="ms-content-block ms-content-block--success">' + renderMissionRichBlock(m.success_criteria) + '</div>';
}
if (m.notify_channels && m.notify_channels.length > 0) {
html += '<div class="job-description"><h3>Notify Channels</h3>'
+ '<div class="job-description-body">' + m.notify_channels.map(escapeHtml).join(', ') + '</div></div>';
html += '<div class="ms-section-title">Notify Channels</div>'
+ '<div class="ms-content-block">' + renderMissionRichBlock(m.notify_channels.map(escapeHtml).join(', ')) + '</div>';
}
if (m.approach_history && m.approach_history.length > 0) {
html += '<div class="job-description"><h3>Approach History</h3>';
m.approach_history.forEach((a, i) => {
html += '<div class="job-description-body" style="margin-bottom:8px">'
+ '<strong>Run ' + (i + 1) + '</strong><br>'
+ renderMarkdown(a) + '</div>';
html += '<div class="ms-section-title">' + escapeHtml(I18n.t('missions.approachHistory')) + '</div>'
+ '<div class="ms-approach-list">';
m.approach_history.forEach(function(a, i) {
html += renderApproachHistoryCard(a, i, i === m.approach_history.length - 1);
});
html += '</div>';
}
if (m.threads && m.threads.length > 0) {
html += '<div class="job-description"><h3>Spawned Threads</h3>'
+ '<table class="missions-table"><thead><tr>'
+ '<th>Goal</th><th>Type</th><th>State</th><th>' + escapeHtml(I18n.t('missions.progress')) + '</th><th>Steps</th><th>Tokens</th><th>Created</th>'
+ '</tr></thead><tbody>';
m.threads.forEach((t) => {
var tState = t.state === 'Done' || t.state === 'Completed' ? 'completed'
html += '<div class="ms-section-title">' + escapeHtml(I18n.t('missions.spawnedThreads')) + '</div>'
+ '<div class="ms-thread-list">';
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 += '<tr class="mission-row" data-action="open-engine-thread" data-id="' + escapeHtml(t.id) + '">'
+ '<td class="truncate">' + escapeHtml(t.goal) + '</td>'
+ '<td>' + escapeHtml(t.thread_type) + '</td>'
+ '<td><span class="badge ' + tState + '">' + escapeHtml(t.state) + '</span></td>'
+ '<td>' + renderMissionThreadProgress(t.id) + '</td>'
+ '<td>' + t.step_count + '</td>'
+ '<td>' + t.total_tokens.toLocaleString() + '</td>'
+ '<td>' + formatDate(t.created_at) + '</td>'
+ '</tr>';
: t.state === 'Running' ? 'running' : 'pending';
var costStr = t.total_cost_usd > 0 ? '$' + t.total_cost_usd.toFixed(2) : '';
html += '<div class="ms-thread-row" data-action="open-engine-thread" data-id="' + escapeHtml(t.id) + '">'
+ '<span class="ms-thread-state ' + tState + '">' + escapeHtml(t.state) + '</span>'
+ '<span class="ms-thread-label">' + escapeHtml(t.goal) + '</span>'
+ '<span class="ms-thread-cost">' + escapeHtml(costStr) + '</span>'
+ '<span class="ms-thread-time">' + formatRelativeTime(t.created_at) + '</span>'
+ '</div>';
});
html += '</tbody></table></div>';
html += '</div>';
}
// Action buttons
html += '<div style="margin-top:16px;">';
if (m.status === 'Active') {
html += '<button class="btn-cancel" data-action="pause-mission" data-id="' + escapeHtml(m.id) + '">' + escapeHtml(I18n.t('missions.pause')) + '</button> ';
}
if (m.status === 'Paused') {
html += '<button class="btn-restart" data-action="resume-mission" data-id="' + escapeHtml(m.id) + '">' + escapeHtml(I18n.t('missions.resume')) + '</button> ';
}
html += '<button class="btn-restart" data-action="fire-mission" data-id="' + escapeHtml(m.id) + '">' + escapeHtml(I18n.t('missions.fireNow')) + '</button>';
html += '</div>';
detail.innerHTML = html;
}
@@ -756,30 +1289,31 @@ function renderEngineThreadDetail(t) {
: 'pending';
var progress = activeWorkStore.getThreadProgress(t.id);
var html = '<div class="job-detail-header">'
+ '<button class="btn-back" data-action="back-to-mission">' + escapeHtml(I18n.t('missions.backToMission')) + '</button>'
+ '<h2>Thread: ' + escapeHtml(t.goal) + '</h2>'
var html = '<button class="ms-detail-back" data-action="back-to-mission">' + escapeHtml(I18n.t('missions.backToMission')) + '</button>';
html += '<div class="ms-detail-header">'
+ '<div class="ms-detail-header-left">'
+ '<div class="ms-detail-title-row">'
+ '<span class="ms-detail-title">' + escapeHtml(t.goal) + '</span>'
+ '<span class="badge ' + stateClass + '">' + escapeHtml(t.state) + '</span>'
+ '</div>';
+ '</div></div></div>';
html += '<div class="job-description mission-thread-progress" data-thread-progress-block-id="' + escapeHtml(t.id) + '"'
+ (progress ? '' : ' hidden')
+ '><h3>Current Progress</h3>'
+ '<div class="job-description-body" data-thread-progress-text-id="' + escapeHtml(t.id) + '">' + escapeHtml(progress || '') + '</div></div>';
if (progress) {
html += '<div class="ms-content-block" data-thread-progress-block-id="' + escapeHtml(t.id) + '">'
+ '<p data-thread-progress-text-id="' + escapeHtml(t.id) + '">' + escapeHtml(progress) + '</p></div>';
}
html += '<div class="job-meta-grid">'
+ 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 += '<div class="ms-meta-grid">'
+ '<div class="ms-meta-cell"><div class="ms-meta-label">' + escapeHtml(I18n.t('missions.type')) + '</div><div class="ms-meta-value">' + escapeHtml(t.thread_type || '-') + '</div></div>'
+ '<div class="ms-meta-cell"><div class="ms-meta-label">' + escapeHtml(I18n.t('missions.steps')) + '</div><div class="ms-meta-value mono">' + t.step_count + '</div></div>'
+ '<div class="ms-meta-cell"><div class="ms-meta-label">' + escapeHtml(I18n.t('missions.tokens')) + '</div><div class="ms-meta-value mono">' + t.total_tokens.toLocaleString() + '</div></div>'
+ '<div class="ms-meta-cell"><div class="ms-meta-label">' + escapeHtml(I18n.t('missions.cost')) + '</div><div class="ms-meta-value mono">' + (t.total_cost_usd > 0 ? '$' + t.total_cost_usd.toFixed(4) : '-') + '</div></div>'
+ '<div class="ms-meta-cell"><div class="ms-meta-label">' + escapeHtml(I18n.t('missions.created')) + '</div><div class="ms-meta-value">' + formatDate(t.created_at) + '</div></div>'
+ '<div class="ms-meta-cell"><div class="ms-meta-label">' + escapeHtml(I18n.t('jobs.completedLabel')) + '</div><div class="ms-meta-value">' + (t.completed_at ? formatDate(t.completed_at) : '-') + '</div></div>'
+ '</div>';
if (t.messages && t.messages.length > 0) {
html += '<div class="job-description"><h3>Messages (' + t.messages.length + ')</h3>';
html += '<div class="ms-section-title">Messages (' + t.messages.length + ')</div>';
t.messages.forEach(function(msg) {
var roleClass = msg.role === 'Assistant' ? 'assistant' : msg.role === 'User' ? 'user' : 'system';
html += '<div class="thread-message thread-msg-' + roleClass + '">'
@@ -787,7 +1321,6 @@ function renderEngineThreadDetail(t) {
+ '<div class="thread-msg-content">' + renderMarkdown(msg.content) + '</div>'
+ '</div>';
});
html += '</div>';
}
detail.innerHTML = html;

View File

@@ -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;
}

View File

@@ -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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

View File

@@ -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 tabs 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.

View File

@@ -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"]

View File

@@ -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)