+ {isRestorable ? (
+
+ ) : null}
{pr ? (
Open PR #{pr.number}
diff --git a/packages/web/src/components/__tests__/Dashboard.doneBar.test.tsx b/packages/web/src/components/__tests__/Dashboard.doneBar.test.tsx
index 24cfe6a05..6d838dd70 100644
--- a/packages/web/src/components/__tests__/Dashboard.doneBar.test.tsx
+++ b/packages/web/src/components/__tests__/Dashboard.doneBar.test.tsx
@@ -60,10 +60,10 @@ describe("Dashboard done bar", () => {
expect(screen.queryByText(/No active sessions/i)).not.toBeInTheDocument();
});
- it("does not render a restore action for merged sessions", () => {
+ it("renders a restore action for merged sessions", () => {
render(
);
const toggle = screen.getByText("Done / Terminated").closest("button")!;
fireEvent.click(toggle);
- expect(screen.queryByRole("button", { name: /restore/i })).toBeNull();
+ expect(screen.getByRole("button", { name: /restore/i })).toBeInTheDocument();
});
});
diff --git a/packages/web/src/components/__tests__/SessionDetail.desktop.test.tsx b/packages/web/src/components/__tests__/SessionDetail.desktop.test.tsx
index 07a77e48f..d92e80e6a 100644
--- a/packages/web/src/components/__tests__/SessionDetail.desktop.test.tsx
+++ b/packages/web/src/components/__tests__/SessionDetail.desktop.test.tsx
@@ -223,6 +223,32 @@ describe("SessionDetail desktop layout", () => {
"/projects/my-app",
);
expect(screen.queryByTestId("direct-terminal")).not.toBeInTheDocument();
+ // The ended-session body also exposes a prominent "Restore session" button
+ // so users don't have to find the small icon in the header.
+ expect(
+ within(screen.getByRole("region", { name: "Session ended summary" })).getByRole("button", {
+ name: "Restore session",
+ }),
+ ).toBeInTheDocument();
+ });
+
+ it("shows the Restore button in the ended-summary for pr_merged sessions (status=cleanup)", () => {
+ render(
+
,
+ );
+ expect(
+ within(screen.getByRole("region", { name: "Session ended summary" })).getByRole("button", {
+ name: "Restore session",
+ }),
+ ).toBeInTheDocument();
});
it("keeps restored working sessions live when terminatedAt is stale", () => {
diff --git a/packages/web/src/lib/__tests__/types.test.ts b/packages/web/src/lib/__tests__/types.test.ts
index ceee22169..04b6036a1 100644
--- a/packages/web/src/lib/__tests__/types.test.ts
+++ b/packages/web/src/lib/__tests__/types.test.ts
@@ -264,7 +264,7 @@ describe("getAttentionLevel", () => {
});
describe("restore affordances", () => {
- it("should not mark merged sessions as restorable", () => {
+ it("should not mark a running merged session as restorable (runtime still alive)", () => {
const session = createSession({
status: "merged",
lifecycle: {
@@ -298,6 +298,41 @@ describe("getAttentionLevel", () => {
expect(isDashboardSessionRestorable(session)).toBe(false);
});
+
+ it("should mark a pr_merged-cleanup session as restorable", () => {
+ const session = createSession({
+ status: "cleanup",
+ lifecycle: {
+ sessionState: "terminated",
+ sessionReason: "pr_merged",
+ prState: "merged",
+ prReason: "merged",
+ runtimeState: "missing",
+ runtimeReason: "pr_merged_cleanup",
+ session: {
+ state: "terminated",
+ reason: "pr_merged",
+ label: "terminated",
+ reasonLabel: "pr merged",
+ },
+ pr: { state: "merged", reason: "merged", label: "merged", reasonLabel: "merged" },
+ runtime: {
+ state: "missing",
+ reason: "pr_merged_cleanup",
+ label: "missing",
+ reasonLabel: "pr merged cleanup",
+ },
+ legacyStatus: "cleanup",
+ evidence: null,
+ detectingAttempts: 0,
+ detectingEscalatedAt: null,
+ summary: "Session cleaned up after PR merge",
+ guidance: null,
+ },
+ });
+
+ expect(isDashboardSessionRestorable(session)).toBe(true);
+ });
});
describe("respond state", () => {
diff --git a/packages/web/src/lib/types.ts b/packages/web/src/lib/types.ts
index 5174e0782..85f93d225 100644
--- a/packages/web/src/lib/types.ts
+++ b/packages/web/src/lib/types.ts
@@ -436,12 +436,9 @@ export function isDashboardSessionRestorable(session: DashboardSession): boolean
isDashboardSessionTerminated(session) ||
session.lifecycle.runtimeState === "missing" ||
session.lifecycle.runtimeState === "exited";
- return (
- terminalByCoreTruth && session.lifecycle.prState !== "merged" && session.status !== "merged"
- );
+ return terminalByCoreTruth && !NON_RESTORABLE_STATUSES.has(session.status);
}
- if (!isDashboardSessionTerminal(session)) return false;
- return session.pr?.state !== "merged" && session.status !== "merged";
+ return isDashboardSessionTerminal(session) && !NON_RESTORABLE_STATUSES.has(session.status);
}
/**