mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
feat(web): improve mission detail with markdown goal and thread table
- Goal rendered as full-width markdown block instead of plain-text meta item (uses existing renderMarkdown/marked) - Current focus and success criteria also rendered as markdown - Spawned threads shown as a clickable table with goal, type, state, steps, tokens, and created date instead of a UUID list - Clicking a thread row opens an inline thread detail view showing metadata grid and full message history with markdown rendering - Back button returns to the mission detail view - Backend: mission detail now returns full thread summaries (goal, state, step_count, tokens) instead of just thread IDs Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
+10
-2
@@ -1153,7 +1153,7 @@ pub struct EngineMissionDetail {
|
||||
pub max_threads_per_day: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next_fire_at: Option<String>,
|
||||
pub thread_ids: Vec<String>,
|
||||
pub threads: Vec<EngineThreadInfo>,
|
||||
}
|
||||
|
||||
// ── Engine query functions ───────────────────────────────────
|
||||
@@ -1427,6 +1427,14 @@ pub async fn get_engine_mission(mission_id: &str) -> Result<Option<EngineMission
|
||||
|
||||
let cadence_json = serde_json::to_value(&m.cadence).unwrap_or(serde_json::Value::Null);
|
||||
|
||||
// Load thread summaries for the spawned threads table
|
||||
let mut threads = Vec::new();
|
||||
for tid in &m.thread_history {
|
||||
if let Ok(Some(thread)) = state.store.load_thread(*tid).await {
|
||||
threads.push(thread_to_info(&thread));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(EngineMissionDetail {
|
||||
info: EngineMissionInfo {
|
||||
id: m.id.to_string(),
|
||||
@@ -1445,7 +1453,7 @@ pub async fn get_engine_mission(mission_id: &str) -> Result<Option<EngineMission
|
||||
threads_today: m.threads_today,
|
||||
max_threads_per_day: m.max_threads_per_day,
|
||||
next_fire_at: m.next_fire_at.map(|dt| dt.to_rfc3339()),
|
||||
thread_ids: m.thread_history.iter().map(|t| t.to_string()).collect(),
|
||||
threads,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -4412,8 +4412,11 @@ function renderMissionDetail(m) {
|
||||
+ '<span class="badge ' + statusClass + '">' + escapeHtml(m.status) + '</span>'
|
||||
+ '</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('Goal', m.goal)
|
||||
+ metaItem('Cadence', m.cadence_type)
|
||||
+ metaItem('Status', m.status)
|
||||
+ metaItem('Threads Today', m.threads_today + ' / ' + (m.max_threads_per_day || '∞'))
|
||||
@@ -4424,12 +4427,12 @@ function renderMissionDetail(m) {
|
||||
|
||||
if (m.current_focus) {
|
||||
html += '<div class="job-description"><h3>Current Focus</h3>'
|
||||
+ '<div class="job-description-body">' + escapeHtml(m.current_focus) + '</div></div>';
|
||||
+ '<div class="job-description-body">' + renderMarkdown(m.current_focus) + '</div></div>';
|
||||
}
|
||||
|
||||
if (m.success_criteria) {
|
||||
html += '<div class="job-description"><h3>Success Criteria</h3>'
|
||||
+ '<div class="job-description-body">' + escapeHtml(m.success_criteria) + '</div></div>';
|
||||
+ '<div class="job-description-body">' + renderMarkdown(m.success_criteria) + '</div></div>';
|
||||
}
|
||||
|
||||
if (m.approach_history && m.approach_history.length > 0) {
|
||||
@@ -4440,12 +4443,26 @@ function renderMissionDetail(m) {
|
||||
html += '</ul></div>';
|
||||
}
|
||||
|
||||
if (m.thread_ids && m.thread_ids.length > 0) {
|
||||
html += '<div class="job-description"><h3>Spawned Threads</h3><ul>';
|
||||
m.thread_ids.forEach((tid) => {
|
||||
html += '<li><code>' + escapeHtml(tid) + '</code></li>';
|
||||
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>Steps</th><th>Tokens</th><th>Created</th>'
|
||||
+ '</tr></thead><tbody>';
|
||||
m.threads.forEach((t) => {
|
||||
var tState = t.state === 'Done' || t.state === 'Completed' ? 'completed'
|
||||
: 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>' + t.step_count + '</td>'
|
||||
+ '<td>' + t.total_tokens.toLocaleString() + '</td>'
|
||||
+ '<td>' + formatDate(t.created_at) + '</td>'
|
||||
+ '</tr>';
|
||||
});
|
||||
html += '</ul></div>';
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
@@ -4462,6 +4479,51 @@ function renderMissionDetail(m) {
|
||||
detail.innerHTML = html;
|
||||
}
|
||||
|
||||
function openEngineThread(threadId) {
|
||||
apiFetch('/api/engine/threads/' + threadId).then((data) => {
|
||||
var t = data.thread;
|
||||
var detail = document.getElementById('mission-detail');
|
||||
|
||||
var stateClass = t.state === 'Done' || t.state === 'Completed' ? 'completed'
|
||||
: t.state === 'Failed' ? 'failed'
|
||||
: t.state === 'Running' ? 'in_progress'
|
||||
: 'pending';
|
||||
|
||||
var html = '<div class="job-detail-header">'
|
||||
+ '<button class="btn-back" data-action="back-to-mission">← Back to Mission</button>'
|
||||
+ '<h2>Thread: ' + escapeHtml(t.goal) + '</h2>'
|
||||
+ '<span class="badge ' + stateClass + '">' + escapeHtml(t.state) + '</span>'
|
||||
+ '</div>';
|
||||
|
||||
html += '<div class="job-meta-grid">'
|
||||
+ metaItem('Thread ID', t.id)
|
||||
+ 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) : '-')
|
||||
+ metaItem('Max Iterations', t.max_iterations)
|
||||
+ metaItem('Created', formatDate(t.created_at))
|
||||
+ metaItem('Completed', t.completed_at ? formatDate(t.completed_at) : '-')
|
||||
+ '</div>';
|
||||
|
||||
if (t.messages && t.messages.length > 0) {
|
||||
html += '<div class="job-description"><h3>Messages (' + t.messages.length + ')</h3>';
|
||||
t.messages.forEach(function(msg, i) {
|
||||
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>';
|
||||
}
|
||||
|
||||
detail.innerHTML = html;
|
||||
}).catch(function(err) {
|
||||
showToast('Failed to load thread: ' + err.message, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function fireMission(id) {
|
||||
apiFetch('/api/engine/missions/' + id + '/fire', { method: 'POST' })
|
||||
.then((data) => {
|
||||
@@ -6215,6 +6277,13 @@ document.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
resumeMission(el.dataset.id);
|
||||
break;
|
||||
case 'open-engine-thread':
|
||||
openEngineThread(el.dataset.id);
|
||||
break;
|
||||
case 'back-to-mission':
|
||||
if (currentMissionId) openMissionDetail(currentMissionId);
|
||||
else closeMissionDetail();
|
||||
break;
|
||||
case 'view-run-job':
|
||||
e.preventDefault();
|
||||
switchTab('jobs');
|
||||
|
||||
@@ -2626,6 +2626,39 @@ body {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.thread-message {
|
||||
border-left: 3px solid var(--border);
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.thread-msg-role {
|
||||
font-weight: 600;
|
||||
font-size: var(--text-xs);
|
||||
text-transform: uppercase;
|
||||
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;
|
||||
}
|
||||
|
||||
.truncate {
|
||||
max-width: 300px;
|
||||
white-space: nowrap;
|
||||
|
||||
Reference in New Issue
Block a user