import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search"; import { resolveSnoozePresets } from "@t3tools/client-runtime/state/thread-settled"; import { CommandId, EnvironmentId, MessageId, ProjectId, ProviderInstanceId, ThreadId, TurnId, } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; import type { PendingNewTask } from "../../state/use-pending-new-tasks"; import { buildThreadListV2Items, buildThreadListV2ListItems, resolveThreadListV2ChangeRequestState, resolveThreadListV2Enabled, resolveThreadListV2SnoozeMenuSelection, resolveThreadListV2SnoozeGateExpiryMs, resolveThreadListV2Status, resolveThreadListV2SwipeActions, sortThreadsForListV2, } from "./threadListV2"; const environmentId = EnvironmentId.make("environment-1"); function makeThread( input: Partial & Pick, ): EnvironmentThreadShell { return { environmentId, projectId: ProjectId.make("project-1"), modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, latestTurn: null, createdAt: "2026-06-01T00:00:00.000Z", updatedAt: "2026-06-01T00:00:00.000Z", archivedAt: null, settledOverride: null, settledAt: null, session: null, latestUserMessageAt: null, hasPendingApprovals: false, hasPendingUserInput: false, hasActionableProposedPlan: false, ...input, }; } const NOW = "2026-06-02T00:00:00.000Z"; const linkedPullRequest = { projectId: ProjectId.make("project-1"), repository: "pingdotgg/t3code", number: 42, url: "https://github.com/pingdotgg/t3code/pull/42", }; describe("resolveThreadListV2ChangeRequestState", () => { it("preserves the previous state while a linked pull request reloads", () => { expect( resolveThreadListV2ChangeRequestState({ linkedPullRequest, state: null, updatedAt: null, }), ).toBeUndefined(); }); it("clears the previous state after a pull request is unlinked", () => { expect( resolveThreadListV2ChangeRequestState({ linkedPullRequest: null, state: null, updatedAt: null, }), ).toBeNull(); }); it("reports a loaded linked pull request", () => { expect( resolveThreadListV2ChangeRequestState({ linkedPullRequest, state: "merged", updatedAt: "2026-06-02T00:00:00.000Z", }), ).toEqual({ state: "merged", updatedAt: "2026-06-02T00:00:00.000Z", linkedPullRequestKey: '["project-1","pingdotgg/t3code",42]', }); }); }); describe("resolveThreadListV2SnoozeMenuSelection", () => { it("accepts a displayed evening preset while its wake time is still future", () => { const menuOpenedAt = new Date(2026, 4, 8, 16, 59, 30); const selectedAt = new Date(2026, 4, 8, 17, 0, 30); const displayedPresets = resolveSnoozePresets(menuOpenedAt); const selection = resolveThreadListV2SnoozeMenuSelection({ event: "snooze:evening", displayedPresets, now: selectedAt, }); expect(selection).toEqual({ _tag: "selected", preset: displayedPresets.find((preset) => preset.id === "evening"), }); }); it("expires a displayed preset once its wake time has passed", () => { const displayedPresets = resolveSnoozePresets(new Date(2026, 4, 8, 16, 59, 30)); expect( resolveThreadListV2SnoozeMenuSelection({ event: "snooze:evening", displayedPresets, now: new Date(2026, 4, 8, 18, 0, 1), }), ).toEqual({ _tag: "expired" }); }); it("recomputes presets that remain available instead of using old timestamps", () => { const displayedPresets = resolveSnoozePresets(new Date(2026, 4, 8, 10)); const selectedAt = new Date(2026, 4, 8, 10, 30); const selection = resolveThreadListV2SnoozeMenuSelection({ event: "snooze:hour", displayedPresets, now: selectedAt, }); expect(selection._tag).toBe("selected"); if (selection._tag === "selected") { expect(selection.preset.snoozedUntil).toBe( new Date(selectedAt.getTime() + 60 * 60 * 1_000).toISOString(), ); } }); }); describe("resolveThreadListV2Enabled", () => { it("defaults on when the device has never chosen", () => { expect( resolveThreadListV2Enabled({ legacyPreference: undefined, preferencesLoaded: true }), ).toBe(true); }); it("honors an explicit legacy opt-in", () => { expect(resolveThreadListV2Enabled({ legacyPreference: true, preferencesLoaded: true })).toBe( false, ); expect(resolveThreadListV2Enabled({ legacyPreference: false, preferencesLoaded: true })).toBe( true, ); }); it("holds the default while preferences are still loading so the list does not remount", () => { expect( resolveThreadListV2Enabled({ legacyPreference: undefined, preferencesLoaded: false }), ).toBe(true); }); }); describe("resolveThreadListV2Status", () => { it("prioritizes approval over a running session", () => { const thread = makeThread({ id: ThreadId.make("t"), title: "t", hasPendingApprovals: true, session: { threadId: ThreadId.make("t"), status: "running", providerName: "Codex", providerInstanceId: ProviderInstanceId.make("codex"), runtimeMode: "full-access", activeTurnId: null, lastError: null, updatedAt: NOW, }, }); expect(resolveThreadListV2Status(thread)).toBe("approval"); }); it("resolves ready for quiescent threads", () => { expect(resolveThreadListV2Status(makeThread({ id: ThreadId.make("t"), title: "t" }))).toBe( "ready", ); }); }); describe("resolveThreadListV2SwipeActions", () => { it("offers settle and snooze for an active snoozable thread", () => { expect( resolveThreadListV2SwipeActions({ variant: "card", settlementSupported: true, snoozeSupported: true, snoozable: true, }), ).toEqual({ primary: "settle", secondary: "snooze" }); }); it("offers un-settle and snooze for settled history", () => { expect( resolveThreadListV2SwipeActions({ variant: "slim", settlementSupported: true, snoozeSupported: true, snoozable: true, }), ).toEqual({ primary: "unsettle", secondary: "snooze" }); }); it("omits snooze when the server or thread does not allow it", () => { expect( resolveThreadListV2SwipeActions({ variant: "card", settlementSupported: true, snoozeSupported: false, snoozable: true, }), ).toEqual({ primary: "settle", secondary: null }); expect( resolveThreadListV2SwipeActions({ variant: "card", settlementSupported: true, snoozeSupported: true, snoozable: false, }), ).toEqual({ primary: "settle", secondary: null }); }); it("falls back to archive only for a pre-lifecycle server", () => { expect( resolveThreadListV2SwipeActions({ variant: "card", settlementSupported: false, snoozeSupported: false, snoozable: true, }), ).toEqual({ primary: "archive", secondary: null }); }); it("offers wake and no snooze on a snoozed row", () => { expect( resolveThreadListV2SwipeActions({ variant: "slim", settlementSupported: true, snoozeSupported: true, snoozable: true, snoozed: true, }), ).toEqual({ primary: "unsnooze", secondary: null }); }); }); describe("resolveThreadListV2SnoozeGateExpiryMs", () => { it("reports when an unadopted turn's grace window lapses", () => { const thread = makeThread({ id: ThreadId.make("t"), title: "t", latestUserMessageAt: "2026-06-02T00:00:30.000Z", }); expect(resolveThreadListV2SnoozeGateExpiryMs(thread, { now: "2026-06-02T00:01:00.000Z" })).toBe( Date.parse("2026-06-02T00:02:30.000Z"), ); }); it("returns null once the thread is snoozable or when only data can unblock it", () => { expect( resolveThreadListV2SnoozeGateExpiryMs( makeThread({ id: ThreadId.make("ready"), title: "Ready" }), { now: NOW }, ), ).toBe(null); expect( resolveThreadListV2SnoozeGateExpiryMs( makeThread({ id: ThreadId.make("blocked"), title: "Blocked", hasPendingApprovals: true, latestUserMessageAt: NOW, }), { now: NOW }, ), ).toBe(null); }); }); describe("sortThreadsForListV2", () => { it("orders by creation time, newest first, ignoring activity", () => { const sorted = sortThreadsForListV2([ { id: "oldest", createdAt: "2026-06-01T08:00:00.000Z" }, { id: "newest", createdAt: "2026-06-01T12:00:00.000Z" }, { id: "middle", createdAt: "2026-06-01T10:00:00.000Z" }, ]); expect(sorted.map((thread) => thread.id)).toEqual(["newest", "middle", "oldest"]); }); }); describe("buildThreadListV2Items", () => { it("ignores the previous pull request state after a different pull request is linked", () => { const thread = makeThread({ id: ThreadId.make("linked"), title: "Linked pull request", linkedPullRequest, }); const layout = buildThreadListV2Items({ threads: [thread], environmentId: null, searchQuery: "", changeRequestByKey: new Map([ [ `${environmentId}:${thread.id}`, { state: "merged" as const, linkedPullRequestKey: '["project-1","pingdotgg/t3code",41]', }, ], ]), now: NOW, }); expect(layout.settledCount).toBe(0); expect(layout.items[0]?.variant).toBe("card"); }); it("settles a thread only when the cached pull request identity matches", () => { const thread = makeThread({ id: ThreadId.make("linked-merged"), title: "Linked merged pull request", linkedPullRequest, }); const layout = buildThreadListV2Items({ threads: [thread], environmentId: null, searchQuery: "", changeRequestByKey: new Map([ [ `${environmentId}:${thread.id}`, { state: "merged" as const, linkedPullRequestKey: '["project-1","pingdotgg/t3code",42]', }, ], ]), now: NOW, }); expect(layout.settledCount).toBe(1); expect(layout.items[0]?.variant).toBe("slim"); }); it("keeps a merged thread active when auto-settle on merge is off", () => { const merged = makeThread({ id: ThreadId.make("merged"), title: "Merged" }); const layout = buildThreadListV2Items({ threads: [merged], environmentId: null, searchQuery: "", changeRequestByKey: new Map([ [`${environmentId}:${merged.id}`, { state: "merged" as const }], ]), autoSettleOnMerge: false, now: NOW, }); expect(layout.items.map((item) => item.thread.id)).toEqual(["merged"]); expect(layout.settledCount).toBe(0); }); it("hides snoozed threads and counts them — visibility parity with web", () => { const layout = buildThreadListV2Items({ threads: [ makeThread({ id: ThreadId.make("active"), title: "Active" }), makeThread({ id: ThreadId.make("snoozed"), title: "Snoozed", snoozedUntil: "2026-06-03T09:00:00.000Z", snoozedAt: "2026-06-01T12:00:00.000Z", }), makeThread({ id: ThreadId.make("woken"), title: "Woken", // Wake time already passed: back in the active list. snoozedUntil: "2026-06-01T18:00:00.000Z", snoozedAt: "2026-06-01T12:00:00.000Z", }), ], environmentId: null, searchQuery: "", now: NOW, }); // Same createdAt → static sort tiebreaks by id; the point is the woken // thread is BACK in the card block and the snoozed one is gone. expect(layout.items.map((item) => item.thread.id)).toEqual(["active", "woken"]); expect(layout.snoozedCount).toBe(1); }); it("places settled pinned threads in the settled shelf", () => { const layout = buildThreadListV2Items({ threads: [ makeThread({ id: ThreadId.make("active"), title: "Active" }), makeThread({ id: ThreadId.make("pinned-settled"), title: "Pinned while settled", pinnedAt: "2026-06-01T12:00:00.000Z", settledOverride: "settled", settledAt: "2026-06-01T12:00:00.000Z", }), ], environmentId: null, searchQuery: "", now: NOW, }); expect(layout.items.map((item) => item.thread.id)).toEqual(["active", "pinned-settled"]); expect(layout.items.map((item) => item.pinned)).toEqual([false, false]); expect(layout.settledCount).toBe(1); }); it("moves pinned threads to the settled shelf when their pull request merges", () => { const merged = makeThread({ id: ThreadId.make("pinned-merged"), title: "Pinned merged pull request", pinnedAt: "2026-06-01T12:00:00.000Z", }); const layout = buildThreadListV2Items({ threads: [makeThread({ id: ThreadId.make("active"), title: "Active" }), merged], environmentId: null, searchQuery: "", changeRequestByKey: new Map([[`${environmentId}:${merged.id}`, { state: "merged" }]]), now: NOW, }); expect(layout.items.map((item) => item.thread.id)).toEqual(["active", "pinned-merged"]); expect(layout.items.map((item) => item.variant)).toEqual(["card", "slim"]); expect(layout.items[1]?.thread.pinnedAt).toBe("2026-06-01T12:00:00.000Z"); expect(layout.settledCount).toBe(1); }); it("moves inactive pinned threads to the settled shelf", () => { const inactive = makeThread({ id: ThreadId.make("pinned-inactive"), title: "Pinned inactive thread", createdAt: "2026-05-20T00:00:00.000Z", pinnedAt: "2026-05-21T00:00:00.000Z", latestTurn: { turnId: TurnId.make("turn-inactive"), state: "completed", requestedAt: "2026-05-21T00:00:00.000Z", startedAt: "2026-05-21T00:00:01.000Z", completedAt: "2026-05-21T00:00:02.000Z", assistantMessageId: null, }, }); const layout = buildThreadListV2Items({ threads: [inactive], environmentId: null, searchQuery: "", now: NOW, }); expect(layout.items[0]).toMatchObject({ thread: { id: "pinned-inactive" }, variant: "slim", pinned: false, }); expect(layout.settledCount).toBe(1); }); it("keeps pinned merged threads pinned when auto-settle on merge is off", () => { const merged = makeThread({ id: ThreadId.make("pinned-merged"), title: "Pinned merged pull request", pinnedAt: "2026-06-01T12:00:00.000Z", }); const layout = buildThreadListV2Items({ threads: [merged], environmentId: null, searchQuery: "", changeRequestByKey: new Map([[`${environmentId}:${merged.id}`, { state: "merged" }]]), autoSettleOnMerge: false, now: NOW, }); expect(layout.items[0]).toMatchObject({ thread: { id: "pinned-merged" }, variant: "card", pinned: true, }); expect(layout.settledCount).toBe(0); }); it("snooze hides a pinned thread and wake restores it to the pinned block", () => { const snoozedInput = { threads: [ makeThread({ id: ThreadId.make("active"), title: "Active" }), makeThread({ id: ThreadId.make("pinned-snoozed"), title: "Pinned and snoozed", pinnedAt: "2026-06-01T12:00:00.000Z", snoozedUntil: "2026-06-03T09:00:00.000Z", snoozedAt: "2026-06-01T11:00:00.000Z", }), ], environmentId: null, searchQuery: "", }; // Before the wake time: the snooze wins; the pin holds underneath. const whileSnoozed = buildThreadListV2Items({ ...snoozedInput, now: NOW }); expect(whileSnoozed.items.map((item) => item.thread.id)).toEqual(["active"]); expect(whileSnoozed.snoozedCount).toBe(1); // After the wake time: the thread returns pinned, back on top. const afterWake = buildThreadListV2Items({ ...snoozedInput, now: "2026-06-03T10:00:00.000Z" }); expect(afterWake.items.map((item) => item.thread.id)).toEqual(["pinned-snoozed", "active"]); expect(afterWake.items[0]?.pinned).toBe(true); expect(afterWake.snoozedCount).toBe(0); }); it("classifies snooze with the second-precise clock and reports the next wake", () => { const layout = buildThreadListV2Items({ threads: [ makeThread({ id: ThreadId.make("just-woke"), title: "Just woke", // Woke 30s ago: hidden under the minute-floored clock, visible // under the precise one. snoozedUntil: "2026-06-02T00:00:30.000Z", snoozedAt: "2026-06-01T12:00:00.000Z", }), makeThread({ id: ThreadId.make("still-snoozed"), title: "Still snoozed", snoozedUntil: "2026-06-02T09:00:00.000Z", snoozedAt: "2026-06-01T12:00:00.000Z", }), ], environmentId: null, searchQuery: "", // Minute-floored partition clock vs precise snooze clock. now: "2026-06-02T00:01:00.000Z", snoozeNow: "2026-06-02T00:01:07.500Z", }); expect(layout.items.map((item) => item.thread.id)).toEqual(["just-woke"]); expect(layout.snoozedCount).toBe(1); expect(layout.nextSnoozeWakeAt).toBe("2026-06-02T09:00:00.000Z"); }); it("builds snoozed rows between active and settled when the shelf is expanded", () => { const layout = buildThreadListV2Items({ threads: [ makeThread({ id: ThreadId.make("active"), title: "Active" }), makeThread({ id: ThreadId.make("settled"), title: "Settled", settledOverride: "settled", settledAt: NOW, }), makeThread({ id: ThreadId.make("later"), title: "Wakes later", snoozedUntil: "2026-06-03T09:00:00.000Z", snoozedAt: "2026-06-01T12:00:00.000Z", }), makeThread({ id: ThreadId.make("sooner"), title: "Wakes sooner", snoozedUntil: "2026-06-02T09:00:00.000Z", snoozedAt: "2026-06-01T12:00:00.000Z", }), ], environmentId: null, searchQuery: "", now: NOW, snoozedShelfExpanded: true, }); expect(layout.items.map((item) => item.thread.id)).toEqual([ "active", "sooner", "later", "settled", ]); expect(layout.items.map((item) => item.snoozed)).toEqual([false, true, true, false]); expect(layout.snoozedShelfHeaderIndex).toBe(1); expect(layout.snoozedCount).toBe(2); }); it("collapses to a header-only shelf", () => { const layout = buildThreadListV2Items({ threads: [ makeThread({ id: ThreadId.make("snoozed"), title: "Snoozed", snoozedUntil: "2026-06-03T09:00:00.000Z", snoozedAt: "2026-06-01T12:00:00.000Z", }), ], environmentId: null, searchQuery: "", now: NOW, }); expect(layout.items).toEqual([]); expect(layout.snoozedCount).toBe(1); expect(layout.snoozedShelfHeaderIndex).toBe(0); }); it("keeps the selected thread on a collapsed shelf", () => { const layout = buildThreadListV2Items({ threads: [ makeThread({ id: ThreadId.make("open"), title: "Open", snoozedUntil: "2026-06-03T09:00:00.000Z", snoozedAt: "2026-06-01T12:00:00.000Z", }), makeThread({ id: ThreadId.make("other"), title: "Other", snoozedUntil: "2026-06-03T10:00:00.000Z", snoozedAt: "2026-06-01T12:00:00.000Z", }), ], environmentId: null, searchQuery: "", now: NOW, selectedThreadKey: `${environmentId}:open`, }); expect(layout.items.map((item) => item.thread.id)).toEqual(["open"]); expect(layout.items[0]?.snoozed).toBe(true); expect(layout.snoozedCount).toBe(2); }); it("keeps snoozed threads visible on environments without the snooze capability", () => { const layout = buildThreadListV2Items({ threads: [ makeThread({ id: ThreadId.make("snoozed"), title: "Snoozed", snoozedUntil: "2026-06-03T09:00:00.000Z", snoozedAt: "2026-06-01T12:00:00.000Z", }), ], environmentId: null, searchQuery: "", snoozeEnvironmentIds: new Set(), now: NOW, }); expect(layout.items.map((item) => item.thread.id)).toEqual(["snoozed"]); expect(layout.snoozedCount).toBe(0); }); it("partitions settled threads into a slim shelf", () => { const layout = buildThreadListV2Items({ threads: [ makeThread({ id: ThreadId.make("active"), title: "Active" }), makeThread({ id: ThreadId.make("settled"), title: "Settled", settledOverride: "settled", settledAt: NOW, }), makeThread({ id: ThreadId.make("settled-2"), title: "Settled 2", settledOverride: "settled", settledAt: NOW, }), ], environmentId: null, searchQuery: "", now: NOW, }); expect(layout.items.map((item) => [item.thread.id, item.variant])).toEqual([ ["active", "card"], ["settled", "slim"], ["settled-2", "slim"], ]); expect(layout.items.map((item) => item.isLast)).toEqual([false, false, true]); expect(layout.settledCount).toBe(2); expect(layout.settledShelfHeaderIndex).toBe(1); }); it("collapses settled threads to a counted shelf header", () => { const layout = buildThreadListV2Items({ threads: [ makeThread({ id: ThreadId.make("active"), title: "Active" }), makeThread({ id: ThreadId.make("settled"), title: "Settled", settledOverride: "settled", settledAt: NOW, }), ], environmentId: null, searchQuery: "", now: NOW, settledShelfExpanded: false, }); expect(layout.items.map((item) => item.thread.id)).toEqual(["active"]); expect(layout.settledCount).toBe(1); expect(layout.settledShelfHeaderIndex).toBe(1); }); it("keeps the selected settled thread visible when its shelf is collapsed", () => { const layout = buildThreadListV2Items({ threads: [ makeThread({ id: ThreadId.make("selected"), title: "Selected", settledOverride: "settled", settledAt: NOW, }), makeThread({ id: ThreadId.make("other"), title: "Other", settledOverride: "settled", settledAt: NOW, }), ], environmentId: null, searchQuery: "", now: NOW, settledShelfExpanded: false, selectedThreadKey: `${environmentId}:selected`, }); expect(layout.items.map((item) => item.thread.id)).toEqual(["selected"]); expect(layout.settledCount).toBe(2); expect(layout.settledShelfHeaderIndex).toBe(0); }); it("keeps cards in creation order while settled sorts by recency", () => { const { items } = buildThreadListV2Items({ threads: [ makeThread({ id: ThreadId.make("older-created"), title: "Older", createdAt: "2026-06-01T08:00:00.000Z", updatedAt: NOW, // recent activity must NOT promote it }), makeThread({ id: ThreadId.make("newer-created"), title: "Newer", createdAt: "2026-06-01T12:00:00.000Z", }), ], environmentId: null, searchQuery: "", now: NOW, }); expect(items.map((item) => item.thread.id)).toEqual(["newer-created", "older-created"]); }); it("keeps settled threads in the tail and filters by search query", () => { const { items } = buildThreadListV2Items({ threads: [ makeThread({ id: ThreadId.make("match"), title: "Fix login bug" }), makeThread({ id: ThreadId.make("miss"), title: "Greeting" }), makeThread({ id: ThreadId.make("settled"), title: "Fix login again", settledOverride: "settled", settledAt: NOW, }), ], environmentId: null, searchQuery: "login", now: NOW, }); expect(items.map((item) => [item.thread.id, item.variant])).toEqual([ ["match", "card"], ["settled", "slim"], ]); }); it("includes a thread matched by message content", () => { const thread = makeThread({ id: ThreadId.make("content-match"), title: "Unrelated title", }); const { items } = buildThreadListV2Items({ threads: [thread], environmentId: null, searchQuery: "relay reconnect", matchedThreadKeys: new Set([ threadSearchMatchKey({ environmentId, threadId: thread.id, }), ]), now: NOW, }); expect(items.map((item) => item.thread.id)).toEqual(["content-match"]); }); it("scopes the flat list to one project", () => { const otherProjectId = ProjectId.make("project-2"); const { items } = buildThreadListV2Items({ threads: [ makeThread({ id: ThreadId.make("included"), title: "Included" }), makeThread({ id: ThreadId.make("excluded"), projectId: otherProjectId, title: "Excluded", }), ], environmentId: null, projectRefs: [{ environmentId, projectId: ProjectId.make("project-1") }], searchQuery: "", now: NOW, }); expect(items.map((item) => item.thread.id)).toEqual(["included"]); }); it("scopes the flat list to every environment member of a logical project", () => { const remoteEnvironmentId = EnvironmentId.make("environment-remote"); const { items } = buildThreadListV2Items({ threads: [ makeThread({ id: ThreadId.make("local"), title: "Local" }), makeThread({ environmentId: remoteEnvironmentId, id: ThreadId.make("remote"), title: "Remote", }), ], environmentId: null, projectRefs: [ { environmentId, projectId: ProjectId.make("project-1") }, { environmentId: remoteEnvironmentId, projectId: ProjectId.make("project-1") }, ], searchQuery: "", now: NOW, }); expect(items.map((item) => item.thread.id)).toEqual(["local", "remote"]); }); }); describe("buildThreadListV2Items settled paging", () => { it("caps the settled tail at settledLimit and reports the hidden count", () => { const threads = [ makeThread({ id: ThreadId.make("active"), title: "Active" }), ...Array.from({ length: 4 }, (_, index) => makeThread({ id: ThreadId.make(`settled-${index}`), title: `Settled ${index}`, settledOverride: "settled", settledAt: NOW, latestUserMessageAt: `2026-06-01T0${index}:00:00.000Z`, // A turn adopted the message (same requestedAt): without it the // thread reads as a queued turn start, which never settles. latestTurn: { turnId: TurnId.make(`turn-${index}`), state: "completed", requestedAt: `2026-06-01T0${index}:00:00.000Z`, startedAt: `2026-06-01T0${index}:00:00.000Z`, completedAt: `2026-06-01T0${index}:10:00.000Z`, assistantMessageId: null, }, }), ), ]; const layout = buildThreadListV2Items({ threads, environmentId: null, searchQuery: "", settledLimit: 2, now: NOW, }); expect(layout.hiddenSettledCount).toBe(2); expect(layout.items.filter((item) => item.variant === "slim")).toHaveLength(2); // Most recent settled first — the hidden ones are the oldest. expect(layout.items.map((item) => item.thread.id)).toEqual([ "active", "settled-3", "settled-2", ]); }); }); function makePendingTask(id: string): PendingNewTask { return { message: { environmentId, threadId: ThreadId.make(`thread-${id}`), messageId: MessageId.make(id), commandId: CommandId.make(`command-${id}`), text: id, attachments: [], createdAt: NOW, creation: { projectId: ProjectId.make("project-1"), workspaceMode: "worktree", branch: null, worktreePath: null, }, }, creation: { projectId: ProjectId.make("project-1"), workspaceMode: "worktree", branch: null, worktreePath: null, }, title: id, }; } describe("buildThreadListV2ListItems", () => { const layout = buildThreadListV2Items({ threads: [ makeThread({ id: ThreadId.make("active"), title: "active" }), makeThread({ id: ThreadId.make("settled"), title: "settled", settledOverride: "settled", settledAt: NOW, }), ], environmentId: null, searchQuery: "", now: NOW, }); it("splices queued tasks between the active block and the settled tail", () => { const items = buildThreadListV2ListItems({ items: layout.items, pendingTasks: [makePendingTask("queued-1"), makePendingTask("queued-2")], settledCount: layout.settledCount, settledShelfHeaderIndex: layout.settledShelfHeaderIndex, }); expect( items.map((item) => item.type === "v2-pending" ? item.pendingTask.title : item.type === "v2-thread" ? item.item.thread.id : item.type === "v2-snoozed-shelf" ? "snoozed-shelf" : "settled-shelf", ), ).toEqual(["active", "queued-1", "queued-2", "settled-shelf", "settled"]); // Only the leading queued row labels the section, exactly like Settled. expect( items.filter((item) => item.type === "v2-pending" && item.showPendingDivider), ).toHaveLength(1); }); it("ends the list with queued tasks when nothing has settled yet", () => { const activeOnly = buildThreadListV2Items({ threads: [makeThread({ id: ThreadId.make("active"), title: "active" })], environmentId: null, searchQuery: "", now: NOW, }); const items = buildThreadListV2ListItems({ items: activeOnly.items, pendingTasks: [makePendingTask("queued-1")], }); expect(items.map((item) => item.type)).toEqual(["v2-thread", "v2-pending"]); }); it("keeps the settled shelf between active and settled rows when nothing is queued", () => { const items = buildThreadListV2ListItems({ items: layout.items, pendingTasks: [], settledCount: layout.settledCount, settledShelfHeaderIndex: layout.settledShelfHeaderIndex, }); expect(items.map((item) => item.key)).toEqual([ `v2-thread:${environmentId}:active`, "v2-settled-shelf", `v2-thread:${environmentId}:settled`, ]); }); it("places queued tasks before a collapsed snoozed shelf", () => { const snoozedLayout = buildThreadListV2Items({ threads: [ makeThread({ id: ThreadId.make("active"), title: "active" }), makeThread({ id: ThreadId.make("snoozed"), title: "snoozed", snoozedUntil: "2026-06-03T09:00:00.000Z", snoozedAt: "2026-06-01T12:00:00.000Z", }), makeThread({ id: ThreadId.make("settled"), title: "settled", settledOverride: "settled", settledAt: NOW, }), ], environmentId: null, searchQuery: "", now: NOW, }); const items = buildThreadListV2ListItems({ items: snoozedLayout.items, pendingTasks: [makePendingTask("queued")], snoozedCount: snoozedLayout.snoozedCount, snoozedShelfExpanded: false, snoozedShelfHeaderIndex: snoozedLayout.snoozedShelfHeaderIndex, settledCount: snoozedLayout.settledCount, settledShelfHeaderIndex: snoozedLayout.settledShelfHeaderIndex, }); expect(items.map((item) => item.type)).toEqual([ "v2-thread", "v2-pending", "v2-snoozed-shelf", "v2-settled-shelf", "v2-thread", ]); }); });