Add stage ID to stages URL for addressable stage navigation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-02-28 16:59:26 -05:00
parent 73d205ca13
commit 043a5e2ef0
4 changed files with 23 additions and 18 deletions

View file

@ -18,7 +18,7 @@ export default [
route("runs", "routes/pipelines.tsx"),
route("runs/:id", "routes/run-detail.tsx", [
index("routes/run-overview.tsx"),
route("stages", "routes/run-stages.tsx"),
route("stages/:stageId", "routes/run-stages.tsx"),
route("files", "routes/run-files-changed.tsx"),
route("usage", "routes/run-usage.tsx"),
]),

View file

@ -6,7 +6,7 @@ import type { Route } from "./+types/run-detail";
const tabs = [
{ name: "Overview", path: "", count: null },
{ name: "Stages", path: "/stages", count: 4 },
{ name: "Stages", path: "/stages/detect-drift", count: 4 },
{ name: "Files Changed", path: "/files", count: 3 },
{ name: "Usage", path: "/usage", count: null },
];
@ -71,7 +71,9 @@ export default function RunDetail({ params }: Route.ComponentProps) {
<nav className="-mb-px flex gap-6">
{tabs.map((tab) => {
const tabPath = `${basePath}${tab.path}`;
const isActive = pathname === tabPath;
const isActive = tab.name === "Stages"
? pathname.startsWith(`${basePath}/stages`)
: pathname === tabPath;
return (
<Link
key={tab.name}

View file

@ -11,16 +11,17 @@ export const handle = { wide: true };
type StageStatus = "completed" | "running" | "pending" | "failed";
interface Stage {
id: string;
name: string;
status: StageStatus;
duration: string;
}
const stages: Stage[] = [
{ name: "Detect Drift", status: "completed", duration: "1m 12s" },
{ name: "Propose Changes", status: "completed", duration: "2m 34s" },
{ name: "Review Changes", status: "completed", duration: "0m 45s" },
{ name: "Apply Changes", status: "running", duration: "1m 58s" },
{ id: "detect-drift", name: "Detect Drift", status: "completed", duration: "1m 12s" },
{ id: "propose-changes", name: "Propose Changes", status: "completed", duration: "2m 34s" },
{ id: "review-changes", name: "Review Changes", status: "completed", duration: "0m 45s" },
{ id: "apply-changes", name: "Apply Changes", status: "running", duration: "1m 58s" },
];
const statusConfig: Record<StageStatus, { icon: typeof CheckCircleIcon; color: string }> = {
@ -273,9 +274,9 @@ export default function RunOverview() {
const config = statusConfig[stage.status];
const Icon = config.icon;
return (
<li key={stage.name}>
<li key={stage.id}>
<Link
to={`/runs/${id}/stages`}
to={`/runs/${id}/stages/${stage.id}`}
className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-ice-300 transition-colors hover:bg-white/[0.04] hover:text-white"
>
<Icon className={`size-4 shrink-0 ${config.color} ${stage.status === "running" ? "animate-spin" : ""}`} />

View file

@ -11,16 +11,17 @@ export const handle = { wide: true };
type StageStatus = "completed" | "running" | "pending" | "failed";
interface Stage {
id: string;
name: string;
status: StageStatus;
duration: string;
}
const stages: Stage[] = [
{ name: "Detect Drift", status: "completed", duration: "1m 12s" },
{ name: "Propose Changes", status: "completed", duration: "2m 34s" },
{ name: "Review Changes", status: "completed", duration: "0m 45s" },
{ name: "Apply Changes", status: "running", duration: "1m 58s" },
{ id: "detect-drift", name: "Detect Drift", status: "completed", duration: "1m 12s" },
{ id: "propose-changes", name: "Propose Changes", status: "completed", duration: "2m 34s" },
{ id: "review-changes", name: "Review Changes", status: "completed", duration: "0m 45s" },
{ id: "apply-changes", name: "Apply Changes", status: "running", duration: "1m 58s" },
];
const statusConfig: Record<StageStatus, { icon: typeof CheckCircleIcon; color: string }> = {
@ -41,7 +42,7 @@ type TurnType =
| { kind: "assistant"; content: string }
| { kind: "tool"; tools: ToolUse[] };
const selectedStage = stages[0];
// selectedStage is resolved from the URL param in RunStages below
const turns: TurnType[] = [
{
@ -148,9 +149,10 @@ function AssistantBlock({ content }: { content: string }) {
}
export default function RunStages() {
const { id } = useParams();
const { id, stageId } = useParams();
const run = findRun(id ?? "");
const workflow = run ? workflowData[run.workflow] : undefined;
const selectedStage = stages.find((s) => s.id === stageId) ?? stages[0];
const selectedConfig = statusConfig[selectedStage.status];
const SelectedIcon = selectedConfig.icon;
@ -163,11 +165,11 @@ export default function RunStages() {
{stages.map((stage) => {
const config = statusConfig[stage.status];
const Icon = config.icon;
const isSelected = stage.name === selectedStage.name;
const isSelected = stage.id === selectedStage.id;
return (
<li key={stage.name}>
<li key={stage.id}>
<Link
to={`/runs/${id}/stages`}
to={`/runs/${id}/stages/${stage.id}`}
className={`flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors ${
isSelected
? "bg-white/[0.06] text-white"