Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/staged/src/lib/features/branches/BranchCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
22 changes: 22 additions & 0 deletions apps/staged/src/lib/features/timeline/BranchTimeline.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -93,6 +94,7 @@
onDeleteNote,
onDeleteReview,
onDeleteImage,
onStartQueued,
reviewCommentBreakdown = {},
onNewNote,
onNewCommit,
Expand Down Expand Up @@ -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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Treat pending starts as active before showing Start

hasActiveSession currently ignores non-queued pendingItems until they have a sessionId, but startBranchSessionWithPendingItem creates a non-queued placeholder before the ID is assigned. In that window, queued rows still get onStartClick, so a user can click Start and trigger drainQueuedSessions while another start request is already in flight. Because start_branch_session does not enforce a branch-level running-session guard, this can launch concurrent sessions on the same branch. Count any non-queued pending item as active (even without sessionId) or otherwise block Start during session-start pending.

Useful? React with 👍 / 👎.

}
return false;
});

$effect(() => {
liveSessionHintPoller.syncRunningSessionIds(runningSessionIds);
});
Expand Down Expand Up @@ -501,6 +520,9 @@
{onSessionClick}
onItemClick={() => handleItemClick(item)}
onDeleteClick={item.deleteDisabledReason ? undefined : () => handleDeleteClick(item)}
onStartClick={item.type.startsWith('queued-') && !hasActiveSession
? onStartQueued
: undefined}
/>
</div>
{/each}
Expand Down
27 changes: 23 additions & 4 deletions apps/staged/src/lib/features/timeline/TimelineRow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
/** When set, the delete button is shown but disabled with this tooltip. */
deleteDisabledReason?: string;
onRetryClick?: () => void;
onStartClick?: () => void;
}

let {
Expand All @@ -71,6 +72,7 @@
onDeleteClick,
deleteDisabledReason,
onRetryClick,
onStartClick,
}: Props = $props();

let isNote = $derived(
Expand Down Expand Up @@ -130,6 +132,11 @@
e.stopPropagation();
onRetryClick?.();
}

function handleStartClick(e: MouseEvent) {
e.stopPropagation();
onStartClick?.();
}
</script>

<!-- svelte-ignore a11y_click_events_have_key_events -->
Expand Down Expand Up @@ -201,13 +208,18 @@
</div>
{/if}
</div>
<div class="timeline-actions" class:always-visible={!!onRetryClick}>
<div class="timeline-actions" class:always-visible={!!onRetryClick || !!onStartClick}>
{#if onStartClick}
<button class="action-btn start-btn" onclick={handleStartClick} title="Start">
Start
</button>
{/if}
{#if onRetryClick}
<button class="action-btn retry-btn" onclick={handleRetryClick} title="Retry">
Retry
</button>
{/if}
{#if hasSession}
{#if hasSession && !onStartClick}
<button class="action-btn session-btn" onclick={handleSessionClick} title="View session">
<MessageSquare size={12} />
</button>
Expand Down Expand Up @@ -472,14 +484,21 @@
cursor: not-allowed;
}

.retry-btn {
.retry-btn,
.start-btn {
width: auto;
padding: 0 8px;
font-size: var(--size-xs);
color: var(--text-muted);
}

.retry-btn:hover {
.start-btn {
border: 1px solid var(--border-muted);
border-radius: 4px;
}

.retry-btn:hover,
.start-btn:hover {
color: var(--text-primary);
background: var(--bg-hover);
}
Expand Down