| ... | @@ -129,10 +129,6 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { | ... | @@ -129,10 +129,6 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { |
| 129 | // False on environments whose server predates thread.settle/unsettle: | 129 | // False on environments whose server predates thread.settle/unsettle: |
| 130 | // the lifecycle affordances hide entirely rather than fail on click. | 130 | // the lifecycle affordances hide entirely rather than fail on click. |
| 131 | settlementSupported: boolean; | 131 | settlementSupported: boolean; |
| 132 | // Marks where active work transitions into history: a quiet labeled | | |
| 133 | // rule above the first settled row, so the tail reads as a named zone | | |
| 134 | // rather than an unexplained gap. | | |
| 135 | showSettledGap?: boolean; | | |
| 136 | isActive: boolean; | 132 | isActive: boolean; |
| 137 | jumpLabel: string | null; | 133 | jumpLabel: string | null; |
| 138 | currentEnvironmentId: string | null; | 134 | currentEnvironmentId: string | null; |
| ... | @@ -398,12 +394,6 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { | ... | @@ -398,12 +394,6 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { |
| 398 | data-thread-item | 394 | data-thread-item |
| 399 | className="list-none [content-visibility:auto] [contain-intrinsic-size:auto_34px]" | 395 | className="list-none [content-visibility:auto] [contain-intrinsic-size:auto_34px]" |
| 400 | > | 396 | > |
| 401 | {props.showSettledGap ? ( | | |
| 402 | <div aria-hidden className="mb-1 mt-3 flex items-center gap-2 px-2.5"> | | |
| 403 | <span className="text-[10px] font-medium text-muted-foreground/50">Settled</span> | | |
| 404 | <span className="h-px flex-1 bg-border/60" /> | | |
| 405 | </div> | | |
| 406 | ) : null} | | |
| 407 | <div | 397 | <div |
| 408 | role="button" | 398 | role="button" |
| 409 | tabIndex={0} | 399 | tabIndex={0} |
| ... | @@ -1529,7 +1519,7 @@ export default function SidebarV2() { | ... | @@ -1529,7 +1519,7 @@ export default function SidebarV2() { |
| 1529 | ) : null} | 1519 | ) : null} |
| 1530 | <SidebarGroup className="min-h-0 flex-1 overflow-y-auto px-2 py-1"> | 1520 | <SidebarGroup className="min-h-0 flex-1 overflow-y-auto px-2 py-1"> |
| 1531 | <ul ref={attachListAutoAnimateRef} className="flex flex-col gap-px"> | 1521 | <ul ref={attachListAutoAnimateRef} className="flex flex-col gap-px"> |
| 1532 | {orderedThreads.map((thread, threadIndex) => { | 1522 | {orderedThreads.flatMap((thread, threadIndex) => { |
| 1533 | const threadKey = scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)); | 1523 | const threadKey = scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)); |
| 1534 | const isSettledRow = settledThreadKeys.has(threadKey); | 1524 | const isSettledRow = settledThreadKeys.has(threadKey); |
| 1535 | // Settled is the ONLY thing that collapses a row: every | 1525 | // Settled is the ONLY thing that collapses a row: every |
| ... | @@ -1544,10 +1534,14 @@ export default function SidebarV2() { | ... | @@ -1544,10 +1534,14 @@ export default function SidebarV2() { |
| 1544 | scopedThreadKey(scopeThreadRef(previousThread.environmentId, previousThread.id)), | 1534 | scopedThreadKey(scopeThreadRef(previousThread.environmentId, previousThread.id)), |
| 1545 | ); | 1535 | ); |
| 1546 | const showSettledGap = !isCard && previousWasCard; | 1536 | const showSettledGap = !isCard && previousWasCard; |
| 1547 | return ( | 1537 | const row = ( |
| 1548 | <SidebarV2Row | 1538 | <SidebarV2Row |
| 1549 | showSettledGap={showSettledGap} | 1539 | // Keyed per variant on purpose: when a thread settles, the |
| 1550 | key={threadKey} | 1540 | // card fades out in place and the slim row fades in at its |
| | 1541 | // settled position instead of one element FLIP-sliding |
| | 1542 | // through every row in between (rows here are translucent, |
| | 1543 | // so a crossing row reads as text painted over text). |
| | 1544 | key={`${threadKey}:${isCard ? "card" : "slim"}`} |
| 1551 | thread={thread} | 1545 | thread={thread} |
| 1552 | variant={isCard ? "card" : "slim"} | 1546 | variant={isCard ? "card" : "slim"} |
| 1553 | // Every settled row can un-settle: explicit settles clear | 1547 | // Every settled row can un-settle: explicit settles clear |
| ... | @@ -1582,6 +1576,28 @@ export default function SidebarV2() { | ... | @@ -1582,6 +1576,28 @@ export default function SidebarV2() { |
| 1582 | onChangeRequestState={handleChangeRequestState} | 1576 | onChangeRequestState={handleChangeRequestState} |
| 1583 | /> | 1577 | /> |
| 1584 | ); | 1578 | ); |
| | 1579 | if (!showSettledGap) return [row]; |
| | 1580 | // The divider is its own keyed list item (not part of the first |
| | 1581 | // settled row): it keeps one stable DOM node at the boundary, |
| | 1582 | // so settling a thread slides it instead of teleporting it |
| | 1583 | // along with whichever row happens to be first in the tail — |
| | 1584 | // and row heights stay independent of neighbor classification. |
| | 1585 | return [ |
| | 1586 | <li |
| | 1587 | key="settled-divider" |
| | 1588 | aria-hidden |
| | 1589 | data-thread-selection-safe |
| | 1590 | className="list-none" |
| | 1591 | > |
| | 1592 | <div className="mb-1 mt-3 flex items-center gap-2 px-2.5"> |
| | 1593 | <span className="text-[10px] font-medium text-muted-foreground/50"> |
| | 1594 | Settled |
| | 1595 | </span> |
| | 1596 | <span className="h-px flex-1 bg-border/60" /> |
| | 1597 | </div> |
| | 1598 | </li>, |
| | 1599 | row, |
| | 1600 | ]; |
| 1585 | })} | 1601 | })} |
| 1586 | {hiddenSettledCount > 0 ? ( | 1602 | {hiddenSettledCount > 0 ? ( |
| 1587 | <li className="list-none"> | 1603 | <li className="list-none"> |