import { presentThreadShell } from "@t3tools/client-runtime/state/models"; import { isThreadWorking as isSidebarThreadWorking } from "@t3tools/client-runtime/state/thread-inbox"; import * as DateTime from "effect/DateTime"; import { deriveActiveWorkStartedAt } from "../session-logic.ts"; import { describe, expect, it } from "vite-plus/test"; import * as Cause from "effect/Cause"; import { AsyncResult } from "effect/reactivity"; import { shouldCreateNewThreadInCurrentProject, deleteSelectedThreadEntries, getFallbackThreadIdAfterDelete, hasUnseenCompletion, resolveSidebarRowThreadId, belongsInSidebarWorkingShelf, isSidebarEnvironmentDisconnected, orderItemsByPreferredIds, pinOrderKeyBetween, resolveSidebarSweepKeys, resolveSidebarThreadStatus, resolveThreadStatusPill, resolveWorkingStartedAt, searchSidebarThreads, shouldRecedeSidebarThread, sortInboxThreadsByReturn, resolveSidebarDropTarget, planSidebarThreadDrop, resolveSidebarReorderableThreadKeys, sortPinnedThreadsForSidebar, sortScopedProjectsForSidebar, sortSidebarV2ProjectGroups, shouldNavigateAfterThreadPark, type SidebarListItem, type SidebarListMarker, type SidebarSection, resolveSidebarDropVerb, groupSidebarThreadsByProject, resolveSidebarFolderDot, sidebarFolderExpansionKeys, } from "./Sidebar.logic"; import { scopeThreadRef, scopedThreadKey } from "@t3tools/client-runtime/environment"; import { sortActiveThreadsByOrderKey, sortPinnedThreadsByOrderKey, } from "@t3tools/client-runtime/state/thread-sort"; import { resolveProjectExpanded } from "../uiStateStore"; import { threadSearchMatchKey } from "@t3tools/client-runtime/state/thread-search"; import { EnvironmentId, ProjectId, ProviderInstanceId, RunId, ThreadId } from "@t3tools/contracts"; import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, type Project, type Thread, } from "../types"; import { makeThreadFixture, type ThreadFixtureOverrides } from "../test-fixtures"; import { completeSidebarRouteReveal, pendingSidebarRouteReveal, resolveSidebarRouteReveal, } from "./sidebar/routeReveal"; const localEnvironmentId = EnvironmentId.make("environment-local"); describe("deleteSelectedThreadEntries", () => { const entries = [{ threadKey: "one" }, { threadKey: "two" }, { threadKey: "three" }] as const; const success = AsyncResult.success(undefined); const failure = AsyncResult.failure(Cause.fail(new Error("Delete failed"))); const interrupted = AsyncResult.failure(Cause.interrupt()); it("waits for each delete and excludes only earlier successes from worktree checks", async () => { let resolveDelete!: (result: typeof success) => void; const pendingDelete = new Promise((resolve) => { resolveDelete = resolve; }); const worktreeChecks: { threadKey: string; deletedThreadKeys: string[] }[] = []; const deletion = deleteSelectedThreadEntries({ entries, delete: async ({ threadKey }, deletedThreadKeys) => { worktreeChecks.push({ threadKey, deletedThreadKeys: [...deletedThreadKeys] }); return threadKey === "one" ? pendingDelete : success; }, }); expect(worktreeChecks).toEqual([{ threadKey: "one", deletedThreadKeys: [] }]); resolveDelete(success); const outcome = await deletion; expect(worktreeChecks).toEqual([ { threadKey: "one", deletedThreadKeys: [] }, { threadKey: "two", deletedThreadKeys: ["one"] }, { threadKey: "three", deletedThreadKeys: ["one", "two"] }, ]); expect(outcome).toEqual({ deletedThreadKeys: new Set(["one", "two", "three"]), firstFailure: null, }); }); it("continues after ordinary failures and keeps the first failure", async () => { const laterFailure = AsyncResult.failure(Cause.fail(new Error("Later failure"))); const deletedKeysAtLastEntry: string[][] = []; const outcome = await deleteSelectedThreadEntries({ entries: [...entries, { threadKey: "four" }], delete: async ({ threadKey }, deletedThreadKeys) => { if (threadKey === "one") return failure; if (threadKey === "three") return laterFailure; if (threadKey === "four") deletedKeysAtLastEntry.push([...deletedThreadKeys]); return success; }, }); expect(deletedKeysAtLastEntry).toEqual([["two"]]); expect(outcome).toEqual({ deletedThreadKeys: new Set(["two", "four"]), firstFailure: failure, }); }); it.each([ { firstResult: success, deletedThreadKeys: new Set(["one"]), firstFailure: null }, { firstResult: failure, deletedThreadKeys: new Set(), firstFailure: failure }, ])("stops on interruption and preserves earlier results %#", async (testCase) => { const attemptedThreadKeys: string[] = []; const outcome = await deleteSelectedThreadEntries({ entries, delete: async ({ threadKey }) => { attemptedThreadKeys.push(threadKey); return threadKey === "one" ? testCase.firstResult : interrupted; }, }); expect(attemptedThreadKeys).toEqual(["one", "two"]); expect(outcome).toEqual({ deletedThreadKeys: testCase.deletedThreadKeys, firstFailure: testCase.firstFailure, }); }); }); describe("sidebar thread lineage helpers", () => { it("maps an open subagent to its nearest visible ancestor's sidebar row", () => { const environmentId = EnvironmentId.make("environment-row"); const otherEnvironmentId = EnvironmentId.make("environment-other"); const subagentOf = (id: string, parentId: ThreadId, environment = environmentId) => makeThreadFixture({ id: ThreadId.make(id), environmentId: environment, lineage: { rootThreadId: parentId, parentThreadId: parentId, relationshipToParent: "subagent", }, }); const root = makeThreadFixture({ id: ThreadId.make("thread-root"), environmentId }); const fork = makeThreadFixture({ id: ThreadId.make("thread-fork"), environmentId, lineage: { rootThreadId: root.id, parentThreadId: root.id, relationshipToParent: "fork", }, }); const child = subagentOf("thread-child", root.id); const grandchild = subagentOf("thread-grandchild", child.id); const forkChild = subagentOf("thread-fork-child", fork.id); const orphan = subagentOf("thread-orphan", ThreadId.make("thread-missing")); const elsewhere = subagentOf("thread-elsewhere", root.id, otherEnvironmentId); const threads = [root, fork, child, grandchild, forkChild, orphan, elsewhere]; expect(resolveSidebarRowThreadId(child.id, environmentId, threads)).toBe(root.id); expect(resolveSidebarRowThreadId(grandchild.id, environmentId, threads)).toBe(root.id); expect(resolveSidebarRowThreadId(forkChild.id, environmentId, threads)).toBe(fork.id); expect(resolveSidebarRowThreadId(elsewhere.id, otherEnvironmentId, threads)).toBe(elsewhere.id); expect(resolveSidebarRowThreadId(orphan.id, environmentId, threads)).toBe(orphan.id); const loopA = subagentOf("thread-loop-a", ThreadId.make("thread-loop-b")); const loopB = subagentOf("thread-loop-b", loopA.id); expect(resolveSidebarRowThreadId(loopA.id, environmentId, [loopA, loopB])).toBe(loopA.id); }); }); function makeLatestRun(overrides?: { completedAt?: string | null; startedAt?: string | null; }): NonNullable { return { runId: "turn-1" as never, status: "completed", assistantMessageId: null, requestedAt: "2026-03-09T10:00:00.000Z", startedAt: overrides?.startedAt !== undefined ? overrides.startedAt : "2026-03-09T10:00:00.000Z", completedAt: overrides?.completedAt !== undefined ? overrides.completedAt : "2026-03-09T10:05:00.000Z", }; } describe("orderItemsByPreferredIds", () => { it("resolves legacy preference aliases without materializing project state", () => { const ordered = orderItemsByPreferredIds({ items: [ { id: "physical-a", cwd: "/work/a" }, { id: "physical-b", cwd: "/work/b" }, { id: "physical-c", cwd: "/work/c" }, ], preferredIds: ["legacy:/work/c", "legacy:/work/a"], getId: (project) => project.id, getPreferenceIds: (project) => [project.id, `legacy:${project.cwd}`], }); expect(ordered.map((project) => project.id)).toEqual([ "physical-c", "physical-a", "physical-b", ]); }); }); describe("resolveSidebarThreadStatus", () => { const runtime = { status: "running" as const, activeRunId: null, providerInstanceId: ProviderInstanceId.make("codex"), providerName: "Codex", lastError: null, updatedAt: "2026-03-09T10:00:00.000Z", }; const idle = { hasPendingApprovals: false, hasPendingUserInput: false, runtime: null }; it("prioritizes awaiting input over a running runtime, below approval", () => { expect( resolveSidebarThreadStatus({ ...idle, hasPendingUserInput: true, runtime, blockedOnThreadWait: true, }), ).toBe("input"); expect( resolveSidebarThreadStatus({ ...idle, hasPendingApprovals: true, hasPendingUserInput: true, blockedOnThreadWait: true, runtime, }), ).toBe("approval"); }); it("shows a waiting guest message as a Request, below the agent's own requests", () => { expect(resolveSidebarThreadStatus({ ...idle, pendingGuestMessageCount: 1, runtime })).toBe( "request", ); expect( resolveSidebarThreadStatus({ ...idle, hasPendingUserInput: true, pendingGuestMessageCount: 1, }), ).toBe("input"); }); it("keeps usage-limit stops Limited and visible until the thread recovers", () => { const limited = { ...runtime, status: "failed" as const, lastError: "Plan limit reached", lastErrorClass: "usage_limit" as const, }; expect(resolveSidebarThreadStatus({ ...idle, runtime: limited })).toBe("limited"); expect( resolveSidebarThreadStatus({ ...idle, runtime: { ...limited, status: "running" } }), ).toBe("working"); expect( resolveSidebarThreadStatus({ ...idle, runtime: { ...limited, status: "completed" } }), ).toBe("ready"); expect( shouldRecedeSidebarThread({ status: "limited", isUnread: false, isWoke: false, isActive: false, isSelected: false, }), ).toBe(false); }); it("reports failed only while the latest run failed", () => { expect( resolveSidebarThreadStatus({ ...idle, runtime: { ...runtime, status: "failed" as const, lastError: "boom" }, }), ).toBe("failed"); expect( resolveSidebarThreadStatus({ ...idle, runtime: { ...runtime, status: "completed" as const, lastError: "persisted" }, }), ).toBe("ready"); expect( resolveSidebarThreadStatus({ ...idle, runtime: { ...runtime, status: "idle" as const, lastError: "persisted" }, }), ).toBe("waiting"); }); }); describe("searchSidebarThreads", () => { const searchThread = (id: string, title: string, latestUserMessageAt: string) => ({ environmentId: localEnvironmentId, id: ThreadId.make(id), title, branch: null, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: latestUserMessageAt, latestUserMessageAt, opens: null, }); const threads = [ searchThread("thread-1", "Fix workspace search", "2026-10-20T00:00:00.000Z"), searchThread("thread-2", "Review providers", "2026-10-22T00:00:00.000Z"), searchThread("thread-3", "WORKTREE cleanup", "2026-09-01T00:00:00.000Z"), searchThread("thread-4", "Homework notes", "2026-10-22T00:00:00.000Z"), ]; const now = Date.parse("2026-10-23T00:00:00.000Z"); const contentKeys = (...ids: ReadonlyArray) => new Set( ids.map((id) => threadSearchMatchKey({ environmentId: localEnvironmentId, threadId: ThreadId.make(id) }), ), ); it("ranks word matches by recent use, then lists message matches once", () => { expect(searchSidebarThreads(threads, "work", contentKeys("thread-1", "thread-2"), now)).toEqual( { titleMatches: [threads[0], threads[2], threads[3]], contentMatches: [threads[1]], }, ); }); }); describe("resolveWorkingStartedAt", () => { const runtime = { status: "running" as const, providerName: "Codex", providerInstanceId: ProviderInstanceId.make("codex"), activeRunId: RunId.make("turn-1"), lastError: null, updatedAt: "2026-03-09T10:02:00.000Z", }; it("does not invent a start from activity updates when the newest run completed", () => { expect( resolveWorkingStartedAt({ latestRun: makeLatestRun(), runtime, }), ).toBeNull(); }); it.each(["queued", "cancelled"] as const)( "shares the detail timer when a newer run is %s", (status) => { const activityStartedAt = "2026-03-09T10:00:00.000Z"; const latestRun = { ...makeLatestRun(), runId: RunId.make("newer-run"), status, startedAt: null, completedAt: status === "queued" ? null : "2026-03-09T10:05:00.000Z", }; for (const updatedAt of ["2026-03-09T10:30:00.000Z", "2026-03-09T10:50:00.000Z"]) { const activeRuntime = { ...runtime, updatedAt, activityStartedAt }; expect(resolveWorkingStartedAt({ latestRun, runtime: activeRuntime })).toBe( activityStartedAt, ); expect(deriveActiveWorkStartedAt(latestRun, activeRuntime, updatedAt)).toBe( activityStartedAt, ); } expect( deriveActiveWorkStartedAt( latestRun, { ...runtime, activityStartedAt: null }, "2026-03-09T10:50:00.000Z", ), ).toBeNull(); }, ); }); describe("resolveThreadStatusPill", () => { const baseThread = { hasActionableProposedPlan: false, hasPendingApprovals: false, hasPendingUserInput: false, interactionMode: "plan" as const, latestRun: null, lastVisitedAt: undefined, runtime: { status: "running" as const, providerName: "Codex", providerInstanceId: ProviderInstanceId.make("codex"), activeRunId: "turn-1" as never, lastError: null, updatedAt: "2026-03-09T10:00:00.000Z", }, }; it("shows an active thread wait without a pulse and resumes Working when it ends", () => { const blocked = { ...baseThread, blockedOnThreadWait: true }; expect(resolveSidebarThreadStatus(blocked)).toBe("waiting"); expect(resolveThreadStatusPill({ thread: blocked })).toMatchObject({ label: "Waiting", pulse: false, }); const resumed = { ...blocked, blockedOnThreadWait: false }; expect(resolveSidebarThreadStatus(resumed)).toBe("working"); expect(resolveThreadStatusPill({ thread: resumed })).toMatchObject({ label: "Working", pulse: true, }); }); it("shows waiting for an idle thread with pending background tasks", () => { expect( resolveThreadStatusPill({ thread: { ...baseThread, pendingBackgroundTasks: [ { taskId: "bg-1", description: "Watch build", kind: "monitor", awaited: true }, ], runtime: { ...baseThread.runtime, status: "idle", activeRunId: null, }, }, }), ).toMatchObject({ label: "Waiting", colorClass: "text-sidebar-muted-foreground", dotClass: "bg-sidebar-muted-foreground", pulse: false, }); }); it("shows plan ready when a settled plan turn has a proposed plan ready for follow-up", () => { expect( resolveThreadStatusPill({ thread: { ...baseThread, hasActionableProposedPlan: true, latestRun: makeLatestRun(), runtime: { ...baseThread.runtime, status: "completed", activeRunId: null, }, }, }), ).toMatchObject({ label: "Plan Ready", pulse: false }); }); it("does not manufacture completed state without a client visit marker", () => { expect( resolveThreadStatusPill({ thread: { ...baseThread, latestRun: makeLatestRun(), runtime: { ...baseThread.runtime, status: "completed", activeRunId: null, }, }, }), ).toBeNull(); }); it("shows completed when there is an unseen completion and no active blocker", () => { expect( resolveThreadStatusPill({ thread: { ...baseThread, interactionMode: "default", latestRun: makeLatestRun(), lastVisitedAt: "2026-03-09T10:04:00.000Z", runtime: { ...baseThread.runtime, status: "completed", activeRunId: null, }, }, }), ).toMatchObject({ label: "Completed", pulse: false }); }); it("shows failed, not completed, when the unseen run failed", () => { expect( resolveThreadStatusPill({ thread: { ...baseThread, interactionMode: "default", latestRun: { ...makeLatestRun(), status: "failed" }, lastVisitedAt: "2026-03-09T10:04:00.000Z", runtime: { ...baseThread.runtime, status: "failed", activeRunId: null, lastError: "Provider turn failed.", }, }, }), ).toMatchObject({ label: "Failed", pulse: false }); }); it("shows failed, not plan ready, when a plan run proposed a plan and then failed", () => { expect( resolveThreadStatusPill({ thread: { ...baseThread, hasActionableProposedPlan: true, latestRun: { ...makeLatestRun(), status: "failed" }, lastVisitedAt: "2026-03-09T10:04:00.000Z", runtime: { ...baseThread.runtime, status: "failed", activeRunId: null }, }, }), ).toMatchObject({ label: "Failed" }); }); it("drops the failed pill once the failure has been seen", () => { expect( resolveThreadStatusPill({ thread: { ...baseThread, interactionMode: "default", latestRun: { ...makeLatestRun(), status: "failed" }, lastVisitedAt: "2026-03-09T10:06:00.000Z", runtime: { ...baseThread.runtime, status: "failed", activeRunId: null }, }, }), ).toBeNull(); }); }); function makeProject(overrides: Partial = {}): Project { const { defaultModelSelection, ...rest } = overrides; return { id: ProjectId.make("project-1"), environmentId: localEnvironmentId, title: "Project", workspaceRoot: "/tmp/project", repositoryIdentity: null, defaultModelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4", ...defaultModelSelection, }, createdAt: "2026-03-09T10:00:00.000Z", updatedAt: "2026-03-09T10:00:00.000Z", scripts: [], ...rest, }; } function makeThread(overrides: ThreadFixtureOverrides = {}): Thread { return makeThreadFixture({ id: ThreadId.make("thread-1"), environmentId: localEnvironmentId, projectId: ProjectId.make("project-1"), title: "Thread", modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4", ...overrides?.modelSelection, }, runtimeMode: DEFAULT_RUNTIME_MODE, interactionMode: DEFAULT_INTERACTION_MODE, runtime: null, messages: [], proposedPlans: [], createdAt: "2026-03-09T10:00:00.000Z", archivedAt: null, settledOverride: null, settledAt: null, deletedAt: null, updatedAt: "2026-03-09T10:00:00.000Z", latestRun: null, branch: null, worktreePath: null, ...overrides, }); } describe("getFallbackThreadIdAfterDelete", () => { it("skips other threads being deleted in the same action", () => { const fallbackThreadId = getFallbackThreadIdAfterDelete({ threads: [ makeThread({ id: ThreadId.make("thread-active"), projectId: ProjectId.make("project-1"), createdAt: "2026-03-09T10:05:00.000Z", messages: [], }), makeThread({ id: ThreadId.make("thread-newest"), projectId: ProjectId.make("project-1"), createdAt: "2026-03-09T10:10:00.000Z", messages: [], }), makeThread({ id: ThreadId.make("thread-next"), projectId: ProjectId.make("project-1"), createdAt: "2026-03-09T10:07:00.000Z", messages: [], }), ], deletedThreadId: ThreadId.make("thread-active"), deletedThreadIds: new Set([ThreadId.make("thread-active"), ThreadId.make("thread-newest")]), sortOrder: "created_at", now: "2026-03-09T10:10:00.000Z", }); expect(fallbackThreadId).toBe(ThreadId.make("thread-next")); }); it.each([ { state: "snoozed", overrides: { snoozedUntil: "2026-03-09T14:00:00.000Z" }, eligible: false, }, { state: "settled", overrides: { settledOverride: "settled" as const }, eligible: false, }, { state: "expired snooze", overrides: { snoozedUntil: "2026-03-09T10:30:00.000Z" }, eligible: true, }, { state: "snoozed but waiting on input", overrides: { snoozedUntil: "2026-03-09T14:00:00.000Z", hasPendingUserInput: true }, eligible: true, }, ])("respects $state visibility when choosing a fallback", ({ overrides, eligible }) => { const deletedThread = makeThread({ id: ThreadId.make("deleted") }); const candidate = makeThread({ id: ThreadId.make("candidate"), createdAt: "2026-03-09T10:10:00.000Z", ...overrides, }); const olderVisibleThread = makeThread({ id: ThreadId.make("older-visible") }); const otherProjectThread = makeThread({ id: ThreadId.make("other-project"), projectId: ProjectId.make("project-2"), createdAt: "2026-03-09T10:15:00.000Z", }); const input = { deletedThreadId: deletedThread.id, sortOrder: "created_at" as const, now: "2026-03-09T11:00:00.000Z", }; expect( getFallbackThreadIdAfterDelete({ ...input, threads: [deletedThread, candidate, olderVisibleThread, otherProjectThread], }), ).toBe(eligible ? candidate.id : olderVisibleThread.id); expect( getFallbackThreadIdAfterDelete({ ...input, threads: [deletedThread, candidate, otherProjectThread], }), ).toBe(eligible ? candidate.id : null); }); it.each([ [ "snoozed", { snoozedUntil: "2099-01-01T00:00:00.000Z", snoozedAt: "2026-03-09T10:06:00.000Z" }, ], ["settled", { settledOverride: "settled", settledAt: "2026-03-09T10:06:00.000Z" }], ] as const)("skips a %s thread parked on its shelf", (_state, parked) => { const fallbackThreadId = getFallbackThreadIdAfterDelete({ threads: [ makeThread({ id: ThreadId.make("thread-visible"), createdAt: "2026-03-09T10:01:00.000Z", updatedAt: "2026-03-09T10:01:00.000Z", }), makeThread({ id: ThreadId.make("thread-parked"), createdAt: "2026-03-09T10:02:00.000Z", updatedAt: "2026-03-09T10:06:00.000Z", ...parked, }), makeThread({ id: ThreadId.make("thread-active"), createdAt: "2026-03-09T10:03:00.000Z", updatedAt: "2026-03-09T10:03:00.000Z", }), ], deletedThreadId: ThreadId.make("thread-active"), sortOrder: "created_at", now: "2026-03-09T10:10:00.000Z", }); expect(fallbackThreadId).toBe(ThreadId.make("thread-visible")); }); }); describe("sortScopedProjectsForSidebar", () => { it("keeps identical project ids in different environments separate", () => { const remoteEnvironmentId = EnvironmentId.make("environment-remote"); const sharedProjectId = ProjectId.make("shared-project"); const projects = [ makeProject({ environmentId: localEnvironmentId, id: sharedProjectId, title: "Local project", }), makeProject({ environmentId: remoteEnvironmentId, id: sharedProjectId, title: "Remote project", }), ]; const threads = [ makeThread({ environmentId: localEnvironmentId, projectId: sharedProjectId, updatedAt: "2026-03-09T10:02:00.000Z", }), makeThread({ environmentId: remoteEnvironmentId, projectId: sharedProjectId, updatedAt: "2026-03-09T10:10:00.000Z", }), ]; const sorted = sortScopedProjectsForSidebar(projects, threads, "updated_at"); expect(sorted.map((project) => project.title)).toEqual(["Remote project", "Local project"]); }); it("does not use archived threads as project activity", () => { const projects = [ makeProject({ id: ProjectId.make("project-visible"), title: "Visible project", updatedAt: "2026-03-09T10:01:00.000Z", }), makeProject({ id: ProjectId.make("project-archived"), title: "Archived-only project", updatedAt: "2026-03-09T10:00:00.000Z", }), ]; const threads = [ makeThread({ id: ThreadId.make("thread-visible"), projectId: ProjectId.make("project-visible"), updatedAt: "2026-03-09T10:02:00.000Z", }), makeThread({ id: ThreadId.make("thread-archived"), projectId: ProjectId.make("project-archived"), updatedAt: "2026-03-09T10:10:00.000Z", archivedAt: "2026-03-09T10:11:00.000Z", }), ]; const sorted = sortScopedProjectsForSidebar(projects, threads, "updated_at"); expect(sorted.map((project) => project.title)).toEqual([ "Visible project", "Archived-only project", ]); }); }); describe("sortSidebarV2ProjectGroups", () => { it("does not let a hidden subagent thread reorder projects", () => { const olderProjectId = ProjectId.make("project-older"); const newerProjectId = ProjectId.make("project-newer"); const olderRootThreadId = ThreadId.make("thread-older-root"); const projects = [ { ...makeProject({ id: olderProjectId, title: "A older project" }), projectKey: "logical-older", memberProjectRefs: [{ environmentId: localEnvironmentId, projectId: olderProjectId }], }, { ...makeProject({ id: newerProjectId, title: "Z newer project" }), projectKey: "logical-newer", memberProjectRefs: [{ environmentId: localEnvironmentId, projectId: newerProjectId }], }, ]; const threads = [ makeThread({ id: olderRootThreadId, projectId: olderProjectId, updatedAt: "2026-03-09T10:01:00.000Z", }), makeThread({ id: ThreadId.make("thread-newer-root"), projectId: newerProjectId, updatedAt: "2026-03-09T10:05:00.000Z", }), makeThread({ id: ThreadId.make("thread-hidden-subagent"), projectId: olderProjectId, updatedAt: "2026-03-09T10:10:00.000Z", lineage: { rootThreadId: olderRootThreadId, parentThreadId: olderRootThreadId, relationshipToParent: "subagent", }, }), ]; expect( sortSidebarV2ProjectGroups(projects, threads, "updated_at", new Set()).map( (project) => project.projectKey, ), ).toEqual(["logical-newer", "logical-older"]); }); }); describe("pinOrderKeyBetween", () => { it("stays strictly ordered under repeated top insertion", () => { let head: string | null = null; const keys: string[] = []; for (let i = 0; i < 100; i += 1) { const key: string = pinOrderKeyBetween(null, head)!; expect(key).not.toBeNull(); if (head !== null) expect(key < head).toBe(true); keys.push(key); head = key; } expect(new Set(keys).size).toBe(100); }); it("stays strictly ordered under repeated middle insertion", () => { let low = pinOrderKeyBetween(null, null)!; let high = pinOrderKeyBetween(low, null)!; for (let i = 0; i < 100; i += 1) { const key: string = pinOrderKeyBetween(low, high)!; expect(low < key && key < high).toBe(true); if (i % 2 === 0) low = key; else high = key; } }); it("returns null for corrupt or out-of-order bounds instead of throwing", () => { expect(pinOrderKeyBetween("z", "a")).toBeNull(); expect(pinOrderKeyBetween("A!", null)).toBeNull(); expect(pinOrderKeyBetween(null, "ma")).toBeNull(); expect(pinOrderKeyBetween("m", "m")).toBeNull(); }); }); describe("sortPinnedThreadsForSidebar", () => { const pinnable = (input: { id: string; createdAt: string; pinOrderKey?: string | null }) => ({ id: input.id, createdAt: input.createdAt, pinOrderKey: input.pinOrderKey ?? null, }); it("sorts keyed threads by key ahead of keyless threads in creation order", () => { const sorted = sortPinnedThreadsForSidebar([ pinnable({ id: "keyless-old", createdAt: "2026-03-09T08:00:00.000Z" }), pinnable({ id: "second", createdAt: "2026-03-09T09:00:00.000Z", pinOrderKey: "t" }), pinnable({ id: "keyless-new", createdAt: "2026-03-09T12:00:00.000Z" }), pinnable({ id: "first", createdAt: "2026-03-09T07:00:00.000Z", pinOrderKey: "g" }), ]); expect(sorted.map((thread) => thread.id)).toEqual([ "first", "second", "keyless-new", "keyless-old", ]); }); it("breaks equal keys by id so raced writes render identically everywhere", () => { const sorted = sortPinnedThreadsForSidebar([ pinnable({ id: "b", createdAt: "2026-03-09T10:00:00.000Z", pinOrderKey: "m" }), pinnable({ id: "a", createdAt: "2026-03-09T11:00:00.000Z", pinOrderKey: "m" }), ]); expect(sorted.map((thread) => thread.id)).toEqual(["a", "b"]); }); }); describe("resolveSidebarSweepKeys", () => { const ordered = ["a", "b", "c", "d", "blocked"]; const canSettle = (key: string) => key !== "blocked"; it("leaves out rows that cannot settle and rows that left the list", () => { expect(resolveSidebarSweepKeys(ordered, "c", "blocked", canSettle)).toEqual(["c", "d"]); expect(resolveSidebarSweepKeys(ordered, "gone", "a", canSettle)).toEqual([]); }); }); describe("navigation after parking a thread", () => { it.each([ ["settle", "settled", null, "thread", true], ["settle", "active", null, "thread", false], ["settle", "settled", null, "other-thread", false], ["snooze", null, "2099-01-01T00:00:00.000Z", "thread", true], ["snooze", null, null, "thread", false], ["snooze", null, "2026-09-12T09:00:00.000Z", "thread", false], ["snooze", null, "2099-01-01T00:00:00.000Z", "thread", false, true], ["snooze", null, "2099-01-01T00:00:00.000Z", "other-thread", false], ] as const)( "%s with state %s / %s on %s navigates: %s", ( action, settledOverride, snoozedUntil, currentThreadKey, expected, hasPendingApprovals: boolean = false, ) => { expect( shouldNavigateAfterThreadPark({ threadKey: "thread", currentThreadKey, action, now: "2026-09-12T10:00:00.000Z", thread: { settledOverride, snoozedUntil, snoozedAt: null, session: null, latestTurn: null, hasPendingApprovals, hasPendingUserInput: false, }, }), ).toBe(expected); }, ); }); describe("unseen completion with background work", () => { it.each([ { roster: "dev server", task: { kind: "command" }, status: "ready", receded: false, pill: "Completed", }, { roster: "dev server watchdog", task: { kind: "monitor" }, status: "ready", receded: false, pill: "Completed", }, { roster: "awaited command", task: { kind: "command", awaited: true }, status: "waiting", receded: true, pill: "Waiting", }, ] as const)("presents a completed thread with a $roster roster", (expected) => { const thread = presentThreadShell(localEnvironmentId, { ...makeThreadFixture().source, latestRunId: RunId.make("run-background-completion"), status: "completed", latestRunCompletedAt: DateTime.makeUnsafe("2026-06-20T01:00:00.000Z"), lastVisitedAt: DateTime.makeUnsafe("2026-06-20T00:59:00.000Z"), pendingBackgroundTasks: [{ taskId: "background-work", ...expected.task }], }); const status = resolveSidebarThreadStatus(thread); const isUnread = hasUnseenCompletion(thread); expect(isUnread).toBe(true); expect(status).toBe(expected.status); expect( shouldRecedeSidebarThread({ status, isUnread, isWoke: false, isActive: false, isSelected: false, }), ).toBe(expected.receded); expect(isSidebarThreadWorking(thread)).toBe(expected.receded); expect(resolveThreadStatusPill({ thread })).toMatchObject({ label: expected.pill }); }); }); describe("Working shelf (beta)", () => { const runtime = { status: "running" as const, activeRunId: null, providerInstanceId: ProviderInstanceId.make("codex"), providerName: "Codex", lastError: null, updatedAt: "2026-03-09T10:00:00.000Z", }; const backgroundTask = { taskId: "bg-1", description: "Watch build", kind: "monitor" as const, awaited: true as const, }; const idle = { hasActionableProposedPlan: false, hasPendingApprovals: false, hasPendingUserInput: false, interactionMode: "default" as const, latestRun: makeLatestRun(), runtime: null, }; const waiting = { ...idle, runtime: { ...runtime, status: "idle" as const }, pendingBackgroundTasks: [backgroundTask], }; it("folds away running threads and threads waiting on background work only", () => { expect(isSidebarThreadWorking({ ...idle, runtime })).toBe(true); expect(isSidebarThreadWorking(waiting)).toBe(true); expect(isSidebarThreadWorking(idle)).toBe(false); expect(isSidebarThreadWorking({ ...idle, runtime, hasPendingApprovals: true })).toBe(false); expect(isSidebarThreadWorking({ ...idle, runtime, hasPendingUserInput: true })).toBe(false); expect( isSidebarThreadWorking({ ...waiting, runtime: { ...runtime, status: "failed" as const, lastError: "boom" }, }), ).toBe(false); }); it("keeps a ready plan in the inbox while background work runs", () => { expect( isSidebarThreadWorking({ ...waiting, interactionMode: "plan", hasActionableProposedPlan: true, }), ).toBe(false); }); it.each([ ["connected", true, false], ["connected", false, true], ["connecting", false, false], ["reconnecting", false, true], ["offline", false, true], ["error", false, true], ["unsupported", false, true], ["available", false, true], ] as const)( "lists threads outside the shelf on a %s environment: %s", (phase, shellIsLive, disconnected) => { expect(isSidebarEnvironmentDisconnected(phase, shellIsLive)).toBe(disconnected); }, ); it("lists a running thread outside the shelf while its environment is disconnected", () => { const thread = { ...idle, runtime, environmentId: localEnvironmentId }; const shelve = (disconnected: ReadonlyArray, enabled = true) => belongsInSidebarWorkingShelf(thread, { enabled, disconnectedEnvironmentIds: new Set(disconnected), isWorking: isSidebarThreadWorking, }); expect(shelve([])).toBe(true); expect(shelve([localEnvironmentId])).toBe(false); expect(shelve([])).toBe(true); expect(shelve([EnvironmentId.make("environment-other")])).toBe(true); expect(shelve([], false)).toBe(false); }); describe("sortInboxThreadsByReturn", () => { const thread = ( id: string, input: { createdAt: string; completedAt?: string | null; unsettledAt?: string }, ) => ({ id: ThreadId.make(id), environmentId: localEnvironmentId, createdAt: input.createdAt, unsettledAt: input.unsettledAt ?? null, latestRun: input.completedAt === undefined ? null : { ...makeLatestRun({ completedAt: input.completedAt }), requestedAt: input.createdAt }, }); it("puts the thread that finished last on top, whatever its age", () => { const sorted = sortInboxThreadsByReturn([ thread("new", { createdAt: "2026-03-09T11:00:00.000Z" }), thread("old-finished-now", { createdAt: "2026-03-01T09:00:00.000Z", completedAt: "2026-03-09T12:00:00.000Z", }), thread("reopened", { createdAt: "2026-03-02T09:00:00.000Z", unsettledAt: "2026-03-09T11:30:00.000Z", }), ]); expect(sorted.map((entry) => entry.id)).toEqual(["old-finished-now", "reopened", "new"]); }); it("counts a return the server does not stamp, like an approval request", () => { const waiting = thread("asks-approval", { createdAt: "2026-03-09T09:00:00.000Z", completedAt: null, }); const finished = thread("finished", { createdAt: "2026-03-09T09:30:00.000Z", completedAt: "2026-03-09T11:00:00.000Z", }); expect(sortInboxThreadsByReturn([finished, waiting]).map((entry) => entry.id)).toEqual([ "finished", "asks-approval", ]); expect( sortInboxThreadsByReturn([finished, waiting], (entry) => entry === waiting ? Date.parse("2026-03-09T11:05:00.000Z") : undefined, ).map((entry) => entry.id), ).toEqual(["asks-approval", "finished"]); }); }); describe("dragging", () => { const marker = (name: SidebarListMarker): SidebarListItem => ({ kind: "marker", marker: name }); const row = (key: string, section: SidebarSection): SidebarListItem => ({ kind: "thread", key, section, }); const items: readonly SidebarListItem[] = [ marker("pinned-header"), row("p1", "pinned"), marker("pinned-divider"), row("a1", "active"), row("a2", "active"), marker("working-header"), row("w1", "working"), marker("settled-header"), row("s1", "settled"), ]; it("never drops into the Working shelf, and keeps it out of the inbox order", () => { expect(resolveSidebarDropTarget(items, "a1", "w1")).toBeNull(); expect(resolveSidebarDropTarget(items, "p1", "a2")).toEqual({ section: "active", pinnedOrder: [], activeOrder: ["a1", "a2", "p1"], }); expect(resolveSidebarDropVerb("active", "working")).toBeNull(); }); it.each([ ["pinned", "p1"], ["active", "a2"], ["settled", "s1"], ] as const)( "does not move a Working thread into %s when its reference drag ends in the sidebar", (section, overKey) => { const target = resolveSidebarDropTarget(items, "w1", overKey); expect(target?.section).toBe(section); expect( planSidebarThreadDrop({ activeKey: "w1", activeSection: "working", target: target!, pinnedOrder: ["p1"], pinnedKeysById: new Map([["p1", "m"]]), activeOrder: ["a1", "a2"], activeKeysById: new Map([ ["a1", "f"], ["a2", "t"], ]), activeTimeOrdered: true, }), ).toEqual({ kind: "none" }); }, ); it("arranges the rows it can write when another row's server cannot store an order", () => { const plan = planSidebarThreadDrop({ activeKey: "a2", activeSection: "active", target: { section: "active", pinnedOrder: [], activeOrder: ["offline", "a2", "a1"], }, pinnedOrder: [], pinnedKeysById: new Map(), activeOrder: ["offline", "a1", "a2"], activeKeysById: new Map([ ["offline", null], ["a1", null], ["a2", null], ]), activeReorderableKeys: new Set(["a1", "a2"]), }); expect(plan.kind).toBe("move-active"); if (plan.kind !== "move-active") return; expect(plan.assignments.map(({ id }) => id)).toEqual(["a2", "a1"]); const [a2, a1] = plan.assignments.map(({ orderKey }) => orderKey); expect(a2! < a1!).toBe(true); const above = planSidebarThreadDrop({ activeKey: "a2", activeSection: "active", target: { section: "active", pinnedOrder: [], activeOrder: ["a2", "offline"] }, pinnedOrder: [], pinnedKeysById: new Map(), activeOrder: ["offline", "a2"], activeKeysById: new Map([ ["offline", "m"], ["a2", "t"], ]), activeReorderableKeys: new Set(["a2"]), }); expect(above.kind === "move-active" && above.assignments[0]!.orderKey < "m").toBe(true); }); describe("reordering with cached offline configuration", () => { const onlineEnvironmentId = EnvironmentId.make("online"); const offlineEnvironmentId = EnvironmentId.make("offline"); const capabilities = { repositoryIdentity: true, threadPinning: true, threadPinReorder: true, threadActiveReorder: true, }; const config = (environmentId: EnvironmentId) => ({ environment: { environmentId, label: environmentId, platform: { os: "linux" as const, arch: "x64" as const }, serverVersion: "0.1.1", capabilities, }, }); const serverConfigs = new Map([ [onlineEnvironmentId, config(onlineEnvironmentId)], [offlineEnvironmentId, config(offlineEnvironmentId)], ]); const thread = ( id: string, environmentId = onlineEnvironmentId, key: string | null = null, ) => ({ id: ThreadId.make(id), environmentId, createdAt: `2026-03-09T${id === "offline" ? "12" : id === "online" ? "11" : "10"}:00:00.000Z`, activeOrderKey: key, pinOrderKey: key, }); const keyOf = (entry: ReturnType) => scopedThreadKey(scopeThreadRef(entry.environmentId, entry.id)); const online = thread("online"); const offline = thread("offline", offlineEnvironmentId); const membership = (connectedEnvironmentIds = [onlineEnvironmentId]) => resolveSidebarReorderableThreadKeys({ threads: [online, offline], serverConfigs, connectedEnvironmentIds, }); const planDrop = (input: { section: "active" | "pinned"; rows: ReturnType[]; moved: ReturnType; order: ReturnType[]; from?: "active" | "pinned"; connectedEnvironmentIds?: EnvironmentId[]; }) => { const writable = resolveSidebarReorderableThreadKeys({ threads: input.rows, serverConfigs, connectedEnvironmentIds: input.connectedEnvironmentIds ?? [onlineEnvironmentId], }); const sort = (rows: typeof input.rows) => input.section === "active" ? sortActiveThreadsByOrderKey(rows) : sortPinnedThreadsByOrderKey(rows); const originalOrder = sort(input.rows).map(keyOf); const targetOrder = input.order.map(keyOf); const keysById = new Map(input.rows.map((entry) => [keyOf(entry), entry.activeOrderKey])); const plan = planSidebarThreadDrop({ activeKey: keyOf(input.moved), activeSection: input.from ?? input.section, target: { section: input.section, pinnedOrder: targetOrder, activeOrder: targetOrder }, pinnedOrder: originalOrder, activeOrder: originalOrder, pinnedKeysById: keysById, activeKeysById: keysById, reorderableKeys: writable.draggableThreadKeys, activeReorderableKeys: writable.activeReorderableThreadKeys, }); const assignments = plan.kind === "move-active" || plan.kind === "reorder-pinned" ? plan.assignments : plan.kind === "pin" ? [ ...plan.extraAssignments, ...(plan.orderKey === undefined ? [] : [{ id: keyOf(input.moved), orderKey: plan.orderKey }]), ] : []; const assignedKeys = new Map(assignments.map(({ id, orderKey }) => [id, orderKey])); const actualOrder = sort( input.rows.map((entry) => ({ ...entry, activeOrderKey: assignedKeys.get(keyOf(entry)) ?? entry.activeOrderKey, pinOrderKey: assignedKeys.get(keyOf(entry)) ?? entry.pinOrderKey, })), ).map(keyOf); return { plan, assignments, actualOrder, targetOrder }; }; it("excludes cached capable offline rows from both writable sets", () => { const writable = membership(); expect([...writable.draggableThreadKeys]).toEqual([keyOf(online)]); expect([...writable.activeReorderableThreadKeys]).toEqual([keyOf(online)]); expect(serverConfigs.has(offlineEnvironmentId)).toBe(true); }); it("updates membership through disconnect, reconnect, and missing configuration", () => { expect(membership([]).draggableThreadKeys.size).toBe(0); const connected = membership([onlineEnvironmentId, offlineEnvironmentId]); expect(connected.draggableThreadKeys.size).toBe(2); expect(connected.activeReorderableThreadKeys.size).toBe(2); const changed = resolveSidebarReorderableThreadKeys({ threads: [online, offline], connectedEnvironmentIds: [onlineEnvironmentId, offlineEnvironmentId], serverConfigs: new Map([[onlineEnvironmentId, config(onlineEnvironmentId)]]), }); expect([...changed.draggableThreadKeys]).toEqual([keyOf(online)]); expect([...changed.activeReorderableThreadKeys]).toEqual([keyOf(online)]); }); it.each(["active", "pinned"] as const)( "replans a disconnected drag source in %s", (section) => { const bound = thread("offline", offlineEnvironmentId, "m"); const moved = thread("online", onlineEnvironmentId, "t"); const input = { section, rows: [bound, moved], moved, order: [moved, bound] }; expect(planDrop(input).plan.kind).toBe( section === "active" ? "move-active" : "reorder-pinned", ); const disconnected = planDrop({ ...input, connectedEnvironmentIds: [] }); expect(disconnected.plan).toEqual({ kind: "none" }); expect(disconnected.assignments).toEqual([]); }, ); it.each(["active", "pinned"] as const)("preserves a keyed offline bound in %s", (section) => { const bound = thread("offline", offlineEnvironmentId, "m"); const moved = thread("online", onlineEnvironmentId, "t"); const result = planDrop({ section, rows: [bound, moved], moved, order: [moved, bound] }); expect(result.plan.kind).toBe(section === "active" ? "move-active" : "reorder-pinned"); expect(result.assignments.map(({ id }) => id)).toEqual([keyOf(moved)]); expect(result.actualOrder).toEqual(result.targetOrder); }); it.each(["active", "pinned"] as const)( "rejects rebalancing a keyed offline bound in %s", (section) => { const bound = thread("offline", offlineEnvironmentId, "m"); const neighbor = thread("neighbor"); const result = planDrop({ section, rows: [bound, online, neighbor], moved: online, order: [bound, neighbor, online], }); expect(result.plan).toEqual({ kind: "none" }); expect(result.assignments).toEqual([]); }, ); it.each(["active", "pinned"] as const)( "rejects crossing the frozen keyless boundary in %s", (section) => { const bound = { ...offline, createdAt: section === "active" ? "2026-03-09T12:00:00.000Z" : "2026-03-09T10:00:00.000Z", }; const order = section === "active" ? [online, bound] : [bound, online]; const result = planDrop({ section, rows: [bound, online], moved: online, order }); expect(result.plan).toEqual({ kind: "none" }); expect(result.assignments).toEqual([]); }, ); it.each(["active", "pinned"] as const)( "allows realizable moves beside a frozen keyless row in %s", (section) => { const neighbor = thread("neighbor"); const order = section === "active" ? [offline, neighbor, online] : [online, neighbor, offline]; const result = planDrop({ section, rows: [offline, online, neighbor], moved: online, order, }); expect(result.plan.kind).toBe(section === "active" ? "move-active" : "reorder-pinned"); expect(result.assignments.length).toBeGreaterThan(0); expect(result.assignments.every(({ id }) => id !== keyOf(offline))).toBe(true); expect(result.actualOrder).toEqual(result.targetOrder); }, ); it.each(["active", "pinned"] as const)( "checks fallback assignments to other rows at the %s boundary", (section) => { const neighbor = thread("neighbor"); const order = section === "active" ? [neighbor, offline, online] : [online, offline, neighbor]; const result = planDrop({ section, rows: [offline, online, neighbor], moved: online, order, }); expect(result.plan).toEqual({ kind: "none" }); }, ); it.each(["active", "pinned"] as const)( "rejects impossible pin/unpin placement across the %s boundary", (section) => { const order = section === "active" ? [online, offline] : [offline, online]; const result = planDrop({ section, rows: [offline, online], moved: online, order, from: section === "active" ? "pinned" : "active", }); expect(result.plan).toEqual({ kind: "none" }); }, ); }); it("only changes lifecycle when the inbox is time-ordered", () => { const base = { pinnedOrder: ["p1"], pinnedKeysById: new Map([["p1", "m"]]), activeOrder: ["a1", "a2"], activeKeysById: new Map([ ["a1", "f"], ["a2", "t"], ]), activeTimeOrdered: true, }; expect( planSidebarThreadDrop({ ...base, activeKey: "a1", activeSection: "active", target: { section: "active", pinnedOrder: ["p1"], activeOrder: ["a2", "a1"] }, }), ).toEqual({ kind: "none" }); expect( planSidebarThreadDrop({ ...base, activeKey: "p1", activeSection: "pinned", target: { section: "active", pinnedOrder: [], activeOrder: ["a1", "p1", "a2"] }, }), ).toEqual({ kind: "move-active", order: null, assignments: [], unpin: true, unsettle: false, unsnooze: false, }); }); }); }); describe("pendingSidebarRouteReveal", () => { it("keeps a revealed thread settled across a mount that reads no route params", () => { const reveal = { observedKey: null, pendingKey: null }; expect(pendingSidebarRouteReveal(reveal, "env:a1")).toBe("env:a1"); completeSidebarRouteReveal(reveal); // Back from Settings: location commits before the thread match does. expect(pendingSidebarRouteReveal(reveal, null)).toBeNull(); expect(pendingSidebarRouteReveal(reveal, "env:a1")).toBeNull(); expect(pendingSidebarRouteReveal(reveal, "env:b1")).toBe("env:b1"); }); it("keeps a pending reveal through a render without route params", () => { const reveal = { observedKey: null, pendingKey: null }; pendingSidebarRouteReveal(reveal, "env:a1"); expect(pendingSidebarRouteReveal(reveal, null)).toBeNull(); expect(pendingSidebarRouteReveal(reveal, "env:a1")).toBe("env:a1"); }); }); describe("resolveSidebarRouteReveal", () => { const scopedProjectKeys = new Set(["environment-test:project-scoped"]); const hidden = makeThreadFixture({ projectId: ProjectId.make("project-other") }); const hiddenKey = "environment-test:thread-test"; const findThread = (key: string) => (key === hiddenKey ? hidden : undefined); it("widens the scope for a newly opened thread the scope hides", () => { expect( resolveSidebarRouteReveal({ reveal: { observedKey: "environment-test:other", pendingKey: null }, routeKey: hiddenKey, isSearching: false, scopedProjectKeys, delegatedUnderParent: new Set(), findThread, }), ).toEqual({ pendingKey: hiddenKey, widensScope: true }); }); it("never widens for a thread whose reveal already completed", () => { // A remount's once-per-mount scroll must keep the scope the user chose. expect( resolveSidebarRouteReveal({ reveal: { observedKey: hiddenKey, pendingKey: null }, routeKey: hiddenKey, isSearching: false, scopedProjectKeys, delegatedUnderParent: new Set(), findThread, }), ).toEqual({ pendingKey: null, widensScope: false }); }); it("widens again for a thread reopened before the thread between them listed its row", () => { // The user scoped away from the open thread, then went to a thread whose row // never mounted, then came back. const reveal = { observedKey: hiddenKey, pendingKey: null }; const resolve = (routeKey: string) => resolveSidebarRouteReveal({ reveal, routeKey, isSearching: false, scopedProjectKeys, delegatedUnderParent: new Set(), findThread, }); resolve("environment-test:unlisted"); expect(resolve(hiddenKey)).toEqual({ pendingKey: hiddenKey, widensScope: true }); }); it("holds a pending reveal without widening while searching", () => { expect( resolveSidebarRouteReveal({ reveal: { observedKey: hiddenKey, pendingKey: hiddenKey }, routeKey: hiddenKey, isSearching: true, scopedProjectKeys, delegatedUnderParent: new Set(), findThread, }), ).toEqual({ pendingKey: hiddenKey, widensScope: false }); }); }); describe("sidebarFolderExpansionKeys", () => { it("honors a collapse the legacy sidebar saved under a member's cwd", () => { const keys = sidebarFolderExpansionKeys({ projectKey: "github.com/pingdotgg/t3code", memberProjects: [{ physicalProjectKey: "local:/code/t3code", workspaceRoot: "/code/t3code" }], }); const cwdKey = keys.at(-1)!; expect(resolveProjectExpanded({}, keys)).toBe(true); expect(resolveProjectExpanded({ [cwdKey]: false }, keys)).toBe(false); expect(resolveProjectExpanded({ [keys[0]!]: true, [cwdKey]: false }, keys)).toBe(true); }); }); describe("groupSidebarThreadsByProject", () => { it("puts each thread in its project's folder and drops empty projects unless kept", () => { const remoteEnvironmentId = EnvironmentId.make("environment-remote"); const t3code = ProjectId.make("t3code"); const t3codeRemote = ProjectId.make("t3code-remote"); const fleet = ProjectId.make("fleet"); const thread = (id: string, environmentId: EnvironmentId, projectId: ProjectId) => ({ id, environmentId, projectId, }); const groups = [ { projectKey: "t3code", memberProjectRefs: [ { environmentId: localEnvironmentId, projectId: t3code }, { environmentId: remoteEnvironmentId, projectId: t3codeRemote }, ], }, { projectKey: "fleet", memberProjectRefs: [{ environmentId: localEnvironmentId, projectId: fleet }], }, { projectKey: "notes", memberProjectRefs: [ { environmentId: localEnvironmentId, projectId: ProjectId.make("notes") }, ], }, ]; const threads = [ thread("a", localEnvironmentId, t3code), thread("orphan", localEnvironmentId, ProjectId.make("deleted")), thread("b", remoteEnvironmentId, t3codeRemote), ]; const summarize = (keepEmpty: (group: (typeof groups)[number]) => boolean) => groupSidebarThreadsByProject(groups, threads, keepEmpty).map((folder) => ({ projectKey: folder.group.projectKey, threads: folder.threads.map((entry) => entry.id), })); expect(summarize(() => true)).toEqual([ { projectKey: "t3code", threads: ["a", "b"] }, { projectKey: "fleet", threads: [] }, { projectKey: "notes", threads: [] }, ]); expect(summarize(() => false)).toEqual([{ projectKey: "t3code", threads: ["a", "b"] }]); // A pinned project keeps its empty folder while other empty projects drop out. expect(summarize((group) => group.projectKey === "fleet")).toEqual([ { projectKey: "t3code", threads: ["a", "b"] }, { projectKey: "fleet", threads: [] }, ]); }); }); describe("resolveSidebarFolderDot", () => { it("flags the most urgent hidden thread and stays quiet otherwise", () => { const idle = { hasActionableProposedPlan: false, hasPendingApprovals: false, hasPendingUserInput: false, interactionMode: "default" as const, latestRun: makeLatestRun(), lastVisitedAt: "2026-03-09T10:06:00.000Z", runtime: null, }; const runtime = { status: "running" as const, activeRunId: null, providerInstanceId: ProviderInstanceId.make("codex"), providerName: "Codex", lastError: null, updatedAt: "2026-03-09T10:00:00.000Z", }; const working = { ...idle, runtime }; const unread = { ...idle, lastVisitedAt: "2026-03-09T10:04:00.000Z" }; const limited = { ...idle, runtime: { ...runtime, status: "failed" as const, lastErrorClass: "usage_limit" as const }, }; expect(resolveSidebarFolderDot([idle])).toBeNull(); // A waiting thread stays quiet even with an unread completion, like its row. expect( resolveSidebarFolderDot([{ ...unread, runtime: { ...runtime, status: "idle" as const } }]), ).toBeNull(); expect(resolveSidebarFolderDot([idle, working])).toBe("working"); expect(resolveSidebarFolderDot([working, unread])).toBe("unread"); expect(resolveSidebarFolderDot([unread, limited])).toBe("attention"); }); }); describe("shouldCreateNewThreadInCurrentProject", () => { it("opens the picker when several projects are open, or only archived ones exist", () => { expect(shouldCreateNewThreadInCurrentProject(false, 2, 3)).toBe(false); expect(shouldCreateNewThreadInCurrentProject(false, 0, 2)).toBe(false); expect(shouldCreateNewThreadInCurrentProject(false, 1, 3)).toBe(true); expect(shouldCreateNewThreadInCurrentProject(false, 0, 0)).toBe(true); expect(shouldCreateNewThreadInCurrentProject(true, 2, 2)).toBe(true); }); });