diff --git a/apps/staged/src/lib/features/branches/BranchCard.svelte b/apps/staged/src/lib/features/branches/BranchCard.svelte
index d3c5dc81e..90c756a2f 100644
--- a/apps/staged/src/lib/features/branches/BranchCard.svelte
+++ b/apps/staged/src/lib/features/branches/BranchCard.svelte
@@ -995,6 +995,20 @@
deletingItems={timelineDeletingItems}
reviewCommentBreakdown={timelineReviewDetailsById}
onSessionClick={(sid) => sessionMgr.handleTimelineSessionClick(sid)}
+ onResumeClick={(sid) => {
+ commands
+ .resumeSession(sid, 'Continue where you left off.', undefined, branch.id)
+ .then(() => loadTimeline())
+ .catch((e) => {
+ console.error('Failed to resume session:', e);
+ alerts.error(
+ e instanceof Error
+ ? e.message
+ : 'Could not resume the session. Please try again.',
+ 'Resume failed'
+ );
+ });
+ }}
onCommitClick={handleCommitClick}
onNoteClick={handleNoteClick}
onReviewClick={handleReviewClick}
diff --git a/apps/staged/src/lib/features/sessions/SessionModal.svelte b/apps/staged/src/lib/features/sessions/SessionModal.svelte
index 8e376b9d2..23d2d0bde 100644
--- a/apps/staged/src/lib/features/sessions/SessionModal.svelte
+++ b/apps/staged/src/lib/features/sessions/SessionModal.svelte
@@ -45,6 +45,7 @@
import Spinner from '../../shared/Spinner.svelte';
import { marked } from 'marked';
import { sanitize } from '../../shared/sanitize';
+ import { isResumableReason } from '../../types';
import type { Session, SessionMessage, HashtagItem } from '../../types';
import {
cancelSession,
@@ -1215,7 +1216,7 @@
{session.errorMessage}
{:else if session && session.status !== 'running' && session.status !== 'queued'}
- {#if session.completionReason === 'crashed' || session.completionReason === 'app_quit' || session.completionReason === 'interrupted'}
+ {#if isResumableReason(session.completionReason)}
void;
+ onResumeClick?: (sessionId: string) => void;
onCommitClick?: (sha: string) => void;
onNoteClick?: (noteId: string, title: string, content: string, sessionId?: string) => void;
onReviewClick?: (reviewId: string) => void;
@@ -95,6 +97,7 @@
prunedSessionIds = new Set(),
deletingItems = [],
onSessionClick,
+ onResumeClick,
onCommitClick,
onNoteClick,
onReviewClick,
@@ -199,6 +202,7 @@
badges?: TimelineBadge[];
/** When set, delete button is shown but disabled with this tooltip. */
deleteDisabledReason?: string;
+ completionReason?: string | null;
};
let runningSessionIds = $derived.by(() => collectRunningSessionIds(timeline, pendingItems));
@@ -285,6 +289,7 @@
commitSha: commit.sha || undefined,
commitId: commit.id ?? undefined,
deleteDisabledReason: isDeleting ? 'Deleting...' : undefined,
+ completionReason: commit.completionReason,
});
}
@@ -326,6 +331,7 @@
noteTitle: stripXmlTags(note.title),
noteContent: note.content,
deleteDisabledReason: isDeleting ? 'Deleting...' : undefined,
+ completionReason: note.completionReason,
});
}
@@ -388,6 +394,7 @@
sessionId: review.sessionId ?? undefined,
reviewId: review.id,
deleteDisabledReason: isDeleting ? 'Deleting...' : undefined,
+ completionReason: review.completionReason,
});
}
@@ -488,6 +495,10 @@
}
}
+ function isResumable(item: DisplayItem): boolean {
+ return !!item.sessionId && isResumableReason(item.completionReason) && !item.deleting;
+ }
+
function handleDeleteClick(item: DisplayItem, opts?: { altKey: boolean }) {
if (item.type === 'commit' && item.commitSha && onDeleteCommit) {
onDeleteCommit(item.commitSha, item.sessionId, opts);
@@ -555,6 +566,9 @@
onStartClick={item.type.startsWith('queued-') && !hasActiveSession
? onStartQueued
: undefined}
+ onResumeClick={isResumable(item) && onResumeClick && item.sessionId && !hasActiveSession
+ ? () => onResumeClick!(item.sessionId!)
+ : undefined}
/>
{/each}
diff --git a/apps/staged/src/lib/features/timeline/TimelineRow.svelte b/apps/staged/src/lib/features/timeline/TimelineRow.svelte
index 2f825ea90..7a3dfbef2 100644
--- a/apps/staged/src/lib/features/timeline/TimelineRow.svelte
+++ b/apps/staged/src/lib/features/timeline/TimelineRow.svelte
@@ -58,6 +58,7 @@
deleteDisabledReason?: string;
onRetryClick?: () => void;
onStartClick?: () => void;
+ onResumeClick?: () => void;
}
let {
@@ -76,6 +77,7 @@
deleteDisabledReason,
onRetryClick,
onStartClick,
+ onResumeClick,
}: Props = $props();
let isNote = $derived(
@@ -140,6 +142,11 @@
e.stopPropagation();
onStartClick?.();
}
+
+ function handleResumeClick(e: MouseEvent) {
+ e.stopPropagation();
+ onResumeClick?.();
+ }
@@ -217,7 +224,10 @@
{/if}
-
+
{#if onStartClick}
{/if}
+ {#if onResumeClick}
+
+ {/if}
{#if hasSession && !onStartClick && !isQueued}