loops: N/M INTs progress pill on source-bound loop cards
Users couldn't see how a research-bound loop was progressing through
its integration plan — the consumed_int_ids state existed in the DB
but nothing surfaced it. Now each loop card in the sidebar shows a
cyan pill "3/8 INTs" + source topic title + a thin progress bar, when
the loop is bound to a research topic.
Backend:
- GET /api/loops/progress — bulk read for every loop with a source
topic. Returns {loop_id, source_topic_id, source_topic_title,
source_outcome_version, consumed_count, total_int_count,
current_int_index} per loop. Standalone loops are omitted.
- count_int_ids parses unique INT-<number> ids out of the source
outcome's markdown — same permissive matcher as the completion
hook, so what the pill counts matches what the loop can advance.
- Memoized by topic_id inside the endpoint so N loops sharing 1
source topic only fetch the outcome once.
Frontend:
- listLoopProgress helper + LoopProgress type in the loops API.
- LoopsList fetches loops + progress in parallel on mount.
- Each card looks up progress by loop_id and, when found, renders
under the schedule line: pill "3/8 INTs · Topic title" with a
3px cyan progress bar. Title hover shows artifact version.
Follow-ups:
- Refresh button on the card — re-snapshot artifact into
task_template (cosmetic; the enqueue path already reads latest).
- Reorder rationale extraction — parse "REORDER: <text>" out of run
output, index as a per-loop event log for a mini-timeline UI.
This commit is contained in:
@@ -18,8 +18,10 @@ import {
|
||||
deleteLoop,
|
||||
disableLoop,
|
||||
enableLoop,
|
||||
listLoopProgress,
|
||||
listLoops,
|
||||
type Loop,
|
||||
type LoopProgress,
|
||||
} from "@/lib/api/loops";
|
||||
import type { Agent } from "@/lib/api/schemas";
|
||||
|
||||
@@ -43,6 +45,7 @@ export function LoopsList({
|
||||
}) {
|
||||
const noAgents = agents.length === 0;
|
||||
const [loops, setLoops] = useState<Loop[]>([]);
|
||||
const [progress, setProgress] = useState<Map<string, LoopProgress>>(new Map());
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [wizardOpen, setWizardOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<Loop | null>(null);
|
||||
@@ -56,8 +59,14 @@ export function LoopsList({
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const rows = await listLoops();
|
||||
if (alive) setLoops(rows);
|
||||
const [rows, prog] = await Promise.all([
|
||||
listLoops(),
|
||||
listLoopProgress().catch(() => [] as LoopProgress[]),
|
||||
]);
|
||||
if (alive) {
|
||||
setLoops(rows);
|
||||
setProgress(new Map(prog.map((p) => [p.loop_id, p])));
|
||||
}
|
||||
} catch {
|
||||
if (alive) setLoops([]);
|
||||
} finally {
|
||||
@@ -256,6 +265,74 @@ export function LoopsList({
|
||||
: "no schedule"}
|
||||
</span>
|
||||
</div>
|
||||
{(() => {
|
||||
const p = progress.get(l.id);
|
||||
if (!p) return null;
|
||||
const done = p.consumed_count;
|
||||
const total = p.total_int_count;
|
||||
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
marginTop: 2,
|
||||
fontFamily: mono,
|
||||
fontSize: 9.5,
|
||||
color: "#5ec8d8",
|
||||
}}
|
||||
title={`Based on research topic "${p.source_topic_title}" (artifact v${p.source_outcome_version})`}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
padding: "2px 6px",
|
||||
borderRadius: 4,
|
||||
border: "1px solid rgba(94,200,216,.35)",
|
||||
background: "rgba(94,200,216,.08)",
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
{done}/{total || "?"} INTs
|
||||
</span>
|
||||
<span style={{ opacity: 0.7, color: "#8a8a92" }}>·</span>
|
||||
<span
|
||||
style={{
|
||||
opacity: 0.7,
|
||||
color: "#8a8a92",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
maxWidth: 200,
|
||||
}}
|
||||
>
|
||||
{p.source_topic_title}
|
||||
</span>
|
||||
{total > 0 ? (
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 3,
|
||||
borderRadius: 2,
|
||||
background: "rgba(255,255,255,.06)",
|
||||
overflow: "hidden",
|
||||
minWidth: 20,
|
||||
marginLeft: 4,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: "block",
|
||||
width: `${pct}%`,
|
||||
height: "100%",
|
||||
background: "#5ec8d8",
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</button>
|
||||
|
||||
{confirming ? (
|
||||
|
||||
Reference in New Issue
Block a user