66 ArrowDown ,
77 ArrowUp ,
88 Check ,
9+ Archive ,
910 Copy ,
1011 Folder ,
1112 GitBranch ,
@@ -95,6 +96,8 @@ export interface LeftSidebarProps {
9596 onSelectThread : ( threadId : string ) => void ;
9697 onDeleteWorktree ?: ( worktreeId : string ) => void ;
9798 onDeleteThread ?: ( threadId : string ) => void ;
99+ onArchiveWorktree ?: ( worktreeId : string ) => void ;
100+ onArchiveThread ?: ( threadId : string ) => void ;
98101 onAddRepository : ( ) => void ;
99102 onOpenFilter ?: ( ) => void ;
100103 onNewAgent ?: ( ) => void ;
@@ -256,20 +259,36 @@ interface WorktreeRowProps {
256259 threadLastViewedAt ?: Record < string , number > ;
257260 onSelect : ( id : string ) => void ;
258261 onSelectThread : ( threadId : string ) => void ;
262+ onContextMenu ?: (
263+ e : React . MouseEvent ,
264+ worktreeId : string ,
265+ worktreeLabel : string ,
266+ ) => void ;
267+ onThreadContextMenu ?: (
268+ e : React . MouseEvent ,
269+ threadId : string ,
270+ threadTitle : string ,
271+ ) => void ;
259272}
260273
261274interface ThreadRowProps {
262275 thread : WorktreeSnapshot [ "threads" ] [ number ] ;
263276 isActive : boolean ;
264277 indicatorState : IndicatorState ;
265278 onSelect : ( id : string ) => void ;
279+ onContextMenu ?: (
280+ e : React . MouseEvent ,
281+ threadId : string ,
282+ threadTitle : string ,
283+ ) => void ;
266284}
267285
268286function ThreadRowImpl ( {
269287 thread,
270288 isActive,
271289 indicatorState,
272290 onSelect,
291+ onContextMenu,
273292} : ThreadRowProps ) {
274293 const threadTitle = thread . title . trim ( ) || "Untitled thread" ;
275294
@@ -278,6 +297,11 @@ function ThreadRowImpl({
278297 < button
279298 type = "button"
280299 onClick = { ( ) => onSelect ( thread . id ) }
300+ onContextMenu = { ( e ) => {
301+ e . preventDefault ( ) ;
302+ e . stopPropagation ( ) ;
303+ onContextMenu ?.( e , thread . id , threadTitle ) ;
304+ } }
281305 className = { cn (
282306 "group flex w-full min-w-0 items-center gap-2 pl-[11px] pr-2 py-1.5 text-left text-[12px]" ,
283307 "transition-colors duration-150" ,
@@ -306,6 +330,8 @@ function WorktreeRowImpl({
306330 threadLastViewedAt,
307331 onSelect,
308332 onSelectThread,
333+ onContextMenu,
334+ onThreadContextMenu,
309335} : WorktreeRowProps ) {
310336 const ahead = session . git . ahead ?? 0 ;
311337 const behind = session . git . behind ?? 0 ;
@@ -340,6 +366,11 @@ function WorktreeRowImpl({
340366 < button
341367 type = "button"
342368 onClick = { ( ) => onSelect ( session . id ) }
369+ onContextMenu = { ( e ) => {
370+ e . preventDefault ( ) ;
371+ e . stopPropagation ( ) ;
372+ onContextMenu ?.( e , session . id , session . label ) ;
373+ } }
343374 className = { cn (
344375 "group flex w-full min-w-0 items-center gap-2 pl-[22px] pr-2 py-2 text-left text-[13px]" ,
345376 "transition-colors duration-150" ,
@@ -404,6 +435,7 @@ function WorktreeRowImpl({
404435 isActive = { isThreadActive }
405436 indicatorState = { threadIndicatorState }
406437 onSelect = { onSelectThread }
438+ onContextMenu = { onThreadContextMenu }
407439 />
408440 ) ;
409441 } ) }
@@ -515,8 +547,10 @@ export function LeftSidebarImpl({
515547 onSelectRepository,
516548 onSelectWorktree,
517549 onSelectThread,
518- onDeleteThread : _onDeleteThread ,
519- onDeleteWorktree : _onDeleteWorktree ,
550+ onDeleteThread,
551+ onDeleteWorktree,
552+ onArchiveThread,
553+ onArchiveWorktree,
520554 onRemoveRepository,
521555 onCopyRepositoryPath,
522556 onOpenInFinder,
@@ -551,6 +585,27 @@ export function LeftSidebarImpl({
551585 const contextMenuRef = React . useRef < HTMLDivElement > ( null ) ;
552586 const [ isCreatingSession , setIsCreatingSession ] = React . useState ( false ) ;
553587
588+ // Item context menu for threads/worktrees
589+ const [ itemMenu , setItemMenu ] = React . useState < {
590+ isOpen : boolean ;
591+ x : number ;
592+ y : number ;
593+ type : "thread" | "worktree" ;
594+ id : string ;
595+ label : string ;
596+ confirming : "archive" | "delete" | null ;
597+ } > ( {
598+ isOpen : false ,
599+ x : 0 ,
600+ y : 0 ,
601+ type : "thread" ,
602+ id : "" ,
603+ label : "" ,
604+ confirming : null ,
605+ } ) ;
606+
607+ const itemMenuRef = React . useRef < HTMLDivElement > ( null ) ;
608+
554609 const [ expandedRepositoryIds , setExpandedRepositoryIds ] = React . useState <
555610 Set < string >
556611 > ( ( ) => ( activeRepositoryId ? new Set ( [ activeRepositoryId ] ) : new Set ( ) ) ) ;
@@ -598,6 +653,108 @@ export function LeftSidebarImpl({
598653 } ;
599654 } , [ contextMenu . isOpen ] ) ;
600655
656+ React . useEffect ( ( ) => {
657+ if ( ! itemMenu . isOpen ) return ;
658+
659+ const handleClickOutside = ( e : MouseEvent ) => {
660+ if (
661+ itemMenuRef . current &&
662+ ! itemMenuRef . current . contains ( e . target as Node )
663+ ) {
664+ setItemMenu ( ( prev ) => ( { ...prev , isOpen : false , confirming : null } ) ) ;
665+ }
666+ } ;
667+
668+ const handleEscape = ( e : KeyboardEvent ) => {
669+ if ( e . key === "Escape" ) {
670+ setItemMenu ( ( prev ) => ( { ...prev , isOpen : false , confirming : null } ) ) ;
671+ }
672+ } ;
673+
674+ document . addEventListener ( "mousedown" , handleClickOutside ) ;
675+ document . addEventListener ( "keydown" , handleEscape ) ;
676+
677+ return ( ) => {
678+ document . removeEventListener ( "mousedown" , handleClickOutside ) ;
679+ document . removeEventListener ( "keydown" , handleEscape ) ;
680+ } ;
681+ } , [ itemMenu . isOpen ] ) ;
682+
683+ const closeItemMenu = React . useCallback ( ( ) => {
684+ setItemMenu ( ( prev ) => ( { ...prev , isOpen : false , confirming : null } ) ) ;
685+ } , [ ] ) ;
686+
687+ const handleItemContextMenu = React . useCallback (
688+ (
689+ e : React . MouseEvent ,
690+ type : "thread" | "worktree" ,
691+ id : string ,
692+ label : string ,
693+ ) => {
694+ e . preventDefault ( ) ;
695+ e . stopPropagation ( ) ;
696+ setItemMenu ( {
697+ isOpen : true ,
698+ x : e . clientX ,
699+ y : e . clientY ,
700+ type,
701+ id,
702+ label,
703+ confirming : null ,
704+ } ) ;
705+ } ,
706+ [ ] ,
707+ ) ;
708+
709+ const handleThreadContextMenu = React . useCallback (
710+ ( e : React . MouseEvent , threadId : string , threadTitle : string ) => {
711+ handleItemContextMenu ( e , "thread" , threadId , threadTitle ) ;
712+ } ,
713+ [ handleItemContextMenu ] ,
714+ ) ;
715+
716+ const handleWorktreeContextMenu = React . useCallback (
717+ ( e : React . MouseEvent , worktreeId : string , worktreeLabel : string ) => {
718+ handleItemContextMenu ( e , "worktree" , worktreeId , worktreeLabel ) ;
719+ } ,
720+ [ handleItemContextMenu ] ,
721+ ) ;
722+
723+ const handleItemMenuConfirmAction = React . useCallback (
724+ ( action : "archive" | "delete" ) => {
725+ if ( itemMenu . confirming === action ) {
726+ // Second click — execute
727+ if ( action === "delete" ) {
728+ if ( itemMenu . type === "thread" ) {
729+ onDeleteThread ?.( itemMenu . id ) ;
730+ } else {
731+ onDeleteWorktree ?.( itemMenu . id ) ;
732+ }
733+ } else {
734+ if ( itemMenu . type === "thread" ) {
735+ onArchiveThread ?.( itemMenu . id ) ;
736+ } else {
737+ onArchiveWorktree ?.( itemMenu . id ) ;
738+ }
739+ }
740+ closeItemMenu ( ) ;
741+ } else {
742+ // First click — enter confirming state
743+ setItemMenu ( ( prev ) => ( { ...prev , confirming : action } ) ) ;
744+ }
745+ } ,
746+ [
747+ itemMenu . confirming ,
748+ itemMenu . type ,
749+ itemMenu . id ,
750+ onDeleteThread ,
751+ onDeleteWorktree ,
752+ onArchiveThread ,
753+ onArchiveWorktree ,
754+ closeItemMenu ,
755+ ] ,
756+ ) ;
757+
601758 const handleCreateSession = React . useCallback ( async ( ) => {
602759 if ( isCreatingSession ) return ;
603760 setIsCreatingSession ( true ) ;
@@ -844,6 +1001,10 @@ export function LeftSidebarImpl({
8441001 threadLastViewedAt = { threadLastViewedAt }
8451002 onSelect = { onSelectWorktree }
8461003 onSelectThread = { onSelectThread }
1004+ onContextMenu = { handleWorktreeContextMenu }
1005+ onThreadContextMenu = {
1006+ handleThreadContextMenu
1007+ }
8471008 />
8481009 ) ;
8491010 } ) }
@@ -966,6 +1127,94 @@ export function LeftSidebarImpl({
9661127 ) }
9671128 </ div >
9681129 ) }
1130+
1131+ { /* Thread / Worktree Context Menu */ }
1132+ { itemMenu . isOpen && (
1133+ < div
1134+ ref = { itemMenuRef }
1135+ className = "fixed z-[100] min-w-[180px] border border-white/[0.06] bg-[var(--color-bg-primary)] p-0 shadow-lg"
1136+ style = { { left : itemMenu . x , top : itemMenu . y } }
1137+ >
1138+ < div className = "px-3 py-2 border-b border-white/[0.06]" >
1139+ < span className = "block truncate text-[12px] text-white/60" >
1140+ { itemMenu . label }
1141+ </ span >
1142+ < span className = "text-[10px] text-white/30" >
1143+ { itemMenu . type === "thread" ? "Thread" : "Worktree" }
1144+ </ span >
1145+ </ div >
1146+
1147+ { itemMenu . confirming === "archive" ? (
1148+ < div className = "px-3 py-2 space-y-1.5" >
1149+ < p className = "text-[11px] text-white/50" >
1150+ Archive this { itemMenu . type } ?
1151+ </ p >
1152+ < div className = "flex items-center gap-2" >
1153+ < button
1154+ type = "button"
1155+ onClick = { ( ) => handleItemMenuConfirmAction ( "archive" ) }
1156+ className = "flex-1 py-1 text-[11px] text-amber-400 hover:bg-amber-500/10 transition-colors duration-150"
1157+ >
1158+ Archive
1159+ </ button >
1160+ < button
1161+ type = "button"
1162+ onClick = { ( ) =>
1163+ setItemMenu ( ( prev ) => ( { ...prev , confirming : null } ) )
1164+ }
1165+ className = "flex-1 py-1 text-[11px] text-white/40 hover:bg-white/[0.04] transition-colors duration-150"
1166+ >
1167+ Cancel
1168+ </ button >
1169+ </ div >
1170+ </ div >
1171+ ) : itemMenu . confirming === "delete" ? (
1172+ < div className = "px-3 py-2 space-y-1.5" >
1173+ < p className = "text-[11px] text-white/50" >
1174+ Delete this { itemMenu . type } ? This cannot be undone.
1175+ </ p >
1176+ < div className = "flex items-center gap-2" >
1177+ < button
1178+ type = "button"
1179+ onClick = { ( ) => handleItemMenuConfirmAction ( "delete" ) }
1180+ className = "flex-1 py-1 text-[11px] text-rose-400 hover:bg-rose-500/10 transition-colors duration-150"
1181+ >
1182+ Delete
1183+ </ button >
1184+ < button
1185+ type = "button"
1186+ onClick = { ( ) =>
1187+ setItemMenu ( ( prev ) => ( { ...prev , confirming : null } ) )
1188+ }
1189+ className = "flex-1 py-1 text-[11px] text-white/40 hover:bg-white/[0.04] transition-colors duration-150"
1190+ >
1191+ Cancel
1192+ </ button >
1193+ </ div >
1194+ </ div >
1195+ ) : (
1196+ < >
1197+ < button
1198+ type = "button"
1199+ onClick = { ( ) => handleItemMenuConfirmAction ( "archive" ) }
1200+ className = "flex w-full items-center gap-2 px-3 py-2 text-left text-[12px] text-white/70 hover:bg-white/[0.04] transition-colors duration-150"
1201+ >
1202+ < Archive className = "size-4" />
1203+ Archive
1204+ </ button >
1205+ < div className = "my-0.5 border-t border-white/[0.06]" />
1206+ < button
1207+ type = "button"
1208+ onClick = { ( ) => handleItemMenuConfirmAction ( "delete" ) }
1209+ className = "flex w-full items-center gap-2 px-3 py-2 text-left text-[12px] text-rose-400 hover:bg-rose-500/10 transition-colors duration-150"
1210+ >
1211+ < Trash className = "size-4" />
1212+ Delete
1213+ </ button >
1214+ </ >
1215+ ) }
1216+ </ div >
1217+ ) }
9691218 </ >
9701219 ) }
9711220 </ aside >
0 commit comments