/** * Thread-scoped right-panel surface state. * * This is intentionally a shallow workspace model: it owns an ordered set of * surface descriptors and the active surface, while each feature continues to * own its durable resource state. Browser surfaces point at preview tab ids, * terminal surfaces point at terminal session ids, file surfaces point at * workspace paths, and diff/plan/files remain singleton surfaces. */import { scopedThreadKey } from "@t3tools/client-runtime/environment";import type { ScopedThreadRef } from "@t3tools/contracts";import { create } from "zustand";import { createJSONStorage, persist } from "zustand/middleware"; import { resolveStorage } from "./lib/storage"; export const RIGHT_PANEL_KINDS = ["plan", "diff", "files", "file", "preview", "terminal"] as const;export type RightPanelKind = (typeof RIGHT_PANEL_KINDS)[number]; export type RightPanelSurface = | { id: `browser:${string}`; kind: "preview"; resourceId: string } | { id: "browser:new"; kind: "preview"; resourceId: null } | { id: `terminal:${string}`; kind: "terminal"; resourceId: string; terminalIds: string[]; activeTerminalId: string; splitDirection?: "horizontal" | "vertical"; } | { id: "diff"; kind: "diff" } | { id: "files"; kind: "files" } | { id: `file:${string}`; kind: "file"; relativePath: string; revealLine: number | null; revealRequestId: number; } | { id: "plan"; kind: "plan" }; const RIGHT_PANEL_STORAGE_KEY = "t3code:right-panel-state:v2";const RIGHT_PANEL_STORAGE_VERSION = 7; export interface ThreadRightPanelState { isOpen: boolean; activeSurfaceId: string | null; surfaces: RightPanelSurface[];} interface RightPanelStoreState { byThreadKey: Record<string, ThreadRightPanelState>; open: (ref: ScopedThreadRef, kind: Exclude<RightPanelKind, "file" | "terminal">) => void; openBrowser: (ref: ScopedThreadRef, tabId: string | null) => void; openFile: (ref: ScopedThreadRef, relativePath: string, line?: number) => void; openTerminal: (ref: ScopedThreadRef, terminalId: string) => void; splitTerminal: ( ref: ScopedThreadRef, surfaceId: string, terminalId: string, direction?: "horizontal" | "vertical", ) => void; activateTerminal: (ref: ScopedThreadRef, surfaceId: string, terminalId: string) => void; closeTerminal: (ref: ScopedThreadRef, surfaceId: string, terminalId: string) => void; activateSurface: (ref: ScopedThreadRef, surfaceId: string) => void; closeSurface: (ref: ScopedThreadRef, surfaceId: string) => void; closeOtherSurfaces: (ref: ScopedThreadRef, surfaceId: string) => void; closeSurfacesToRight: (ref: ScopedThreadRef, surfaceId: string) => void; closeAllSurfaces: (ref: ScopedThreadRef) => void; reconcileBrowserSurfaces: (ref: ScopedThreadRef, tabIds: readonly string[]) => void; reconcileFileSurfaces: (ref: ScopedThreadRef, workspaceAvailable: boolean) => void; show: (ref: ScopedThreadRef) => void; close: (ref: ScopedThreadRef) => void; toggleVisibility: (ref: ScopedThreadRef) => void; toggle: (ref: ScopedThreadRef, kind: Exclude<RightPanelKind, "file" | "terminal">) => void; removeThread: (ref: ScopedThreadRef) => void;} const EMPTY_THREAD_STATE: ThreadRightPanelState = { isOpen: false, activeSurfaceId: null, surfaces: [],}; const singletonSurface = ( kind: Exclude<RightPanelKind, "file" | "preview" | "terminal">,): RightPanelSurface => { switch (kind) { case "diff": return { id: "diff", kind }; case "files": return { id: "files", kind }; case "plan": return { id: "plan", kind }; }}; const browserSurface = (tabId: string | null): RightPanelSurface => tabId ? { id: `browser:${tabId}`, kind: "preview", resourceId: tabId } : { id: "browser:new", kind: "preview", resourceId: null }; const fileSurface = ( relativePath: string, revealLine: number | null, revealRequestId: number,): RightPanelSurface => ({ id: `file:${relativePath}`, kind: "file", relativePath, revealLine, revealRequestId,}); const terminalSurface = (terminalId: string): RightPanelSurface => ({ id: `terminal:${terminalId}`, kind: "terminal", resourceId: terminalId, terminalIds: [terminalId], activeTerminalId: terminalId,}); const upsertSurface = ( current: ThreadRightPanelState, surface: RightPanelSurface, activate = true,): ThreadRightPanelState => ({ isOpen: true, surfaces: current.surfaces.some((entry) => entry.id === surface.id) ? current.surfaces : [...current.surfaces, surface], activeSurfaceId: activate ? surface.id : current.activeSurfaceId,}); const updateThread = ( byThreadKey: Record<string, ThreadRightPanelState>, threadKey: string, updater: (current: ThreadRightPanelState) => ThreadRightPanelState,): Record<string, ThreadRightPanelState> => { const current = byThreadKey[threadKey] ?? EMPTY_THREAD_STATE; const next = updater(current); if (!next.isOpen && next.activeSurfaceId === null && next.surfaces.length === 0) { if (!(threadKey in byThreadKey)) return byThreadKey; const { [threadKey]: _removed, ...rest } = byThreadKey; return rest; } if (next === current) return byThreadKey; return { ...byThreadKey, [threadKey]: next };}; function normalizeRevealLine(line: number | undefined): number | null { if (line === undefined || !Number.isFinite(line)) return null; return Math.max(1, Math.trunc(line));} export function migratePersistedRightPanelState(persistedState: unknown): { byThreadKey: Record<string, ThreadRightPanelState>;} { if (!persistedState || typeof persistedState !== "object") { return { byThreadKey: {} }; } const byThreadKey = "byThreadKey" in persistedState && persistedState.byThreadKey && typeof persistedState.byThreadKey === "object" ? Object.fromEntries( Object.entries(persistedState.byThreadKey as Record<string, ThreadRightPanelState>).map( ([threadKey, threadState]) => { const validThreadState = threadState && typeof threadState === "object" ? threadState : null; const surfaces = Array.isArray(validThreadState?.surfaces) ? validThreadState.surfaces.flatMap<RightPanelSurface>((surface) => { if (surface.kind === "file") { const revealLine = typeof surface.revealLine === "number" && Number.isFinite(surface.revealLine) ? Math.max(1, Math.trunc(surface.revealLine)) : null; const revealRequestId = typeof surface.revealRequestId === "number" && Number.isSafeInteger(surface.revealRequestId) && surface.revealRequestId >= 0 ? surface.revealRequestId : 0; return [{ ...surface, revealLine, revealRequestId }]; } if (surface.kind !== "terminal") return [surface]; if ( !("resourceId" in surface) || typeof surface.resourceId !== "string" || surface.id !== `terminal:${surface.resourceId}` ) { return []; } const terminalIds = "terminalIds" in surface && Array.isArray(surface.terminalIds) ? [ ...new Set( surface.terminalIds.filter( (terminalId): terminalId is string => typeof terminalId === "string", ), ), ] : [surface.resourceId]; const activeTerminalId = "activeTerminalId" in surface && typeof surface.activeTerminalId === "string" && terminalIds.includes(surface.activeTerminalId) ? surface.activeTerminalId : (terminalIds[0] ?? surface.resourceId); return [ { ...surface, terminalIds: terminalIds.length > 0 ? terminalIds : [surface.resourceId], activeTerminalId, }, ]; }) : []; const activeSurfaceId = surfaces.some( (surface) => surface.id === validThreadState?.activeSurfaceId, ) ? (validThreadState?.activeSurfaceId ?? null) : null; const isOpen = typeof validThreadState?.isOpen === "boolean" ? validThreadState.isOpen : activeSurfaceId !== null; return [threadKey, { isOpen, surfaces, activeSurfaceId }]; }, ), ) : {}; return { byThreadKey };} export const useRightPanelStore = create<RightPanelStoreState>()( persist( (set) => ({ byThreadKey: {}, open: (ref, kind) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { if (kind === "preview") { const existing = current.surfaces.find((surface) => surface.kind === "preview"); return upsertSurface(current, existing ?? browserSurface(null)); } return upsertSurface(current, singletonSurface(kind)); }), })), openBrowser: (ref, tabId) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { const surface = browserSurface(tabId); const withoutPlaceholder = tabId ? current.surfaces.filter((entry) => entry.id !== "browser:new") : current.surfaces; return upsertSurface({ ...current, surfaces: withoutPlaceholder }, surface); }), })), openFile: (ref, relativePath, line) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { const withoutStandaloneExplorer = current.surfaces.filter( (surface) => surface.kind !== "files", ); const surfaceId = `file:${relativePath}` as const; const existing = withoutStandaloneExplorer.find( (surface): surface is Extract<RightPanelSurface, { kind: "file" }> => surface.id === surfaceId && surface.kind === "file", ); const surface = fileSurface( relativePath, normalizeRevealLine(line), (existing?.revealRequestId ?? 0) + 1, ); return { isOpen: true, activeSurfaceId: surface.id, surfaces: existing ? withoutStandaloneExplorer.map((entry) => entry.id === surface.id ? surface : entry, ) : [...withoutStandaloneExplorer, surface], }; }), })), openTerminal: (ref, terminalId) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => upsertSurface(current, terminalSurface(terminalId)), ), })), splitTerminal: (ref, surfaceId, terminalId, direction = "horizontal") => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => ({ ...current, isOpen: true, activeSurfaceId: surfaceId, surfaces: current.surfaces.map((surface) => { if (surface.id !== surfaceId || surface.kind !== "terminal") return surface; const { splitDirection: _splitDirection, ...baseSurface } = surface; return { ...baseSurface, terminalIds: surface.terminalIds.includes(terminalId) ? surface.terminalIds : [...surface.terminalIds, terminalId], activeTerminalId: terminalId, ...(direction === "vertical" ? { splitDirection: "vertical" as const } : {}), }; }), })), })), activateTerminal: (ref, surfaceId, terminalId) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => ({ ...current, activeSurfaceId: surfaceId, surfaces: current.surfaces.map((surface) => surface.id === surfaceId && surface.kind === "terminal" && surface.terminalIds.includes(terminalId) ? { ...surface, activeTerminalId: terminalId } : surface, ), })), })), closeTerminal: (ref, surfaceId, terminalId) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { const surface = current.surfaces.find( (entry) => entry.id === surfaceId && entry.kind === "terminal", ); if (!surface || surface.kind !== "terminal") return current; const terminalIds = surface.terminalIds.filter((id) => id !== terminalId); if (terminalIds.length === 0) { const index = current.surfaces.findIndex((entry) => entry.id === surfaceId); const surfaces = current.surfaces.filter((entry) => entry.id !== surfaceId); const fallback = surfaces[Math.min(index, surfaces.length - 1)] ?? null; return { ...current, isOpen: surfaces.length > 0 && current.isOpen, surfaces, activeSurfaceId: current.activeSurfaceId === surfaceId ? (fallback?.id ?? null) : current.activeSurfaceId, }; } return { ...current, surfaces: current.surfaces.map((entry) => entry.id === surfaceId && entry.kind === "terminal" ? { ...entry, terminalIds, activeTerminalId: entry.activeTerminalId === terminalId ? (terminalIds.at(-1) ?? terminalIds[0]!) : entry.activeTerminalId, } : entry, ), }; }), })), activateSurface: (ref, surfaceId) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => current.surfaces.some((surface) => surface.id === surfaceId) ? { ...current, isOpen: true, activeSurfaceId: surfaceId } : current, ), })), closeSurface: (ref, surfaceId) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { const index = current.surfaces.findIndex((surface) => surface.id === surfaceId); if (index < 0) return current; const surfaces = current.surfaces.filter((surface) => surface.id !== surfaceId); if (current.activeSurfaceId !== surfaceId) { return { ...current, isOpen: surfaces.length > 0 && current.isOpen, surfaces }; } const fallback = surfaces[Math.min(index, surfaces.length - 1)] ?? null; return { ...current, isOpen: surfaces.length > 0 && current.isOpen, surfaces, activeSurfaceId: fallback?.id ?? null, }; }), })), closeOtherSurfaces: (ref, surfaceId) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { const surface = current.surfaces.find((entry) => entry.id === surfaceId); if (!surface || current.surfaces.length === 1) return current; return { ...current, isOpen: true, surfaces: [surface], activeSurfaceId: surface.id, }; }), })), closeSurfacesToRight: (ref, surfaceId) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { const index = current.surfaces.findIndex((surface) => surface.id === surfaceId); if (index < 0 || index === current.surfaces.length - 1) return current; const surfaces = current.surfaces.slice(0, index + 1); const activeStillExists = surfaces.some( (surface) => surface.id === current.activeSurfaceId, ); return { ...current, surfaces, activeSurfaceId: activeStillExists ? current.activeSurfaceId : surfaceId, }; }), })), closeAllSurfaces: (ref) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => current.surfaces.length === 0 ? current : { ...current, isOpen: false, surfaces: [], activeSurfaceId: null }, ), })), reconcileBrowserSurfaces: (ref, tabIds) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { const validIds = new Set(tabIds.map((tabId) => `browser:${tabId}`)); const nonBrowser = current.surfaces.filter((surface) => surface.kind !== "preview"); const existingBrowser = current.surfaces.filter( (surface): surface is Extract<RightPanelSurface, { kind: "preview" }> => surface.kind === "preview" && surface.id !== "browser:new" && validIds.has(surface.id), ); const knownIds = new Set(existingBrowser.map((surface) => surface.id)); const added = tabIds .filter((tabId) => !knownIds.has(`browser:${tabId}`)) .map((tabId) => browserSurface(tabId)); const surfaces = [...nonBrowser, ...existingBrowser, ...added]; const activeStillExists = surfaces.some( (surface) => surface.id === current.activeSurfaceId, ); const fallbackBrowser = surfaces.find((surface) => surface.kind === "preview"); return { ...current, surfaces, activeSurfaceId: activeStillExists ? current.activeSurfaceId : (fallbackBrowser?.id ?? surfaces[0]?.id ?? null), }; }), })), reconcileFileSurfaces: (ref, workspaceAvailable) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { if (workspaceAvailable) return current; const surfaces = current.surfaces.filter( (surface) => surface.kind !== "files" && surface.kind !== "file", ); if (surfaces.length === current.surfaces.length) return current; const activeStillExists = surfaces.some( (surface) => surface.id === current.activeSurfaceId, ); return { ...current, isOpen: surfaces.length > 0 ? current.isOpen : false, surfaces, activeSurfaceId: activeStillExists ? current.activeSurfaceId : (surfaces.at(-1)?.id ?? null), }; }), })), show: (ref) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => current.isOpen ? current : { ...current, isOpen: true }, ), })), close: (ref) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => current.isOpen ? { ...current, isOpen: false } : current, ), })), toggleVisibility: (ref) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => ({ ...current, isOpen: !current.isOpen, })), })), toggle: (ref, kind) =>Lines 1–500 of 560. Load more
/** * Thread-scoped right-panel surface state. * * This is intentionally a shallow workspace model: it owns an ordered set of * surface descriptors and the active surface, while each feature continues to * own its durable resource state. Browser surfaces point at preview tab ids, * terminal surfaces point at terminal session ids, file surfaces point at * workspace paths, and diff/plan/files remain singleton surfaces. */ import { scopedThreadKey } from "@t3tools/client-runtime/environment"; import type { ScopedThreadRef } from "@t3tools/contracts"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; import { resolveStorage } from "./lib/storage"; export const RIGHT_PANEL_KINDS = ["plan", "diff", "files", "file", "preview", "terminal"] as const; export type RightPanelKind = (typeof RIGHT_PANEL_KINDS)[number]; export type RightPanelSurface = | { id: `browser:${string}`; kind: "preview"; resourceId: string } | { id: "browser:new"; kind: "preview"; resourceId: null } | { id: `terminal:${string}`; kind: "terminal"; resourceId: string; terminalIds: string[]; activeTerminalId: string; splitDirection?: "horizontal" | "vertical"; } | { id: "diff"; kind: "diff" } | { id: "files"; kind: "files" } | { id: `file:${string}`; kind: "file"; relativePath: string; revealLine: number | null; revealRequestId: number; } | { id: "plan"; kind: "plan" }; const RIGHT_PANEL_STORAGE_KEY = "t3code:right-panel-state:v2"; const RIGHT_PANEL_STORAGE_VERSION = 7; export interface ThreadRightPanelState { isOpen: boolean; activeSurfaceId: string | null; surfaces: RightPanelSurface[]; } interface RightPanelStoreState { byThreadKey: Record<string, ThreadRightPanelState>; open: (ref: ScopedThreadRef, kind: Exclude<RightPanelKind, "file" | "terminal">) => void; openBrowser: (ref: ScopedThreadRef, tabId: string | null) => void; openFile: (ref: ScopedThreadRef, relativePath: string, line?: number) => void; openTerminal: (ref: ScopedThreadRef, terminalId: string) => void; splitTerminal: ( ref: ScopedThreadRef, surfaceId: string, terminalId: string, direction?: "horizontal" | "vertical", ) => void; activateTerminal: (ref: ScopedThreadRef, surfaceId: string, terminalId: string) => void; closeTerminal: (ref: ScopedThreadRef, surfaceId: string, terminalId: string) => void; activateSurface: (ref: ScopedThreadRef, surfaceId: string) => void; closeSurface: (ref: ScopedThreadRef, surfaceId: string) => void; closeOtherSurfaces: (ref: ScopedThreadRef, surfaceId: string) => void; closeSurfacesToRight: (ref: ScopedThreadRef, surfaceId: string) => void; closeAllSurfaces: (ref: ScopedThreadRef) => void; reconcileBrowserSurfaces: (ref: ScopedThreadRef, tabIds: readonly string[]) => void; reconcileFileSurfaces: (ref: ScopedThreadRef, workspaceAvailable: boolean) => void; show: (ref: ScopedThreadRef) => void; close: (ref: ScopedThreadRef) => void; toggleVisibility: (ref: ScopedThreadRef) => void; toggle: (ref: ScopedThreadRef, kind: Exclude<RightPanelKind, "file" | "terminal">) => void; removeThread: (ref: ScopedThreadRef) => void; } const EMPTY_THREAD_STATE: ThreadRightPanelState = { isOpen: false, activeSurfaceId: null, surfaces: [], }; const singletonSurface = ( kind: Exclude<RightPanelKind, "file" | "preview" | "terminal">, ): RightPanelSurface => { switch (kind) { case "diff": return { id: "diff", kind }; case "files": return { id: "files", kind }; case "plan": return { id: "plan", kind }; } }; const browserSurface = (tabId: string | null): RightPanelSurface => tabId ? { id: `browser:${tabId}`, kind: "preview", resourceId: tabId } : { id: "browser:new", kind: "preview", resourceId: null }; const fileSurface = ( relativePath: string, revealLine: number | null, revealRequestId: number, ): RightPanelSurface => ({ id: `file:${relativePath}`, kind: "file", relativePath, revealLine, revealRequestId, }); const terminalSurface = (terminalId: string): RightPanelSurface => ({ id: `terminal:${terminalId}`, kind: "terminal", resourceId: terminalId, terminalIds: [terminalId], activeTerminalId: terminalId, }); const upsertSurface = ( current: ThreadRightPanelState, surface: RightPanelSurface, activate = true, ): ThreadRightPanelState => ({ isOpen: true, surfaces: current.surfaces.some((entry) => entry.id === surface.id) ? current.surfaces : [...current.surfaces, surface], activeSurfaceId: activate ? surface.id : current.activeSurfaceId, }); const updateThread = ( byThreadKey: Record<string, ThreadRightPanelState>, threadKey: string, updater: (current: ThreadRightPanelState) => ThreadRightPanelState, ): Record<string, ThreadRightPanelState> => { const current = byThreadKey[threadKey] ?? EMPTY_THREAD_STATE; const next = updater(current); if (!next.isOpen && next.activeSurfaceId === null && next.surfaces.length === 0) { if (!(threadKey in byThreadKey)) return byThreadKey; const { [threadKey]: _removed, ...rest } = byThreadKey; return rest; } if (next === current) return byThreadKey; return { ...byThreadKey, [threadKey]: next }; }; function normalizeRevealLine(line: number | undefined): number | null { if (line === undefined || !Number.isFinite(line)) return null; return Math.max(1, Math.trunc(line)); } export function migratePersistedRightPanelState(persistedState: unknown): { byThreadKey: Record<string, ThreadRightPanelState>; } { if (!persistedState || typeof persistedState !== "object") { return { byThreadKey: {} }; } const byThreadKey = "byThreadKey" in persistedState && persistedState.byThreadKey && typeof persistedState.byThreadKey === "object" ? Object.fromEntries( Object.entries(persistedState.byThreadKey as Record<string, ThreadRightPanelState>).map( ([threadKey, threadState]) => { const validThreadState = threadState && typeof threadState === "object" ? threadState : null; const surfaces = Array.isArray(validThreadState?.surfaces) ? validThreadState.surfaces.flatMap<RightPanelSurface>((surface) => { if (surface.kind === "file") { const revealLine = typeof surface.revealLine === "number" && Number.isFinite(surface.revealLine) ? Math.max(1, Math.trunc(surface.revealLine)) : null; const revealRequestId = typeof surface.revealRequestId === "number" && Number.isSafeInteger(surface.revealRequestId) && surface.revealRequestId >= 0 ? surface.revealRequestId : 0; return [{ ...surface, revealLine, revealRequestId }]; } if (surface.kind !== "terminal") return [surface]; if ( !("resourceId" in surface) || typeof surface.resourceId !== "string" || surface.id !== `terminal:${surface.resourceId}` ) { return []; } const terminalIds = "terminalIds" in surface && Array.isArray(surface.terminalIds) ? [ ...new Set( surface.terminalIds.filter( (terminalId): terminalId is string => typeof terminalId === "string", ), ), ] : [surface.resourceId]; const activeTerminalId = "activeTerminalId" in surface && typeof surface.activeTerminalId === "string" && terminalIds.includes(surface.activeTerminalId) ? surface.activeTerminalId : (terminalIds[0] ?? surface.resourceId); return [ { ...surface, terminalIds: terminalIds.length > 0 ? terminalIds : [surface.resourceId], activeTerminalId, }, ]; }) : []; const activeSurfaceId = surfaces.some( (surface) => surface.id === validThreadState?.activeSurfaceId, ) ? (validThreadState?.activeSurfaceId ?? null) : null; const isOpen = typeof validThreadState?.isOpen === "boolean" ? validThreadState.isOpen : activeSurfaceId !== null; return [threadKey, { isOpen, surfaces, activeSurfaceId }]; }, ), ) : {}; return { byThreadKey }; } export const useRightPanelStore = create<RightPanelStoreState>()( persist( (set) => ({ byThreadKey: {}, open: (ref, kind) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { if (kind === "preview") { const existing = current.surfaces.find((surface) => surface.kind === "preview"); return upsertSurface(current, existing ?? browserSurface(null)); } return upsertSurface(current, singletonSurface(kind)); }), })), openBrowser: (ref, tabId) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { const surface = browserSurface(tabId); const withoutPlaceholder = tabId ? current.surfaces.filter((entry) => entry.id !== "browser:new") : current.surfaces; return upsertSurface({ ...current, surfaces: withoutPlaceholder }, surface); }), })), openFile: (ref, relativePath, line) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { const withoutStandaloneExplorer = current.surfaces.filter( (surface) => surface.kind !== "files", ); const surfaceId = `file:${relativePath}` as const; const existing = withoutStandaloneExplorer.find( (surface): surface is Extract<RightPanelSurface, { kind: "file" }> => surface.id === surfaceId && surface.kind === "file", ); const surface = fileSurface( relativePath, normalizeRevealLine(line), (existing?.revealRequestId ?? 0) + 1, ); return { isOpen: true, activeSurfaceId: surface.id, surfaces: existing ? withoutStandaloneExplorer.map((entry) => entry.id === surface.id ? surface : entry, ) : [...withoutStandaloneExplorer, surface], }; }), })), openTerminal: (ref, terminalId) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => upsertSurface(current, terminalSurface(terminalId)), ), })), splitTerminal: (ref, surfaceId, terminalId, direction = "horizontal") => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => ({ ...current, isOpen: true, activeSurfaceId: surfaceId, surfaces: current.surfaces.map((surface) => { if (surface.id !== surfaceId || surface.kind !== "terminal") return surface; const { splitDirection: _splitDirection, ...baseSurface } = surface; return { ...baseSurface, terminalIds: surface.terminalIds.includes(terminalId) ? surface.terminalIds : [...surface.terminalIds, terminalId], activeTerminalId: terminalId, ...(direction === "vertical" ? { splitDirection: "vertical" as const } : {}), }; }), })), })), activateTerminal: (ref, surfaceId, terminalId) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => ({ ...current, activeSurfaceId: surfaceId, surfaces: current.surfaces.map((surface) => surface.id === surfaceId && surface.kind === "terminal" && surface.terminalIds.includes(terminalId) ? { ...surface, activeTerminalId: terminalId } : surface, ), })), })), closeTerminal: (ref, surfaceId, terminalId) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { const surface = current.surfaces.find( (entry) => entry.id === surfaceId && entry.kind === "terminal", ); if (!surface || surface.kind !== "terminal") return current; const terminalIds = surface.terminalIds.filter((id) => id !== terminalId); if (terminalIds.length === 0) { const index = current.surfaces.findIndex((entry) => entry.id === surfaceId); const surfaces = current.surfaces.filter((entry) => entry.id !== surfaceId); const fallback = surfaces[Math.min(index, surfaces.length - 1)] ?? null; return { ...current, isOpen: surfaces.length > 0 && current.isOpen, surfaces, activeSurfaceId: current.activeSurfaceId === surfaceId ? (fallback?.id ?? null) : current.activeSurfaceId, }; } return { ...current, surfaces: current.surfaces.map((entry) => entry.id === surfaceId && entry.kind === "terminal" ? { ...entry, terminalIds, activeTerminalId: entry.activeTerminalId === terminalId ? (terminalIds.at(-1) ?? terminalIds[0]!) : entry.activeTerminalId, } : entry, ), }; }), })), activateSurface: (ref, surfaceId) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => current.surfaces.some((surface) => surface.id === surfaceId) ? { ...current, isOpen: true, activeSurfaceId: surfaceId } : current, ), })), closeSurface: (ref, surfaceId) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { const index = current.surfaces.findIndex((surface) => surface.id === surfaceId); if (index < 0) return current; const surfaces = current.surfaces.filter((surface) => surface.id !== surfaceId); if (current.activeSurfaceId !== surfaceId) { return { ...current, isOpen: surfaces.length > 0 && current.isOpen, surfaces }; } const fallback = surfaces[Math.min(index, surfaces.length - 1)] ?? null; return { ...current, isOpen: surfaces.length > 0 && current.isOpen, surfaces, activeSurfaceId: fallback?.id ?? null, }; }), })), closeOtherSurfaces: (ref, surfaceId) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { const surface = current.surfaces.find((entry) => entry.id === surfaceId); if (!surface || current.surfaces.length === 1) return current; return { ...current, isOpen: true, surfaces: [surface], activeSurfaceId: surface.id, }; }), })), closeSurfacesToRight: (ref, surfaceId) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { const index = current.surfaces.findIndex((surface) => surface.id === surfaceId); if (index < 0 || index === current.surfaces.length - 1) return current; const surfaces = current.surfaces.slice(0, index + 1); const activeStillExists = surfaces.some( (surface) => surface.id === current.activeSurfaceId, ); return { ...current, surfaces, activeSurfaceId: activeStillExists ? current.activeSurfaceId : surfaceId, }; }), })), closeAllSurfaces: (ref) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => current.surfaces.length === 0 ? current : { ...current, isOpen: false, surfaces: [], activeSurfaceId: null }, ), })), reconcileBrowserSurfaces: (ref, tabIds) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { const validIds = new Set(tabIds.map((tabId) => `browser:${tabId}`)); const nonBrowser = current.surfaces.filter((surface) => surface.kind !== "preview"); const existingBrowser = current.surfaces.filter( (surface): surface is Extract<RightPanelSurface, { kind: "preview" }> => surface.kind === "preview" && surface.id !== "browser:new" && validIds.has(surface.id), ); const knownIds = new Set(existingBrowser.map((surface) => surface.id)); const added = tabIds .filter((tabId) => !knownIds.has(`browser:${tabId}`)) .map((tabId) => browserSurface(tabId)); const surfaces = [...nonBrowser, ...existingBrowser, ...added]; const activeStillExists = surfaces.some( (surface) => surface.id === current.activeSurfaceId, ); const fallbackBrowser = surfaces.find((surface) => surface.kind === "preview"); return { ...current, surfaces, activeSurfaceId: activeStillExists ? current.activeSurfaceId : (fallbackBrowser?.id ?? surfaces[0]?.id ?? null), }; }), })), reconcileFileSurfaces: (ref, workspaceAvailable) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => { if (workspaceAvailable) return current; const surfaces = current.surfaces.filter( (surface) => surface.kind !== "files" && surface.kind !== "file", ); if (surfaces.length === current.surfaces.length) return current; const activeStillExists = surfaces.some( (surface) => surface.id === current.activeSurfaceId, ); return { ...current, isOpen: surfaces.length > 0 ? current.isOpen : false, surfaces, activeSurfaceId: activeStillExists ? current.activeSurfaceId : (surfaces.at(-1)?.id ?? null), }; }), })), show: (ref) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => current.isOpen ? current : { ...current, isOpen: true }, ), })), close: (ref) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => current.isOpen ? { ...current, isOpen: false } : current, ), })), toggleVisibility: (ref) => set((state) => ({ byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => ({ ...current, isOpen: !current.isOpen, })), })), toggle: (ref, kind) =>