Skip to content

Commit cae247f

Browse files
JustYanniccclaude
andcommitted
fix(web): unopened and plan-mode failures also show Failed
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 2fcf697 commit cae247f

2 files changed

Lines changed: 51 additions & 9 deletions

File tree

‎apps/web/src/components/Sidebar.logic.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,6 +1387,34 @@ describe("resolveThreadStatusPill", () => {
13871387
).toMatchObject({ label: "Failed", pulse: false });
13881388
});
13891389

1390+
it("shows failed for a thread nobody has opened yet", () => {
1391+
expect(
1392+
resolveThreadStatusPill({
1393+
thread: {
1394+
...baseThread,
1395+
interactionMode: "default",
1396+
latestRun: { ...makeLatestRun(), status: "failed" },
1397+
lastVisitedAt: null,
1398+
runtime: { ...baseThread.runtime, status: "failed", activeRunId: null },
1399+
},
1400+
}),
1401+
).toMatchObject({ label: "Failed" });
1402+
});
1403+
1404+
it("shows failed, not plan ready, when a plan run proposed a plan and then failed", () => {
1405+
expect(
1406+
resolveThreadStatusPill({
1407+
thread: {
1408+
...baseThread,
1409+
hasActionableProposedPlan: true,
1410+
latestRun: { ...makeLatestRun(), status: "failed" },
1411+
lastVisitedAt: "2026-03-09T10:04:00.000Z",
1412+
runtime: { ...baseThread.runtime, status: "failed", activeRunId: null },
1413+
},
1414+
}),
1415+
).toMatchObject({ label: "Failed" });
1416+
});
1417+
13901418
it("drops the failed pill once the failure has been seen", () => {
13911419
expect(
13921420
resolveThreadStatusPill({

‎apps/web/src/components/Sidebar.logic.ts‎

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,20 @@ export function hasUnseenCompletion(thread: ThreadStatusInput): boolean {
720720
return completedAt > lastVisitedAt;
721721
}
722722

723+
/**
724+
* A failed latest run the person hasn't looked at since it ended. Unlike a
725+
* completion, a thread nobody has opened yet (an automation's, say) counts:
726+
* its failure still needs someone.
727+
*/
728+
export function hasUnseenFailure(thread: ThreadStatusInput): boolean {
729+
if (thread.latestRun?.status !== "failed") return false;
730+
if (!thread.lastVisitedAt) return true;
731+
const completedAt = Date.parse(thread.latestRun.completedAt ?? "");
732+
const lastVisitedAt = Date.parse(thread.lastVisitedAt);
733+
if (Number.isNaN(completedAt) || Number.isNaN(lastVisitedAt)) return true;
734+
return completedAt > lastVisitedAt;
735+
}
736+
723737
export function shouldClearThreadSelectionOnMouseDown(target: HTMLElement | null): boolean {
724738
if (target === null) return true;
725739
return !target.closest(THREAD_SELECTION_SAFE_SELECTOR);
@@ -1133,6 +1147,15 @@ export function resolveThreadStatusPill(input: {
11331147
};
11341148
}
11351149

1150+
if (hasUnseenFailure(thread)) {
1151+
return {
1152+
label: "Failed",
1153+
colorClass: "text-red-600 dark:text-red-300/90",
1154+
dotClass: "bg-red-500 dark:bg-red-300/90",
1155+
pulse: false,
1156+
};
1157+
}
1158+
11361159
const hasPlanReadyPrompt =
11371160
!thread.hasPendingUserInput &&
11381161
thread.interactionMode === "plan" &&
@@ -1147,15 +1170,6 @@ export function resolveThreadStatusPill(input: {
11471170
};
11481171
}
11491172

1150-
if (hasUnseenCompletion(thread) && thread.latestRun?.status === "failed") {
1151-
return {
1152-
label: "Failed",
1153-
colorClass: "text-red-600 dark:text-red-300/90",
1154-
dotClass: "bg-red-500 dark:bg-red-300/90",
1155-
pulse: false,
1156-
};
1157-
}
1158-
11591173
if (hasUnseenCompletion(thread)) {
11601174
return {
11611175
label: "Completed",

0 commit comments

Comments
 (0)