From ca29b14ba9a24277c52a09afd957d1d629345860 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Mon, 19 Jan 2026 07:44:39 +0100 Subject: [PATCH 1/2] feat: add pinned threads in sidebar --- src/App.tsx | 8 + src/features/app/components/Sidebar.tsx | 27 +++- src/features/app/components/ThreadList.tsx | 112 +++++++------ .../app/components/WorktreeSection.tsx | 31 +++- src/features/app/hooks/useSidebarMenus.ts | 34 +++- src/features/app/hooks/useThreadRows.ts | 55 +++++-- src/features/layout/hooks/useLayoutNodes.tsx | 8 + src/features/threads/hooks/useThreads.ts | 148 ++++++++++++++++-- src/styles/sidebar.css | 20 +++ 9 files changed, 356 insertions(+), 87 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 843ad4eb7..54f2a305a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -579,6 +579,10 @@ function MainApp() { lastAgentMessageByThread, interruptTurn, removeThread, + pinThread, + unpinThread, + isThreadPinned, + getPinTimestamp, startThreadForWorkspace, listThreadsForWorkspace, loadOlderThreadsForWorkspace, @@ -1044,6 +1048,10 @@ function MainApp() { }); removeImagesForThread(threadId); }, + pinThread, + unpinThread, + isThreadPinned, + getPinTimestamp, onDeleteWorkspace: (workspaceId) => { void removeWorkspace(workspaceId); }, diff --git a/src/features/app/components/Sidebar.tsx b/src/features/app/components/Sidebar.tsx index 10b5aba28..325955311 100644 --- a/src/features/app/components/Sidebar.tsx +++ b/src/features/app/components/Sidebar.tsx @@ -54,6 +54,10 @@ type SidebarProps = { onToggleWorkspaceCollapse: (workspaceId: string, collapsed: boolean) => void; onSelectThread: (workspaceId: string, threadId: string) => void; onDeleteThread: (workspaceId: string, threadId: string) => void; + pinThread: (workspaceId: string, threadId: string) => boolean; + unpinThread: (workspaceId: string, threadId: string) => void; + isThreadPinned: (workspaceId: string, threadId: string) => boolean; + getPinTimestamp: (workspaceId: string, threadId: string) => number | null; onDeleteWorkspace: (workspaceId: string) => void; onDeleteWorktree: (workspaceId: string) => void; onLoadOlderThreads: (workspaceId: string) => void; @@ -86,6 +90,10 @@ export function Sidebar({ onToggleWorkspaceCollapse, onSelectThread, onDeleteThread, + pinThread, + unpinThread, + isThreadPinned, + getPinTimestamp, onDeleteWorkspace, onDeleteWorktree, onLoadOlderThreads, @@ -114,6 +122,9 @@ export function Sidebar({ const { showThreadMenu, showWorkspaceMenu, showWorktreeMenu } = useSidebarMenus({ onDeleteThread, + onPinThread: pinThread, + onUnpinThread: unpinThread, + isThreadPinned, onReloadWorkspaceThreads, onDeleteWorkspace, onDeleteWorktree, @@ -215,9 +226,15 @@ export function Sidebar({ const isCollapsed = entry.settings.sidebarCollapsed; const isExpanded = expandedWorkspaces.has(entry.id); const { - rows: threadRows, + pinnedRows, + unpinnedRows, totalRoots: totalThreadRoots, - } = getThreadRows(threads, isExpanded); + } = getThreadRows( + threads, + isExpanded, + entry.id, + getPinTimestamp, + ); const showThreads = !isCollapsed && threads.length > 0; const isLoadingThreads = threadListLoadingByWorkspace[entry.id] ?? false; @@ -290,6 +307,8 @@ export function Sidebar({ activeThreadId={activeThreadId} getThreadRows={getThreadRows} getThreadTime={getThreadTime} + isThreadPinned={isThreadPinned} + getPinTimestamp={getPinTimestamp} onSelectWorkspace={onSelectWorkspace} onConnectWorkspace={onConnectWorkspace} onToggleWorkspaceCollapse={onToggleWorkspaceCollapse} @@ -303,7 +322,8 @@ export function Sidebar({ {showThreads && ( string | null; + isThreadPinned: (workspaceId: string, threadId: string) => boolean; onToggleExpanded: (workspaceId: string) => void; onLoadOlderThreads: (workspaceId: string) => void; onSelectThread: (workspaceId: string, threadId: string) => void; @@ -31,12 +33,14 @@ type ThreadListProps = { event: MouseEvent, workspaceId: string, threadId: string, + canPin: boolean, ) => void; }; export function ThreadList({ workspaceId, - threadRows, + pinnedRows, + unpinnedRows, totalThreadRoots, isExpanded, nextCursor, @@ -46,59 +50,71 @@ export function ThreadList({ activeThreadId, threadStatusById, getThreadTime, + isThreadPinned, onToggleExpanded, onLoadOlderThreads, onSelectThread, onShowThreadMenu, }: ThreadListProps) { - return ( -
- {threadRows.map(({ thread, depth }) => { - const relativeTime = getThreadTime(thread); - const indentStyle = - depth > 0 - ? ({ "--thread-indent": `${depth * 14}px` } as CSSProperties) - : undefined; - const status = threadStatusById[thread.id]; - const statusClass = status?.isReviewing - ? "reviewing" - : status?.isProcessing - ? "processing" - : status?.hasUnread - ? "unread" - : "ready"; + const renderThreadRow = ({ thread, depth }: ThreadRow) => { + const relativeTime = getThreadTime(thread); + const indentStyle = + depth > 0 + ? ({ "--thread-indent": `${depth * 14}px` } as CSSProperties) + : undefined; + const status = threadStatusById[thread.id]; + const statusClass = status?.isReviewing + ? "reviewing" + : status?.isProcessing + ? "processing" + : status?.hasUnread + ? "unread" + : "ready"; + const canPin = depth === 0; + const isPinned = canPin && isThreadPinned(workspaceId, thread.id); - return ( -
onSelectThread(workspaceId, thread.id)} - onContextMenu={(event) => onShowThreadMenu(event, workspaceId, thread.id)} - role="button" - tabIndex={0} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - onSelectThread(workspaceId, thread.id); - } - }} - > - - {thread.name} -
- {relativeTime && {relativeTime}} -
- -
+ return ( +
onSelectThread(workspaceId, thread.id)} + onContextMenu={(event) => + onShowThreadMenu(event, workspaceId, thread.id, canPin) + } + role="button" + tabIndex={0} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + onSelectThread(workspaceId, thread.id); + } + }} + > + + {isPinned && 📌} + {thread.name} +
+ {relativeTime && {relativeTime}} +
+ - ); - })} +
+
+ ); + }; + + return ( +
+ {pinnedRows.map((row) => renderThreadRow(row))} + {pinnedRows.length > 0 && unpinnedRows.length > 0 && ( +