| ... | @@ -396,7 +396,6 @@ export default function Sidebar() { | ... | @@ -396,7 +396,6 @@ export default function Sidebar() { |
| 396 | const addProjectInputRef = useRef<HTMLInputElement | null>(null); | 396 | const addProjectInputRef = useRef<HTMLInputElement | null>(null); |
| 397 | const [renamingThreadId, setRenamingThreadId] = useState<ThreadId | null>(null); | 397 | const [renamingThreadId, setRenamingThreadId] = useState<ThreadId | null>(null); |
| 398 | const [renamingTitle, setRenamingTitle] = useState(""); | 398 | const [renamingTitle, setRenamingTitle] = useState(""); |
| 399 | const [hoveredThreadId, setHoveredThreadId] = useState<ThreadId | null>(null); | | |
| 400 | const [confirmingArchiveThreadId, setConfirmingArchiveThreadId] = useState<ThreadId | null>(null); | 399 | const [confirmingArchiveThreadId, setConfirmingArchiveThreadId] = useState<ThreadId | null>(null); |
| 401 | const [expandedThreadListsByProject, setExpandedThreadListsByProject] = useState< | 400 | const [expandedThreadListsByProject, setExpandedThreadListsByProject] = useState< |
| 402 | ReadonlySet<ProjectId> | 401 | ReadonlySet<ProjectId> |
| ... | @@ -404,6 +403,7 @@ export default function Sidebar() { | ... | @@ -404,6 +403,7 @@ export default function Sidebar() { |
| 404 | const { showThreadJumpHints, updateThreadJumpHintsVisibility } = useThreadJumpHintVisibility(); | 403 | const { showThreadJumpHints, updateThreadJumpHintsVisibility } = useThreadJumpHintVisibility(); |
| 405 | const renamingCommittedRef = useRef(false); | 404 | const renamingCommittedRef = useRef(false); |
| 406 | const renamingInputRef = useRef<HTMLInputElement | null>(null); | 405 | const renamingInputRef = useRef<HTMLInputElement | null>(null); |
| | 406 | const confirmArchiveButtonRefs = useRef(new Map<ThreadId, HTMLButtonElement>()); |
| 407 | const dragInProgressRef = useRef(false); | 407 | const dragInProgressRef = useRef(false); |
| 408 | const suppressProjectClickAfterDragRef = useRef(false); | 408 | const suppressProjectClickAfterDragRef = useRef(false); |
| 409 | const suppressProjectClickForContextMenuRef = useRef(false); | 409 | const suppressProjectClickForContextMenuRef = useRef(false); |
| ... | @@ -1272,28 +1272,40 @@ export default function Sidebar() { | ... | @@ -1272,28 +1272,40 @@ export default function Sidebar() { |
| 1272 | const terminalStatus = terminalStatusFromRunningIds( | 1272 | const terminalStatus = terminalStatusFromRunningIds( |
| 1273 | selectThreadTerminalState(terminalStateByThreadId, thread.id).runningTerminalIds, | 1273 | selectThreadTerminalState(terminalStateByThreadId, thread.id).runningTerminalIds, |
| 1274 | ); | 1274 | ); |
| | 1275 | const isConfirmingArchive = confirmingArchiveThreadId === thread.id && !isThreadRunning; |
| | 1276 | const threadMetaClassName = isConfirmingArchive |
| | 1277 | ? "pointer-events-none opacity-0" |
| | 1278 | : !isThreadRunning |
| | 1279 | ? "pointer-events-none transition-opacity duration-150 group-hover/menu-sub-item:opacity-0 group-focus-within/menu-sub-item:opacity-0" |
| | 1280 | : "pointer-events-none"; |
| 1275 | | 1281 | |
| 1276 | return ( | 1282 | return ( |
| 1277 | <SidebarMenuSubItem | 1283 | <SidebarMenuSubItem |
| 1278 | key={thread.id} | 1284 | key={thread.id} |
| 1279 | className="w-full" | 1285 | className="w-full" |
| 1280 | data-thread-item | 1286 | data-thread-item |
| 1281 | onMouseEnter={() => { | | |
| 1282 | setHoveredThreadId(thread.id); | | |
| 1283 | }} | | |
| 1284 | onMouseLeave={() => { | 1287 | onMouseLeave={() => { |
| 1285 | setHoveredThreadId((current) => (current === thread.id ? null : current)); | | |
| 1286 | setConfirmingArchiveThreadId((current) => (current === thread.id ? null : current)); | 1288 | setConfirmingArchiveThreadId((current) => (current === thread.id ? null : current)); |
| 1287 | }} | 1289 | }} |
| | 1290 | onBlurCapture={(event) => { |
| | 1291 | const currentTarget = event.currentTarget; |
| | 1292 | requestAnimationFrame(() => { |
| | 1293 | if (currentTarget.contains(document.activeElement)) { |
| | 1294 | return; |
| | 1295 | } |
| | 1296 | setConfirmingArchiveThreadId((current) => (current === thread.id ? null : current)); |
| | 1297 | }); |
| | 1298 | }} |
| 1288 | > | 1299 | > |
| 1289 | <SidebarMenuSubButton | 1300 | <SidebarMenuSubButton |
| 1290 | render={<div role="button" tabIndex={0} />} | 1301 | render={<div role="button" tabIndex={0} />} |
| 1291 | size="sm" | 1302 | size="sm" |
| 1292 | isActive={isActive} | 1303 | isActive={isActive} |
| | 1304 | data-testid={`thread-row-${thread.id}`} |
| 1293 | className={`${resolveThreadRowClassName({ | 1305 | className={`${resolveThreadRowClassName({ |
| 1294 | isActive, | 1306 | isActive, |
| 1295 | isSelected, | 1307 | isSelected, |
| 1296 | })} relative`} | 1308 | })} relative isolate`} |
| 1297 | onClick={(event) => { | 1309 | onClick={(event) => { |
| 1298 | handleThreadClick(event, thread.id, orderedProjectThreadIds); | 1310 | handleThreadClick(event, thread.id, orderedProjectThreadIds); |
| 1299 | }} | 1311 | }} |
| ... | @@ -1304,7 +1316,6 @@ export default function Sidebar() { | ... | @@ -1304,7 +1316,6 @@ export default function Sidebar() { |
| 1304 | }} | 1316 | }} |
| 1305 | onContextMenu={(event) => { | 1317 | onContextMenu={(event) => { |
| 1306 | event.preventDefault(); | 1318 | event.preventDefault(); |
| 1307 | setHoveredThreadId((current) => (current === thread.id ? null : current)); | | |
| 1308 | if (selectedThreadIds.size > 0 && selectedThreadIds.has(thread.id)) { | 1319 | if (selectedThreadIds.size > 0 && selectedThreadIds.has(thread.id)) { |
| 1309 | void handleMultiSelectContextMenu({ | 1320 | void handleMultiSelectContextMenu({ |
| 1310 | x: event.clientX, | 1321 | x: event.clientX, |
| ... | @@ -1391,10 +1402,18 @@ export default function Sidebar() { | ... | @@ -1391,10 +1402,18 @@ export default function Sidebar() { |
| 1391 | </span> | 1402 | </span> |
| 1392 | )} | 1403 | )} |
| 1393 | <div className="flex min-w-12 justify-end"> | 1404 | <div className="flex min-w-12 justify-end"> |
| 1394 | {confirmingArchiveThreadId === thread.id && !isThreadRunning ? ( | 1405 | {isConfirmingArchive ? ( |
| 1395 | <button | 1406 | <button |
| | 1407 | ref={(element) => { |
| | 1408 | if (element) { |
| | 1409 | confirmArchiveButtonRefs.current.set(thread.id, element); |
| | 1410 | } else { |
| | 1411 | confirmArchiveButtonRefs.current.delete(thread.id); |
| | 1412 | } |
| | 1413 | }} |
| 1396 | type="button" | 1414 | type="button" |
| 1397 | data-thread-selection-safe | 1415 | data-thread-selection-safe |
| | 1416 | data-testid={`thread-archive-confirm-${thread.id}`} |
| 1398 | aria-label={`Confirm archive ${thread.title}`} | 1417 | aria-label={`Confirm archive ${thread.title}`} |
| 1399 | className="absolute top-1/2 right-1 inline-flex h-5 -translate-y-1/2 cursor-pointer items-center rounded-full bg-destructive/12 px-2 text-[10px] font-medium text-destructive transition-colors hover:bg-destructive/18 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-destructive/40" | 1418 | className="absolute top-1/2 right-1 inline-flex h-5 -translate-y-1/2 cursor-pointer items-center rounded-full bg-destructive/12 px-2 text-[10px] font-medium text-destructive transition-colors hover:bg-destructive/18 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-destructive/40" |
| 1400 | onPointerDown={(event) => { | 1419 | onPointerDown={(event) => { |
| ... | @@ -1406,80 +1425,84 @@ export default function Sidebar() { | ... | @@ -1406,80 +1425,84 @@ export default function Sidebar() { |
| 1406 | setConfirmingArchiveThreadId((current) => | 1425 | setConfirmingArchiveThreadId((current) => |
| 1407 | current === thread.id ? null : current, | 1426 | current === thread.id ? null : current, |
| 1408 | ); | 1427 | ); |
| 1409 | setHoveredThreadId((current) => (current === thread.id ? null : current)); | | |
| 1410 | void attemptArchiveThread(thread.id); | 1428 | void attemptArchiveThread(thread.id); |
| 1411 | }} | 1429 | }} |
| 1412 | > | 1430 | > |
| 1413 | Confirm | 1431 | Confirm |
| 1414 | </button> | 1432 | </button> |
| 1415 | ) : hoveredThreadId === thread.id && !isThreadRunning ? ( | 1433 | ) : !isThreadRunning ? ( |
| 1416 | appSettings.confirmThreadArchive ? ( | 1434 | appSettings.confirmThreadArchive ? ( |
| 1417 | <button | 1435 | <div className="pointer-events-none absolute top-1/2 right-1 -translate-y-1/2 opacity-0 transition-opacity duration-150 group-hover/menu-sub-item:pointer-events-auto group-hover/menu-sub-item:opacity-100 group-focus-within/menu-sub-item:pointer-events-auto group-focus-within/menu-sub-item:opacity-100"> |
| 1418 | type="button" | 1436 | <button |
| 1419 | data-thread-selection-safe | 1437 | type="button" |
| 1420 | aria-label={`Archive ${thread.title}`} | 1438 | data-thread-selection-safe |
| 1421 | className="absolute top-1/2 right-1 inline-flex size-5 -translate-y-1/2 cursor-pointer items-center justify-center text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring" | 1439 | data-testid={`thread-archive-${thread.id}`} |
| 1422 | onPointerDown={(event) => { | 1440 | aria-label={`Archive ${thread.title}`} |
| 1423 | event.stopPropagation(); | 1441 | className="inline-flex size-5 cursor-pointer items-center justify-center text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring" |
| 1424 | }} | 1442 | onPointerDown={(event) => { |
| 1425 | onClick={(event) => { | 1443 | event.stopPropagation(); |
| 1426 | event.preventDefault(); | 1444 | }} |
| 1427 | event.stopPropagation(); | 1445 | onClick={(event) => { |
| 1428 | setConfirmingArchiveThreadId(thread.id); | 1446 | event.preventDefault(); |
| 1429 | }} | 1447 | event.stopPropagation(); |
| 1430 | > | 1448 | setConfirmingArchiveThreadId(thread.id); |
| 1431 | <ArchiveIcon className="size-3.5" /> | 1449 | requestAnimationFrame(() => { |
| 1432 | </button> | 1450 | confirmArchiveButtonRefs.current.get(thread.id)?.focus(); |
| | 1451 | }); |
| | 1452 | }} |
| | 1453 | > |
| | 1454 | <ArchiveIcon className="size-3.5" /> |
| | 1455 | </button> |
| | 1456 | </div> |
| 1433 | ) : ( | 1457 | ) : ( |
| 1434 | <Tooltip> | 1458 | <Tooltip> |
| 1435 | <TooltipTrigger | 1459 | <TooltipTrigger |
| 1436 | render={ | 1460 | render={ |
| 1437 | <button | 1461 | <div className="pointer-events-none absolute top-1/2 right-1 -translate-y-1/2 opacity-0 transition-opacity duration-150 group-hover/menu-sub-item:pointer-events-auto group-hover/menu-sub-item:opacity-100 group-focus-within/menu-sub-item:pointer-events-auto group-focus-within/menu-sub-item:opacity-100"> |
| 1438 | type="button" | 1462 | <button |
| 1439 | data-thread-selection-safe | 1463 | type="button" |
| 1440 | aria-label={`Archive ${thread.title}`} | 1464 | data-thread-selection-safe |
| 1441 | className="absolute top-1/2 right-1 inline-flex size-5 -translate-y-1/2 cursor-pointer items-center justify-center text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring" | 1465 | data-testid={`thread-archive-${thread.id}`} |
| 1442 | onPointerDown={(event) => { | 1466 | aria-label={`Archive ${thread.title}`} |
| 1443 | event.stopPropagation(); | 1467 | className="inline-flex size-5 cursor-pointer items-center justify-center text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring" |
| 1444 | }} | 1468 | onPointerDown={(event) => { |
| 1445 | onClick={(event) => { | 1469 | event.stopPropagation(); |
| 1446 | event.preventDefault(); | 1470 | }} |
| 1447 | event.stopPropagation(); | 1471 | onClick={(event) => { |
| 1448 | setHoveredThreadId((current) => | 1472 | event.preventDefault(); |
| 1449 | current === thread.id ? null : current, | 1473 | event.stopPropagation(); |
| 1450 | ); | 1474 | void attemptArchiveThread(thread.id); |
| 1451 | void attemptArchiveThread(thread.id); | 1475 | }} |
| 1452 | }} | 1476 | > |
| 1453 | > | 1477 | <ArchiveIcon className="size-3.5" /> |
| 1454 | <ArchiveIcon className="size-3.5" /> | 1478 | </button> |
| 1455 | </button> | 1479 | </div> |
| 1456 | } | 1480 | } |
| 1457 | /> | 1481 | /> |
| 1458 | <TooltipPopup side="top">Archive</TooltipPopup> | 1482 | <TooltipPopup side="top">Archive</TooltipPopup> |
| 1459 | </Tooltip> | 1483 | </Tooltip> |
| 1460 | ) | 1484 | ) |
| 1461 | ) : ( | 1485 | ) : null} |
| 1462 | <> | 1486 | <span className={threadMetaClassName}> |
| 1463 | {showThreadJumpHints && jumpLabel ? ( | 1487 | {showThreadJumpHints && jumpLabel ? ( |
| 1464 | <span | 1488 | <span |
| 1465 | className="inline-flex h-5 items-center rounded-full border border-border/80 bg-background/90 px-1.5 font-mono text-[10px] font-medium tracking-tight text-foreground shadow-sm" | 1489 | className="inline-flex h-5 items-center rounded-full border border-border/80 bg-background/90 px-1.5 font-mono text-[10px] font-medium tracking-tight text-foreground shadow-sm" |
| 1466 | title={jumpLabel} | 1490 | title={jumpLabel} |
| 1467 | > | 1491 | > |
| 1468 | {jumpLabel} | 1492 | {jumpLabel} |
| 1469 | </span> | 1493 | </span> |
| 1470 | ) : ( | 1494 | ) : ( |
| 1471 | <span | 1495 | <span |
| 1472 | className={`text-[10px] ${ | 1496 | className={`text-[10px] ${ |
| 1473 | isHighlighted | 1497 | isHighlighted |
| 1474 | ? "text-foreground/72 dark:text-foreground/82" | 1498 | ? "text-foreground/72 dark:text-foreground/82" |
| 1475 | : "text-muted-foreground/40" | 1499 | : "text-muted-foreground/40" |
| 1476 | }`} | 1500 | }`} |
| 1477 | > | 1501 | > |
| 1478 | {formatRelativeTimeLabel(thread.updatedAt ?? thread.createdAt)} | 1502 | {formatRelativeTimeLabel(thread.updatedAt ?? thread.createdAt)} |
| 1479 | </span> | 1503 | </span> |
| 1480 | )} | 1504 | )} |
| 1481 | </> | 1505 | </span> |
| 1482 | )} | | |
| 1483 | </div> | 1506 | </div> |
| 1484 | </div> | 1507 | </div> |
| 1485 | </SidebarMenuSubButton> | 1508 | </SidebarMenuSubButton> |