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