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
60 changes: 30 additions & 30 deletions apps/staged/src-tauri/src/timeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,42 +42,42 @@ fn build_branch_timeline(store: &Arc<Store>, branch_id: &str) -> Result<BranchTi
format!("{}..HEAD", &branch.base_branch)
};
let format_arg = "--format=%H|%h|%s|%an|%ct";
if let Ok(output) = branches::run_workspace_git(
let output = branches::run_workspace_git(
ws_name,
repo_subpath.as_deref(),
&["log", format_arg, &range],
) {
// git log returns newest-first; parse then assign order so 0 = oldest.
for line in output.lines() {
if line.is_empty() {
continue;
}
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,
});
}
)
.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);
Expand Down
27 changes: 19 additions & 8 deletions apps/staged/src/lib/features/branches/BranchCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,7 @@
{:else if error && !timeline}
<div class="error">
<span>{error}</span>
<button class="retry-btn" onclick={() => loadTimeline()}>Retry</button>
</div>
{:else if timeline || isProvisioning}
<BranchTimeline
Expand All @@ -847,6 +848,8 @@
pendingItems={sessionMgr.pendingSessionItems}
{prunedSessionIds}
{revalidating}
{error}
onRetry={() => loadTimeline()}
deletingItems={timelineDeletingItems}
reviewCommentBreakdown={timelineReviewDetailsById}
onSessionClick={(sid) => sessionMgr.handleTimelineSessionClick(sid)}
Expand Down Expand Up @@ -895,11 +898,6 @@
{/if}
{/snippet}
</BranchTimeline>
{#if error}
<div class="error revalidation-error">
<span>{error}</span>
</div>
{/if}
{/if}
</div>
{/if}
Expand Down Expand Up @@ -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 */
Expand Down
33 changes: 30 additions & 3 deletions apps/staged/src/lib/features/timeline/BranchTimeline.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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). */
Expand Down Expand Up @@ -95,6 +99,8 @@
onNewReview,
newSessionDisabled = false,
revalidating = false,
error,
onRetry,
provisioningLabel,
provisioningDetail,
footerActions,
Expand Down Expand Up @@ -488,7 +494,8 @@
!onNewCommit &&
pendingDropNotes.length === 0 &&
pendingItems.length === 0 &&
!revalidating}
!revalidating &&
!error}
sessionId={item.sessionId}
deleteDisabledReason={item.deleteDisabledReason}
{onSessionClick}
Expand All @@ -506,6 +513,7 @@
isLast={index === pendingDropNotes.length - 1 &&
pendingItems.length === 0 &&
!revalidating &&
!error &&
!onNewNote &&
!onNewCommit}
/>
Expand All @@ -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}
/>
</div>
{/each}
{#if revalidating}
<div transition:slide={{ duration: 200 }}>
<TimelineRow type="revalidating" title="Looking for changes..." isLast={true} />
<TimelineRow
type="revalidating"
title="Looking for changes..."
isLast={!error && !onNewNote && !onNewCommit}
/>
</div>
{/if}
{#if error && !revalidating}
<div transition:slide={{ duration: 200 }}>
<TimelineRow
type="load-error"
title="Failed to load commits"
secondaryMeta={error}
isLast={true}
onRetryClick={onRetry}
/>
</div>
{/if}
{#if onNewNote || onNewCommit || onNewReview || footerActions}
Expand Down
40 changes: 35 additions & 5 deletions apps/staged/src/lib/features/timeline/TimelineRow.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
| 'failed-review'
| 'image'
| 'revalidating'
| 'provisioning';
| 'provisioning'
| 'load-error';

export type TimelineBadge = {
icon: 'comment' | 'warning';
Expand All @@ -53,6 +54,7 @@
onDeleteClick?: () => void;
/** When set, the delete button is shown but disabled with this tooltip. */
deleteDisabledReason?: string;
onRetryClick?: () => void;
}

let {
Expand All @@ -68,6 +70,7 @@
onSessionClick,
onDeleteClick,
deleteDisabledReason,
onRetryClick,
}: Props = $props();

let isNote = $derived(
Expand Down Expand Up @@ -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);
Expand All @@ -118,6 +125,11 @@
e.stopPropagation();
onDeleteClick?.();
}

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

<!-- svelte-ignore a11y_click_events_have_key_events -->
Expand All @@ -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}
>
<div class="timeline-marker">
Expand Down Expand Up @@ -189,7 +201,12 @@
</div>
{/if}
</div>
<div class="timeline-actions">
<div class="timeline-actions" class:always-visible={!!onRetryClick}>
{#if onRetryClick}
<button class="action-btn retry-btn" onclick={handleRetryClick} title="Retry">
Retry
</button>
{/if}
{#if hasSession}
<button class="action-btn session-btn" onclick={handleSessionClick} title="View session">
<MessageSquare size={12} />
Expand Down Expand Up @@ -418,7 +435,8 @@
transition: opacity 0.1s;
}

.timeline-row:hover .timeline-actions {
.timeline-row:hover .timeline-actions,
.timeline-actions.always-visible {
opacity: 1;
}

Expand Down Expand Up @@ -454,6 +472,18 @@
cursor: not-allowed;
}

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

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

@keyframes pulse {
0%,
100% {
Expand Down