diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx
index 83a5b2c72..8788ffb6a 100644
--- a/apps/web/src/components/Sidebar.tsx
+++ b/apps/web/src/components/Sidebar.tsx
@@ -1144,6 +1144,177 @@ export default function Sidebar() {
});
}, []);
+ const renderThreadRowButton = (
+ thread: (typeof threads)[number],
+ orderedProjectThreadIds: readonly ThreadId[],
+ ) => {
+ const isActive = routeThreadId === thread.id;
+ const isSelected = selectedThreadIds.has(thread.id);
+ const isHighlighted = isActive || isSelected;
+ const threadStatus = resolveThreadStatusPill({
+ thread,
+ hasPendingApprovals: derivePendingApprovals(thread.activities).length > 0,
+ hasPendingUserInput: derivePendingUserInputs(thread.activities).length > 0,
+ });
+ const prStatus = prStatusIndicator(prByThreadId.get(thread.id) ?? null);
+ const terminalStatus = terminalStatusFromRunningIds(
+ selectThreadTerminalState(terminalStateByThreadId, thread.id).runningTerminalIds,
+ );
+
+ return (
+ }
+ size="sm"
+ isActive={isActive}
+ className={resolveThreadRowClassName({
+ isActive,
+ isSelected,
+ })}
+ data-thread-item
+ onClick={(event) => {
+ handleThreadClick(event, thread.id, orderedProjectThreadIds);
+ }}
+ onKeyDown={(event) => {
+ if (event.key !== "Enter" && event.key !== " ") return;
+ event.preventDefault();
+ if (selectedThreadIds.size > 0) {
+ clearSelection();
+ }
+ setSelectionAnchor(thread.id);
+ void navigate({
+ to: "/$threadId",
+ params: { threadId: thread.id },
+ });
+ }}
+ onContextMenu={(event) => {
+ event.preventDefault();
+ if (selectedThreadIds.size > 0 && selectedThreadIds.has(thread.id)) {
+ void handleMultiSelectContextMenu({
+ x: event.clientX,
+ y: event.clientY,
+ });
+ } else {
+ if (selectedThreadIds.size > 0) {
+ clearSelection();
+ }
+ void handleThreadContextMenu(thread.id, {
+ x: event.clientX,
+ y: event.clientY,
+ });
+ }
+ }}
+ >
+
+ {prStatus && (
+
+ {
+ openPrLink(event, prStatus.url);
+ }}
+ >
+
+
+ }
+ />
+ {prStatus.tooltip}
+
+ )}
+ {threadStatus && (
+
+
+ {threadStatus.label}
+
+ )}
+ {renamingThreadId === thread.id ? (
+ {
+ if (el && renamingInputRef.current !== el) {
+ renamingInputRef.current = el;
+ el.focus();
+ el.select();
+ }
+ }}
+ className="min-w-0 flex-1 truncate text-xs bg-transparent outline-none border border-ring rounded px-0.5"
+ value={renamingTitle}
+ onChange={(e) => setRenamingTitle(e.target.value)}
+ onKeyDown={(e) => {
+ e.stopPropagation();
+ if (e.key === "Enter") {
+ e.preventDefault();
+ renamingCommittedRef.current = true;
+ void commitRename(thread.id, renamingTitle, thread.title);
+ } else if (e.key === "Escape") {
+ e.preventDefault();
+ renamingCommittedRef.current = true;
+ cancelRename();
+ }
+ }}
+ onBlur={() => {
+ if (!renamingCommittedRef.current) {
+ void commitRename(thread.id, renamingTitle, thread.title);
+ }
+ }}
+ onClick={(e) => e.stopPropagation()}
+ />
+ ) : (
+ {thread.title}
+ )}
+
+
+ {terminalStatus && (
+
+
+
+ )}
+
+ {formatRelativeTime(thread.createdAt)}
+
+
+
+ );
+ };
+
+ const renderThreadListItem = (
+ thread: (typeof threads)[number],
+ orderedProjectThreadIds: readonly ThreadId[],
+ ) => (
+
+ {renderThreadRowButton(thread, orderedProjectThreadIds)}
+
+ );
+
+ const renderThreadListToggleButton = (label: "Show more" | "Show less", onClick: () => void) => (
+ }
+ data-thread-selection-safe
+ size="sm"
+ className="h-6 w-full translate-x-0 justify-start px-2 text-left text-[10px] text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground/80"
+ onClick={onClick}
+ >
+ {label}
+
+ );
+
const wordmark = (
@@ -1339,13 +1510,39 @@ export default function Sidebar() {
if (byDate !== 0) return byDate;
return b.id.localeCompare(a.id);
});
+ const activeThreadIndex =
+ routeThreadId !== null
+ ? projectThreads.findIndex((thread) => thread.id === routeThreadId)
+ : -1;
+ const activeThread =
+ activeThreadIndex >= 0 ? (projectThreads[activeThreadIndex] ?? null) : null;
const isThreadListExpanded = expandedThreadListsByProject.has(project.id);
const hasHiddenThreads = projectThreads.length > THREAD_PREVIEW_LIMIT;
+ const shouldForceThreadListOpen =
+ activeThread !== null && activeThreadIndex >= THREAD_PREVIEW_LIMIT;
+ const shouldShowExpandedThreadList =
+ isThreadListExpanded || shouldForceThreadListOpen;
const visibleThreads =
- hasHiddenThreads && !isThreadListExpanded
+ hasHiddenThreads && !shouldShowExpandedThreadList
? projectThreads.slice(0, THREAD_PREVIEW_LIMIT)
: projectThreads;
const orderedProjectThreadIds = projectThreads.map((t) => t.id);
+ const visibleActiveThreadIndex =
+ activeThread !== null
+ ? visibleThreads.findIndex((thread) => thread.id === activeThread.id)
+ : -1;
+ const shouldSplitAroundActiveThread =
+ activeThread !== null && visibleActiveThreadIndex >= 0;
+ const threadsAboveActive = shouldSplitAroundActiveThread
+ ? visibleThreads.slice(0, visibleActiveThreadIndex)
+ : [];
+ const threadsBelowActive = shouldSplitAroundActiveThread
+ ? visibleThreads.slice(visibleActiveThreadIndex + 1)
+ : [];
+ const canShowMoreThreads =
+ hasHiddenThreads && !isThreadListExpanded && !shouldForceThreadListOpen;
+ const canShowLessThreads =
+ hasHiddenThreads && isThreadListExpanded && !shouldForceThreadListOpen;
return (
@@ -1413,222 +1610,91 @@ export default function Sidebar() {
-
+ {shouldSplitAroundActiveThread && activeThread ? (
- {visibleThreads.map((thread) => {
- const isActive = routeThreadId === thread.id;
- const isSelected = selectedThreadIds.has(thread.id);
- const isHighlighted = isActive || isSelected;
- const threadStatus = resolveThreadStatusPill({
- thread,
- hasPendingApprovals:
- derivePendingApprovals(thread.activities).length > 0,
- hasPendingUserInput:
- derivePendingUserInputs(thread.activities).length > 0,
- });
- const prStatus = prStatusIndicator(
- prByThreadId.get(thread.id) ?? null,
- );
- const terminalStatus = terminalStatusFromRunningIds(
- selectThreadTerminalState(terminalStateByThreadId, thread.id)
- .runningTerminalIds,
- );
-
- return (
-
- }
- size="sm"
- isActive={isActive}
- className={resolveThreadRowClassName({
- isActive,
- isSelected,
- })}
- onClick={(event) => {
- handleThreadClick(
- event,
- thread.id,
- orderedProjectThreadIds,
- );
- }}
- onKeyDown={(event) => {
- if (event.key !== "Enter" && event.key !== " ") return;
- event.preventDefault();
- if (selectedThreadIds.size > 0) {
- clearSelection();
- }
- setSelectionAnchor(thread.id);
- void navigate({
- to: "/$threadId",
- params: { threadId: thread.id },
- });
- }}
- onContextMenu={(event) => {
- event.preventDefault();
- if (
- selectedThreadIds.size > 0 &&
- selectedThreadIds.has(thread.id)
- ) {
- void handleMultiSelectContextMenu({
- x: event.clientX,
- y: event.clientY,
- });
- } else {
- if (selectedThreadIds.size > 0) {
- clearSelection();
- }
- void handleThreadContextMenu(thread.id, {
- x: event.clientX,
- y: event.clientY,
- });
- }
- }}
- >
-
- {prStatus && (
-
- {
- openPrLink(event, prStatus.url);
- }}
- >
-
-
- }
- />
-
- {prStatus.tooltip}
-
-
- )}
- {threadStatus && (
-
0 ? (
+
+
+
+
+ {threadsAboveActive.map((thread) => (
+
-
-
- {threadStatus.label}
-
-
- )}
- {renamingThreadId === thread.id ? (
- {
- if (el && renamingInputRef.current !== el) {
- renamingInputRef.current = el;
- el.focus();
- el.select();
- }
- }}
- className="min-w-0 flex-1 truncate text-xs bg-transparent outline-none border border-ring rounded px-0.5"
- value={renamingTitle}
- onChange={(e) => setRenamingTitle(e.target.value)}
- onKeyDown={(e) => {
- e.stopPropagation();
- if (e.key === "Enter") {
- e.preventDefault();
- renamingCommittedRef.current = true;
- void commitRename(
- thread.id,
- renamingTitle,
- thread.title,
- );
- } else if (e.key === "Escape") {
- e.preventDefault();
- renamingCommittedRef.current = true;
- cancelRename();
- }
- }}
- onBlur={() => {
- if (!renamingCommittedRef.current) {
- void commitRename(
- thread.id,
- renamingTitle,
- thread.title,
- );
- }
- }}
- onClick={(e) => e.stopPropagation()}
- />
- ) : (
-
- {thread.title}
-
- )}
+ {renderThreadRowButton(thread, orderedProjectThreadIds)}
+
+ ))}
-
- {terminalStatus && (
-
+
+
+ ) : null}
+
+ {renderThreadListItem(activeThread, orderedProjectThreadIds)}
+
+ {threadsBelowActive.length > 0 ||
+ canShowMoreThreads ||
+ canShowLessThreads ? (
+
+
+
+
+ {threadsBelowActive.map((thread) => (
+
-
-
- )}
-
- {formatRelativeTime(thread.createdAt)}
-
+ {renderThreadRowButton(thread, orderedProjectThreadIds)}
+
+ ))}
+
+ {canShowMoreThreads ? (
+
+ {renderThreadListToggleButton("Show more", () => {
+ expandThreadListForProject(project.id);
+ })}
+
+ ) : null}
+
+ {canShowLessThreads ? (
+
+ {renderThreadListToggleButton("Show less", () => {
+ collapseThreadListForProject(project.id);
+ })}
+
+ ) : null}
-
-
- );
- })}
-
- {hasHiddenThreads && !isThreadListExpanded && (
-
- }
- data-thread-selection-safe
- size="sm"
- className="h-6 w-full translate-x-0 justify-start px-2 text-left text-[10px] text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground/80"
- onClick={() => {
- expandThreadListForProject(project.id);
- }}
- >
- Show more
-
-
- )}
- {hasHiddenThreads && isThreadListExpanded && (
-
- }
- data-thread-selection-safe
- size="sm"
- className="h-6 w-full translate-x-0 justify-start px-2 text-left text-[10px] text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground/80"
- onClick={() => {
- collapseThreadListForProject(project.id);
- }}
- >
- Show less
-
+
+
- )}
+ ) : null}
-
+ ) : (
+
+
+ {visibleThreads.map((thread) =>
+ renderThreadListItem(thread, orderedProjectThreadIds),
+ )}
+
+ {canShowMoreThreads ? (
+
+ {renderThreadListToggleButton("Show more", () => {
+ expandThreadListForProject(project.id);
+ })}
+
+ ) : null}
+
+ {canShowLessThreads ? (
+
+ {renderThreadListToggleButton("Show less", () => {
+ collapseThreadListForProject(project.id);
+ })}
+
+ ) : null}
+
+
+ )}
)}