mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 05:11:49 +01:00
Clarify issue run ledger statuses
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
cd2c2995e3
commit
d8f6623afa
3 files changed
+124
-38
No files matched your search
@@ -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", () => {
|
||||
|
||||
@@ -30,7 +30,13 @@ type LedgerRun = RunForIssue & {
|
||||
agentName?: string;
|
||||
};
|
||||
|
||||
const LIVENESS_COPY: Record<RunLivenessState, { label: string; tone: string; description: string }> = {
|
||||
type LivenessCopy = {
|
||||
label: string;
|
||||
tone: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
const LIVENESS_COPY: Record<RunLivenessState, LivenessCopy> = {
|
||||
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<RunLivenessState, { label: string; tone: string; des
|
||||
},
|
||||
};
|
||||
|
||||
const PENDING_LIVENESS_COPY: LivenessCopy = {
|
||||
label: "Checks after finish",
|
||||
tone: "border-border bg-background text-muted-foreground",
|
||||
description: "Liveness is evaluated after the run finishes.",
|
||||
};
|
||||
|
||||
const MISSING_LIVENESS_COPY: LivenessCopy = {
|
||||
label: "No liveness data",
|
||||
tone: "border-border bg-background text-muted-foreground",
|
||||
description: "This run has no persisted liveness classification.",
|
||||
};
|
||||
|
||||
const TERMINAL_CHILD_STATUSES = new Set<Issue["status"]>(["done", "cancelled"]);
|
||||
const ACTIVE_RUN_STATUSES = new Set(["queued", "running"]);
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | 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<LedgerRun, "status" | "isLive">) {
|
||||
return run.isLive || ACTIVE_RUN_STATUSES.has(run.status);
|
||||
}
|
||||
|
||||
function runSummary(run: LedgerRun, agentMap: ReadonlyMap<string, Pick<Agent, "name">>) {
|
||||
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({
|
||||
<h3 className="text-sm font-medium text-muted-foreground">Run ledger</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{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({
|
||||
) : (
|
||||
<div className="divide-y divide-border rounded-md border border-border/70">
|
||||
{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 (
|
||||
<article key={run.runId} className="space-y-2 px-3 py-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -319,31 +379,23 @@ export function IssueRunLedgerContent({
|
||||
live
|
||||
</span>
|
||||
) : null}
|
||||
{liveness ? (
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-md border px-1.5 py-0.5 text-[11px] font-medium",
|
||||
liveness.tone,
|
||||
)}
|
||||
title={liveness.description}
|
||||
>
|
||||
{liveness.label}
|
||||
</span>
|
||||
) : (
|
||||
<span className="rounded-md border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground">
|
||||
Liveness pending
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-md border px-1.5 py-0.5 text-[11px] font-medium",
|
||||
liveness.tone,
|
||||
)}
|
||||
title={liveness.description}
|
||||
>
|
||||
{liveness.label}
|
||||
</span>
|
||||
{exhausted ? (
|
||||
<span className="rounded-md border border-red-500/30 bg-red-500/10 px-1.5 py-0.5 text-[11px] font-medium text-red-700 dark:text-red-300">
|
||||
Exhausted
|
||||
</span>
|
||||
) : null}
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{run.continuationAttempt && run.continuationAttempt > 0
|
||||
? `continuation ${run.continuationAttempt}`
|
||||
: "initial attempt"}
|
||||
</span>
|
||||
{continuation ? (
|
||||
<span className="text-[11px] text-muted-foreground">{continuation}</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 text-xs text-muted-foreground sm:grid-cols-3">
|
||||
@@ -353,11 +405,11 @@ export function IssueRunLedgerContent({
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<span className="text-foreground">Last useful action</span>{" "}
|
||||
{run.lastUsefulActionAt ? relativeTime(run.lastUsefulActionAt) : "none recorded"}
|
||||
{lastUsefulActionLabel(run)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<span className="text-foreground">Stop</span>{" "}
|
||||
{stopReason ?? "not recorded"}
|
||||
{stopStatusLabel(run, stopReason)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -723,7 +723,10 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({
|
||||
|
||||
type IssueDetailActivityTabProps = {
|
||||
issueId: string;
|
||||
issueStatus: Issue["status"];
|
||||
childIssues: Issue[];
|
||||
agentMap: Map<string, Agent>;
|
||||
hasLiveRuns: boolean;
|
||||
currentUserId: string | null;
|
||||
userProfileMap: Map<string, import("../lib/company-members").CompanyUserProfile>;
|
||||
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 (
|
||||
<>
|
||||
<div className="mb-3">
|
||||
<IssueRunLedger
|
||||
issueId={issueId}
|
||||
issueStatus={issueStatus}
|
||||
childIssues={childIssues}
|
||||
agentMap={agentMap}
|
||||
hasLiveRuns={hasLiveRuns}
|
||||
/>
|
||||
</div>
|
||||
{linkedApprovals && linkedApprovals.length > 0 && (
|
||||
<div className="mb-3 space-y-3">
|
||||
{linkedApprovals.map((approval) => (
|
||||
@@ -2595,14 +2610,6 @@ export function IssueDetail() {
|
||||
onOpenChange={setGalleryOpen}
|
||||
/>
|
||||
|
||||
<IssueRunLedger
|
||||
issueId={issue.id}
|
||||
issueStatus={issue.status}
|
||||
childIssues={childIssues}
|
||||
agentMap={agentMap}
|
||||
hasLiveRuns={hasLiveRuns}
|
||||
/>
|
||||
|
||||
<IssueWorkspaceCard
|
||||
issue={issue}
|
||||
project={resolvedProject}
|
||||
@@ -2670,7 +2677,10 @@ export function IssueDetail() {
|
||||
{detailTab === "activity" ? (
|
||||
<IssueDetailActivityTab
|
||||
issueId={issue.id}
|
||||
issueStatus={issue.status}
|
||||
childIssues={childIssues}
|
||||
agentMap={agentMap}
|
||||
hasLiveRuns={hasLiveRuns}
|
||||
currentUserId={currentUserId}
|
||||
userProfileMap={userProfileMap}
|
||||
pendingApprovalAction={pendingApprovalAction}
|
||||
|
||||
Reference in new issue
Block a user