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:
@@ -196,40 +196,67 @@ function RunOutputPanel({ runId }: { runId: string }) {
|
|||||||
to reach their working directory or found nothing to act on.
|
to reach their working directory or found nothing to act on.
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
data.outputs.map((o, i) => (
|
data.outputs.map((o, i) => {
|
||||||
<div key={i}>
|
// First turn open by default so the operator sees something
|
||||||
<div
|
// without a click; every subsequent turn collapses so the
|
||||||
style={{
|
// panel stays glanceable — click to expand each turn.
|
||||||
color: "#5ec8d8",
|
const firstLine =
|
||||||
fontSize: 9,
|
o.preview
|
||||||
letterSpacing: ".08em",
|
.split("\n")
|
||||||
textTransform: "uppercase",
|
.find((l) => l.trim().length > 0)
|
||||||
marginBottom: 3,
|
?.trim() ?? "";
|
||||||
}}
|
const preview =
|
||||||
>
|
firstLine.length > 140 ? `${firstLine.slice(0, 140)}…` : firstLine;
|
||||||
turn {i + 1}
|
return (
|
||||||
{o.truncated
|
<details key={i} open={i === 0}>
|
||||||
? ` · showing first ${o.preview.length.toLocaleString()} of ${o.full_len.toLocaleString()} chars`
|
<summary
|
||||||
: ""}
|
style={{
|
||||||
</div>
|
cursor: "pointer",
|
||||||
<pre
|
color: "#5ec8d8",
|
||||||
style={{
|
fontSize: 10,
|
||||||
margin: 0,
|
letterSpacing: ".06em",
|
||||||
padding: 6,
|
textTransform: "uppercase",
|
||||||
borderRadius: 4,
|
padding: "3px 0",
|
||||||
background: "rgba(0,0,0,.5)",
|
listStyle: "revert",
|
||||||
color: "#e0e0e5",
|
}}
|
||||||
fontSize: 10.5,
|
>
|
||||||
whiteSpace: "pre-wrap",
|
turn {i + 1}
|
||||||
wordBreak: "break-word",
|
{o.truncated
|
||||||
maxHeight: 300,
|
? ` · ${o.preview.length.toLocaleString()}/${o.full_len.toLocaleString()} chars`
|
||||||
overflow: "auto",
|
: ""}
|
||||||
}}
|
{preview && (
|
||||||
>
|
<span
|
||||||
{o.preview}
|
style={{
|
||||||
</pre>
|
color: "#8a8a92",
|
||||||
</div>
|
textTransform: "none",
|
||||||
))
|
letterSpacing: 0,
|
||||||
|
marginLeft: 8,
|
||||||
|
fontSize: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
· {preview}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</summary>
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
margin: "4px 0 0",
|
||||||
|
padding: 6,
|
||||||
|
borderRadius: 4,
|
||||||
|
background: "rgba(0,0,0,.5)",
|
||||||
|
color: "#e0e0e5",
|
||||||
|
fontSize: 10.5,
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
maxHeight: 300,
|
||||||
|
overflow: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{o.preview}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,9 +8,11 @@
|
|||||||
// 404 while `phase_summarizer` hasn't run yet — shows a "waiting"
|
// 404 while `phase_summarizer` hasn't run yet — shows a "waiting"
|
||||||
// pill in that case instead of an error.
|
// 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";
|
import { getPhaseSummary, type PhaseSummary } from "@/lib/api/missions";
|
||||||
|
|
||||||
|
const COLLAPSE_KEY_PREFIX = "cm.mission.summary.collapsed:";
|
||||||
|
|
||||||
const mono =
|
const mono =
|
||||||
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
|
"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 [summary, setSummary] = useState<PhaseSummary | null>(null);
|
||||||
const [waiting, setWaiting] = useState(true);
|
const [waiting, setWaiting] = useState(true);
|
||||||
const [err, setErr] = useState<string | null>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
let alive = true;
|
let alive = true;
|
||||||
@@ -122,6 +138,16 @@ export function PhaseSummaryCard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const metrics = Object.entries(summary.metrics ?? {});
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -138,31 +164,54 @@ export function PhaseSummaryCard({
|
|||||||
gap: 10,
|
gap: 10,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", gap: 8, alignItems: "baseline" }}>
|
<button
|
||||||
<span
|
type="button"
|
||||||
style={{
|
onClick={toggleCollapsed}
|
||||||
fontSize: 9,
|
aria-expanded={!collapsed}
|
||||||
letterSpacing: ".1em",
|
style={{
|
||||||
textTransform: "uppercase",
|
appearance: "none",
|
||||||
color: "#5ec8d8",
|
background: "transparent",
|
||||||
}}
|
border: "none",
|
||||||
>
|
padding: 0,
|
||||||
phase summary · {summary.kind}
|
margin: 0,
|
||||||
</span>
|
textAlign: "left",
|
||||||
<span style={{ fontSize: 9, color: "#6a6a72" }}>
|
cursor: "pointer",
|
||||||
{summary.model} · {new Date(summary.generated_at).toLocaleTimeString()}
|
color: "inherit",
|
||||||
</span>
|
fontFamily: "inherit",
|
||||||
</div>
|
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 }}>
|
{!collapsed && (
|
||||||
{summary.narrative}
|
<div style={{ whiteSpace: "pre-wrap", color: "#e0e0e5", lineHeight: 1.4 }}>
|
||||||
</div>
|
{summary.narrative}
|
||||||
|
</div>
|
||||||
{metrics.length > 0 && (
|
|
||||||
<MetricsGrid entries={metrics} />
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{summary.sources && summary.sources.length > 0 && (
|
{!collapsed && metrics.length > 0 && <MetricsGrid entries={metrics} />}
|
||||||
|
|
||||||
|
{!collapsed && summary.sources && summary.sources.length > 0 && (
|
||||||
<Section
|
<Section
|
||||||
label="sources consulted"
|
label="sources consulted"
|
||||||
items={summary.sources.map((s) => ({
|
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
|
<Section
|
||||||
label="tooling recommendations"
|
label="tooling recommendations"
|
||||||
items={summary.tooling.map((t) => ({
|
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
|
<Section
|
||||||
label="artifacts saved"
|
label="artifacts saved"
|
||||||
items={summary.artifacts.map((a) => ({
|
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
|
<Section
|
||||||
label="next actions"
|
label="next actions"
|
||||||
items={summary.next_actions.map((n) => ({
|
items={summary.next_actions.map((n) => ({
|
||||||
@@ -277,7 +326,15 @@ function Section({
|
|||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</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) => (
|
{items.map((it, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
|
|||||||
Reference in New Issue
Block a user