authorgravatar for me@pixp.ccAlex <me@pixp.cc> 2026-07-31 11:55:29-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-31 14:55:29-04:00
log916cff733cd00c85fe0419df5bcb9372a6384746
treea0be843dc1c3db78cca235f4146c063945bfee18
parenta041981276b4c789fed8132e3b8a320749bf25e8
signature Signed by PGP key B5690EEEBB952194

feat(mobile): make settled threads collapsible (#5056)


5 files changed, 215 insertions(+), 48 deletions(-)

apps/mobile/src/features/home/HomeScreen.tsx+22-3
......@@ -44,6 +44,7 @@ import {
4444import {
4545 ThreadListV2PendingRow,
4646 ThreadListV2Row,
47 ThreadListV2SettledShelfHeader,
4748 ThreadListV2SnoozedShelfHeader,
4849} from "../threads/thread-list-v2-items";
4950import {
......@@ -534,6 +535,8 @@ export function HomeScreen(props: HomeScreenProps) {
534535 );
535536 const [snoozedShelfExpanded, setSnoozedShelfExpanded] = useState(false);
536537 const toggleSnoozedShelf = useCallback(() => setSnoozedShelfExpanded((value) => !value), []);
538 const [settledShelfExpanded, setSettledShelfExpanded] = useState(true);
539 const toggleSettledShelf = useCallback(() => setSettledShelfExpanded((value) => !value), []);
537540 // now is quantized to the minute and ticks so the inactivity auto-settle
538541 // boundary is actually crossed while the app stays open (mirrors web);
539542 // without a clock dependency the partition memoizes a frozen "now".
......@@ -579,6 +582,8 @@ export function HomeScreen(props: HomeScreenProps) {
579582 hiddenSettledCount: 0,
580583 snoozedCount: 0,
581584 snoozedShelfHeaderIndex: null,
585 settledCount: 0,
586 settledShelfHeaderIndex: null,
582587 nextSnoozeWakeAt: null,
583588 };
584589 // Settled threads are live shells; archived threads keep their original
......@@ -596,6 +601,7 @@ export function HomeScreen(props: HomeScreenProps) {
596601 now: `${nowMinute}:00.000Z`,
597602 snoozeNow: new Date().toISOString(),
598603 snoozedShelfExpanded,
604 settledShelfExpanded,
599605 selectedThreadKey: null,
600606 });
601607 }, [
......@@ -603,6 +609,7 @@ export function HomeScreen(props: HomeScreenProps) {
603609 nowMinute,
604610 snoozeWakeTick,
605611 snoozedShelfExpanded,
612 settledShelfExpanded,
606613 settledVisibleCount,
607614 settlementEnvironmentIds,
608615 snoozeEnvironmentIds,
......@@ -655,9 +662,12 @@ export function HomeScreen(props: HomeScreenProps) {
655662 snoozedCount: threadListV2Layout.snoozedCount,
656663 snoozedShelfExpanded,
657664 snoozedShelfHeaderIndex: threadListV2Layout.snoozedShelfHeaderIndex,
665 settledCount: threadListV2Layout.settledCount,
666 settledShelfExpanded,
667 settledShelfHeaderIndex: threadListV2Layout.settledShelfHeaderIndex,
658668 snoozeLabelNow: `${nowMinute}:00.000Z`,
659669 }),
660 [snoozedShelfExpanded, threadListV2Layout, v2PendingTasks],
670 [settledShelfExpanded, snoozedShelfExpanded, threadListV2Layout, v2PendingTasks],
661671 );
662672
663673 const renderV2Item = useCallback(
......@@ -693,12 +703,20 @@ export function HomeScreen(props: HomeScreenProps) {
693703 />
694704 );
695705 }
706 if (item.type === "v2-settled-shelf") {
707 return (
708 <ThreadListV2SettledShelfHeader
709 count={item.count}
710 expanded={item.expanded}
711 onToggle={toggleSettledShelf}
712 />
713 );
714 }
696715 const thread = item.item.thread;
697716 return (
698717 <ThreadListV2Row
699718 thread={thread}
700719 variant={item.item.variant}
701 showSettledDivider={item.item.showSettledDivider}
702720 snoozed={item.item.snoozed}
703721 snoozePresetMinute={nowMinute}
704722 snoozeWakeLabelText={item.snoozeWakeLabelText}
......@@ -767,6 +785,7 @@ export function HomeScreen(props: HomeScreenProps) {
767785 settlementEnvironmentIds,
768786 snoozeEnvironmentIds,
769787 threadSearchMatchByKey,
788 toggleSettledShelf,
770789 toggleSnoozedShelf,
771790 v2ProjectTitleByProjectKey,
772791 props.searchQuery,
......@@ -1036,7 +1055,7 @@ export function HomeScreen(props: HomeScreenProps) {
10361055 extraData={v2ExtraData}
10371056 ListHeaderComponent={v2ListHeader}
10381057 ListFooterComponent={
1039 threadListV2Layout.hiddenSettledCount > 0 ? (
1058 settledShelfExpanded && threadListV2Layout.hiddenSettledCount > 0 ? (
10401059 <Pressable
10411060 accessibilityRole="button"
10421061 accessibilityLabel={`Show ${Math.min(threadListV2Layout.hiddenSettledCount, THREAD_LIST_V2_SETTLED_PAGE_COUNT)} more settled threads`}
apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx+28-4
......@@ -70,6 +70,7 @@ import {
7070import {
7171 ThreadListV2PendingRow,
7272 ThreadListV2Row,
73 ThreadListV2SettledShelfHeader,
7374 ThreadListV2SnoozedShelfHeader,
7475} from "./thread-list-v2-items";
7576import {
......@@ -440,6 +441,8 @@ function ThreadNavigationSidebarPane(
440441 );
441442 const [snoozedShelfExpanded, setSnoozedShelfExpanded] = useState(false);
442443 const toggleSnoozedShelf = useCallback(() => setSnoozedShelfExpanded((value) => !value), []);
444 const [settledShelfExpanded, setSettledShelfExpanded] = useState(true);
445 const toggleSettledShelf = useCallback(() => setSettledShelfExpanded((value) => !value), []);
443446 // now ticks per minute so the inactivity auto-settle boundary is actually
444447 // crossed while the pane stays open; without a clock dependency the
445448 // partition memoizes a frozen "now".
......@@ -485,6 +488,8 @@ function ThreadNavigationSidebarPane(
485488 hiddenSettledCount: 0,
486489 snoozedCount: 0,
487490 snoozedShelfHeaderIndex: null,
491 settledCount: 0,
492 settledShelfHeaderIndex: null,
488493 nextSnoozeWakeAt: null,
489494 };
490495 return buildThreadListV2Items({
......@@ -500,6 +505,7 @@ function ThreadNavigationSidebarPane(
500505 now: `${nowMinute}:00.000Z`,
501506 snoozeNow: new Date().toISOString(),
502507 snoozedShelfExpanded,
508 settledShelfExpanded,
503509 selectedThreadKey: props.selectedThreadKey ?? null,
504510 });
505511 }, [
......@@ -507,6 +513,7 @@ function ThreadNavigationSidebarPane(
507513 nowMinute,
508514 snoozeWakeTick,
509515 snoozedShelfExpanded,
516 settledShelfExpanded,
510517 props.selectedThreadKey,
511518 options.selectedEnvironmentId,
512519 props.searchQuery,
......@@ -557,9 +564,12 @@ function ThreadNavigationSidebarPane(
557564 snoozedCount: threadListV2Layout.snoozedCount,
558565 snoozedShelfExpanded,
559566 snoozedShelfHeaderIndex: threadListV2Layout.snoozedShelfHeaderIndex,
567 settledCount: threadListV2Layout.settledCount,
568 settledShelfExpanded,
569 settledShelfHeaderIndex: threadListV2Layout.settledShelfHeaderIndex,
560570 snoozeLabelNow: `${nowMinute}:00.000Z`,
561571 });
562 if (threadListV2Layout.hiddenSettledCount > 0) {
572 if (settledShelfExpanded && threadListV2Layout.hiddenSettledCount > 0) {
563573 items.push({
564574 type: "v2-show-more",
565575 key: "v2-show-more",
......@@ -574,6 +584,7 @@ function ThreadNavigationSidebarPane(
574584 pendingTasks,
575585 props.searchQuery,
576586 selectedProjectRefs,
587 settledShelfExpanded,
577588 snoozedShelfExpanded,
578589 threadListV2Enabled,
579590 threadListV2Layout,
......@@ -780,7 +791,6 @@ function ThreadNavigationSidebarPane(
780791 previous.key === item.key &&
781792 previous.item.thread === item.item.thread &&
782793 previous.item.variant === item.item.variant &&
783 previous.item.showSettledDivider === item.item.showSettledDivider &&
784794 previous.item.snoozed === item.item.snoozed &&
785795 previous.snoozeWakeLabelText === item.snoozeWakeLabelText
786796 );
......@@ -797,15 +807,20 @@ function ThreadNavigationSidebarPane(
797807 if (previous.type === "v2-snoozed-shelf" && item.type === "v2-snoozed-shelf") {
798808 return previous.count === item.count && previous.expanded === item.expanded;
799809 }
810 if (previous.type === "v2-settled-shelf" && item.type === "v2-settled-shelf") {
811 return previous.count === item.count && previous.expanded === item.expanded;
812 }
800813 if (
801814 previous.type === "v2-thread" ||
802815 previous.type === "v2-show-more" ||
803816 previous.type === "v2-pending" ||
804817 previous.type === "v2-snoozed-shelf" ||
818 previous.type === "v2-settled-shelf" ||
805819 item.type === "v2-thread" ||
806820 item.type === "v2-show-more" ||
807821 item.type === "v2-pending" ||
808 item.type === "v2-snoozed-shelf"
822 item.type === "v2-snoozed-shelf" ||
823 item.type === "v2-settled-shelf"
809824 ) {
810825 return false;
811826 }
......@@ -863,7 +878,6 @@ function ThreadNavigationSidebarPane(
863878 <ThreadListV2Row
864879 thread={thread}
865880 variant={item.item.variant}
866 showSettledDivider={item.item.showSettledDivider}
867881 snoozed={item.item.snoozed}
868882 snoozePresetMinute={nowMinute}
869883 snoozeWakeLabelText={item.snoozeWakeLabelText}
......@@ -921,6 +935,15 @@ function ThreadNavigationSidebarPane(
921935 pane="sidebar"
922936 />
923937 );
938 case "v2-settled-shelf":
939 return (
940 <ThreadListV2SettledShelfHeader
941 count={item.count}
942 expanded={item.expanded}
943 onToggle={toggleSettledShelf}
944 pane="sidebar"
945 />
946 );
924947 case "v2-show-more":
925948 return (
926949 <Pressable
......@@ -1039,6 +1062,7 @@ function ThreadNavigationSidebarPane(
10391062 snoozeEnvironmentIds,
10401063 snoozeThread,
10411064 nowMinute,
1065 toggleSettledShelf,
10421066 toggleSnoozedShelf,
10431067 unsettleThread,
10441068 unsnoozeThread,
apps/mobile/src/features/threads/thread-list-v2-items.tsx+36-4
......@@ -149,6 +149,42 @@ export const ThreadListV2SnoozedShelfHeader = memo(function ThreadListV2SnoozedS
149149 );
150150});
151151
152export const ThreadListV2SettledShelfHeader = memo(function ThreadListV2SettledShelfHeader(props: {
153 readonly count: number;
154 readonly expanded: boolean;
155 readonly onToggle: () => void;
156 readonly pane?: "screen" | "sidebar";
157}) {
158 const mutedColor = useThemeColor("--color-foreground-muted");
159 return (
160 <Pressable
161 accessibilityHint={
162 props.expanded ? "Collapses the settled threads." : "Expands the settled threads."
163 }
164 accessibilityLabel={props.count === 1 ? "1 settled thread" : `${props.count} settled threads`}
165 accessibilityRole="button"
166 accessibilityState={{ expanded: props.expanded }}
167 className={cn(
168 "mb-1.5 mt-4 flex-row items-center gap-2.5",
169 props.pane === "sidebar" ? "px-3" : "px-5",
170 )}
171 onPress={props.onToggle}
172 style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })}
173 >
174 <Text className="text-xs font-t3-medium text-foreground-tertiary">
175 {props.expanded ? "Settled" : `Settled (${props.count})`}
176 </Text>
177 <View className="h-px flex-1 bg-border" />
178 <SymbolView
179 name={props.expanded ? "chevron.up" : "chevron.down"}
180 size={10}
181 tintColor={mutedColor}
182 type="monochrome"
183 />
184 </Pressable>
185 );
186});
187
152188const PENDING_TASK_MENU_ACTIONS: MenuAction[] = [
153189 { id: "delete", title: "Delete", image: "trash", attributes: { destructive: true } },
154190];
......@@ -267,7 +303,6 @@ export const ThreadListV2PendingRow = memo(function ThreadListV2PendingRow(props
267303export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
268304 readonly thread: EnvironmentThreadShell;
269305 readonly variant: "card" | "slim";
270 readonly showSettledDivider: boolean;
271306 /** Snoozed-shelf row: shows its wake time and offers Wake. */
272307 readonly snoozed?: boolean;
273308 /** Preformatted against the parent minute tick so this memoized row's
......@@ -739,9 +774,6 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
739774
740775 return (
741776 <>
742 {props.showSettledDivider ? (
743 <ThreadListV2SectionDivider label="Settled" pane={props.pane} />
744 ) : null}
745777 <ThreadSwipeable
746778 backgroundColor={sidebarPane ? drawerColor : screenColor}
747779 compactActions={variant === "slim"}
apps/mobile/src/features/threads/threadListV2.test.ts+73-9
......@@ -425,8 +425,8 @@ describe("buildThreadListV2Items", () => {
425425 expect(layout.snoozedCount).toBe(0);
426426 });
427427
428 it("partitions settled threads into a slim tail with one divider", () => {
429 const { items } = buildThreadListV2Items({
428 it("partitions settled threads into a slim shelf", () => {
429 const layout = buildThreadListV2Items({
430430 threads: [
431431 makeThread({ id: ThreadId.make("active"), title: "Active" }),
432432 makeThread({
......@@ -447,13 +447,64 @@ describe("buildThreadListV2Items", () => {
447447 now: NOW,
448448 });
449449
450 expect(items.map((item) => [item.thread.id, item.variant])).toEqual([
450 expect(layout.items.map((item) => [item.thread.id, item.variant])).toEqual([
451451 ["active", "card"],
452452 ["settled", "slim"],
453453 ["settled-2", "slim"],
454454 ]);
455 expect(items.map((item) => item.showSettledDivider)).toEqual([false, true, false]);
456 expect(items.map((item) => item.isLast)).toEqual([false, false, true]);
455 expect(layout.items.map((item) => item.isLast)).toEqual([false, false, true]);
456 expect(layout.settledCount).toBe(2);
457 expect(layout.settledShelfHeaderIndex).toBe(1);
458 });
459
460 it("collapses settled threads to a counted shelf header", () => {
461 const layout = buildThreadListV2Items({
462 threads: [
463 makeThread({ id: ThreadId.make("active"), title: "Active" }),
464 makeThread({
465 id: ThreadId.make("settled"),
466 title: "Settled",
467 settledOverride: "settled",
468 settledAt: NOW,
469 }),
470 ],
471 environmentId: null,
472 searchQuery: "",
473 now: NOW,
474 settledShelfExpanded: false,
475 });
476
477 expect(layout.items.map((item) => item.thread.id)).toEqual(["active"]);
478 expect(layout.settledCount).toBe(1);
479 expect(layout.settledShelfHeaderIndex).toBe(1);
480 });
481
482 it("keeps the selected settled thread visible when its shelf is collapsed", () => {
483 const layout = buildThreadListV2Items({
484 threads: [
485 makeThread({
486 id: ThreadId.make("selected"),
487 title: "Selected",
488 settledOverride: "settled",
489 settledAt: NOW,
490 }),
491 makeThread({
492 id: ThreadId.make("other"),
493 title: "Other",
494 settledOverride: "settled",
495 settledAt: NOW,
496 }),
497 ],
498 environmentId: null,
499 searchQuery: "",
500 now: NOW,
501 settledShelfExpanded: false,
502 selectedThreadKey: `${environmentId}:selected`,
503 });
504
505 expect(layout.items.map((item) => item.thread.id)).toEqual(["selected"]);
506 expect(layout.settledCount).toBe(2);
507 expect(layout.settledShelfHeaderIndex).toBe(0);
457508 });
458509
459510 it("keeps cards in creation order while settled sorts by recency", () => {
......@@ -658,6 +709,8 @@ describe("buildThreadListV2ListItems", () => {
658709 const items = buildThreadListV2ListItems({
659710 items: layout.items,
660711 pendingTasks: [makePendingTask("queued-1"), makePendingTask("queued-2")],
712 settledCount: layout.settledCount,
713 settledShelfHeaderIndex: layout.settledShelfHeaderIndex,
661714 });
662715
663716 expect(
......@@ -666,9 +719,11 @@ describe("buildThreadListV2ListItems", () => {
666719 ? item.pendingTask.title
667720 : item.type === "v2-thread"
668721 ? item.item.thread.id
669 : "snoozed-shelf",
722 : item.type === "v2-snoozed-shelf"
723 ? "snoozed-shelf"
724 : "settled-shelf",
670725 ),
671 ).toEqual(["active", "queued-1", "queued-2", "settled"]);
726 ).toEqual(["active", "queued-1", "queued-2", "settled-shelf", "settled"]);
672727 // Only the leading queued row labels the section, exactly like Settled.
673728 expect(
674729 items.filter((item) => item.type === "v2-pending" && item.showPendingDivider),
......@@ -690,11 +745,17 @@ describe("buildThreadListV2ListItems", () => {
690745 expect(items.map((item) => item.type)).toEqual(["v2-thread", "v2-pending"]);
691746 });
692747
693 it("leaves the thread order untouched when nothing is queued", () => {
694 const items = buildThreadListV2ListItems({ items: layout.items, pendingTasks: [] });
748 it("keeps the settled shelf between active and settled rows when nothing is queued", () => {
749 const items = buildThreadListV2ListItems({
750 items: layout.items,
751 pendingTasks: [],
752 settledCount: layout.settledCount,
753 settledShelfHeaderIndex: layout.settledShelfHeaderIndex,
754 });
695755
696756 expect(items.map((item) => item.key)).toEqual([
697757 `v2-thread:${environmentId}:active`,
758 "v2-settled-shelf",
698759 `v2-thread:${environmentId}:settled`,
699760 ]);
700761 });
......@@ -726,12 +787,15 @@ describe("buildThreadListV2ListItems", () => {
726787 snoozedCount: snoozedLayout.snoozedCount,
727788 snoozedShelfExpanded: false,
728789 snoozedShelfHeaderIndex: snoozedLayout.snoozedShelfHeaderIndex,
790 settledCount: snoozedLayout.settledCount,
791 settledShelfHeaderIndex: snoozedLayout.settledShelfHeaderIndex,
729792 });
730793
731794 expect(items.map((item) => item.type)).toEqual([
732795 "v2-thread",
733796 "v2-pending",
734797 "v2-snoozed-shelf",
798 "v2-settled-shelf",
735799 "v2-thread",
736800 ]);
737801 });
apps/mobile/src/features/threads/threadListV2.ts+56-28
......@@ -178,8 +178,6 @@ export function sortThreadsForListV2<T extends { readonly id: string; readonly c
178178export interface ThreadListV2Item {
179179 readonly thread: EnvironmentThreadShell;
180180 readonly variant: "card" | "slim";
181 /** First settled row after the card block draws the SETTLED divider. */
182 readonly showSettledDivider: boolean;
183181 /** Snoozed-shelf row: shows the wake countdown and offers Wake. */
184182 readonly snoozed: boolean;
185183 readonly isLast: boolean;
......@@ -194,6 +192,10 @@ export interface ThreadListV2Layout {
194192 /** Index in `items` where the Snoozed shelf header belongs. The header is
195193 still rendered when the shelf is collapsed and no snoozed rows exist. */
196194 readonly snoozedShelfHeaderIndex: number | null;
195 /** Total settled threads in scope, including rows hidden by collapse/paging. */
196 readonly settledCount: number;
197 /** Index in `items` where the Settled shelf header belongs. */
198 readonly settledShelfHeaderIndex: number | null;
197199 /** Soonest wake time among snoozed threads, or null. Callers arm
198200 a timeout at this boundary so the list re-partitions the moment a
199201 snooze expires instead of on the next minute tick. */
......@@ -223,10 +225,18 @@ export interface ThreadListV2SnoozedShelfListItem {
223225 readonly expanded: boolean;
224226}
225227
228export interface ThreadListV2SettledShelfListItem {
229 readonly type: "v2-settled-shelf";
230 readonly key: "v2-settled-shelf";
231 readonly count: number;
232 readonly expanded: boolean;
233}
234
226235export type ThreadListV2ListItem =
227236 | ThreadListV2ThreadListItem
228237 | ThreadListV2PendingListItem
229 | ThreadListV2SnoozedShelfListItem;
238 | ThreadListV2SnoozedShelfListItem
239 | ThreadListV2SettledShelfListItem;
230240
231241/**
232242 * Builds the shared mobile order: active → pending → snoozed shelf → settled.
......@@ -239,6 +249,9 @@ export function buildThreadListV2ListItems(input: {
239249 readonly snoozedCount?: number;
240250 readonly snoozedShelfExpanded?: boolean;
241251 readonly snoozedShelfHeaderIndex?: number | null;
252 readonly settledCount?: number;
253 readonly settledShelfExpanded?: boolean;
254 readonly settledShelfHeaderIndex?: number | null;
242255 readonly snoozeLabelNow?: string;
243256}): ThreadListV2ListItem[] {
244257 const threadItems = input.items.map(
......@@ -262,27 +275,30 @@ export function buildThreadListV2ListItems(input: {
262275 );
263276 const snoozedCount = input.snoozedCount ?? 0;
264277 const snoozedShelfHeaderIndex = input.snoozedShelfHeaderIndex ?? null;
278 const settledCount = input.settledCount ?? 0;
279 const settledShelfHeaderIndex = input.settledShelfHeaderIndex ?? null;
280 const activeEnd = snoozedShelfHeaderIndex ?? settledShelfHeaderIndex ?? threadItems.length;
281 const snoozedEnd = settledShelfHeaderIndex ?? threadItems.length;
282 const result: ThreadListV2ListItem[] = [...threadItems.slice(0, activeEnd), ...pendingItems];
265283 if (snoozedShelfHeaderIndex !== null && snoozedCount > 0) {
266 return [
267 ...threadItems.slice(0, snoozedShelfHeaderIndex),
268 ...pendingItems,
269 {
270 type: "v2-snoozed-shelf",
271 key: "v2-snoozed-shelf",
272 count: snoozedCount,
273 expanded: input.snoozedShelfExpanded === true,
274 },
275 ...threadItems.slice(snoozedShelfHeaderIndex),
276 ];
284 result.push({
285 type: "v2-snoozed-shelf",
286 key: "v2-snoozed-shelf",
287 count: snoozedCount,
288 expanded: input.snoozedShelfExpanded === true,
289 });
290 result.push(...threadItems.slice(snoozedShelfHeaderIndex, snoozedEnd));
277291 }
278 if (pendingItems.length === 0) return threadItems;
279
280 const settledStart = threadItems.findIndex(
281 (entry) => entry.type === "v2-thread" && entry.item.showSettledDivider,
282 );
283 return settledStart === -1
284 ? [...threadItems, ...pendingItems]
285 : [...threadItems.slice(0, settledStart), ...pendingItems, ...threadItems.slice(settledStart)];
292 if (settledShelfHeaderIndex !== null && settledCount > 0) {
293 result.push({
294 type: "v2-settled-shelf",
295 key: "v2-settled-shelf",
296 count: settledCount,
297 expanded: input.settledShelfExpanded !== false,
298 });
299 result.push(...threadItems.slice(settledShelfHeaderIndex));
300 }
301 return result;
286302}
287303
288304/**
......@@ -321,6 +337,8 @@ export function buildThreadListV2Items(input: {
321337 readonly snoozeNow?: string;
322338 /** Expands the snoozed shelf into rows. Collapsed is the default. */
323339 readonly snoozedShelfExpanded?: boolean;
340 /** Expands the settled shelf into rows. Expanded is the default. */
341 readonly settledShelfExpanded?: boolean;
324342 /** The selected thread remains visible on an otherwise collapsed shelf so
325343 a split-view detail can never lose its navigation row. */
326344 readonly selectedThreadKey?: string | null;
......@@ -402,15 +420,24 @@ export function buildThreadListV2Items(input: {
402420 firstValidTimestampMs(left.latestUserMessageAt, left.updatedAt),
403421 );
404422 const settledLimit = input.settledLimit ?? Number.POSITIVE_INFINITY;
405 const visibleSettled =
423 const pagedSettled =
406424 orderedSettled.length > settledLimit ? orderedSettled.slice(0, settledLimit) : orderedSettled;
425 const selectedSettled = orderedSettled
426 .slice(pagedSettled.length)
427 .find((thread) => `${thread.environmentId}:${thread.id}` === selectedThreadKey);
428 if (selectedSettled !== undefined) pagedSettled.push(selectedSettled);
429 const visibleSettled =
430 input.settledShelfExpanded !== false
431 ? pagedSettled
432 : pagedSettled.filter(
433 (thread) => `${thread.environmentId}:${thread.id}` === selectedThreadKey,
434 );
407435
408436 const items: ThreadListV2Item[] = [];
409437 for (const thread of orderedActive) {
410438 items.push({
411439 thread,
412440 variant: "card",
413 showSettledDivider: false,
414441 snoozed: false,
415442 isLast: false,
416443 });
......@@ -420,16 +447,15 @@ export function buildThreadListV2Items(input: {
420447 items.push({
421448 thread,
422449 variant: "slim",
423 showSettledDivider: false,
424450 snoozed: true,
425451 isLast: false,
426452 });
427453 }
428 for (const [index, thread] of visibleSettled.entries()) {
454 const settledShelfHeaderIndex = orderedSettled.length > 0 ? items.length : null;
455 for (const thread of visibleSettled) {
429456 items.push({
430457 thread,
431458 variant: "slim",
432 showSettledDivider: index === 0,
433459 snoozed: false,
434460 isLast: false,
435461 });
......@@ -440,9 +466,11 @@ export function buildThreadListV2Items(input: {
440466 }
441467 return {
442468 items,
443 hiddenSettledCount: orderedSettled.length - visibleSettled.length,
469 hiddenSettledCount: orderedSettled.length - pagedSettled.length,
444470 snoozedCount: orderedSnoozed.length,
445471 snoozedShelfHeaderIndex,
472 settledCount: orderedSettled.length,
473 settledShelfHeaderIndex,
446474 nextSnoozeWakeAt,
447475 };
448476}