mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-02 23:56:24 +08:00
feat(missions): redesign missions overview surface (#2894)
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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">← 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">← 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">← ' + 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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
BIN
docs/drafts/assets/screenshots/pr-2894-missions-dossier.png
Normal file
BIN
docs/drafts/assets/screenshots/pr-2894-missions-dossier.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
BIN
docs/drafts/assets/screenshots/pr-2894-projects-control-room.png
Normal file
BIN
docs/drafts/assets/screenshots/pr-2894-projects-control-room.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 216 KiB |
@@ -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.
|
||||
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user