feat(missions): document reader + three-tab IA for the mission page
The mission page made its own output unreadable. Reviewing a research
brief meant scrolling a 300px <pre> nested inside a 260px run box nested
inside the page scroller (plus a 4th scroll region for the description) —
and the text was capped at 6,000 chars server-side with no way to fetch
the rest, so a 53kB brief showed ~11% of itself and silently dropped the
remainder. Eight flat tabs (overview/phases/tasks/team/live/artifacts/
benchmarks/pane) mixed lifecycle, work items, people, telemetry, outputs
and infra at one level, so nothing indicated where the deliverable lived.
Reader:
- GET /api/missions/{id}/documents lists every agent output (titles +
sizes, no bodies); GET .../documents/{run_id}/{index} returns one in
full. Scoped to the mission so a run id from elsewhere can't be read.
- MissionOutputReader: rail (documents grouped by phase) · document ·
outline (headings, click to jump). Exactly one scroll container per
column, never nested. Copy + download .md.
- MarkdownBlock gains fenced code blocks (agent output is full of ```rust,
previously mangled into paragraphs), h4-h6, heading anchors, and an
outlineOf() helper.
Information architecture:
- Three primary tabs with shallow sub-views: RUN (phases/tasks/live) ·
OUTPUT (documents/artifacts/benchmarks) · SETUP (overview/team/pane).
- PhaseRunsList shows a short excerpt with no inner scrollbar and points
at the reader for the full text.
- The header description is clipped, not scrollable; its full text now
has a home in Setup → Overview.
Missions list:
- /api/missions returns MissionListItem — Mission flattened plus
phases_total/phases_done/current_phase, so the JSON stays a strict
superset. Cards render a progress bar and "Coding · 1/2" instead of a
bare status dot.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d676a9e089
commit
0785ac9c79
@@ -743,3 +743,42 @@ pub async fn benchmark_snapshots_for(
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Phase progress for a set of missions, for the missions list cards.
|
||||
/// Returns `(mission_id, total, done, running_phase_kind)`.
|
||||
///
|
||||
/// A status dot alone doesn't tell you where a mission actually is; this
|
||||
/// is what lets a card say "Coding · 1/2" instead of just "running".
|
||||
pub async fn phase_progress(
|
||||
pool: &PgPool,
|
||||
mission_ids: &[Uuid],
|
||||
) -> Result<Vec<(Uuid, i64, i64, Option<String>)>, DbError> {
|
||||
use sqlx::Row;
|
||||
if mission_ids.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let rows = sqlx::query(
|
||||
"SELECT mission_id,
|
||||
count(*) AS total,
|
||||
count(*) FILTER (WHERE status IN ('completed','skipped')) AS done,
|
||||
(array_agg(kind ORDER BY order_idx)
|
||||
FILTER (WHERE status = 'running'))[1] AS running_kind
|
||||
FROM mission_phases
|
||||
WHERE mission_id = ANY($1)
|
||||
GROUP BY mission_id",
|
||||
)
|
||||
.bind(mission_ids)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
(
|
||||
r.get("mission_id"),
|
||||
r.get("total"),
|
||||
r.get("done"),
|
||||
r.get("running_kind"),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -476,3 +476,38 @@ pub async fn status(
|
||||
updated_at: row.updated_at,
|
||||
})
|
||||
}
|
||||
|
||||
/// Graph + checkpoint for every run of a mission, oldest first — the raw
|
||||
/// material the mission Output reader turns into a document list.
|
||||
///
|
||||
/// Deliberately returns the FULL checkpoint: the reader exists precisely
|
||||
/// because the 6kB preview in `routes::topology::get_run_output` throws
|
||||
/// away ~90% of a research brief. This is fetched on demand when the
|
||||
/// operator opens the Output tab, never on a poll loop.
|
||||
pub async fn documents_source_for_mission(
|
||||
pool: &PgPool,
|
||||
mission_id: Uuid,
|
||||
) -> Result<Vec<(Uuid, Option<Uuid>, String, Option<Value>, Option<Value>)>, DbError> {
|
||||
use sqlx::Row;
|
||||
let rows = sqlx::query(
|
||||
"SELECT id, mission_phase_id, status, graph, checkpoint
|
||||
FROM topology_runs
|
||||
WHERE mission_id = $1
|
||||
ORDER BY created_at ASC",
|
||||
)
|
||||
.bind(mission_id)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| {
|
||||
(
|
||||
r.get("id"),
|
||||
r.get("mission_phase_id"),
|
||||
r.get("status"),
|
||||
r.get("graph"),
|
||||
r.get("checkpoint"),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user