/** * TerminalManager - Terminal session orchestration service interface. * * Owns terminal lifecycle operations, output fanout, and session state * transitions for thread-scoped terminals. * * @module TerminalManager */import { DEFAULT_TERMINAL_ID, TerminalCwdError, TerminalCwdNotDirectoryError, TerminalCwdNotFoundError, TerminalCwdStatError, TerminalError, TerminalHistoryError, TerminalNotRunningError, TerminalResizeError, TerminalSessionLookupError, TerminalWriteError, type TerminalAttachInput, type TerminalAttachStreamEvent, type TerminalClearInput, type TerminalCloseInput, type TerminalEvent, type TerminalMetadataStreamEvent, type TerminalOpenInput, type TerminalResizeInput, type TerminalRestartInput, type TerminalSessionSnapshot, type TerminalSessionStatus, type TerminalSummary, type TerminalWriteInput,} from "@t3tools/contracts";import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker";import { HostProcessPlatform } from "@t3tools/shared/hostProcess";import { getTerminalLabel } from "@t3tools/shared/terminalLabels";import * as DateTime from "effect/DateTime";import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Encoding from "effect/Encoding";import * as Equal from "effect/Equal";import * as Exit from "effect/Exit";import * as Fiber from "effect/Fiber";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope";import * as Semaphore from "effect/Semaphore";import * as SynchronizedRef from "effect/SynchronizedRef"; import * as ServerConfig from "../config.ts";import { increment, terminalRestartsTotal, terminalSessionsTotal,} from "../observability/Metrics.ts";import * as ProcessRunner from "../processRunner.ts";import * as PortScanner from "../preview/PortScanner.ts";import * as PtyAdapter from "./PtyAdapter.ts"; export { TerminalCwdError, TerminalCwdNotDirectoryError, TerminalCwdNotFoundError, TerminalCwdStatError, TerminalError, TerminalHistoryError, TerminalNotRunningError, TerminalResizeError, TerminalSessionLookupError, TerminalWriteError,}; const DEFAULT_HISTORY_LINE_LIMIT = 5_000;const DEFAULT_PERSIST_DEBOUNCE_MS = 40;const DEFAULT_SUBPROCESS_POLL_INTERVAL_MS = 1_000;const DEFAULT_PROCESS_KILL_GRACE_MS = 1_000;const DEFAULT_MAX_RETAINED_INACTIVE_SESSIONS = 128;const DEFAULT_OPEN_COLS = 120;const DEFAULT_OPEN_ROWS = 30;const TERMINAL_ENV_BLOCKLIST = new Set(["PORT", "ELECTRON_RENDERER_PORT", "ELECTRON_RUN_AS_NODE"]);const nowIso = Effect.map(DateTime.now, DateTime.formatIso);const MAX_TERMINAL_LABEL_LENGTH = 128; class TerminalSubprocessCheckError extends Schema.TaggedErrorClass<TerminalSubprocessCheckError>()( "TerminalSubprocessCheckError", { cause: Schema.optional(Schema.Defect()), terminalPid: Schema.Number, command: Schema.Literals(["powershell", "pgrep", "ps"]), },) { override get message(): string { return `Failed to inspect terminal subprocesses for PID ${this.terminalPid} with ${this.command}`; }} class TerminalProcessSignalError extends Schema.TaggedErrorClass<TerminalProcessSignalError>()( "TerminalProcessSignalError", { cause: Schema.optional(Schema.Defect()), signal: Schema.Literals(["SIGTERM", "SIGKILL"]), terminalPid: Schema.Number, },) { override get message(): string { return `Failed to send ${this.signal} to terminal process ${this.terminalPid}`; }} /** * TerminalManager - Service tag for terminal session orchestration. */export class TerminalManager extends Context.Service< TerminalManager, { /** * Open or attach to a terminal session. * * Reuses an existing session for the same thread/terminal id and restores * persisted history on first open. */ readonly open: ( input: TerminalOpenInput, ) => Effect.Effect<TerminalSessionSnapshot, TerminalError>; /** * Attach to a terminal and stream its initial snapshot followed by live events. * * Returns an unsubscribe function. */ readonly attachStream: ( input: TerminalAttachInput, listener: (event: TerminalAttachStreamEvent) => Effect.Effect<void>, ) => Effect.Effect<() => void, TerminalError>; /** * Write input bytes to a terminal session. */ readonly write: (input: TerminalWriteInput) => Effect.Effect<void, TerminalError>; /** * Resize the PTY backing a terminal session. */ readonly resize: (input: TerminalResizeInput) => Effect.Effect<void, TerminalError>; /** * Clear terminal output history. */ readonly clear: (input: TerminalClearInput) => Effect.Effect<void, TerminalError>; /** * Restart a terminal session in place. * * Always resets history before spawning the new process. */ readonly restart: ( input: TerminalRestartInput, ) => Effect.Effect<TerminalSessionSnapshot, TerminalError>; /** * Close an active terminal session. * * When `terminalId` is omitted, closes all sessions for the thread. */ readonly close: (input: TerminalCloseInput) => Effect.Effect<void, TerminalError>; /** * Subscribe to terminal runtime events with a direct callback. * * Returns an unsubscribe function. */ readonly subscribe: ( listener: (event: TerminalEvent) => Effect.Effect<void>, ) => Effect.Effect<() => void>; /** * Subscribe to lightweight terminal metadata with an initial full snapshot. * * Returns an unsubscribe function. */ readonly subscribeMetadata: ( listener: (event: TerminalMetadataStreamEvent) => Effect.Effect<void>, ) => Effect.Effect<() => void>; }>()("t3/terminal/Manager/TerminalManager") {} interface TerminalSubprocessInspectResult { readonly hasRunningSubprocess: boolean; readonly childCommand: string | null; readonly processIds: ReadonlyArray<number>;} interface TerminalSubprocessInspector { ( terminalPid: number, ): Effect.Effect<TerminalSubprocessInspectResult, TerminalSubprocessCheckError>;} const resizePtyProcess = ( session: TerminalSessionState, process: PtyAdapter.PtyProcess, cols: number, rows: number,) => Effect.try({ try: () => process.resize(cols, rows), catch: (cause) => new TerminalResizeError({ threadId: session.threadId, terminalId: session.terminalId, terminalPid: process.pid, cols, rows, cause, }), }); export interface ShellCandidate { shell: string; args?: string[];} export interface TerminalStartInput extends TerminalOpenInput { cols: number; rows: number;} export interface TerminalSessionState { threadId: string; terminalId: string; cwd: string; worktreePath: string | null; status: TerminalSessionStatus; pid: number | null; history: string; pendingHistoryControlSequence: string; pendingProcessEvents: Array<PendingProcessEvent>; pendingProcessEventIndex: number; processEventDrainRunning: boolean; exitCode: number | null; exitSignal: number | null; updatedAt: string; eventSequence: number; cols: number; rows: number; process: PtyAdapter.PtyProcess | null; unsubscribeData: (() => void) | null; unsubscribeExit: (() => void) | null; hasRunningSubprocess: boolean; /** Normalized child command name when `hasRunningSubprocess`; cleared when idle. */ childCommandLabel: string | null; runtimeEnv: Record<string, string> | null;} interface PersistHistoryRequest { history: string; immediate: boolean;} type PendingProcessEvent = | { type: "output"; data: string } | { type: "exit"; event: PtyAdapter.PtyExitEvent }; type DrainProcessEventAction = | { type: "idle" } | { type: "output"; threadId: string; terminalId: string; sequence: number; history: string | null; data: string; } | { type: "exit"; process: PtyAdapter.PtyProcess | null; threadId: string; terminalId: string; sequence: number; exitCode: number | null; exitSignal: number | null; }; interface TerminalManagerState { sessions: Map<string, TerminalSessionState>; killFibers: Map<PtyAdapter.PtyProcess, Fiber.Fiber<void, never>>;} function truncateTerminalWireLabel(value: string): string { if (value.length <= MAX_TERMINAL_LABEL_LENGTH) return value; return value.slice(0, MAX_TERMINAL_LABEL_LENGTH);} function normalizeChildCommandName(raw: string, platform: NodeJS.Platform): string | null { let trimmed = raw.trim(); if (trimmed.length === 0) return null; if ( (trimmed.startsWith("[") && trimmed.endsWith("]")) || (trimmed.startsWith("(") && trimmed.endsWith(")")) ) { trimmed = trimmed.slice(1, -1).trim(); } const firstToken = (trimmed.split(/\s+/)[0] ?? trimmed).trim(); if (firstToken.length === 0) return null; const separators = platform === "win32" ? /[\\/]/ : /\//; const base = firstToken.split(separators).at(-1) ?? firstToken; const withoutExe = platform === "win32" && base.toLowerCase().endsWith(".exe") ? base.slice(0, -4) : base; return withoutExe.length > 0 ? withoutExe : null;} function terminalWireLabel(session: TerminalSessionState): string { if (session.hasRunningSubprocess && session.childCommandLabel) { const trimmed = session.childCommandLabel.trim(); if (trimmed.length > 0) { return truncateTerminalWireLabel(trimmed); } } return truncateTerminalWireLabel(getTerminalLabel(session.terminalId));} function snapshot(session: TerminalSessionState): TerminalSessionSnapshot { return { threadId: session.threadId, terminalId: session.terminalId, cwd: session.cwd, worktreePath: session.worktreePath, status: session.status, pid: session.pid, history: session.history, exitCode: session.exitCode, exitSignal: session.exitSignal, label: terminalWireLabel(session), updatedAt: session.updatedAt, sequence: session.eventSequence, };} function summary(session: TerminalSessionState): TerminalSummary { return { threadId: session.threadId, terminalId: session.terminalId, cwd: session.cwd, worktreePath: session.worktreePath, status: session.status, pid: session.pid, exitCode: session.exitCode, exitSignal: session.exitSignal, hasRunningSubprocess: session.hasRunningSubprocess, label: terminalWireLabel(session), updatedAt: session.updatedAt, };} function shouldPublishTerminalMetadataEvent(event: TerminalEvent): boolean { switch (event.type) { case "started": case "restarted": case "exited": case "closed": case "error": case "activity": return true; case "output": case "cleared": return false; }} function terminalEventToAttachEvent(event: TerminalEvent): TerminalAttachStreamEvent | null { switch (event.type) { case "started": return { type: "snapshot", snapshot: event.snapshot, }; case "output": case "exited": case "closed": case "error": case "cleared": case "restarted": case "activity": return event; }} function isDuplicateAttachSnapshotEvent( event: TerminalEvent, initialSnapshot: TerminalSessionSnapshot,) { return typeof event.sequence === "number" && typeof initialSnapshot.sequence === "number" ? event.sequence <= initialSnapshot.sequence : event.type === "started" && event.snapshot.threadId === initialSnapshot.threadId && event.snapshot.terminalId === initialSnapshot.terminalId && event.snapshot.updatedAt <= initialSnapshot.updatedAt;} function advanceEventSequence(session: TerminalSessionState): { readonly updatedAt: string; readonly sequence: number;} { const updatedAt = DateTime.formatIso(DateTime.nowUnsafe()); session.eventSequence += 1; session.updatedAt = updatedAt; return { updatedAt, sequence: session.eventSequence };} function cleanupProcessHandles(session: TerminalSessionState): void { session.unsubscribeData?.(); session.unsubscribeData = null; session.unsubscribeExit?.(); session.unsubscribeExit = null;} function enqueueProcessEvent( session: TerminalSessionState, expectedPid: number, event: PendingProcessEvent,): boolean { if (!session.process || session.status !== "running" || session.pid !== expectedPid) { return false; } session.pendingProcessEvents.push(event); if (session.processEventDrainRunning) { return false; } session.processEventDrainRunning = true; return true;} function defaultShellResolver(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): string { if (platform === "win32") { return "pwsh.exe"; } return env.SHELL ?? "bash";} function normalizeShellCommand( value: string | undefined, platform: NodeJS.Platform,): string | null { if (!value) return null; const trimmed = value.trim(); if (trimmed.length === 0) return null; if (platform === "win32") { return trimmed; } const firstToken = trimmed.split(/\s+/g)[0]?.trim(); if (!firstToken) return null; return firstToken.replace(/^['"]|['"]$/g, "");} function basenameForPlatform(command: string, platform: NodeJS.Platform): string { const normalized = platform === "win32" ? command.replaceAll("/", "\\") : command.replaceAll("\\", "/"); const parts = normalized .split(platform === "win32" ? /\\+/ : /\/+/) .filter((part) => part.length > 0); return parts.at(-1) ?? normalized;} function joinWindowsPath(...parts: ReadonlyArray<string>): string { return parts .map((part, index) => { if (index === 0) return part.replace(/[\\/]+$/g, ""); return part.replace(/^[\\/]+|[\\/]+$/g, ""); }) .filter((part) => part.length > 0) .join("\\");} function shellCandidateFromCommand( command: string | null, platform: NodeJS.Platform,): ShellCandidate | null { if (!command || command.length === 0) return null; const shellName = basenameForPlatform(command, platform).toLowerCase(); if (platform === "win32" && (shellName === "pwsh.exe" || shellName === "powershell.exe")) { return { shell: command, args: ["-NoLogo"] }; } if (platform !== "win32" && shellName === "zsh") { return { shell: command, args: ["-o", "nopromptsp"] }; } return { shell: command };} function windowsSystemRoot(env: NodeJS.ProcessEnv): string { return env.SystemRoot?.trim() || env.windir?.trim() || "C:\\Windows";} Lines 1–500 of 2670. Load more
/** * TerminalManager - Terminal session orchestration service interface. * * Owns terminal lifecycle operations, output fanout, and session state * transitions for thread-scoped terminals. * * @module TerminalManager */ import { DEFAULT_TERMINAL_ID, TerminalCwdError, TerminalCwdNotDirectoryError, TerminalCwdNotFoundError, TerminalCwdStatError, TerminalError, TerminalHistoryError, TerminalNotRunningError, TerminalResizeError, TerminalSessionLookupError, TerminalWriteError, type TerminalAttachInput, type TerminalAttachStreamEvent, type TerminalClearInput, type TerminalCloseInput, type TerminalEvent, type TerminalMetadataStreamEvent, type TerminalOpenInput, type TerminalResizeInput, type TerminalRestartInput, type TerminalSessionSnapshot, type TerminalSessionStatus, type TerminalSummary, type TerminalWriteInput, } from "@t3tools/contracts"; import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import { getTerminalLabel } from "@t3tools/shared/terminalLabels"; import * as DateTime from "effect/DateTime"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; import * as Equal from "effect/Equal"; import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import * as Semaphore from "effect/Semaphore"; import * as SynchronizedRef from "effect/SynchronizedRef"; import * as ServerConfig from "../config.ts"; import { increment, terminalRestartsTotal, terminalSessionsTotal, } from "../observability/Metrics.ts"; import * as ProcessRunner from "../processRunner.ts"; import * as PortScanner from "../preview/PortScanner.ts"; import * as PtyAdapter from "./PtyAdapter.ts"; export { TerminalCwdError, TerminalCwdNotDirectoryError, TerminalCwdNotFoundError, TerminalCwdStatError, TerminalError, TerminalHistoryError, TerminalNotRunningError, TerminalResizeError, TerminalSessionLookupError, TerminalWriteError, }; const DEFAULT_HISTORY_LINE_LIMIT = 5_000; const DEFAULT_PERSIST_DEBOUNCE_MS = 40; const DEFAULT_SUBPROCESS_POLL_INTERVAL_MS = 1_000; const DEFAULT_PROCESS_KILL_GRACE_MS = 1_000; const DEFAULT_MAX_RETAINED_INACTIVE_SESSIONS = 128; const DEFAULT_OPEN_COLS = 120; const DEFAULT_OPEN_ROWS = 30; const TERMINAL_ENV_BLOCKLIST = new Set(["PORT", "ELECTRON_RENDERER_PORT", "ELECTRON_RUN_AS_NODE"]); const nowIso = Effect.map(DateTime.now, DateTime.formatIso); const MAX_TERMINAL_LABEL_LENGTH = 128; class TerminalSubprocessCheckError extends Schema.TaggedErrorClass<TerminalSubprocessCheckError>()( "TerminalSubprocessCheckError", { cause: Schema.optional(Schema.Defect()), terminalPid: Schema.Number, command: Schema.Literals(["powershell", "pgrep", "ps"]), }, ) { override get message(): string { return `Failed to inspect terminal subprocesses for PID ${this.terminalPid} with ${this.command}`; } } class TerminalProcessSignalError extends Schema.TaggedErrorClass<TerminalProcessSignalError>()( "TerminalProcessSignalError", { cause: Schema.optional(Schema.Defect()), signal: Schema.Literals(["SIGTERM", "SIGKILL"]), terminalPid: Schema.Number, }, ) { override get message(): string { return `Failed to send ${this.signal} to terminal process ${this.terminalPid}`; } } /** * TerminalManager - Service tag for terminal session orchestration. */ export class TerminalManager extends Context.Service< TerminalManager, { /** * Open or attach to a terminal session. * * Reuses an existing session for the same thread/terminal id and restores * persisted history on first open. */ readonly open: ( input: TerminalOpenInput, ) => Effect.Effect<TerminalSessionSnapshot, TerminalError>; /** * Attach to a terminal and stream its initial snapshot followed by live events. * * Returns an unsubscribe function. */ readonly attachStream: ( input: TerminalAttachInput, listener: (event: TerminalAttachStreamEvent) => Effect.Effect<void>, ) => Effect.Effect<() => void, TerminalError>; /** * Write input bytes to a terminal session. */ readonly write: (input: TerminalWriteInput) => Effect.Effect<void, TerminalError>; /** * Resize the PTY backing a terminal session. */ readonly resize: (input: TerminalResizeInput) => Effect.Effect<void, TerminalError>; /** * Clear terminal output history. */ readonly clear: (input: TerminalClearInput) => Effect.Effect<void, TerminalError>; /** * Restart a terminal session in place. * * Always resets history before spawning the new process. */ readonly restart: ( input: TerminalRestartInput, ) => Effect.Effect<TerminalSessionSnapshot, TerminalError>; /** * Close an active terminal session. * * When `terminalId` is omitted, closes all sessions for the thread. */ readonly close: (input: TerminalCloseInput) => Effect.Effect<void, TerminalError>; /** * Subscribe to terminal runtime events with a direct callback. * * Returns an unsubscribe function. */ readonly subscribe: ( listener: (event: TerminalEvent) => Effect.Effect<void>, ) => Effect.Effect<() => void>; /** * Subscribe to lightweight terminal metadata with an initial full snapshot. * * Returns an unsubscribe function. */ readonly subscribeMetadata: ( listener: (event: TerminalMetadataStreamEvent) => Effect.Effect<void>, ) => Effect.Effect<() => void>; } >()("t3/terminal/Manager/TerminalManager") {} interface TerminalSubprocessInspectResult { readonly hasRunningSubprocess: boolean; readonly childCommand: string | null; readonly processIds: ReadonlyArray<number>; } interface TerminalSubprocessInspector { ( terminalPid: number, ): Effect.Effect<TerminalSubprocessInspectResult, TerminalSubprocessCheckError>; } const resizePtyProcess = ( session: TerminalSessionState, process: PtyAdapter.PtyProcess, cols: number, rows: number, ) => Effect.try({ try: () => process.resize(cols, rows), catch: (cause) => new TerminalResizeError({ threadId: session.threadId, terminalId: session.terminalId, terminalPid: process.pid, cols, rows, cause, }), }); export interface ShellCandidate { shell: string; args?: string[]; } export interface TerminalStartInput extends TerminalOpenInput { cols: number; rows: number; } export interface TerminalSessionState { threadId: string; terminalId: string; cwd: string; worktreePath: string | null; status: TerminalSessionStatus; pid: number | null; history: string; pendingHistoryControlSequence: string; pendingProcessEvents: Array<PendingProcessEvent>; pendingProcessEventIndex: number; processEventDrainRunning: boolean; exitCode: number | null; exitSignal: number | null; updatedAt: string; eventSequence: number; cols: number; rows: number; process: PtyAdapter.PtyProcess | null; unsubscribeData: (() => void) | null; unsubscribeExit: (() => void) | null; hasRunningSubprocess: boolean; /** Normalized child command name when `hasRunningSubprocess`; cleared when idle. */ childCommandLabel: string | null; runtimeEnv: Record<string, string> | null; } interface PersistHistoryRequest { history: string; immediate: boolean; } type PendingProcessEvent = | { type: "output"; data: string } | { type: "exit"; event: PtyAdapter.PtyExitEvent }; type DrainProcessEventAction = | { type: "idle" } | { type: "output"; threadId: string; terminalId: string; sequence: number; history: string | null; data: string; } | { type: "exit"; process: PtyAdapter.PtyProcess | null; threadId: string; terminalId: string; sequence: number; exitCode: number | null; exitSignal: number | null; }; interface TerminalManagerState { sessions: Map<string, TerminalSessionState>; killFibers: Map<PtyAdapter.PtyProcess, Fiber.Fiber<void, never>>; } function truncateTerminalWireLabel(value: string): string { if (value.length <= MAX_TERMINAL_LABEL_LENGTH) return value; return value.slice(0, MAX_TERMINAL_LABEL_LENGTH); } function normalizeChildCommandName(raw: string, platform: NodeJS.Platform): string | null { let trimmed = raw.trim(); if (trimmed.length === 0) return null; if ( (trimmed.startsWith("[") && trimmed.endsWith("]")) || (trimmed.startsWith("(") && trimmed.endsWith(")")) ) { trimmed = trimmed.slice(1, -1).trim(); } const firstToken = (trimmed.split(/\s+/)[0] ?? trimmed).trim(); if (firstToken.length === 0) return null; const separators = platform === "win32" ? /[\\/]/ : /\//; const base = firstToken.split(separators).at(-1) ?? firstToken; const withoutExe = platform === "win32" && base.toLowerCase().endsWith(".exe") ? base.slice(0, -4) : base; return withoutExe.length > 0 ? withoutExe : null; } function terminalWireLabel(session: TerminalSessionState): string { if (session.hasRunningSubprocess && session.childCommandLabel) { const trimmed = session.childCommandLabel.trim(); if (trimmed.length > 0) { return truncateTerminalWireLabel(trimmed); } } return truncateTerminalWireLabel(getTerminalLabel(session.terminalId)); } function snapshot(session: TerminalSessionState): TerminalSessionSnapshot { return { threadId: session.threadId, terminalId: session.terminalId, cwd: session.cwd, worktreePath: session.worktreePath, status: session.status, pid: session.pid, history: session.history, exitCode: session.exitCode, exitSignal: session.exitSignal, label: terminalWireLabel(session), updatedAt: session.updatedAt, sequence: session.eventSequence, }; } function summary(session: TerminalSessionState): TerminalSummary { return { threadId: session.threadId, terminalId: session.terminalId, cwd: session.cwd, worktreePath: session.worktreePath, status: session.status, pid: session.pid, exitCode: session.exitCode, exitSignal: session.exitSignal, hasRunningSubprocess: session.hasRunningSubprocess, label: terminalWireLabel(session), updatedAt: session.updatedAt, }; } function shouldPublishTerminalMetadataEvent(event: TerminalEvent): boolean { switch (event.type) { case "started": case "restarted": case "exited": case "closed": case "error": case "activity": return true; case "output": case "cleared": return false; } } function terminalEventToAttachEvent(event: TerminalEvent): TerminalAttachStreamEvent | null { switch (event.type) { case "started": return { type: "snapshot", snapshot: event.snapshot, }; case "output": case "exited": case "closed": case "error": case "cleared": case "restarted": case "activity": return event; } } function isDuplicateAttachSnapshotEvent( event: TerminalEvent, initialSnapshot: TerminalSessionSnapshot, ) { return typeof event.sequence === "number" && typeof initialSnapshot.sequence === "number" ? event.sequence <= initialSnapshot.sequence : event.type === "started" && event.snapshot.threadId === initialSnapshot.threadId && event.snapshot.terminalId === initialSnapshot.terminalId && event.snapshot.updatedAt <= initialSnapshot.updatedAt; } function advanceEventSequence(session: TerminalSessionState): { readonly updatedAt: string; readonly sequence: number; } { const updatedAt = DateTime.formatIso(DateTime.nowUnsafe()); session.eventSequence += 1; session.updatedAt = updatedAt; return { updatedAt, sequence: session.eventSequence }; } function cleanupProcessHandles(session: TerminalSessionState): void { session.unsubscribeData?.(); session.unsubscribeData = null; session.unsubscribeExit?.(); session.unsubscribeExit = null; } function enqueueProcessEvent( session: TerminalSessionState, expectedPid: number, event: PendingProcessEvent, ): boolean { if (!session.process || session.status !== "running" || session.pid !== expectedPid) { return false; } session.pendingProcessEvents.push(event); if (session.processEventDrainRunning) { return false; } session.processEventDrainRunning = true; return true; } function defaultShellResolver(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): string { if (platform === "win32") { return "pwsh.exe"; } return env.SHELL ?? "bash"; } function normalizeShellCommand( value: string | undefined, platform: NodeJS.Platform, ): string | null { if (!value) return null; const trimmed = value.trim(); if (trimmed.length === 0) return null; if (platform === "win32") { return trimmed; } const firstToken = trimmed.split(/\s+/g)[0]?.trim(); if (!firstToken) return null; return firstToken.replace(/^['"]|['"]$/g, ""); } function basenameForPlatform(command: string, platform: NodeJS.Platform): string { const normalized = platform === "win32" ? command.replaceAll("/", "\\") : command.replaceAll("\\", "/"); const parts = normalized .split(platform === "win32" ? /\\+/ : /\/+/) .filter((part) => part.length > 0); return parts.at(-1) ?? normalized; } function joinWindowsPath(...parts: ReadonlyArray<string>): string { return parts .map((part, index) => { if (index === 0) return part.replace(/[\\/]+$/g, ""); return part.replace(/^[\\/]+|[\\/]+$/g, ""); }) .filter((part) => part.length > 0) .join("\\"); } function shellCandidateFromCommand( command: string | null, platform: NodeJS.Platform, ): ShellCandidate | null { if (!command || command.length === 0) return null; const shellName = basenameForPlatform(command, platform).toLowerCase(); if (platform === "win32" && (shellName === "pwsh.exe" || shellName === "powershell.exe")) { return { shell: command, args: ["-NoLogo"] }; } if (platform !== "win32" && shellName === "zsh") { return { shell: command, args: ["-o", "nopromptsp"] }; } return { shell: command }; } function windowsSystemRoot(env: NodeJS.ProcessEnv): string { return env.SystemRoot?.trim() || env.windir?.trim() || "C:\\Windows"; }