diff --git a/apps/staged/src-tauri/src/timeline.rs b/apps/staged/src-tauri/src/timeline.rs index dd7295cab..d2c5f26cf 100644 --- a/apps/staged/src-tauri/src/timeline.rs +++ b/apps/staged/src-tauri/src/timeline.rs @@ -42,42 +42,42 @@ fn build_branch_timeline(store: &Arc, branch_id: &str) -> Result = line.splitn(5, '|').collect(); - if parts.len() >= 5 { - let sha = parts[0].to_string(); - let our_commit = store.get_commit_by_sha(branch_id, &sha).unwrap_or(None); - let (session_id, session_status) = store.resolve_session_status( - our_commit.as_ref().and_then(|c| c.session_id.as_deref()), - ); - - commits.push(CommitTimelineItem { - id: our_commit.as_ref().map(|c| c.id.clone()), - sha, - short_sha: parts[1].to_string(), - subject: parts[2].to_string(), - author: parts[3].to_string(), - timestamp: parts[4].parse().unwrap_or(0), - order: 0, // placeholder, assigned below - session_id, - session_status, - }); - } + ) + .map_err(|e| format!("Failed to load commits from workspace: {e}"))?; + // git log returns newest-first; parse then assign order so 0 = oldest. + for line in output.lines() { + if line.is_empty() { + continue; } - let len = commits.len() as i64; - for (i, commit) in commits.iter_mut().enumerate() { - commit.order = len - 1 - i as i64; + let parts: Vec<&str> = line.splitn(5, '|').collect(); + if parts.len() >= 5 { + let sha = parts[0].to_string(); + let our_commit = store.get_commit_by_sha(branch_id, &sha).unwrap_or(None); + let (session_id, session_status) = store.resolve_session_status( + our_commit.as_ref().and_then(|c| c.session_id.as_deref()), + ); + + commits.push(CommitTimelineItem { + id: our_commit.as_ref().map(|c| c.id.clone()), + sha, + short_sha: parts[1].to_string(), + subject: parts[2].to_string(), + author: parts[3].to_string(), + timestamp: parts[4].parse().unwrap_or(0), + order: 0, // placeholder, assigned below + session_id, + session_status, + }); } } + let len = commits.len() as i64; + for (i, commit) in commits.iter_mut().enumerate() { + commit.order = len - 1 - i as i64; + } } else if let Some(ref wd) = workdir { // Local branch: fetch commits from the local worktree let worktree_path = Path::new(&wd.path); diff --git a/apps/staged/src/lib/features/branches/BranchCard.svelte b/apps/staged/src/lib/features/branches/BranchCard.svelte index 7584ffc78..cab0169c7 100644 --- a/apps/staged/src/lib/features/branches/BranchCard.svelte +++ b/apps/staged/src/lib/features/branches/BranchCard.svelte @@ -838,6 +838,7 @@ {:else if error && !timeline}
{error} +
{:else if timeline || isProvisioning} loadTimeline()} deletingItems={timelineDeletingItems} reviewCommentBreakdown={timelineReviewDetailsById} onSessionClick={(sid) => sessionMgr.handleTimelineSessionClick(sid)} @@ -895,11 +898,6 @@ {/if} {/snippet} - {#if error} -
- {error} -
- {/if} {/if} {/if} @@ -1153,13 +1151,26 @@ } .error { + display: flex; + align-items: center; + gap: 8px; color: var(--ui-danger); font-size: var(--size-sm); } - .revalidation-error { - padding: 4px 12px; - color: var(--text-faint); + .error .retry-btn { + padding: 2px 10px; + border-radius: 4px; + border: 1px solid var(--border-subtle); + background: none; + color: var(--text-muted); + font-size: var(--size-xs); + cursor: pointer; + } + + .error .retry-btn:hover { + color: var(--text-primary); + background: var(--bg-hover); } /* Worktree error state */ diff --git a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte index cff040c44..afd47677f 100644 --- a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte +++ b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte @@ -65,6 +65,10 @@ newSessionDisabled?: boolean; /** Whether the timeline is being revalidated in the background. */ revalidating?: boolean; + /** Error message from a failed load/revalidation. */ + error?: string | null; + /** Callback to retry loading the timeline. */ + onRetry?: () => void; /** When set, a provisioning row is shown at the start of the timeline. */ provisioningLabel?: string; /** Optional detail text for the provisioning row (e.g. git progress). */ @@ -95,6 +99,8 @@ onNewReview, newSessionDisabled = false, revalidating = false, + error, + onRetry, provisioningLabel, provisioningDetail, footerActions, @@ -488,7 +494,8 @@ !onNewCommit && pendingDropNotes.length === 0 && pendingItems.length === 0 && - !revalidating} + !revalidating && + !error} sessionId={item.sessionId} deleteDisabledReason={item.deleteDisabledReason} {onSessionClick} @@ -506,6 +513,7 @@ isLast={index === pendingDropNotes.length - 1 && pendingItems.length === 0 && !revalidating && + !error && !onNewNote && !onNewCommit} /> @@ -521,13 +529,32 @@ item.secondaryMeta ?? fallbackHintForPendingType(item.type)) : item.secondaryMeta} - isLast={index === pendingItems.length - 1 && !revalidating && !onNewNote && !onNewCommit} + isLast={index === pendingItems.length - 1 && + !revalidating && + !error && + !onNewNote && + !onNewCommit} /> {/each} {#if revalidating}
- + +
+ {/if} + {#if error && !revalidating} +
+
{/if} {#if onNewNote || onNewCommit || onNewReview || footerActions} diff --git a/apps/staged/src/lib/features/timeline/TimelineRow.svelte b/apps/staged/src/lib/features/timeline/TimelineRow.svelte index 8377d1d66..310f8affb 100644 --- a/apps/staged/src/lib/features/timeline/TimelineRow.svelte +++ b/apps/staged/src/lib/features/timeline/TimelineRow.svelte @@ -32,7 +32,8 @@ | 'failed-review' | 'image' | 'revalidating' - | 'provisioning'; + | 'provisioning' + | 'load-error'; export type TimelineBadge = { icon: 'comment' | 'warning'; @@ -53,6 +54,7 @@ onDeleteClick?: () => void; /** When set, the delete button is shown but disabled with this tooltip. */ deleteDisabledReason?: string; + onRetryClick?: () => void; } let { @@ -68,6 +70,7 @@ onSessionClick, onDeleteClick, deleteDisabledReason, + onRetryClick, }: Props = $props(); let isNote = $derived( @@ -96,7 +99,11 @@ type === 'provisioning' ); let isFailed = $derived( - !deleting && (type === 'failed-commit' || type === 'failed-note' || type === 'failed-review') + !deleting && + (type === 'failed-commit' || + type === 'failed-note' || + type === 'failed-review' || + type === 'load-error') ); let isClickable = $derived(!!onItemClick && !isPending && !isFailed); let hasSession = $derived(!!sessionId && !deleting); @@ -118,6 +125,11 @@ e.stopPropagation(); onDeleteClick?.(); } + + function handleRetryClick(e: MouseEvent) { + e.stopPropagation(); + onRetryClick?.(); + } @@ -127,7 +139,7 @@ class:pending={isPending} class:failed={isFailed} class:clickable={isClickable} - class:compact={type === 'revalidating'} + class:compact={type === 'revalidating' || type === 'load-error'} onclick={handleRowClick} >
@@ -189,7 +201,12 @@
{/if} -
+
+ {#if onRetryClick} + + {/if} {#if hasSession}