import type { ProviderEvent, ProviderSession, TerminalEvent } from "@t3tools/contracts";import { describe, expect, it } from "vitest"; import { type AppState, reducer } from "./store";import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID, MAX_THREAD_TERMINAL_COUNT,} from "./types";import type { Thread } from "./types"; type TerminalStartedEvent = Extract<TerminalEvent, { type: "started" }>;type TerminalActivityEvent = Extract<TerminalEvent, { type: "activity" }>; function makeSession(overrides: Partial<ProviderSession> = {}): ProviderSession { return { sessionId: "sess-1", provider: "codex", status: "ready", createdAt: "2026-02-09T00:00:00.000Z", updatedAt: "2026-02-09T00:00:00.000Z", ...overrides, };} function makeEvent(overrides: Partial<ProviderEvent> = {}): ProviderEvent { return { id: "evt-1", kind: "notification", provider: "codex", sessionId: "sess-1", createdAt: "2026-02-09T00:00:01.000Z", method: "thread/started", ...overrides, };} function makeThread(overrides: Partial<Thread> = {}): Thread { return { id: "thread-local-1", codexThreadId: null, projectId: "project-1", title: "Thread", model: "gpt-5.3-codex", terminalOpen: false, terminalHeight: DEFAULT_THREAD_TERMINAL_HEIGHT, terminalIds: [DEFAULT_THREAD_TERMINAL_ID], runningTerminalIds: [], activeTerminalId: DEFAULT_THREAD_TERMINAL_ID, terminalGroups: [ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID], }, ], activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, session: makeSession(), messages: [], events: [], turnDiffSummaries: [], error: null, createdAt: "2026-02-09T00:00:00.000Z", branch: null, worktreePath: null, ...overrides, };} function makeState(thread: Thread): AppState { return { projects: [ { id: "project-1", name: "Project", cwd: "/tmp/project", model: "gpt-5.3-codex", expanded: true, scripts: [], }, ], threads: [thread], threadsHydrated: true, runtimeMode: "full-access", diffOpen: false, diffThreadId: null, diffTurnId: null, diffFilePath: null, };} function makeTerminalStartedEvent( overrides: Partial<TerminalStartedEvent> = {},): TerminalStartedEvent { return { type: "started", threadId: "thread-local-1", terminalId: DEFAULT_THREAD_TERMINAL_ID, createdAt: "2026-02-09T00:00:01.000Z", snapshot: { threadId: "thread-local-1", terminalId: DEFAULT_THREAD_TERMINAL_ID, cwd: "/tmp/project", status: "running", pid: 1234, history: "", exitCode: null, exitSignal: null, updatedAt: "2026-02-09T00:00:01.000Z", }, ...overrides, };} function makeTerminalActivityEvent( overrides: Partial<TerminalActivityEvent> = {},): TerminalActivityEvent { return { type: "activity", threadId: "thread-local-1", terminalId: DEFAULT_THREAD_TERMINAL_ID, createdAt: "2026-02-09T00:00:02.000Z", hasRunningSubprocess: true, ...overrides, };} describe("store reducer thread continuity", () => { it("stores codexThreadId from UPDATE_SESSION", () => { const state = makeState( makeThread({ session: null, }), ); const next = reducer(state, { type: "UPDATE_SESSION", threadId: "thread-local-1", session: makeSession({ threadId: "thr_123" }), }); expect(next.threads[0]?.codexThreadId).toBe("thr_123"); }); it("toggles terminal open state per thread", () => { const state = makeState(makeThread({ terminalOpen: false })); const next = reducer(state, { type: "TOGGLE_THREAD_TERMINAL", threadId: "thread-local-1", }); expect(next.threads[0]?.terminalOpen).toBe(true); }); it("opens diff panel with an explicit turn/file target", () => { const state = makeState(makeThread()); const next = reducer(state, { type: "OPEN_DIFF", threadId: "thread-local-1", turnId: "turn-1", filePath: "src/app.ts", }); expect(next.diffOpen).toBe(true); expect(next.diffThreadId).toBe("thread-local-1"); expect(next.diffTurnId).toBe("turn-1"); expect(next.diffFilePath).toBe("src/app.ts"); }); it("sets terminal open state per thread", () => { const state = makeState(makeThread({ terminalOpen: true })); const next = reducer(state, { type: "SET_THREAD_TERMINAL_OPEN", threadId: "thread-local-1", open: false, }); expect(next.threads[0]?.terminalOpen).toBe(false); }); it("sets terminal height per thread", () => { const state = makeState(makeThread({ terminalHeight: 280 })); const next = reducer(state, { type: "SET_THREAD_TERMINAL_HEIGHT", threadId: "thread-local-1", height: 360, }); expect(next.threads[0]?.terminalHeight).toBe(360); }); it("splits the active terminal into side-by-side mode", () => { const state = makeState(makeThread()); const next = reducer(state, { type: "SPLIT_THREAD_TERMINAL", threadId: "thread-local-1", terminalId: "term-2", }); expect(next.threads[0]?.terminalIds).toEqual([DEFAULT_THREAD_TERMINAL_ID, "term-2"]); expect(next.threads[0]?.activeTerminalId).toBe("term-2"); expect(next.threads[0]?.terminalGroups).toEqual([ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2"], }, ]); expect(next.threads[0]?.activeTerminalGroupId).toBe(`group-${DEFAULT_THREAD_TERMINAL_ID}`); }); it("creates a new full-width terminal and switches to tab mode", () => { const state = makeState(makeThread()); const next = reducer(state, { type: "NEW_THREAD_TERMINAL", threadId: "thread-local-1", terminalId: "term-2", }); expect(next.threads[0]?.terminalIds).toEqual([DEFAULT_THREAD_TERMINAL_ID, "term-2"]); expect(next.threads[0]?.activeTerminalId).toBe("term-2"); expect(next.threads[0]?.terminalGroups).toEqual([ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID], }, { id: "group-term-2", terminalIds: ["term-2"] }, ]); expect(next.threads[0]?.activeTerminalGroupId).toBe("group-term-2"); }); it("switches the active terminal and restores its owning group", () => { const state = makeState( makeThread({ terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3"], activeTerminalId: DEFAULT_THREAD_TERMINAL_ID, terminalGroups: [ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2"], }, { id: "group-term-3", terminalIds: ["term-3"] }, ], activeTerminalGroupId: "group-term-3", }), ); const next = reducer(state, { type: "SET_THREAD_ACTIVE_TERMINAL", threadId: "thread-local-1", terminalId: "term-2", }); expect(next.threads[0]?.activeTerminalId).toBe("term-2"); expect(next.threads[0]?.activeTerminalGroupId).toBe(`group-${DEFAULT_THREAD_TERMINAL_ID}`); }); it("supports splitting beyond two terminals in the same group", () => { const state = makeState( makeThread({ terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2"], activeTerminalId: "term-2", terminalGroups: [ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2"], }, ], activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, }), ); const next = reducer(state, { type: "SPLIT_THREAD_TERMINAL", threadId: "thread-local-1", terminalId: "term-3", }); expect(next.threads[0]?.terminalIds).toEqual([DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3"]); expect(next.threads[0]?.terminalGroups).toEqual([ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3"], }, ]); expect(next.threads[0]?.activeTerminalId).toBe("term-3"); expect(next.threads[0]?.activeTerminalGroupId).toBe(`group-${DEFAULT_THREAD_TERMINAL_ID}`); }); it("caps split terminals at four per thread", () => { const state = makeState( makeThread({ terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3", "term-4"], activeTerminalId: "term-4", terminalGroups: [ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3", "term-4"], }, ], activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, }), ); const next = reducer(state, { type: "SPLIT_THREAD_TERMINAL", threadId: "thread-local-1", terminalId: "term-5", }); expect(next.threads[0]?.terminalIds).toHaveLength(MAX_THREAD_TERMINAL_COUNT); expect(next.threads[0]?.terminalIds).toEqual([ DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3", "term-4", ]); expect(next.threads[0]?.activeTerminalId).toBe("term-4"); }); it("caps new terminals at four per thread", () => { const state = makeState( makeThread({ terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3", "term-4"], activeTerminalId: "term-4", terminalGroups: [ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID], }, { id: "group-term-2", terminalIds: ["term-2"] }, { id: "group-term-3", terminalIds: ["term-3"] }, { id: "group-term-4", terminalIds: ["term-4"] }, ], activeTerminalGroupId: "group-term-4", }), ); const next = reducer(state, { type: "NEW_THREAD_TERMINAL", threadId: "thread-local-1", terminalId: "term-5", }); expect(next.threads[0]?.terminalIds).toHaveLength(MAX_THREAD_TERMINAL_COUNT); expect(next.threads[0]?.terminalIds).toEqual([ DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3", "term-4", ]); expect(next.threads[0]?.activeTerminalId).toBe("term-4"); expect(next.threads[0]?.activeTerminalGroupId).toBe("group-term-4"); }); it("closes a terminal and keeps grouped layout coherent", () => { const state = makeState( makeThread({ terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3"], activeTerminalId: "term-2", terminalGroups: [ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2"], }, { id: "group-term-3", terminalIds: ["term-3"] }, ], activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, }), ); const next = reducer(state, { type: "CLOSE_THREAD_TERMINAL", threadId: "thread-local-1", terminalId: "term-2", }); expect(next.threads[0]?.terminalIds).toEqual([DEFAULT_THREAD_TERMINAL_ID, "term-3"]); expect(next.threads[0]?.activeTerminalId).toBe(DEFAULT_THREAD_TERMINAL_ID); expect(next.threads[0]?.terminalGroups).toEqual([ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID], }, { id: "group-term-3", terminalIds: ["term-3"] }, ]); }); it("closes the final terminal and hides the drawer", () => { const state = makeState( makeThread({ terminalOpen: true, runningTerminalIds: [DEFAULT_THREAD_TERMINAL_ID], }), ); const next = reducer(state, { type: "CLOSE_THREAD_TERMINAL", threadId: "thread-local-1", terminalId: DEFAULT_THREAD_TERMINAL_ID, }); expect(next.threads[0]?.terminalOpen).toBe(false); expect(next.threads[0]?.terminalIds).toEqual([DEFAULT_THREAD_TERMINAL_ID]); expect(next.threads[0]?.runningTerminalIds).toEqual([]); expect(next.threads[0]?.activeTerminalId).toBe(DEFAULT_THREAD_TERMINAL_ID); expect(next.threads[0]?.terminalGroups).toEqual([ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID], }, ]); }); it("tracks running terminals from subprocess activity events", () => { const state = makeState(makeThread()); const started = reducer(state, { type: "APPLY_TERMINAL_EVENT", event: makeTerminalStartedEvent(), }); expect(started.threads[0]?.runningTerminalIds).toEqual([]); const active = reducer(started, { type: "APPLY_TERMINAL_EVENT", event: makeTerminalActivityEvent(), }); expect(active.threads[0]?.runningTerminalIds).toEqual([DEFAULT_THREAD_TERMINAL_ID]); const idle = reducer(active, { type: "APPLY_TERMINAL_EVENT", event: makeTerminalActivityEvent({ hasRunningSubprocess: false }), }); expect(idle.threads[0]?.runningTerminalIds).toEqual([]); const exited = reducer(active, { type: "APPLY_TERMINAL_EVENT", event: { type: "exited", threadId: "thread-local-1", terminalId: DEFAULT_THREAD_TERMINAL_ID, createdAt: "2026-02-09T00:00:05.000Z", exitCode: 0, exitSignal: null, }, }); expect(exited.threads[0]?.runningTerminalIds).toEqual([]); }); it("keeps running status when another terminal in the thread is still running", () => { const state = makeState( makeThread({ terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2"], runningTerminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2"], activeTerminalId: "term-2", terminalGroups: [ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2"], }, ], activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, }), ); const next = reducer(state, { type: "APPLY_TERMINAL_EVENT", event: { type: "exited", threadId: "thread-local-1", terminalId: DEFAULT_THREAD_TERMINAL_ID, createdAt: "2026-02-09T00:00:07.000Z", exitCode: 0, exitSignal: null, }, }); expect(next.threads[0]?.runningTerminalIds).toEqual(["term-2"]); }); it("backfills codexThreadId from routed provider events", () => { const state = makeState(makeThread({ codexThreadId: null })); const next = reducer(state, { type: "APPLY_EVENT", event: makeEvent({ method: "thread/started", payload: { thread: { id: "thr_backfilled" } }, }), activeAssistantItemRef: { current: null }, }); expect(next.threads[0]?.codexThreadId).toBe("thr_backfilled"); }); it("ignores events from a foreign thread within the same session", () => { const state = makeState(makeThread({ codexThreadId: "thr_expected" })); const next = reducer(state, { type: "APPLY_EVENT", event: makeEvent({ method: "turn/started", threadId: "thr_unexpected", payload: { turn: { id: "turn-1" } }, }), activeAssistantItemRef: { current: null }, }); expect(next).toBe(state); }); it("rebases thread identity on thread/started during connect", () => { const state = makeState( makeThread({ codexThreadId: "thr_old",Lines 1–500 of 1004. Load more
import type { ProviderEvent, ProviderSession, TerminalEvent } from "@t3tools/contracts"; import { describe, expect, it } from "vitest"; import { type AppState, reducer } from "./store"; import { DEFAULT_THREAD_TERMINAL_HEIGHT, DEFAULT_THREAD_TERMINAL_ID, MAX_THREAD_TERMINAL_COUNT, } from "./types"; import type { Thread } from "./types"; type TerminalStartedEvent = Extract<TerminalEvent, { type: "started" }>; type TerminalActivityEvent = Extract<TerminalEvent, { type: "activity" }>; function makeSession(overrides: Partial<ProviderSession> = {}): ProviderSession { return { sessionId: "sess-1", provider: "codex", status: "ready", createdAt: "2026-02-09T00:00:00.000Z", updatedAt: "2026-02-09T00:00:00.000Z", ...overrides, }; } function makeEvent(overrides: Partial<ProviderEvent> = {}): ProviderEvent { return { id: "evt-1", kind: "notification", provider: "codex", sessionId: "sess-1", createdAt: "2026-02-09T00:00:01.000Z", method: "thread/started", ...overrides, }; } function makeThread(overrides: Partial<Thread> = {}): Thread { return { id: "thread-local-1", codexThreadId: null, projectId: "project-1", title: "Thread", model: "gpt-5.3-codex", terminalOpen: false, terminalHeight: DEFAULT_THREAD_TERMINAL_HEIGHT, terminalIds: [DEFAULT_THREAD_TERMINAL_ID], runningTerminalIds: [], activeTerminalId: DEFAULT_THREAD_TERMINAL_ID, terminalGroups: [ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID], }, ], activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, session: makeSession(), messages: [], events: [], turnDiffSummaries: [], error: null, createdAt: "2026-02-09T00:00:00.000Z", branch: null, worktreePath: null, ...overrides, }; } function makeState(thread: Thread): AppState { return { projects: [ { id: "project-1", name: "Project", cwd: "/tmp/project", model: "gpt-5.3-codex", expanded: true, scripts: [], }, ], threads: [thread], threadsHydrated: true, runtimeMode: "full-access", diffOpen: false, diffThreadId: null, diffTurnId: null, diffFilePath: null, }; } function makeTerminalStartedEvent( overrides: Partial<TerminalStartedEvent> = {}, ): TerminalStartedEvent { return { type: "started", threadId: "thread-local-1", terminalId: DEFAULT_THREAD_TERMINAL_ID, createdAt: "2026-02-09T00:00:01.000Z", snapshot: { threadId: "thread-local-1", terminalId: DEFAULT_THREAD_TERMINAL_ID, cwd: "/tmp/project", status: "running", pid: 1234, history: "", exitCode: null, exitSignal: null, updatedAt: "2026-02-09T00:00:01.000Z", }, ...overrides, }; } function makeTerminalActivityEvent( overrides: Partial<TerminalActivityEvent> = {}, ): TerminalActivityEvent { return { type: "activity", threadId: "thread-local-1", terminalId: DEFAULT_THREAD_TERMINAL_ID, createdAt: "2026-02-09T00:00:02.000Z", hasRunningSubprocess: true, ...overrides, }; } describe("store reducer thread continuity", () => { it("stores codexThreadId from UPDATE_SESSION", () => { const state = makeState( makeThread({ session: null, }), ); const next = reducer(state, { type: "UPDATE_SESSION", threadId: "thread-local-1", session: makeSession({ threadId: "thr_123" }), }); expect(next.threads[0]?.codexThreadId).toBe("thr_123"); }); it("toggles terminal open state per thread", () => { const state = makeState(makeThread({ terminalOpen: false })); const next = reducer(state, { type: "TOGGLE_THREAD_TERMINAL", threadId: "thread-local-1", }); expect(next.threads[0]?.terminalOpen).toBe(true); }); it("opens diff panel with an explicit turn/file target", () => { const state = makeState(makeThread()); const next = reducer(state, { type: "OPEN_DIFF", threadId: "thread-local-1", turnId: "turn-1", filePath: "src/app.ts", }); expect(next.diffOpen).toBe(true); expect(next.diffThreadId).toBe("thread-local-1"); expect(next.diffTurnId).toBe("turn-1"); expect(next.diffFilePath).toBe("src/app.ts"); }); it("sets terminal open state per thread", () => { const state = makeState(makeThread({ terminalOpen: true })); const next = reducer(state, { type: "SET_THREAD_TERMINAL_OPEN", threadId: "thread-local-1", open: false, }); expect(next.threads[0]?.terminalOpen).toBe(false); }); it("sets terminal height per thread", () => { const state = makeState(makeThread({ terminalHeight: 280 })); const next = reducer(state, { type: "SET_THREAD_TERMINAL_HEIGHT", threadId: "thread-local-1", height: 360, }); expect(next.threads[0]?.terminalHeight).toBe(360); }); it("splits the active terminal into side-by-side mode", () => { const state = makeState(makeThread()); const next = reducer(state, { type: "SPLIT_THREAD_TERMINAL", threadId: "thread-local-1", terminalId: "term-2", }); expect(next.threads[0]?.terminalIds).toEqual([DEFAULT_THREAD_TERMINAL_ID, "term-2"]); expect(next.threads[0]?.activeTerminalId).toBe("term-2"); expect(next.threads[0]?.terminalGroups).toEqual([ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2"], }, ]); expect(next.threads[0]?.activeTerminalGroupId).toBe(`group-${DEFAULT_THREAD_TERMINAL_ID}`); }); it("creates a new full-width terminal and switches to tab mode", () => { const state = makeState(makeThread()); const next = reducer(state, { type: "NEW_THREAD_TERMINAL", threadId: "thread-local-1", terminalId: "term-2", }); expect(next.threads[0]?.terminalIds).toEqual([DEFAULT_THREAD_TERMINAL_ID, "term-2"]); expect(next.threads[0]?.activeTerminalId).toBe("term-2"); expect(next.threads[0]?.terminalGroups).toEqual([ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID], }, { id: "group-term-2", terminalIds: ["term-2"] }, ]); expect(next.threads[0]?.activeTerminalGroupId).toBe("group-term-2"); }); it("switches the active terminal and restores its owning group", () => { const state = makeState( makeThread({ terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3"], activeTerminalId: DEFAULT_THREAD_TERMINAL_ID, terminalGroups: [ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2"], }, { id: "group-term-3", terminalIds: ["term-3"] }, ], activeTerminalGroupId: "group-term-3", }), ); const next = reducer(state, { type: "SET_THREAD_ACTIVE_TERMINAL", threadId: "thread-local-1", terminalId: "term-2", }); expect(next.threads[0]?.activeTerminalId).toBe("term-2"); expect(next.threads[0]?.activeTerminalGroupId).toBe(`group-${DEFAULT_THREAD_TERMINAL_ID}`); }); it("supports splitting beyond two terminals in the same group", () => { const state = makeState( makeThread({ terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2"], activeTerminalId: "term-2", terminalGroups: [ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2"], }, ], activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, }), ); const next = reducer(state, { type: "SPLIT_THREAD_TERMINAL", threadId: "thread-local-1", terminalId: "term-3", }); expect(next.threads[0]?.terminalIds).toEqual([DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3"]); expect(next.threads[0]?.terminalGroups).toEqual([ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3"], }, ]); expect(next.threads[0]?.activeTerminalId).toBe("term-3"); expect(next.threads[0]?.activeTerminalGroupId).toBe(`group-${DEFAULT_THREAD_TERMINAL_ID}`); }); it("caps split terminals at four per thread", () => { const state = makeState( makeThread({ terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3", "term-4"], activeTerminalId: "term-4", terminalGroups: [ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3", "term-4"], }, ], activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, }), ); const next = reducer(state, { type: "SPLIT_THREAD_TERMINAL", threadId: "thread-local-1", terminalId: "term-5", }); expect(next.threads[0]?.terminalIds).toHaveLength(MAX_THREAD_TERMINAL_COUNT); expect(next.threads[0]?.terminalIds).toEqual([ DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3", "term-4", ]); expect(next.threads[0]?.activeTerminalId).toBe("term-4"); }); it("caps new terminals at four per thread", () => { const state = makeState( makeThread({ terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3", "term-4"], activeTerminalId: "term-4", terminalGroups: [ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID], }, { id: "group-term-2", terminalIds: ["term-2"] }, { id: "group-term-3", terminalIds: ["term-3"] }, { id: "group-term-4", terminalIds: ["term-4"] }, ], activeTerminalGroupId: "group-term-4", }), ); const next = reducer(state, { type: "NEW_THREAD_TERMINAL", threadId: "thread-local-1", terminalId: "term-5", }); expect(next.threads[0]?.terminalIds).toHaveLength(MAX_THREAD_TERMINAL_COUNT); expect(next.threads[0]?.terminalIds).toEqual([ DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3", "term-4", ]); expect(next.threads[0]?.activeTerminalId).toBe("term-4"); expect(next.threads[0]?.activeTerminalGroupId).toBe("group-term-4"); }); it("closes a terminal and keeps grouped layout coherent", () => { const state = makeState( makeThread({ terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2", "term-3"], activeTerminalId: "term-2", terminalGroups: [ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2"], }, { id: "group-term-3", terminalIds: ["term-3"] }, ], activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, }), ); const next = reducer(state, { type: "CLOSE_THREAD_TERMINAL", threadId: "thread-local-1", terminalId: "term-2", }); expect(next.threads[0]?.terminalIds).toEqual([DEFAULT_THREAD_TERMINAL_ID, "term-3"]); expect(next.threads[0]?.activeTerminalId).toBe(DEFAULT_THREAD_TERMINAL_ID); expect(next.threads[0]?.terminalGroups).toEqual([ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID], }, { id: "group-term-3", terminalIds: ["term-3"] }, ]); }); it("closes the final terminal and hides the drawer", () => { const state = makeState( makeThread({ terminalOpen: true, runningTerminalIds: [DEFAULT_THREAD_TERMINAL_ID], }), ); const next = reducer(state, { type: "CLOSE_THREAD_TERMINAL", threadId: "thread-local-1", terminalId: DEFAULT_THREAD_TERMINAL_ID, }); expect(next.threads[0]?.terminalOpen).toBe(false); expect(next.threads[0]?.terminalIds).toEqual([DEFAULT_THREAD_TERMINAL_ID]); expect(next.threads[0]?.runningTerminalIds).toEqual([]); expect(next.threads[0]?.activeTerminalId).toBe(DEFAULT_THREAD_TERMINAL_ID); expect(next.threads[0]?.terminalGroups).toEqual([ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID], }, ]); }); it("tracks running terminals from subprocess activity events", () => { const state = makeState(makeThread()); const started = reducer(state, { type: "APPLY_TERMINAL_EVENT", event: makeTerminalStartedEvent(), }); expect(started.threads[0]?.runningTerminalIds).toEqual([]); const active = reducer(started, { type: "APPLY_TERMINAL_EVENT", event: makeTerminalActivityEvent(), }); expect(active.threads[0]?.runningTerminalIds).toEqual([DEFAULT_THREAD_TERMINAL_ID]); const idle = reducer(active, { type: "APPLY_TERMINAL_EVENT", event: makeTerminalActivityEvent({ hasRunningSubprocess: false }), }); expect(idle.threads[0]?.runningTerminalIds).toEqual([]); const exited = reducer(active, { type: "APPLY_TERMINAL_EVENT", event: { type: "exited", threadId: "thread-local-1", terminalId: DEFAULT_THREAD_TERMINAL_ID, createdAt: "2026-02-09T00:00:05.000Z", exitCode: 0, exitSignal: null, }, }); expect(exited.threads[0]?.runningTerminalIds).toEqual([]); }); it("keeps running status when another terminal in the thread is still running", () => { const state = makeState( makeThread({ terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2"], runningTerminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2"], activeTerminalId: "term-2", terminalGroups: [ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "term-2"], }, ], activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, }), ); const next = reducer(state, { type: "APPLY_TERMINAL_EVENT", event: { type: "exited", threadId: "thread-local-1", terminalId: DEFAULT_THREAD_TERMINAL_ID, createdAt: "2026-02-09T00:00:07.000Z", exitCode: 0, exitSignal: null, }, }); expect(next.threads[0]?.runningTerminalIds).toEqual(["term-2"]); }); it("backfills codexThreadId from routed provider events", () => { const state = makeState(makeThread({ codexThreadId: null })); const next = reducer(state, { type: "APPLY_EVENT", event: makeEvent({ method: "thread/started", payload: { thread: { id: "thr_backfilled" } }, }), activeAssistantItemRef: { current: null }, }); expect(next.threads[0]?.codexThreadId).toBe("thr_backfilled"); }); it("ignores events from a foreign thread within the same session", () => { const state = makeState(makeThread({ codexThreadId: "thr_expected" })); const next = reducer(state, { type: "APPLY_EVENT", event: makeEvent({ method: "turn/started", threadId: "thr_unexpected", payload: { turn: { id: "turn-1" } }, }), activeAssistantItemRef: { current: null }, }); expect(next).toBe(state); }); it("rebases thread identity on thread/started during connect", () => { const state = makeState( makeThread({ codexThreadId: "thr_old",