From 00c29afa3f61344f640ea0c4400e291e1c9f1570 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 30 Mar 2026 13:15:57 +1100 Subject: [PATCH 1/3] feat(timeline): add manual Start button for stuck queued sessions Queued sessions can get stuck indefinitely when the drain mechanism fails (e.g. after app restart or agent hang). This adds a "Start" action button on queued timeline rows that manually triggers drainQueuedSessions, giving users an escape hatch to unstick them. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../lib/features/branches/BranchCard.svelte | 5 +++++ .../features/timeline/BranchTimeline.svelte | 3 +++ .../lib/features/timeline/TimelineRow.svelte | 20 ++++++++++++++++--- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/apps/staged/src/lib/features/branches/BranchCard.svelte b/apps/staged/src/lib/features/branches/BranchCard.svelte index cab0169c..2c01da21 100644 --- a/apps/staged/src/lib/features/branches/BranchCard.svelte +++ b/apps/staged/src/lib/features/branches/BranchCard.svelte @@ -862,6 +862,11 @@ onDeleteReview={handleDeleteReview} onImageClick={handleImageClick} onDeleteImage={handleDeleteImage} + onStartQueued={() => { + commands + .drainQueuedSessions(branch.id) + .catch((e) => console.error('Failed to drain queued sessions:', e)); + }} onNewNote={() => sessionMgr.openNewSession('note')} onNewCommit={() => sessionMgr.openNewSession('commit')} onNewReview={hasCodeChanges ? (e) => sessionMgr.openNewSession('review', e) : undefined} diff --git a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte index afd47677..603469aa 100644 --- a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte +++ b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte @@ -50,6 +50,7 @@ onDeleteNote?: (noteId: string, sessionId?: string) => void; onDeleteReview?: (reviewId: string, sessionId?: string) => void; onDeleteImage?: (imageId: string) => void; + onStartQueued?: () => void; /** Optional per-review breakdown of visible comments vs hold-to-reveal annotations. */ reviewCommentBreakdown?: Record< string, @@ -93,6 +94,7 @@ onDeleteNote, onDeleteReview, onDeleteImage, + onStartQueued, reviewCommentBreakdown = {}, onNewNote, onNewCommit, @@ -501,6 +503,7 @@ {onSessionClick} onItemClick={() => handleItemClick(item)} onDeleteClick={item.deleteDisabledReason ? undefined : () => handleDeleteClick(item)} + onStartClick={item.type.startsWith('queued-') ? onStartQueued : undefined} /> {/each} diff --git a/apps/staged/src/lib/features/timeline/TimelineRow.svelte b/apps/staged/src/lib/features/timeline/TimelineRow.svelte index 310f8aff..dba45614 100644 --- a/apps/staged/src/lib/features/timeline/TimelineRow.svelte +++ b/apps/staged/src/lib/features/timeline/TimelineRow.svelte @@ -55,6 +55,7 @@ /** When set, the delete button is shown but disabled with this tooltip. */ deleteDisabledReason?: string; onRetryClick?: () => void; + onStartClick?: () => void; } let { @@ -71,6 +72,7 @@ onDeleteClick, deleteDisabledReason, onRetryClick, + onStartClick, }: Props = $props(); let isNote = $derived( @@ -130,6 +132,11 @@ e.stopPropagation(); onRetryClick?.(); } + + function handleStartClick(e: MouseEvent) { + e.stopPropagation(); + onStartClick?.(); + } @@ -201,7 +208,12 @@ {/if} -
+
+ {#if onStartClick} + + {/if} {#if onRetryClick} {/if} - {#if hasSession} + {#if hasSession && !onStartClick} @@ -492,6 +492,11 @@ color: var(--text-muted); } + .start-btn { + border: 1px solid var(--border-muted); + border-radius: 4px; + } + .retry-btn:hover, .start-btn:hover { color: var(--text-primary); From baa70768047ee31f6ad48821f9fd2ea9d9194858 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 30 Mar 2026 13:33:39 +1100 Subject: [PATCH 3/3] fix(timeline): only show Start button on queued rows when no active session The Start button was always showing on queued rows. Now it only appears when there are no running sessions in the timeline, since the queued session can't start while another is active anyway. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../features/timeline/BranchTimeline.svelte | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte index 603469aa..cc290e4b 100644 --- a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte +++ b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte @@ -172,6 +172,23 @@ let runningSessionIds = $derived.by(() => collectRunningSessionIds(timeline, pendingItems)); + /** True when there is at least one non-queued active session (running in timeline or pending-but-not-queued). */ + let hasActiveSession = $derived.by(() => { + for (const commit of timeline.commits) { + if (commit.sessionStatus === 'running') return true; + } + for (const note of timeline.notes) { + if (note.sessionStatus === 'running') return true; + } + for (const review of timeline.reviews) { + if (review.sessionStatus === 'running') return true; + } + for (const item of pendingItems) { + if (item.sessionId && !item.type.startsWith('queued-')) return true; + } + return false; + }); + $effect(() => { liveSessionHintPoller.syncRunningSessionIds(runningSessionIds); }); @@ -503,7 +520,9 @@ {onSessionClick} onItemClick={() => handleItemClick(item)} onDeleteClick={item.deleteDisabledReason ? undefined : () => handleDeleteClick(item)} - onStartClick={item.type.startsWith('queued-') ? onStartQueued : undefined} + onStartClick={item.type.startsWith('queued-') && !hasActiveSession + ? onStartQueued + : undefined} />
{/each}