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..cc290e4b 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, @@ -170,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); }); @@ -501,6 +520,9 @@ {onSessionClick} onItemClick={() => handleItemClick(item)} onDeleteClick={item.deleteDisabledReason ? undefined : () => handleDeleteClick(item)} + onStartClick={item.type.startsWith('queued-') && !hasActiveSession + ? 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..cc199571 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,13 +208,18 @@ {/if} -