missions: collapsible header + scrollable tabs + wrap phase controls
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 36s
ci / rust (push) Successful in 3m7s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m36s

- Mission header title/description now collapsible via chevron next
  to the title. Persisted in localStorage so it stays hidden across
  mission switches once the operator has read it — clears more room
  for phases/tasks/team panels below.
- Tabs row: overflow-x auto + per-tab flex:none + whiteSpace:nowrap
  so 8+ tabs scroll horizontally instead of wrapping and cutting off.
- Phase card action row (retry/security/benchmark buttons): flexWrap
  wrap so long button rows stack cleanly instead of overflowing.
- Phase card status row wraps too, and the card itself gets
  overflow:hidden + minWidth:0 so long content stays inside the
  border and the parent tab-panel scroll handles vertical growth.
This commit is contained in:
Omar Sobh
2026-07-23 18:40:13 -07:00
parent 71f66e0164
commit c4ecb9baa4
@@ -9,7 +9,17 @@
// This replaces ResearchCanvas + LoopsCanvas after Slice 9's cutover.
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { FileText, Pencil, Play, Plus, RefreshCw, Sparkles, Trash2 } from "lucide-react";
import {
ChevronDown,
ChevronUp,
FileText,
Pencil,
Play,
Plus,
RefreshCw,
Sparkles,
Trash2,
} from "lucide-react";
import {
deleteMission,
@@ -120,6 +130,21 @@ export function MissionCanvas({
const [deleteBusy, setDeleteBusy] = useState(false);
const [runs, setRuns] = useState<MissionRunSummary[]>([]);
const [lastLoadedAt, setLastLoadedAt] = useState<Date | null>(null);
// Persist the collapsed state across mission switches so the operator
// can keep the header hidden once they've read it.
const [headerCollapsed, setHeaderCollapsed] = useState<boolean>(() => {
if (typeof window === "undefined") return false;
return window.localStorage.getItem("cm.mission.headerCollapsed") === "1";
});
const toggleHeader = useCallback(() => {
setHeaderCollapsed((v) => {
const next = !v;
if (typeof window !== "undefined") {
window.localStorage.setItem("cm.mission.headerCollapsed", next ? "1" : "0");
}
return next;
});
}, []);
const load = useCallback(async () => {
if (!selectedId) {
@@ -495,8 +520,26 @@ export function MissionCanvas({
})()}
</div>
</div>
<h1 style={{ margin: 0, fontSize: 20, color: "#f3f3f5" }}>{mission.title}</h1>
{mission.description && (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<h1 style={{ margin: 0, fontSize: 20, color: "#f3f3f5", flex: 1, minWidth: 0 }}>
{mission.title}
</h1>
{mission.description && (
<button
type="button"
onClick={toggleHeader}
title={headerCollapsed ? "Expand description" : "Collapse description"}
aria-label={headerCollapsed ? "Expand description" : "Collapse description"}
style={{
...iconBtn,
flex: "none",
}}
>
{headerCollapsed ? <ChevronDown size={13} /> : <ChevronUp size={13} />}
</button>
)}
</div>
{mission.description && !headerCollapsed && (
<div
style={{
marginTop: 4,
@@ -539,7 +582,16 @@ export function MissionCanvas({
}}
/>
)}
<div style={{ display: "flex", gap: 4, marginTop: 4 }}>
<div
style={{
display: "flex",
gap: 4,
marginTop: 4,
overflowX: "auto",
paddingBottom: 2,
scrollbarWidth: "thin",
}}
>
{(
[
"overview",
@@ -582,6 +634,8 @@ export function MissionCanvas({
display: "inline-flex",
alignItems: "center",
gap: 6,
flex: "none",
whiteSpace: "nowrap",
}}
>
{t}
@@ -641,15 +695,26 @@ export function MissionCanvas({
display: "flex",
flexDirection: "column",
gap: 4,
minWidth: 0,
overflow: "hidden",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
flexWrap: "wrap",
minWidth: 0,
}}
>
<span
style={{
width: 7,
height: 7,
borderRadius: "50%",
background: PHASE_STATUS_COLOR[p.status],
flex: "none",
}}
/>
<span
@@ -686,7 +751,14 @@ export function MissionCanvas({
<PhaseSummaryCard missionId={mission.id} phaseId={p.id} />
)}
{mission.status === "running" || mission.status === "completed" ? (
<div style={{ display: "flex", gap: 6, marginTop: 6 }}>
<div
style={{
display: "flex",
gap: 6,
marginTop: 6,
flexWrap: "wrap",
}}
>
{p.status === "failed" && mission.status === "running" && (
<button
type="button"