setScrollTop(event.currentTarget.scrollTop)}
+ className="min-w-0 flex-1 overflow-auto"
+ >
+ {loading ? (
+
+
+ Reading repository history…
+
+ ) : error ? (
+
+
Could not load the commit graph
+
{error}
+
+ ) : rows.length === 0 ? (
+
+ {isFiltering ? `Nothing matches “${query.trim()}”.` : 'No commits to show yet.'}
+
+ ) : (
+ <>
+ {/*
+ Work git has not been told about yet. Every other client calls
+ this the WIP row; Pane can name the session it belongs to.
+ */}
+ {!isFiltering && dirtySessions.map(session => (
+
openSession(session.id)}
+ className="flex w-full items-center gap-2 border-b border-dashed border-border-secondary px-3 py-1.5 text-left transition-colors hover:bg-surface-hover"
+ >
+
+
+
+
+ Uncommitted changes in {session.name}
+
+
+ {session.gitStatus?.filesChanged ? `${session.gitStatus.filesChanged} files` : 'modified'}
+
+
+ ))}
+
+
+
+ {visibleRows.map(row => {
+ const isSelected = row.node.hash === selectedHash;
+ const accent = laneColor(row.colorIndex);
+ const inlineRefs = row.node.refs.slice(0, INLINE_REFS);
+ const hiddenRefs = row.node.refs.slice(INLINE_REFS);
+
+ return (
+
+
setSelectedHash(row.node.hash)}
+ aria-pressed={isSelected}
+ aria-label={`Select commit ${row.node.subject}`}
+ title={`${row.node.subject}\n${row.node.shortHash} · ${row.node.authorName} <${row.node.authorEmail}> · ${formatDate(row.node.authorDate)}`}
+ className="absolute inset-0 z-0"
+ />
+
+ {/*
+ Lanes past the gutter's width are clipped rather
+ than squeezed — the positions of the ones on screen
+ must not move because a thirty-branch repo exists.
+ */}
+
+ {isFiltering ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {inlineRefs.map(ref => (
+
+ ))}
+ {hiddenRefs.length > 0 && (
+ ref.name).join('\n')}
+ >
+ +{hiddenRefs.length}
+
+ )}
+
+ {row.node.subject}
+
+
+
+ {row.node.shortHash}
+ ·
+ {row.node.authorName}
+
+
+
+ {/* Fixed-width right column, so dates line up down the list. */}
+
+
+ {initials(row.node.authorName)}
+
+
+ {formatAge(row.node.authorDate)}
+
+
+
+
+ {/* Sibling of the row button — a button inside a button is invalid. */}
+
+ copyToClipboard(row.node.hash),
+ },
+ {
+ id: 'copy-short',
+ label: `Copy ${row.node.shortHash}`,
+ icon: Hash,
+ onClick: () => copyToClipboard(row.node.shortHash),
+ },
+ {
+ id: 'copy-subject',
+ label: 'Copy subject',
+ icon: Copy,
+ onClick: () => copyToClipboard(row.node.subject),
+ },
+ ...row.node.refs
+ .map(ref => worktreeByBranch.get(ref.name))
+ .filter(hasSessionId)
+ .map(worktree => ({
+ id: `open-${worktree.sessionId}`,
+ label: `Open session “${worktree.sessionName ?? worktree.branch}”`,
+ icon: GitBranch,
+ onClick: () => openSession(worktree.sessionId),
+ })),
+ ]}
+ trigger={
+
+
+
+ }
+ />
+
+
+ );
+ })}
+
+
+ >
+ )}
+
+ {graph?.truncated && !loading && !error && !isFiltering && (
+
+
+ Showing the {graph.limit} most recent commits.
+
+ {limit < MAX_GRAPH_LIMIT && (
+ setLimit(current => Math.min(
+ LIMIT_OPTIONS.find(option => option > current) ?? MAX_GRAPH_LIMIT,
+ MAX_GRAPH_LIMIT
+ ))}
+ disabled={refreshing}
+ className="rounded border border-border-secondary px-2 py-0.5 text-[11px] text-text-secondary transition-colors hover:bg-surface-hover disabled:opacity-50"
+ >
+ Load more
+
+ )}
+
+ )}
+
+
+ {/* Detail pane */}
+ {!detailHidden && (
+ <>
+