From 162318a2301d8d784175deb97919ae93d728bbec Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 1 Mar 2026 15:02:12 -0500 Subject: [PATCH] Improve retros and usage UI: search, filters, layout tweaks - Add search bar and smoothness dropdown filter to retros list - Replace Cost column with Frictions count on retros list - Make retro table rows fully clickable - Move Retro tab before Usage tab in run detail - Move Stage Breakdown below Open Items on retro page - Add per-model breakdown table to usage page with 3 models Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/arc-web/app/routes/retros.tsx | 134 +++++++++++++++ apps/arc-web/app/routes/run-detail.tsx | 1 + apps/arc-web/app/routes/run-retro.tsx | 225 +++++++++++++++++++++++++ apps/arc-web/app/routes/run-usage.tsx | 129 ++++++++++---- 4 files changed, 452 insertions(+), 37 deletions(-) create mode 100644 apps/arc-web/app/routes/retros.tsx create mode 100644 apps/arc-web/app/routes/run-retro.tsx diff --git a/apps/arc-web/app/routes/retros.tsx b/apps/arc-web/app/routes/retros.tsx new file mode 100644 index 000000000..5c9421fbe --- /dev/null +++ b/apps/arc-web/app/routes/retros.tsx @@ -0,0 +1,134 @@ +import { useState } from "react"; +import { useNavigate } from "react-router"; +import { MagnifyingGlassIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; +import { allRetros, smoothnessConfig, formatDuration } from "../data/retros"; +import type { Retro, SmoothnessRating } from "../data/retros"; +import type { Route } from "./+types/retros"; + +export function meta({}: Route.MetaArgs) { + return [{ title: "Retros \u2014 Arc" }]; +} + +const smoothnessOptions: Array<{ value: SmoothnessRating; label: string }> = [ + { value: "effortless", label: "Effortless" }, + { value: "smooth", label: "Smooth" }, + { value: "bumpy", label: "Bumpy" }, + { value: "struggled", label: "Struggled" }, + { value: "failed", label: "Failed" }, +]; + +function SmoothnesssBadge({ smoothness }: { smoothness: Retro["smoothness"] }) { + if (!smoothness) { + return --; + } + const config = smoothnessConfig[smoothness]; + return ( + + + {config.label} + + ); +} + +function formatTimestamp(ts: string): string { + const date = new Date(ts); + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + }); +} + +function truncate(text: string, maxLength: number): string { + if (text.length <= maxLength) return text; + return text.slice(0, maxLength) + "\u2026"; +} + +export default function Retros() { + const retros = allRetros(); + const navigate = useNavigate(); + const [query, setQuery] = useState(""); + const [smoothnessFilter, setSmoothnessFilter] = useState("all"); + + if (retros.length === 0) { + return

No retrospectives yet.

; + } + + const lowerQuery = query.toLowerCase(); + const filtered = retros.filter( + (r) => + (smoothnessFilter === "all" || r.smoothness === smoothnessFilter) && + (r.goal.toLowerCase().includes(lowerQuery) || + r.pipeline_name.toLowerCase().includes(lowerQuery)), + ); + + return ( +
+
+
+ + setQuery(e.target.value)} + className="w-full rounded-md border border-white/[0.06] bg-navy-800/80 py-2 pl-9 pr-3 text-sm text-ice-100 placeholder-navy-600 outline-none transition-colors focus:border-teal-500/40 focus:ring-0" + /> +
+
+ + +
+
+ +
+ + + + + + + + + + + + + {filtered.map((retro) => ( + navigate(`/runs/${retro.run_id}/retro`)}> + + + + + + + + ))} + +
PipelineGoalSmoothnessDurationFrictionsWhen
+ {retro.pipeline_name} + + {truncate(retro.goal, 60)} + + + + {formatDuration(retro.stats.total_duration_ms)} + + {retro.friction_points?.length ?? 0} + + {formatTimestamp(retro.timestamp)} +
+
+
+ ); +} diff --git a/apps/arc-web/app/routes/run-detail.tsx b/apps/arc-web/app/routes/run-detail.tsx index f21d03c86..e0a892c78 100644 --- a/apps/arc-web/app/routes/run-detail.tsx +++ b/apps/arc-web/app/routes/run-detail.tsx @@ -8,6 +8,7 @@ const tabs = [ { name: "Overview", path: "", count: null }, { name: "Stages", path: "/stages/detect-drift", count: 4 }, { name: "Files Changed", path: "/files", count: 3 }, + { name: "Retro", path: "/retro", count: null }, { name: "Usage", path: "/usage", count: null }, ]; diff --git a/apps/arc-web/app/routes/run-retro.tsx b/apps/arc-web/app/routes/run-retro.tsx new file mode 100644 index 000000000..fc61edd86 --- /dev/null +++ b/apps/arc-web/app/routes/run-retro.tsx @@ -0,0 +1,225 @@ +import { Link } from "react-router"; +import { + findRetro, + smoothnessConfig, + learningCategoryConfig, + frictionKindConfig, + openItemKindConfig, + formatDuration, +} from "../data/retros"; +import type { Route } from "./+types/run-retro"; + +export function meta({ params }: Route.MetaArgs) { + const retro = findRetro(params.id); + return [{ title: retro ? `Retro: ${retro.goal} \u2014 Arc` : "Retro \u2014 Arc" }]; +} + +function formatCost(cost: number | undefined): string { + if (cost == null) return "--"; + return `$${cost.toFixed(2)}`; +} + +export default function RunRetro({ params }: Route.ComponentProps) { + const retro = findRetro(params.id); + + if (!retro) { + return

No retrospective found for this run.

; + } + + const smoothness = retro.smoothness ? smoothnessConfig[retro.smoothness] : null; + + return ( +
+ {/* Smoothness + Summary Header */} +
+ {smoothness && ( + + + {smoothness.label} + + )} +
+

{retro.goal}

+

+ {retro.pipeline_name} · {new Date(retro.timestamp).toLocaleString()} +

+
+
+ + {/* Aggregate Stats */} +
+ + + 0} /> + +
+ + {/* Intent + Outcome */} + {(retro.intent ?? retro.outcome) && ( +
+ {retro.intent && ( +
+

Intent

+

{retro.intent}

+
+ )} + {retro.outcome && ( +
+

Outcome

+

{retro.outcome}

+
+ )} +
+ )} + + {/* Learnings */} + {retro.learnings && retro.learnings.length > 0 && ( +
+

Learnings

+
+ {retro.learnings.map((learning, i) => { + const config = learningCategoryConfig[learning.category]; + return ( +
+ + {config.label} + +

{learning.text}

+
+ ); + })} +
+
+ )} + + {/* Friction Points */} + {retro.friction_points && retro.friction_points.length > 0 && ( +
+

Friction Points

+
+ {retro.friction_points.map((fp, i) => { + const config = frictionKindConfig[fp.kind]; + return ( +
+ + {config.label} + +
+

{fp.description}

+ {fp.stage_id && ( +

+ Stage: {retro.stages.find((s) => s.stage_id === fp.stage_id)?.stage_label ?? fp.stage_id} +

+ )} +
+
+ ); + })} +
+
+ )} + + {/* Open Items */} + {retro.open_items && retro.open_items.length > 0 && ( +
+

Open Items

+
+ {retro.open_items.map((item, i) => { + const config = openItemKindConfig[item.kind]; + return ( +
+ + {config.label} + +

{item.description}

+
+ ); + })} +
+
+ )} + + {/* Stage Breakdown */} +
+

Stage Breakdown

+
+ + + + + + + + + + + + + {retro.stages.map((stage) => ( + + + + + + + + + ))} + +
StageStatusDurationRetriesCostFiles
+ + {stage.stage_label} + + {stage.notes && ( +

{stage.notes}

+ )} + {stage.failure_reason && ( +

{stage.failure_reason}

+ )} +
+ + + {formatDuration(stage.duration_ms)} + + 0 ? "text-amber" : "text-ice-300"}> + {stage.retries} + + + {formatCost(stage.cost)} + + {stage.files_touched.length} +
+
+
+
+ ); +} + +function StatCard({ label, value, warn }: { label: string; value: string; warn?: boolean }) { + return ( +
+

{label}

+

+ {value} +

+
+ ); +} + +function StageStatusBadge({ status }: { status: string }) { + const styles: Record = { + completed: "text-mint", + running: "text-teal-500", + pending: "text-navy-600", + failed: "text-coral", + }; + const colorClass = styles[status] ?? "text-ice-300"; + return ( + + {status} + + ); +} diff --git a/apps/arc-web/app/routes/run-usage.tsx b/apps/arc-web/app/routes/run-usage.tsx index c9dd9ed27..98402a034 100644 --- a/apps/arc-web/app/routes/run-usage.tsx +++ b/apps/arc-web/app/routes/run-usage.tsx @@ -1,7 +1,7 @@ const stages = [ { stage: "Detect Drift", model: "Opus 4.6", inputTokens: 12_480, outputTokens: 3_210, runtime: "1m 12s", cost: 0.48 }, - { stage: "Propose Changes", model: "Opus 4.6", inputTokens: 28_640, outputTokens: 8_750, runtime: "2m 34s", cost: 1.12 }, - { stage: "Review Changes", model: "Opus 4.6", inputTokens: 9_120, outputTokens: 2_640, runtime: "0m 45s", cost: 0.31 }, + { stage: "Propose Changes", model: "Gemini 3.1", inputTokens: 28_640, outputTokens: 8_750, runtime: "2m 34s", cost: 0.72 }, + { stage: "Review Changes", model: "Codex 5.3", inputTokens: 9_120, outputTokens: 2_640, runtime: "0m 45s", cost: 0.19 }, { stage: "Apply Changes", model: "Opus 4.6", inputTokens: 21_300, outputTokens: 6_480, runtime: "1m 58s", cost: 0.87 }, ]; @@ -10,48 +10,103 @@ const totalCost = stages.reduce((sum, s) => sum + s.cost, 0); const totalInput = stages.reduce((sum, s) => sum + s.inputTokens, 0); const totalOutput = stages.reduce((sum, s) => sum + s.outputTokens, 0); +const modelBreakdown = Object.values( + stages.reduce>( + (acc, s) => { + const entry = acc[s.model] ?? { model: s.model, inputTokens: 0, outputTokens: 0, cost: 0, stages: 0 }; + entry.inputTokens += s.inputTokens; + entry.outputTokens += s.outputTokens; + entry.cost += s.cost; + entry.stages += 1; + acc[s.model] = entry; + return acc; + }, + {}, + ), +).sort((a, b) => b.cost - a.cost); + function formatTokens(n: number) { return `${(n / 1000).toFixed(1)}k`; } export default function RunUsage() { return ( -
- - - - - - - - - - - - {stages.map((row) => ( - - - - - - +
+
+
StageModelTokensRun timeCost
{row.stage}{row.model} - {formatTokens(row.inputTokens)} / {formatTokens(row.outputTokens)} - {row.runtime}${row.cost.toFixed(2)}
+ + + + + + + - ))} - - - - - - - - - -
StageModelTokensRun timeCost
Total - - {formatTokens(totalInput)} / {formatTokens(totalOutput)} - {totalRuntime}${totalCost.toFixed(2)}
+ + + {stages.map((row) => ( + + {row.stage} + {row.model} + + {formatTokens(row.inputTokens)} / {formatTokens(row.outputTokens)} + + {row.runtime} + ${row.cost.toFixed(2)} + + ))} + + + + Total + + + {formatTokens(totalInput)} / {formatTokens(totalOutput)} + + {totalRuntime} + ${totalCost.toFixed(2)} + + + +
+ +
+

By Model

+
+ + + + + + + + + + + {modelBreakdown.map((row) => ( + + + + + + + ))} + + + + + + + + + +
ModelStagesTokensCost
{row.model}{row.stages} + {formatTokens(row.inputTokens)} / {formatTokens(row.outputTokens)} + ${row.cost.toFixed(2)}
Total{stages.length} + {formatTokens(totalInput)} / {formatTokens(totalOutput)} + ${totalCost.toFixed(2)}
+
+
); }