import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createThreadJumpHintVisibilityController, getSidebarThreadIdsToPrewarm, getVisibleSidebarThreadIds, resolveAdjacentThreadId, getFallbackThreadIdAfterDelete, getVisibleThreadsForProject, getProjectSortTimestamp, hasUnseenCompletion, isContextMenuPointerDown, orderItemsByPreferredIds, resolveProjectStatusIndicator, resolveSidebarNewThreadSeedContext, resolveSidebarNewThreadEnvMode, resolveThreadRowClassName, resolveThreadStatusPill, shouldClearThreadSelectionOnMouseDown, sortProjectsForSidebar, THREAD_JUMP_HINT_SHOW_DELAY_MS,} from "./Sidebar.logic";import { EnvironmentId, OrchestrationLatestTurn, ProjectId, ThreadId } from "@t3tools/contracts";import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, type Project, type Thread,} from "../types"; const localEnvironmentId = EnvironmentId.make("environment-local"); function makeLatestTurn(overrides?: { completedAt?: string | null; startedAt?: string | null;}): OrchestrationLatestTurn { return { turnId: "turn-1" as never, state: "completed", assistantMessageId: null, requestedAt: "2026-03-09T10:00:00.000Z", startedAt: overrides?.startedAt ?? "2026-03-09T10:00:00.000Z", completedAt: overrides?.completedAt ?? "2026-03-09T10:05:00.000Z", };} describe("hasUnseenCompletion", () => { it("returns true when a thread completed after its last visit", () => { expect( hasUnseenCompletion({ hasActionableProposedPlan: false, hasPendingApprovals: false, hasPendingUserInput: false, interactionMode: "default", latestTurn: makeLatestTurn(), lastVisitedAt: "2026-03-09T10:04:00.000Z", session: null, }), ).toBe(true); });}); describe("createThreadJumpHintVisibilityController", () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it("delays showing jump hints until the configured delay elapses", () => { const visibilityChanges: boolean[] = []; const controller = createThreadJumpHintVisibilityController({ delayMs: THREAD_JUMP_HINT_SHOW_DELAY_MS, onVisibilityChange: (visible) => { visibilityChanges.push(visible); }, }); controller.sync(true); vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS - 1); expect(visibilityChanges).toEqual([]); vi.advanceTimersByTime(1); expect(visibilityChanges).toEqual([true]); }); it("hides immediately when the modifiers are released", () => { const visibilityChanges: boolean[] = []; const controller = createThreadJumpHintVisibilityController({ delayMs: THREAD_JUMP_HINT_SHOW_DELAY_MS, onVisibilityChange: (visible) => { visibilityChanges.push(visible); }, }); controller.sync(true); vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS); controller.sync(false); expect(visibilityChanges).toEqual([true, false]); }); it("cancels a pending reveal when the modifier is released early", () => { const visibilityChanges: boolean[] = []; const controller = createThreadJumpHintVisibilityController({ delayMs: THREAD_JUMP_HINT_SHOW_DELAY_MS, onVisibilityChange: (visible) => { visibilityChanges.push(visible); }, }); controller.sync(true); vi.advanceTimersByTime(Math.floor(THREAD_JUMP_HINT_SHOW_DELAY_MS / 2)); controller.sync(false); vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS); expect(visibilityChanges).toEqual([]); });}); describe("getSidebarThreadIdsToPrewarm", () => { it("returns only the first visible thread ids up to the prewarm limit", () => { expect(getSidebarThreadIdsToPrewarm(["t1", "t2", "t3"], 2)).toEqual(["t1", "t2"]); }); it("returns all visible thread ids when they fit within the limit", () => { expect(getSidebarThreadIdsToPrewarm(["t1", "t2"], 10)).toEqual(["t1", "t2"]); }); it("returns no thread ids when the limit is zero", () => { expect(getSidebarThreadIdsToPrewarm(["t1", "t2"], 0)).toEqual([]); });}); describe("shouldClearThreadSelectionOnMouseDown", () => { it("preserves selection for thread items", () => { const child = { closest: (selector: string) => selector.includes("[data-thread-item]") ? ({} as Element) : null, } as unknown as HTMLElement; expect(shouldClearThreadSelectionOnMouseDown(child)).toBe(false); }); it("preserves selection for thread list toggle controls", () => { const selectionSafe = { closest: (selector: string) => selector.includes("[data-thread-selection-safe]") ? ({} as Element) : null, } as unknown as HTMLElement; expect(shouldClearThreadSelectionOnMouseDown(selectionSafe)).toBe(false); }); it("clears selection for unrelated sidebar clicks", () => { const unrelated = { closest: () => null, } as unknown as HTMLElement; expect(shouldClearThreadSelectionOnMouseDown(unrelated)).toBe(true); });}); describe("resolveSidebarNewThreadEnvMode", () => { it("uses the app default when the caller does not request a specific mode", () => { expect( resolveSidebarNewThreadEnvMode({ defaultEnvMode: "worktree", }), ).toBe("worktree"); }); it("preserves an explicit requested mode over the app default", () => { expect( resolveSidebarNewThreadEnvMode({ requestedEnvMode: "local", defaultEnvMode: "worktree", }), ).toBe("local"); });}); describe("resolveSidebarNewThreadSeedContext", () => { it("prefers the default worktree mode over active thread context", () => { expect( resolveSidebarNewThreadSeedContext({ projectId: "project-1", defaultEnvMode: "worktree", activeThread: { projectId: "project-1", branch: "feature/existing", worktreePath: "/repo/.t3/worktrees/existing", }, activeDraftThread: { projectId: "project-1", branch: "feature/draft", worktreePath: "/repo/.t3/worktrees/draft", envMode: "worktree", }, }), ).toEqual({ envMode: "worktree", }); }); it("inherits the active server thread context when creating a new thread in the same project", () => { expect( resolveSidebarNewThreadSeedContext({ projectId: "project-1", defaultEnvMode: "local", activeThread: { projectId: "project-1", branch: "effect-atom", worktreePath: null, }, activeDraftThread: null, }), ).toEqual({ branch: "effect-atom", worktreePath: null, envMode: "local", }); }); it("prefers the active draft thread context when it matches the target project", () => { expect( resolveSidebarNewThreadSeedContext({ projectId: "project-1", defaultEnvMode: "local", activeThread: { projectId: "project-1", branch: "effect-atom", worktreePath: null, }, activeDraftThread: { projectId: "project-1", branch: "feature/new-draft", worktreePath: "/repo/worktree", envMode: "worktree", }, }), ).toEqual({ branch: "feature/new-draft", worktreePath: "/repo/worktree", envMode: "worktree", }); }); it("falls back to the default env mode when there is no matching active thread context", () => { expect( resolveSidebarNewThreadSeedContext({ projectId: "project-2", defaultEnvMode: "worktree", activeThread: { projectId: "project-1", branch: "effect-atom", worktreePath: null, }, activeDraftThread: null, }), ).toEqual({ envMode: "worktree", }); });}); describe("orderItemsByPreferredIds", () => { it("keeps preferred ids first, skips stale ids, and preserves the relative order of remaining items", () => { const ordered = orderItemsByPreferredIds({ items: [ { id: ProjectId.make("project-1"), name: "One" }, { id: ProjectId.make("project-2"), name: "Two" }, { id: ProjectId.make("project-3"), name: "Three" }, ], preferredIds: [ ProjectId.make("project-3"), ProjectId.make("project-missing"), ProjectId.make("project-1"), ], getId: (project) => project.id, }); expect(ordered.map((project) => project.id)).toEqual([ ProjectId.make("project-3"), ProjectId.make("project-1"), ProjectId.make("project-2"), ]); }); it("does not duplicate items when preferred ids repeat", () => { const ordered = orderItemsByPreferredIds({ items: [ { id: ProjectId.make("project-1"), name: "One" }, { id: ProjectId.make("project-2"), name: "Two" }, ], preferredIds: [ ProjectId.make("project-2"), ProjectId.make("project-1"), ProjectId.make("project-2"), ], getId: (project) => project.id, }); expect(ordered.map((project) => project.id)).toEqual([ ProjectId.make("project-2"), ProjectId.make("project-1"), ]); }); it("honors projectOrder physical keys via getProjectOrderKey", async () => { // Regression guard for #1904 / the regression introduced by #2055: // `projectOrder` is populated with physical keys (envId + cwd-derived) // by the store and by drag-end handlers. Readers must identify projects // with the same key format, or manual sort silently snaps back. const { getProjectOrderKey } = await import("../logicalProject"); const projects = [ { environmentId: EnvironmentId.make("environment-local"), id: ProjectId.make("id-alpha"), cwd: "/work/alpha", }, { environmentId: EnvironmentId.make("environment-local"), id: ProjectId.make("id-beta"), cwd: "/work/beta", }, { environmentId: EnvironmentId.make("environment-local"), id: ProjectId.make("id-gamma"), cwd: "/work/gamma", }, ]; const ordered = orderItemsByPreferredIds({ items: projects, preferredIds: [getProjectOrderKey(projects[2]!), getProjectOrderKey(projects[0]!)], getId: getProjectOrderKey, }); expect(ordered.map((project) => project.cwd)).toEqual([ "/work/gamma", "/work/alpha", "/work/beta", ]); });}); describe("resolveAdjacentThreadId", () => { it("resolves adjacent thread ids in ordered sidebar traversal", () => { const threads = [ ThreadId.make("thread-1"), ThreadId.make("thread-2"), ThreadId.make("thread-3"), ]; expect( resolveAdjacentThreadId({ threadIds: threads, currentThreadId: threads[1] ?? null, direction: "previous", }), ).toBe(threads[0]); expect( resolveAdjacentThreadId({ threadIds: threads, currentThreadId: threads[1] ?? null, direction: "next", }), ).toBe(threads[2]); expect( resolveAdjacentThreadId({ threadIds: threads, currentThreadId: null, direction: "next", }), ).toBe(threads[0]); expect( resolveAdjacentThreadId({ threadIds: threads, currentThreadId: null, direction: "previous", }), ).toBe(threads[2]); expect( resolveAdjacentThreadId({ threadIds: threads, currentThreadId: threads[0] ?? null, direction: "previous", }), ).toBeNull(); });}); describe("getVisibleSidebarThreadIds", () => { it("returns only the rendered visible thread order across projects", () => { expect( getVisibleSidebarThreadIds([ { renderedThreadIds: [ ThreadId.make("thread-12"), ThreadId.make("thread-11"), ThreadId.make("thread-10"), ], }, { renderedThreadIds: [ThreadId.make("thread-8"), ThreadId.make("thread-6")], }, ]), ).toEqual([ ThreadId.make("thread-12"), ThreadId.make("thread-11"), ThreadId.make("thread-10"), ThreadId.make("thread-8"), ThreadId.make("thread-6"), ]); }); it("skips threads from collapsed projects whose thread panels are not shown", () => { expect( getVisibleSidebarThreadIds([ { shouldShowThreadPanel: false, renderedThreadIds: [ThreadId.make("thread-hidden-2"), ThreadId.make("thread-hidden-1")], }, { shouldShowThreadPanel: true, renderedThreadIds: [ThreadId.make("thread-12"), ThreadId.make("thread-11")], }, ]), ).toEqual([ThreadId.make("thread-12"), ThreadId.make("thread-11")]); });}); describe("isContextMenuPointerDown", () => { it("treats secondary-button presses as context menu gestures on all platforms", () => { expect( isContextMenuPointerDown({ button: 2, ctrlKey: false, isMac: false, }), ).toBe(true); }); it("treats ctrl+primary-click as a context menu gesture on macOS", () => { expect( isContextMenuPointerDown({ button: 0, ctrlKey: true, isMac: true, }), ).toBe(true); }); it("does not treat ctrl+primary-click as a context menu gesture off macOS", () => { expect( isContextMenuPointerDown({ button: 0, ctrlKey: true, isMac: false, }), ).toBe(false); });}); describe("resolveThreadStatusPill", () => { const baseThread = { hasActionableProposedPlan: false, hasPendingApprovals: false, hasPendingUserInput: false, interactionMode: "plan" as const, latestTurn: null, lastVisitedAt: undefined, session: { provider: "codex" as const, status: "running" as const, createdAt: "2026-03-09T10:00:00.000Z", updatedAt: "2026-03-09T10:00:00.000Z", orchestrationStatus: "running" as const, }, }; it("shows pending approval before all other statuses", () => { expect( resolveThreadStatusPill({ thread: { ...baseThread, hasPendingApprovals: true, hasPendingUserInput: true, }, }), ).toMatchObject({ label: "Pending Approval", pulse: false }); }); it("shows awaiting input when plan mode is blocked on user answers", () => { expect( resolveThreadStatusPill({ thread: {Lines 1–500 of 964. Load more
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createThreadJumpHintVisibilityController, getSidebarThreadIdsToPrewarm, getVisibleSidebarThreadIds, resolveAdjacentThreadId, getFallbackThreadIdAfterDelete, getVisibleThreadsForProject, getProjectSortTimestamp, hasUnseenCompletion, isContextMenuPointerDown, orderItemsByPreferredIds, resolveProjectStatusIndicator, resolveSidebarNewThreadSeedContext, resolveSidebarNewThreadEnvMode, resolveThreadRowClassName, resolveThreadStatusPill, shouldClearThreadSelectionOnMouseDown, sortProjectsForSidebar, THREAD_JUMP_HINT_SHOW_DELAY_MS, } from "./Sidebar.logic"; import { EnvironmentId, OrchestrationLatestTurn, ProjectId, ThreadId } from "@t3tools/contracts"; import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, type Project, type Thread, } from "../types"; const localEnvironmentId = EnvironmentId.make("environment-local"); function makeLatestTurn(overrides?: { completedAt?: string | null; startedAt?: string | null; }): OrchestrationLatestTurn { return { turnId: "turn-1" as never, state: "completed", assistantMessageId: null, requestedAt: "2026-03-09T10:00:00.000Z", startedAt: overrides?.startedAt ?? "2026-03-09T10:00:00.000Z", completedAt: overrides?.completedAt ?? "2026-03-09T10:05:00.000Z", }; } describe("hasUnseenCompletion", () => { it("returns true when a thread completed after its last visit", () => { expect( hasUnseenCompletion({ hasActionableProposedPlan: false, hasPendingApprovals: false, hasPendingUserInput: false, interactionMode: "default", latestTurn: makeLatestTurn(), lastVisitedAt: "2026-03-09T10:04:00.000Z", session: null, }), ).toBe(true); }); }); describe("createThreadJumpHintVisibilityController", () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it("delays showing jump hints until the configured delay elapses", () => { const visibilityChanges: boolean[] = []; const controller = createThreadJumpHintVisibilityController({ delayMs: THREAD_JUMP_HINT_SHOW_DELAY_MS, onVisibilityChange: (visible) => { visibilityChanges.push(visible); }, }); controller.sync(true); vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS - 1); expect(visibilityChanges).toEqual([]); vi.advanceTimersByTime(1); expect(visibilityChanges).toEqual([true]); }); it("hides immediately when the modifiers are released", () => { const visibilityChanges: boolean[] = []; const controller = createThreadJumpHintVisibilityController({ delayMs: THREAD_JUMP_HINT_SHOW_DELAY_MS, onVisibilityChange: (visible) => { visibilityChanges.push(visible); }, }); controller.sync(true); vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS); controller.sync(false); expect(visibilityChanges).toEqual([true, false]); }); it("cancels a pending reveal when the modifier is released early", () => { const visibilityChanges: boolean[] = []; const controller = createThreadJumpHintVisibilityController({ delayMs: THREAD_JUMP_HINT_SHOW_DELAY_MS, onVisibilityChange: (visible) => { visibilityChanges.push(visible); }, }); controller.sync(true); vi.advanceTimersByTime(Math.floor(THREAD_JUMP_HINT_SHOW_DELAY_MS / 2)); controller.sync(false); vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS); expect(visibilityChanges).toEqual([]); }); }); describe("getSidebarThreadIdsToPrewarm", () => { it("returns only the first visible thread ids up to the prewarm limit", () => { expect(getSidebarThreadIdsToPrewarm(["t1", "t2", "t3"], 2)).toEqual(["t1", "t2"]); }); it("returns all visible thread ids when they fit within the limit", () => { expect(getSidebarThreadIdsToPrewarm(["t1", "t2"], 10)).toEqual(["t1", "t2"]); }); it("returns no thread ids when the limit is zero", () => { expect(getSidebarThreadIdsToPrewarm(["t1", "t2"], 0)).toEqual([]); }); }); describe("shouldClearThreadSelectionOnMouseDown", () => { it("preserves selection for thread items", () => { const child = { closest: (selector: string) => selector.includes("[data-thread-item]") ? ({} as Element) : null, } as unknown as HTMLElement; expect(shouldClearThreadSelectionOnMouseDown(child)).toBe(false); }); it("preserves selection for thread list toggle controls", () => { const selectionSafe = { closest: (selector: string) => selector.includes("[data-thread-selection-safe]") ? ({} as Element) : null, } as unknown as HTMLElement; expect(shouldClearThreadSelectionOnMouseDown(selectionSafe)).toBe(false); }); it("clears selection for unrelated sidebar clicks", () => { const unrelated = { closest: () => null, } as unknown as HTMLElement; expect(shouldClearThreadSelectionOnMouseDown(unrelated)).toBe(true); }); }); describe("resolveSidebarNewThreadEnvMode", () => { it("uses the app default when the caller does not request a specific mode", () => { expect( resolveSidebarNewThreadEnvMode({ defaultEnvMode: "worktree", }), ).toBe("worktree"); }); it("preserves an explicit requested mode over the app default", () => { expect( resolveSidebarNewThreadEnvMode({ requestedEnvMode: "local", defaultEnvMode: "worktree", }), ).toBe("local"); }); }); describe("resolveSidebarNewThreadSeedContext", () => { it("prefers the default worktree mode over active thread context", () => { expect( resolveSidebarNewThreadSeedContext({ projectId: "project-1", defaultEnvMode: "worktree", activeThread: { projectId: "project-1", branch: "feature/existing", worktreePath: "/repo/.t3/worktrees/existing", }, activeDraftThread: { projectId: "project-1", branch: "feature/draft", worktreePath: "/repo/.t3/worktrees/draft", envMode: "worktree", }, }), ).toEqual({ envMode: "worktree", }); }); it("inherits the active server thread context when creating a new thread in the same project", () => { expect( resolveSidebarNewThreadSeedContext({ projectId: "project-1", defaultEnvMode: "local", activeThread: { projectId: "project-1", branch: "effect-atom", worktreePath: null, }, activeDraftThread: null, }), ).toEqual({ branch: "effect-atom", worktreePath: null, envMode: "local", }); }); it("prefers the active draft thread context when it matches the target project", () => { expect( resolveSidebarNewThreadSeedContext({ projectId: "project-1", defaultEnvMode: "local", activeThread: { projectId: "project-1", branch: "effect-atom", worktreePath: null, }, activeDraftThread: { projectId: "project-1", branch: "feature/new-draft", worktreePath: "/repo/worktree", envMode: "worktree", }, }), ).toEqual({ branch: "feature/new-draft", worktreePath: "/repo/worktree", envMode: "worktree", }); }); it("falls back to the default env mode when there is no matching active thread context", () => { expect( resolveSidebarNewThreadSeedContext({ projectId: "project-2", defaultEnvMode: "worktree", activeThread: { projectId: "project-1", branch: "effect-atom", worktreePath: null, }, activeDraftThread: null, }), ).toEqual({ envMode: "worktree", }); }); }); describe("orderItemsByPreferredIds", () => { it("keeps preferred ids first, skips stale ids, and preserves the relative order of remaining items", () => { const ordered = orderItemsByPreferredIds({ items: [ { id: ProjectId.make("project-1"), name: "One" }, { id: ProjectId.make("project-2"), name: "Two" }, { id: ProjectId.make("project-3"), name: "Three" }, ], preferredIds: [ ProjectId.make("project-3"), ProjectId.make("project-missing"), ProjectId.make("project-1"), ], getId: (project) => project.id, }); expect(ordered.map((project) => project.id)).toEqual([ ProjectId.make("project-3"), ProjectId.make("project-1"), ProjectId.make("project-2"), ]); }); it("does not duplicate items when preferred ids repeat", () => { const ordered = orderItemsByPreferredIds({ items: [ { id: ProjectId.make("project-1"), name: "One" }, { id: ProjectId.make("project-2"), name: "Two" }, ], preferredIds: [ ProjectId.make("project-2"), ProjectId.make("project-1"), ProjectId.make("project-2"), ], getId: (project) => project.id, }); expect(ordered.map((project) => project.id)).toEqual([ ProjectId.make("project-2"), ProjectId.make("project-1"), ]); }); it("honors projectOrder physical keys via getProjectOrderKey", async () => { // Regression guard for #1904 / the regression introduced by #2055: // `projectOrder` is populated with physical keys (envId + cwd-derived) // by the store and by drag-end handlers. Readers must identify projects // with the same key format, or manual sort silently snaps back. const { getProjectOrderKey } = await import("../logicalProject"); const projects = [ { environmentId: EnvironmentId.make("environment-local"), id: ProjectId.make("id-alpha"), cwd: "/work/alpha", }, { environmentId: EnvironmentId.make("environment-local"), id: ProjectId.make("id-beta"), cwd: "/work/beta", }, { environmentId: EnvironmentId.make("environment-local"), id: ProjectId.make("id-gamma"), cwd: "/work/gamma", }, ]; const ordered = orderItemsByPreferredIds({ items: projects, preferredIds: [getProjectOrderKey(projects[2]!), getProjectOrderKey(projects[0]!)], getId: getProjectOrderKey, }); expect(ordered.map((project) => project.cwd)).toEqual([ "/work/gamma", "/work/alpha", "/work/beta", ]); }); }); describe("resolveAdjacentThreadId", () => { it("resolves adjacent thread ids in ordered sidebar traversal", () => { const threads = [ ThreadId.make("thread-1"), ThreadId.make("thread-2"), ThreadId.make("thread-3"), ]; expect( resolveAdjacentThreadId({ threadIds: threads, currentThreadId: threads[1] ?? null, direction: "previous", }), ).toBe(threads[0]); expect( resolveAdjacentThreadId({ threadIds: threads, currentThreadId: threads[1] ?? null, direction: "next", }), ).toBe(threads[2]); expect( resolveAdjacentThreadId({ threadIds: threads, currentThreadId: null, direction: "next", }), ).toBe(threads[0]); expect( resolveAdjacentThreadId({ threadIds: threads, currentThreadId: null, direction: "previous", }), ).toBe(threads[2]); expect( resolveAdjacentThreadId({ threadIds: threads, currentThreadId: threads[0] ?? null, direction: "previous", }), ).toBeNull(); }); }); describe("getVisibleSidebarThreadIds", () => { it("returns only the rendered visible thread order across projects", () => { expect( getVisibleSidebarThreadIds([ { renderedThreadIds: [ ThreadId.make("thread-12"), ThreadId.make("thread-11"), ThreadId.make("thread-10"), ], }, { renderedThreadIds: [ThreadId.make("thread-8"), ThreadId.make("thread-6")], }, ]), ).toEqual([ ThreadId.make("thread-12"), ThreadId.make("thread-11"), ThreadId.make("thread-10"), ThreadId.make("thread-8"), ThreadId.make("thread-6"), ]); }); it("skips threads from collapsed projects whose thread panels are not shown", () => { expect( getVisibleSidebarThreadIds([ { shouldShowThreadPanel: false, renderedThreadIds: [ThreadId.make("thread-hidden-2"), ThreadId.make("thread-hidden-1")], }, { shouldShowThreadPanel: true, renderedThreadIds: [ThreadId.make("thread-12"), ThreadId.make("thread-11")], }, ]), ).toEqual([ThreadId.make("thread-12"), ThreadId.make("thread-11")]); }); }); describe("isContextMenuPointerDown", () => { it("treats secondary-button presses as context menu gestures on all platforms", () => { expect( isContextMenuPointerDown({ button: 2, ctrlKey: false, isMac: false, }), ).toBe(true); }); it("treats ctrl+primary-click as a context menu gesture on macOS", () => { expect( isContextMenuPointerDown({ button: 0, ctrlKey: true, isMac: true, }), ).toBe(true); }); it("does not treat ctrl+primary-click as a context menu gesture off macOS", () => { expect( isContextMenuPointerDown({ button: 0, ctrlKey: true, isMac: false, }), ).toBe(false); }); }); describe("resolveThreadStatusPill", () => { const baseThread = { hasActionableProposedPlan: false, hasPendingApprovals: false, hasPendingUserInput: false, interactionMode: "plan" as const, latestTurn: null, lastVisitedAt: undefined, session: { provider: "codex" as const, status: "running" as const, createdAt: "2026-03-09T10:00:00.000Z", updatedAt: "2026-03-09T10:00:00.000Z", orchestrationStatus: "running" as const, }, }; it("shows pending approval before all other statuses", () => { expect( resolveThreadStatusPill({ thread: { ...baseThread, hasPendingApprovals: true, hasPendingUserInput: true, }, }), ).toMatchObject({ label: "Pending Approval", pulse: false }); }); it("shows awaiting input when plan mode is blocked on user answers", () => { expect( resolveThreadStatusPill({ thread: {