From d8f6623afa28c43ea137b06cbeceb4ddf4d22331 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 19 Apr 2026 07:55:58 -0500 Subject: [PATCH] Clarify issue run ledger statuses Co-Authored-By: Paperclip --- ui/src/components/IssueRunLedger.test.tsx | 34 ++++++-- ui/src/components/IssueRunLedger.tsx | 102 ++++++++++++++++------ ui/src/pages/IssueDetail.tsx | 26 ++++-- 3 files changed, 124 insertions(+), 38 deletions(-) diff --git a/ui/src/components/IssueRunLedger.test.tsx b/ui/src/components/IssueRunLedger.test.tsx index 3800e31098f..bbb4152a9e7 100644 --- a/ui/src/components/IssueRunLedger.test.tsx +++ b/ui/src/components/IssueRunLedger.test.tsx @@ -146,10 +146,10 @@ describe("IssueRunLedger", () => { expect(container.textContent).toContain("Completed"); expect(container.textContent).toContain("Needs follow-up"); expect(container.textContent).toContain("Exhausted"); - expect(container.textContent).toContain("continuation 3"); + expect(container.textContent).toContain("Continuation attempt 3"); }); - it("renders historical runs without liveness metadata", () => { + it("renders historical runs without liveness metadata as unavailable", () => { renderLedger({ runs: [ createRun({ @@ -163,9 +163,33 @@ describe("IssueRunLedger", () => { ], }); - expect(container.textContent).toContain("Liveness pending"); - expect(container.textContent).toContain("Stop not recorded"); - expect(container.textContent).toContain("Last useful action none recorded"); + expect(container.textContent).toContain("No liveness data"); + expect(container.textContent).toContain("Stop Unavailable"); + expect(container.textContent).toContain("Last useful action Unavailable"); + }); + + it("shows live runs as pending final checks without missing-data language", () => { + renderLedger({ + runs: [ + createRun({ + status: "running", + finishedAt: null, + livenessState: null, + livenessReason: null, + continuationAttempt: 0, + lastUsefulActionAt: null, + nextAction: null, + resultJson: null, + }), + ], + }); + + expect(container.textContent).toContain("Running now by CodexCoder"); + expect(container.textContent).toContain("Checks after finish"); + expect(container.textContent).toContain("Last useful action No action recorded yet"); + expect(container.textContent).toContain("Stop Still running"); + expect(container.textContent).not.toContain("Liveness pending"); + expect(container.textContent).not.toContain("initial attempt"); }); it("shows timeout, cancel, and budget stop reasons without raw logs", () => { diff --git a/ui/src/components/IssueRunLedger.tsx b/ui/src/components/IssueRunLedger.tsx index e3dec720b53..4df83c189db 100644 --- a/ui/src/components/IssueRunLedger.tsx +++ b/ui/src/components/IssueRunLedger.tsx @@ -30,7 +30,13 @@ type LedgerRun = RunForIssue & { agentName?: string; }; -const LIVENESS_COPY: Record = { +type LivenessCopy = { + label: string; + tone: string; + description: string; +}; + +const LIVENESS_COPY: Record = { completed: { label: "Completed", tone: "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300", @@ -68,7 +74,20 @@ const LIVENESS_COPY: Record(["done", "cancelled"]); +const ACTIVE_RUN_STATUSES = new Set(["queued", "running"]); function asRecord(value: unknown): Record | null { if (typeof value !== "object" || value === null || Array.isArray(value)) return null; @@ -147,6 +166,22 @@ function statusLabel(status: string) { return status.replace(/_/g, " "); } +function isActiveRun(run: Pick) { + return run.isLive || ACTIVE_RUN_STATUSES.has(run.status); +} + +function runSummary(run: LedgerRun, agentMap: ReadonlyMap>) { + const agentName = compactAgentName(run, agentMap); + if (run.status === "running") return `Running now by ${agentName}`; + if (run.status === "queued") return `Queued for ${agentName}`; + return `${statusLabel(run.status)} by ${agentName}`; +} + +function livenessCopyForRun(run: LedgerRun) { + if (run.livenessState) return LIVENESS_COPY[run.livenessState]; + return isActiveRun(run) ? PENDING_LIVENESS_COPY : MISSING_LIVENESS_COPY; +} + function stopReasonLabel(run: RunForIssue) { const result = asRecord(run.resultJson); const stopReason = readString(result?.stopReason); @@ -167,6 +202,30 @@ function stopReasonLabel(run: RunForIssue) { return timeoutText; } +function stopStatusLabel(run: LedgerRun, stopReason: string | null) { + if (stopReason) return stopReason; + if (run.status === "queued") return "Waiting to start"; + if (run.status === "running") return "Still running"; + if (!run.livenessState) return "Unavailable"; + return "No stop reason"; +} + +function lastUsefulActionLabel(run: LedgerRun) { + if (run.lastUsefulActionAt) return relativeTime(run.lastUsefulActionAt); + if (isActiveRun(run)) return "No action recorded yet"; + if (run.livenessState === "plan_only" || run.livenessState === "needs_followup") { + return "No concrete action"; + } + if (run.livenessState === "empty_response") return "No useful output"; + if (!run.livenessState) return "Unavailable"; + return "None recorded"; +} + +function continuationLabel(run: LedgerRun) { + if (!run.continuationAttempt || run.continuationAttempt <= 0) return null; + return `Continuation attempt ${run.continuationAttempt}`; +} + function hasExhaustedContinuation(run: RunForIssue) { return /continuation attempts exhausted/i.test(run.livenessReason ?? ""); } @@ -241,7 +300,7 @@ export function IssueRunLedgerContent({

Run ledger

{latestRun - ? `${statusLabel(latestRun.status)} by ${compactAgentName(latestRun, agentMap)}` + ? runSummary(latestRun, agentMap) : issueStatus === "in_progress" ? "Waiting for the first run record." : "No runs linked yet."} @@ -297,10 +356,11 @@ export function IssueRunLedgerContent({ ) : (

{ledgerRuns.slice(0, 8).map((run) => { - const liveness = run.livenessState ? LIVENESS_COPY[run.livenessState] : null; + const liveness = livenessCopyForRun(run); const stopReason = stopReasonLabel(run); const duration = formatDuration(run.startedAt, run.finishedAt); const exhausted = hasExhaustedContinuation(run); + const continuation = continuationLabel(run); return (
@@ -319,31 +379,23 @@ export function IssueRunLedgerContent({ live ) : null} - {liveness ? ( - - {liveness.label} - - ) : ( - - Liveness pending - - )} + + {liveness.label} + {exhausted ? ( Exhausted ) : null} - - {run.continuationAttempt && run.continuationAttempt > 0 - ? `continuation ${run.continuationAttempt}` - : "initial attempt"} - + {continuation ? ( + {continuation} + ) : null}
@@ -353,11 +405,11 @@ export function IssueRunLedgerContent({
Last useful action{" "} - {run.lastUsefulActionAt ? relativeTime(run.lastUsefulActionAt) : "none recorded"} + {lastUsefulActionLabel(run)}
Stop{" "} - {stopReason ?? "not recorded"} + {stopStatusLabel(run, stopReason)}
diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 4addb853aab..6648375c15b 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -723,7 +723,10 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({ type IssueDetailActivityTabProps = { issueId: string; + issueStatus: Issue["status"]; + childIssues: Issue[]; agentMap: Map; + hasLiveRuns: boolean; currentUserId: string | null; userProfileMap: Map; pendingApprovalAction: { approvalId: string; action: "approve" | "reject" } | null; @@ -732,7 +735,10 @@ type IssueDetailActivityTabProps = { function IssueDetailActivityTab({ issueId, + issueStatus, + childIssues, agentMap, + hasLiveRuns, currentUserId, userProfileMap, pendingApprovalAction, @@ -801,6 +807,15 @@ function IssueDetailActivityTab({ return ( <> +
+ +
{linkedApprovals && linkedApprovals.length > 0 && (
{linkedApprovals.map((approval) => ( @@ -2595,14 +2610,6 @@ export function IssueDetail() { onOpenChange={setGalleryOpen} /> - -