loops: reorder history collapsed under the progress pill
Completes the reorder rationale loop — the previous commit captured
REORDER: markers server-side but nothing surfaced them. Now the loops
sidebar renders a small "N reorders ▸" button under the progress pill
whenever the loop has any reorder events. Click expands to a
newest-first list of "iter <n> · <rationale>" lines so a reviewer can
see, at a glance, when the plan was adjusted and why.
Backend:
- LoopProgress DTO gains recent_reorders: Vec<Value> — newest-first,
capped at 5 so the card stays compact. Full history remains on the
loop row's reorder_events column.
- cm_db::repo::loops::recent_reorders — reads the jsonb array, returns
the last N in newest-first order.
- list_progress populates it per loop.
Frontend:
- LoopReorderEvent + recent_reorders on LoopProgress type.
- LoopsList tracks openHistoryId per-loop (one open at a time).
- Card renders history button + expanded panel styled to match the
progress pill above.
Notes:
- The event object schema is {run_id, iteration, text, ts}. Fields are
optional in the TS type so future schema tweaks don't break the
render.
- 5-item cap chosen so the sidebar card doesn't grow unbounded. If a
loop accumulates a lot of reorders, follow-up UI can render the full
history on the loop detail page.
This commit is contained in:
@@ -46,6 +46,7 @@ export function LoopsList({
|
||||
const noAgents = agents.length === 0;
|
||||
const [loops, setLoops] = useState<Loop[]>([]);
|
||||
const [progress, setProgress] = useState<Map<string, LoopProgress>>(new Map());
|
||||
const [openHistoryId, setOpenHistoryId] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [wizardOpen, setWizardOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<Loop | null>(null);
|
||||
@@ -334,6 +335,69 @@ export function LoopsList({
|
||||
);
|
||||
})()}
|
||||
</button>
|
||||
{(() => {
|
||||
const p = progress.get(l.id);
|
||||
if (!p || p.recent_reorders.length === 0) return null;
|
||||
const open = openHistoryId === l.id;
|
||||
return (
|
||||
<div style={{ marginTop: 2 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setOpenHistoryId(open ? null : l.id)
|
||||
}
|
||||
style={{
|
||||
all: "unset",
|
||||
cursor: "pointer",
|
||||
fontFamily: mono,
|
||||
fontSize: 9.5,
|
||||
color: "#8a8a92",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
aria-expanded={open}
|
||||
>
|
||||
<span style={{ opacity: 0.7 }}>{open ? "▾" : "▸"}</span>
|
||||
<span>
|
||||
{p.recent_reorders.length} reorder
|
||||
{p.recent_reorders.length === 1 ? "" : "s"}
|
||||
</span>
|
||||
</button>
|
||||
{open ? (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 4,
|
||||
padding: "6px 8px",
|
||||
borderRadius: 6,
|
||||
background: "rgba(255,255,255,.02)",
|
||||
border: "1px solid rgba(255,255,255,.05)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 5,
|
||||
}}
|
||||
>
|
||||
{p.recent_reorders.map((e, i) => (
|
||||
<div
|
||||
key={`${e.run_id ?? "unk"}-${i}`}
|
||||
style={{
|
||||
fontFamily: mono,
|
||||
fontSize: 10,
|
||||
color: "#c3c3c8",
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
<span style={{ color: "#8a8a92" }}>
|
||||
iter {e.iteration ?? "?"} ·{" "}
|
||||
</span>
|
||||
{e.text ?? ""}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{confirming ? (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user