missions: collapse each run turn + collapse phase summary card
- RunOutputPanel: each turn now renders as <details> with a 1-line peek in the summary. First turn open by default (so operators see something without a click), subsequent turns collapsed. Same shape applies to research + coding runs (shared component). - PhaseSummaryCard: click the header to collapse the whole card; narrative peek shows in the collapsed state. State persisted per phase_id in localStorage so it stays remembered across visits. - PhaseSummaryCard Section: cap max height at 280px with internal scroll so long tooling / sources / next-action lists dont blow out the card height.
This commit is contained in:
@@ -8,9 +8,11 @@
|
||||
// 404 while `phase_summarizer` hasn't run yet — shows a "waiting"
|
||||
// pill in that case instead of an error.
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { getPhaseSummary, type PhaseSummary } from "@/lib/api/missions";
|
||||
|
||||
const COLLAPSE_KEY_PREFIX = "cm.mission.summary.collapsed:";
|
||||
|
||||
const mono =
|
||||
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
|
||||
|
||||
@@ -24,6 +26,20 @@ export function PhaseSummaryCard({
|
||||
const [summary, setSummary] = useState<PhaseSummary | null>(null);
|
||||
const [waiting, setWaiting] = useState(true);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const collapseKey = `${COLLAPSE_KEY_PREFIX}${phaseId}`;
|
||||
const [collapsed, setCollapsed] = useState<boolean>(() => {
|
||||
if (typeof window === "undefined") return false;
|
||||
return window.localStorage.getItem(collapseKey) === "1";
|
||||
});
|
||||
const toggleCollapsed = useCallback(() => {
|
||||
setCollapsed((v) => {
|
||||
const next = !v;
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.setItem(collapseKey, next ? "1" : "0");
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [collapseKey]);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
@@ -122,6 +138,16 @@ export function PhaseSummaryCard({
|
||||
}
|
||||
|
||||
const metrics = Object.entries(summary.metrics ?? {});
|
||||
// First line of narrative doubles as a peek when collapsed.
|
||||
const narrativeFirstLine =
|
||||
summary.narrative
|
||||
.split("\n")
|
||||
.find((l) => l.trim().length > 0)
|
||||
?.trim() ?? "";
|
||||
const narrativePeek =
|
||||
narrativeFirstLine.length > 180
|
||||
? `${narrativeFirstLine.slice(0, 180)}…`
|
||||
: narrativeFirstLine;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -138,31 +164,54 @@ export function PhaseSummaryCard({
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "baseline" }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 9,
|
||||
letterSpacing: ".1em",
|
||||
textTransform: "uppercase",
|
||||
color: "#5ec8d8",
|
||||
}}
|
||||
>
|
||||
phase summary · {summary.kind}
|
||||
</span>
|
||||
<span style={{ fontSize: 9, color: "#6a6a72" }}>
|
||||
{summary.model} · {new Date(summary.generated_at).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleCollapsed}
|
||||
aria-expanded={!collapsed}
|
||||
style={{
|
||||
appearance: "none",
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
textAlign: "left",
|
||||
cursor: "pointer",
|
||||
color: "inherit",
|
||||
fontFamily: "inherit",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "baseline" }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 9,
|
||||
letterSpacing: ".1em",
|
||||
textTransform: "uppercase",
|
||||
color: "#5ec8d8",
|
||||
}}
|
||||
>
|
||||
{collapsed ? "▸" : "▾"} phase summary · {summary.kind}
|
||||
</span>
|
||||
<span style={{ fontSize: 9, color: "#6a6a72" }}>
|
||||
{summary.model} · {new Date(summary.generated_at).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
{collapsed && narrativePeek && (
|
||||
<div style={{ color: "#8a8a92", fontSize: 10 }}>{narrativePeek}</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div style={{ whiteSpace: "pre-wrap", color: "#e0e0e5", lineHeight: 1.4 }}>
|
||||
{summary.narrative}
|
||||
</div>
|
||||
|
||||
{metrics.length > 0 && (
|
||||
<MetricsGrid entries={metrics} />
|
||||
{!collapsed && (
|
||||
<div style={{ whiteSpace: "pre-wrap", color: "#e0e0e5", lineHeight: 1.4 }}>
|
||||
{summary.narrative}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{summary.sources && summary.sources.length > 0 && (
|
||||
{!collapsed && metrics.length > 0 && <MetricsGrid entries={metrics} />}
|
||||
|
||||
{!collapsed && summary.sources && summary.sources.length > 0 && (
|
||||
<Section
|
||||
label="sources consulted"
|
||||
items={summary.sources.map((s) => ({
|
||||
@@ -174,7 +223,7 @@ export function PhaseSummaryCard({
|
||||
/>
|
||||
)}
|
||||
|
||||
{summary.tooling && summary.tooling.length > 0 && (
|
||||
{!collapsed && summary.tooling && summary.tooling.length > 0 && (
|
||||
<Section
|
||||
label="tooling recommendations"
|
||||
items={summary.tooling.map((t) => ({
|
||||
@@ -185,7 +234,7 @@ export function PhaseSummaryCard({
|
||||
/>
|
||||
)}
|
||||
|
||||
{summary.artifacts && summary.artifacts.length > 0 && (
|
||||
{!collapsed && summary.artifacts && summary.artifacts.length > 0 && (
|
||||
<Section
|
||||
label="artifacts saved"
|
||||
items={summary.artifacts.map((a) => ({
|
||||
@@ -196,7 +245,7 @@ export function PhaseSummaryCard({
|
||||
/>
|
||||
)}
|
||||
|
||||
{summary.next_actions && summary.next_actions.length > 0 && (
|
||||
{!collapsed && summary.next_actions && summary.next_actions.length > 0 && (
|
||||
<Section
|
||||
label="next actions"
|
||||
items={summary.next_actions.map((n) => ({
|
||||
@@ -277,7 +326,15 @@ function Section({
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 4,
|
||||
maxHeight: 280,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{items.map((it, i) => (
|
||||
<div
|
||||
key={i}
|
||||
|
||||
Reference in New Issue
Block a user