import { EventEmitter } from "node:events";import fs from "node:fs";import path from "node:path"; import { DEFAULT_TERMINAL_ID, TerminalClearInput, TerminalCloseInput, TerminalOpenInput, TerminalResizeInput, TerminalRestartInput, TerminalWriteInput, type TerminalEvent, type TerminalSessionSnapshot,} from "@t3tools/contracts";import { Effect, Encoding, Layer, Schema } from "effect"; import { createLogger } from "../../logger";import { PtyAdapter, PtyAdapterShape, type PtyExitEvent, type PtyProcess } from "../Services/PTY";import { runProcess } from "../../processRunner";import { ServerConfig } from "../../config";import { ShellCandidate, TerminalError, TerminalManager, TerminalManagerShape, TerminalSessionState, TerminalStartInput,} from "../Services/Manager"; 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 decodeTerminalOpenInput = Schema.decodeUnknownSync(TerminalOpenInput);const decodeTerminalRestartInput = Schema.decodeUnknownSync(TerminalRestartInput);const decodeTerminalWriteInput = Schema.decodeUnknownSync(TerminalWriteInput);const decodeTerminalResizeInput = Schema.decodeUnknownSync(TerminalResizeInput);const decodeTerminalClearInput = Schema.decodeUnknownSync(TerminalClearInput);const decodeTerminalCloseInput = Schema.decodeUnknownSync(TerminalCloseInput); type TerminalSubprocessChecker = (terminalPid: number) => Promise<boolean>; function defaultShellResolver(): string { if (process.platform === "win32") { return process.env.ComSpec ?? "cmd.exe"; } return process.env.SHELL ?? "bash";} function normalizeShellCommand(value: string | undefined): string | null { if (!value) return null; const trimmed = value.trim(); if (trimmed.length === 0) return null; if (process.platform === "win32") { return trimmed; } const firstToken = trimmed.split(/\s+/g)[0]?.trim(); if (!firstToken) return null; return firstToken.replace(/^['"]|['"]$/g, "");} function shellCandidateFromCommand(command: string | null): ShellCandidate | null { if (!command || command.length === 0) return null; const shellName = path.basename(command).toLowerCase(); if (process.platform !== "win32" && shellName === "zsh") { return { shell: command, args: ["-o", "nopromptsp"] }; } return { shell: command };} function formatShellCandidate(candidate: ShellCandidate): string { if (!candidate.args || candidate.args.length === 0) return candidate.shell; return `${candidate.shell} ${candidate.args.join(" ")}`;} function uniqueShellCandidates(candidates: Array<ShellCandidate | null>): ShellCandidate[] { const seen = new Set<string>(); const ordered: ShellCandidate[] = []; for (const candidate of candidates) { if (!candidate) continue; const key = formatShellCandidate(candidate); if (seen.has(key)) continue; seen.add(key); ordered.push(candidate); } return ordered;} function resolveShellCandidates(shellResolver: () => string): ShellCandidate[] { const requested = shellCandidateFromCommand(normalizeShellCommand(shellResolver())); if (process.platform === "win32") { return uniqueShellCandidates([ requested, shellCandidateFromCommand(process.env.ComSpec ?? null), shellCandidateFromCommand("powershell.exe"), shellCandidateFromCommand("cmd.exe"), ]); } return uniqueShellCandidates([ requested, shellCandidateFromCommand(normalizeShellCommand(process.env.SHELL)), shellCandidateFromCommand("/bin/zsh"), shellCandidateFromCommand("/bin/bash"), shellCandidateFromCommand("/bin/sh"), shellCandidateFromCommand("zsh"), shellCandidateFromCommand("bash"), shellCandidateFromCommand("sh"), ]);} function isRetryableShellSpawnError(error: unknown): boolean { const queue: unknown[] = [error]; const seen = new Set<unknown>(); const messages: string[] = []; while (queue.length > 0) { const current = queue.shift(); if (!current || seen.has(current)) { continue; } seen.add(current); if (typeof current === "string") { messages.push(current); continue; } if (current instanceof Error) { messages.push(current.message); const cause = (current as { cause?: unknown }).cause; if (cause) { queue.push(cause); } continue; } if (typeof current === "object") { const value = current as { message?: unknown; cause?: unknown }; if (typeof value.message === "string") { messages.push(value.message); } if (value.cause) { queue.push(value.cause); } } } const message = messages.join(" ").toLowerCase(); return ( message.includes("posix_spawnp failed") || message.includes("enoent") || message.includes("not found") || message.includes("file not found") || message.includes("no such file") );} async function checkWindowsSubprocessActivity(terminalPid: number): Promise<boolean> { const command = [ `$children = Get-CimInstance Win32_Process -Filter "ParentProcessId = ${terminalPid}" -ErrorAction SilentlyContinue`, "if ($children) { exit 0 }", "exit 1", ].join("; "); try { const result = await runProcess( "powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", command], { timeoutMs: 1_500, allowNonZeroExit: true, maxBufferBytes: 32_768, outputMode: "truncate", }, ); return result.code === 0; } catch { return false; }} async function checkPosixSubprocessActivity(terminalPid: number): Promise<boolean> { try { const pgrepResult = await runProcess("pgrep", ["-P", String(terminalPid)], { timeoutMs: 1_000, allowNonZeroExit: true, maxBufferBytes: 32_768, outputMode: "truncate", }); if (pgrepResult.code === 0) { return pgrepResult.stdout.trim().length > 0; } if (pgrepResult.code === 1) { return false; } } catch { // Fall back to ps when pgrep is unavailable. } try { const psResult = await runProcess("ps", ["-eo", "pid=,ppid="], { timeoutMs: 1_000, allowNonZeroExit: true, maxBufferBytes: 262_144, outputMode: "truncate", }); if (psResult.code !== 0) { return false; } for (const line of psResult.stdout.split(/\r?\n/g)) { const [pidRaw, ppidRaw] = line.trim().split(/\s+/g); const pid = Number(pidRaw); const ppid = Number(ppidRaw); if (!Number.isInteger(pid) || !Number.isInteger(ppid)) continue; if (ppid === terminalPid) { return true; } } return false; } catch { return false; }} async function defaultSubprocessChecker(terminalPid: number): Promise<boolean> { if (!Number.isInteger(terminalPid) || terminalPid <= 0) { return false; } if (process.platform === "win32") { return checkWindowsSubprocessActivity(terminalPid); } return checkPosixSubprocessActivity(terminalPid);} function capHistory(history: string, maxLines: number): string { if (history.length === 0) return history; const hasTrailingNewline = history.endsWith("\n"); const lines = history.split("\n"); if (hasTrailingNewline) { lines.pop(); } if (lines.length <= maxLines) return history; const capped = lines.slice(lines.length - maxLines).join("\n"); return hasTrailingNewline ? `${capped}\n` : capped;} function isCsiFinalByte(codePoint: number): boolean { return codePoint >= 0x40 && codePoint <= 0x7e;} function shouldStripCsiSequence(body: string, finalByte: string): boolean { if (finalByte === "n") { return true; } if (finalByte === "R" && /^[0-9;?]*$/.test(body)) { return true; } if (finalByte === "c" && /^[>0-9;?]*$/.test(body)) { return true; } return false;} function shouldStripOscSequence(content: string): boolean { return /^(10|11|12);(?:\?|rgb:)/.test(content);} function stripStringTerminator(value: string): string { if (value.endsWith("\u001b\\")) { return value.slice(0, -2); } const lastCharacter = value.at(-1); if (lastCharacter === "\u0007" || lastCharacter === "\u009c") { return value.slice(0, -1); } return value;} function findStringTerminatorIndex(input: string, start: number): number | null { for (let index = start; index < input.length; index += 1) { const codePoint = input.charCodeAt(index); if (codePoint === 0x07 || codePoint === 0x9c) { return index + 1; } if (codePoint === 0x1b && input.charCodeAt(index + 1) === 0x5c) { return index + 2; } } return null;} function isEscapeIntermediateByte(codePoint: number): boolean { return codePoint >= 0x20 && codePoint <= 0x2f;} function isEscapeFinalByte(codePoint: number): boolean { return codePoint >= 0x30 && codePoint <= 0x7e;} function findEscapeSequenceEndIndex(input: string, start: number): number | null { let cursor = start; while (cursor < input.length && isEscapeIntermediateByte(input.charCodeAt(cursor))) { cursor += 1; } if (cursor >= input.length) { return null; } return isEscapeFinalByte(input.charCodeAt(cursor)) ? cursor + 1 : start + 1;} function sanitizeTerminalHistoryChunk( pendingControlSequence: string, data: string,): { visibleText: string; pendingControlSequence: string } { const input = `${pendingControlSequence}${data}`; let visibleText = ""; let index = 0; const append = (value: string) => { visibleText += value; }; while (index < input.length) { const codePoint = input.charCodeAt(index); if (codePoint === 0x1b) { const nextCodePoint = input.charCodeAt(index + 1); if (Number.isNaN(nextCodePoint)) { return { visibleText, pendingControlSequence: input.slice(index) }; } if (nextCodePoint === 0x5b) { let cursor = index + 2; while (cursor < input.length) { if (isCsiFinalByte(input.charCodeAt(cursor))) { const sequence = input.slice(index, cursor + 1); const body = input.slice(index + 2, cursor); if (!shouldStripCsiSequence(body, input[cursor] ?? "")) { append(sequence); } index = cursor + 1; break; } cursor += 1; } if (cursor >= input.length) { return { visibleText, pendingControlSequence: input.slice(index) }; } continue; } if ( nextCodePoint === 0x5d || nextCodePoint === 0x50 || nextCodePoint === 0x5e || nextCodePoint === 0x5f ) { const terminatorIndex = findStringTerminatorIndex(input, index + 2); if (terminatorIndex === null) { return { visibleText, pendingControlSequence: input.slice(index) }; } const sequence = input.slice(index, terminatorIndex); const content = stripStringTerminator(input.slice(index + 2, terminatorIndex)); if (nextCodePoint !== 0x5d || !shouldStripOscSequence(content)) { append(sequence); } index = terminatorIndex; continue; } const escapeSequenceEndIndex = findEscapeSequenceEndIndex(input, index + 1); if (escapeSequenceEndIndex === null) { return { visibleText, pendingControlSequence: input.slice(index) }; } append(input.slice(index, escapeSequenceEndIndex)); index = escapeSequenceEndIndex; continue; } if (codePoint === 0x9b) { let cursor = index + 1; while (cursor < input.length) { if (isCsiFinalByte(input.charCodeAt(cursor))) { const sequence = input.slice(index, cursor + 1); const body = input.slice(index + 1, cursor); if (!shouldStripCsiSequence(body, input[cursor] ?? "")) { append(sequence); } index = cursor + 1; break; } cursor += 1; } if (cursor >= input.length) { return { visibleText, pendingControlSequence: input.slice(index) }; } continue; } if (codePoint === 0x9d || codePoint === 0x90 || codePoint === 0x9e || codePoint === 0x9f) { const terminatorIndex = findStringTerminatorIndex(input, index + 1); if (terminatorIndex === null) { return { visibleText, pendingControlSequence: input.slice(index) }; } const sequence = input.slice(index, terminatorIndex); const content = stripStringTerminator(input.slice(index + 1, terminatorIndex)); if (codePoint !== 0x9d || !shouldStripOscSequence(content)) { append(sequence); } index = terminatorIndex; continue; } append(input[index] ?? ""); index += 1; } return { visibleText, pendingControlSequence: "" };} function legacySafeThreadId(threadId: string): string { return threadId.replace(/[^a-zA-Z0-9._-]/g, "_");} function toSafeThreadId(threadId: string): string { return `terminal_${Encoding.encodeBase64Url(threadId)}`;} function toSafeTerminalId(terminalId: string): string { return Encoding.encodeBase64Url(terminalId);} function toSessionKey(threadId: string, terminalId: string): string { return `${threadId}\u0000${terminalId}`;} function shouldExcludeTerminalEnvKey(key: string): boolean { const normalizedKey = key.toUpperCase(); if (normalizedKey.startsWith("T3CODE_")) { return true; } if (normalizedKey.startsWith("VITE_")) { return true; } return TERMINAL_ENV_BLOCKLIST.has(normalizedKey);} function createTerminalSpawnEnv( baseEnv: NodeJS.ProcessEnv, runtimeEnv?: Record<string, string> | null,): NodeJS.ProcessEnv { const spawnEnv: NodeJS.ProcessEnv = {}; for (const [key, value] of Object.entries(baseEnv)) { if (value === undefined) continue; if (shouldExcludeTerminalEnvKey(key)) continue; spawnEnv[key] = value; } if (runtimeEnv) { for (const [key, value] of Object.entries(runtimeEnv)) { spawnEnv[key] = value; } } return spawnEnv;} function normalizedRuntimeEnv( env: Record<string, string> | undefined,): Record<string, string> | null { if (!env) return null; const entries = Object.entries(env); if (entries.length === 0) return null; return Object.fromEntries(entries.toSorted(([left], [right]) => left.localeCompare(right)));} interface TerminalManagerEvents { event: [event: TerminalEvent];} interface TerminalManagerOptions { logsDir?: string; historyLineLimit?: number; ptyAdapter: PtyAdapterShape; shellResolver?: () => string; subprocessChecker?: TerminalSubprocessChecker; subprocessPollIntervalMs?: number; processKillGraceMs?: number; maxRetainedInactiveSessions?: number;} export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents> {Lines 1–500 of 1413. Load more
import { EventEmitter } from "node:events"; import fs from "node:fs"; import path from "node:path"; import { DEFAULT_TERMINAL_ID, TerminalClearInput, TerminalCloseInput, TerminalOpenInput, TerminalResizeInput, TerminalRestartInput, TerminalWriteInput, type TerminalEvent, type TerminalSessionSnapshot, } from "@t3tools/contracts"; import { Effect, Encoding, Layer, Schema } from "effect"; import { createLogger } from "../../logger"; import { PtyAdapter, PtyAdapterShape, type PtyExitEvent, type PtyProcess } from "../Services/PTY"; import { runProcess } from "../../processRunner"; import { ServerConfig } from "../../config"; import { ShellCandidate, TerminalError, TerminalManager, TerminalManagerShape, TerminalSessionState, TerminalStartInput, } from "../Services/Manager"; 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 decodeTerminalOpenInput = Schema.decodeUnknownSync(TerminalOpenInput); const decodeTerminalRestartInput = Schema.decodeUnknownSync(TerminalRestartInput); const decodeTerminalWriteInput = Schema.decodeUnknownSync(TerminalWriteInput); const decodeTerminalResizeInput = Schema.decodeUnknownSync(TerminalResizeInput); const decodeTerminalClearInput = Schema.decodeUnknownSync(TerminalClearInput); const decodeTerminalCloseInput = Schema.decodeUnknownSync(TerminalCloseInput); type TerminalSubprocessChecker = (terminalPid: number) => Promise<boolean>; function defaultShellResolver(): string { if (process.platform === "win32") { return process.env.ComSpec ?? "cmd.exe"; } return process.env.SHELL ?? "bash"; } function normalizeShellCommand(value: string | undefined): string | null { if (!value) return null; const trimmed = value.trim(); if (trimmed.length === 0) return null; if (process.platform === "win32") { return trimmed; } const firstToken = trimmed.split(/\s+/g)[0]?.trim(); if (!firstToken) return null; return firstToken.replace(/^['"]|['"]$/g, ""); } function shellCandidateFromCommand(command: string | null): ShellCandidate | null { if (!command || command.length === 0) return null; const shellName = path.basename(command).toLowerCase(); if (process.platform !== "win32" && shellName === "zsh") { return { shell: command, args: ["-o", "nopromptsp"] }; } return { shell: command }; } function formatShellCandidate(candidate: ShellCandidate): string { if (!candidate.args || candidate.args.length === 0) return candidate.shell; return `${candidate.shell} ${candidate.args.join(" ")}`; } function uniqueShellCandidates(candidates: Array<ShellCandidate | null>): ShellCandidate[] { const seen = new Set<string>(); const ordered: ShellCandidate[] = []; for (const candidate of candidates) { if (!candidate) continue; const key = formatShellCandidate(candidate); if (seen.has(key)) continue; seen.add(key); ordered.push(candidate); } return ordered; } function resolveShellCandidates(shellResolver: () => string): ShellCandidate[] { const requested = shellCandidateFromCommand(normalizeShellCommand(shellResolver())); if (process.platform === "win32") { return uniqueShellCandidates([ requested, shellCandidateFromCommand(process.env.ComSpec ?? null), shellCandidateFromCommand("powershell.exe"), shellCandidateFromCommand("cmd.exe"), ]); } return uniqueShellCandidates([ requested, shellCandidateFromCommand(normalizeShellCommand(process.env.SHELL)), shellCandidateFromCommand("/bin/zsh"), shellCandidateFromCommand("/bin/bash"), shellCandidateFromCommand("/bin/sh"), shellCandidateFromCommand("zsh"), shellCandidateFromCommand("bash"), shellCandidateFromCommand("sh"), ]); } function isRetryableShellSpawnError(error: unknown): boolean { const queue: unknown[] = [error]; const seen = new Set<unknown>(); const messages: string[] = []; while (queue.length > 0) { const current = queue.shift(); if (!current || seen.has(current)) { continue; } seen.add(current); if (typeof current === "string") { messages.push(current); continue; } if (current instanceof Error) { messages.push(current.message); const cause = (current as { cause?: unknown }).cause; if (cause) { queue.push(cause); } continue; } if (typeof current === "object") { const value = current as { message?: unknown; cause?: unknown }; if (typeof value.message === "string") { messages.push(value.message); } if (value.cause) { queue.push(value.cause); } } } const message = messages.join(" ").toLowerCase(); return ( message.includes("posix_spawnp failed") || message.includes("enoent") || message.includes("not found") || message.includes("file not found") || message.includes("no such file") ); } async function checkWindowsSubprocessActivity(terminalPid: number): Promise<boolean> { const command = [ `$children = Get-CimInstance Win32_Process -Filter "ParentProcessId = ${terminalPid}" -ErrorAction SilentlyContinue`, "if ($children) { exit 0 }", "exit 1", ].join("; "); try { const result = await runProcess( "powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", command], { timeoutMs: 1_500, allowNonZeroExit: true, maxBufferBytes: 32_768, outputMode: "truncate", }, ); return result.code === 0; } catch { return false; } } async function checkPosixSubprocessActivity(terminalPid: number): Promise<boolean> { try { const pgrepResult = await runProcess("pgrep", ["-P", String(terminalPid)], { timeoutMs: 1_000, allowNonZeroExit: true, maxBufferBytes: 32_768, outputMode: "truncate", }); if (pgrepResult.code === 0) { return pgrepResult.stdout.trim().length > 0; } if (pgrepResult.code === 1) { return false; } } catch { // Fall back to ps when pgrep is unavailable. } try { const psResult = await runProcess("ps", ["-eo", "pid=,ppid="], { timeoutMs: 1_000, allowNonZeroExit: true, maxBufferBytes: 262_144, outputMode: "truncate", }); if (psResult.code !== 0) { return false; } for (const line of psResult.stdout.split(/\r?\n/g)) { const [pidRaw, ppidRaw] = line.trim().split(/\s+/g); const pid = Number(pidRaw); const ppid = Number(ppidRaw); if (!Number.isInteger(pid) || !Number.isInteger(ppid)) continue; if (ppid === terminalPid) { return true; } } return false; } catch { return false; } } async function defaultSubprocessChecker(terminalPid: number): Promise<boolean> { if (!Number.isInteger(terminalPid) || terminalPid <= 0) { return false; } if (process.platform === "win32") { return checkWindowsSubprocessActivity(terminalPid); } return checkPosixSubprocessActivity(terminalPid); } function capHistory(history: string, maxLines: number): string { if (history.length === 0) return history; const hasTrailingNewline = history.endsWith("\n"); const lines = history.split("\n"); if (hasTrailingNewline) { lines.pop(); } if (lines.length <= maxLines) return history; const capped = lines.slice(lines.length - maxLines).join("\n"); return hasTrailingNewline ? `${capped}\n` : capped; } function isCsiFinalByte(codePoint: number): boolean { return codePoint >= 0x40 && codePoint <= 0x7e; } function shouldStripCsiSequence(body: string, finalByte: string): boolean { if (finalByte === "n") { return true; } if (finalByte === "R" && /^[0-9;?]*$/.test(body)) { return true; } if (finalByte === "c" && /^[>0-9;?]*$/.test(body)) { return true; } return false; } function shouldStripOscSequence(content: string): boolean { return /^(10|11|12);(?:\?|rgb:)/.test(content); } function stripStringTerminator(value: string): string { if (value.endsWith("\u001b\\")) { return value.slice(0, -2); } const lastCharacter = value.at(-1); if (lastCharacter === "\u0007" || lastCharacter === "\u009c") { return value.slice(0, -1); } return value; } function findStringTerminatorIndex(input: string, start: number): number | null { for (let index = start; index < input.length; index += 1) { const codePoint = input.charCodeAt(index); if (codePoint === 0x07 || codePoint === 0x9c) { return index + 1; } if (codePoint === 0x1b && input.charCodeAt(index + 1) === 0x5c) { return index + 2; } } return null; } function isEscapeIntermediateByte(codePoint: number): boolean { return codePoint >= 0x20 && codePoint <= 0x2f; } function isEscapeFinalByte(codePoint: number): boolean { return codePoint >= 0x30 && codePoint <= 0x7e; } function findEscapeSequenceEndIndex(input: string, start: number): number | null { let cursor = start; while (cursor < input.length && isEscapeIntermediateByte(input.charCodeAt(cursor))) { cursor += 1; } if (cursor >= input.length) { return null; } return isEscapeFinalByte(input.charCodeAt(cursor)) ? cursor + 1 : start + 1; } function sanitizeTerminalHistoryChunk( pendingControlSequence: string, data: string, ): { visibleText: string; pendingControlSequence: string } { const input = `${pendingControlSequence}${data}`; let visibleText = ""; let index = 0; const append = (value: string) => { visibleText += value; }; while (index < input.length) { const codePoint = input.charCodeAt(index); if (codePoint === 0x1b) { const nextCodePoint = input.charCodeAt(index + 1); if (Number.isNaN(nextCodePoint)) { return { visibleText, pendingControlSequence: input.slice(index) }; } if (nextCodePoint === 0x5b) { let cursor = index + 2; while (cursor < input.length) { if (isCsiFinalByte(input.charCodeAt(cursor))) { const sequence = input.slice(index, cursor + 1); const body = input.slice(index + 2, cursor); if (!shouldStripCsiSequence(body, input[cursor] ?? "")) { append(sequence); } index = cursor + 1; break; } cursor += 1; } if (cursor >= input.length) { return { visibleText, pendingControlSequence: input.slice(index) }; } continue; } if ( nextCodePoint === 0x5d || nextCodePoint === 0x50 || nextCodePoint === 0x5e || nextCodePoint === 0x5f ) { const terminatorIndex = findStringTerminatorIndex(input, index + 2); if (terminatorIndex === null) { return { visibleText, pendingControlSequence: input.slice(index) }; } const sequence = input.slice(index, terminatorIndex); const content = stripStringTerminator(input.slice(index + 2, terminatorIndex)); if (nextCodePoint !== 0x5d || !shouldStripOscSequence(content)) { append(sequence); } index = terminatorIndex; continue; } const escapeSequenceEndIndex = findEscapeSequenceEndIndex(input, index + 1); if (escapeSequenceEndIndex === null) { return { visibleText, pendingControlSequence: input.slice(index) }; } append(input.slice(index, escapeSequenceEndIndex)); index = escapeSequenceEndIndex; continue; } if (codePoint === 0x9b) { let cursor = index + 1; while (cursor < input.length) { if (isCsiFinalByte(input.charCodeAt(cursor))) { const sequence = input.slice(index, cursor + 1); const body = input.slice(index + 1, cursor); if (!shouldStripCsiSequence(body, input[cursor] ?? "")) { append(sequence); } index = cursor + 1; break; } cursor += 1; } if (cursor >= input.length) { return { visibleText, pendingControlSequence: input.slice(index) }; } continue; } if (codePoint === 0x9d || codePoint === 0x90 || codePoint === 0x9e || codePoint === 0x9f) { const terminatorIndex = findStringTerminatorIndex(input, index + 1); if (terminatorIndex === null) { return { visibleText, pendingControlSequence: input.slice(index) }; } const sequence = input.slice(index, terminatorIndex); const content = stripStringTerminator(input.slice(index + 1, terminatorIndex)); if (codePoint !== 0x9d || !shouldStripOscSequence(content)) { append(sequence); } index = terminatorIndex; continue; } append(input[index] ?? ""); index += 1; } return { visibleText, pendingControlSequence: "" }; } function legacySafeThreadId(threadId: string): string { return threadId.replace(/[^a-zA-Z0-9._-]/g, "_"); } function toSafeThreadId(threadId: string): string { return `terminal_${Encoding.encodeBase64Url(threadId)}`; } function toSafeTerminalId(terminalId: string): string { return Encoding.encodeBase64Url(terminalId); } function toSessionKey(threadId: string, terminalId: string): string { return `${threadId}\u0000${terminalId}`; } function shouldExcludeTerminalEnvKey(key: string): boolean { const normalizedKey = key.toUpperCase(); if (normalizedKey.startsWith("T3CODE_")) { return true; } if (normalizedKey.startsWith("VITE_")) { return true; } return TERMINAL_ENV_BLOCKLIST.has(normalizedKey); } function createTerminalSpawnEnv( baseEnv: NodeJS.ProcessEnv, runtimeEnv?: Record<string, string> | null, ): NodeJS.ProcessEnv { const spawnEnv: NodeJS.ProcessEnv = {}; for (const [key, value] of Object.entries(baseEnv)) { if (value === undefined) continue; if (shouldExcludeTerminalEnvKey(key)) continue; spawnEnv[key] = value; } if (runtimeEnv) { for (const [key, value] of Object.entries(runtimeEnv)) { spawnEnv[key] = value; } } return spawnEnv; } function normalizedRuntimeEnv( env: Record<string, string> | undefined, ): Record<string, string> | null { if (!env) return null; const entries = Object.entries(env); if (entries.length === 0) return null; return Object.fromEntries(entries.toSorted(([left], [right]) => left.localeCompare(right))); } interface TerminalManagerEvents { event: [event: TerminalEvent]; } interface TerminalManagerOptions { logsDir?: string; historyLineLimit?: number; ptyAdapter: PtyAdapterShape; shellResolver?: () => string; subprocessChecker?: TerminalSubprocessChecker; subprocessPollIntervalMs?: number; processKillGraceMs?: number; maxRetainedInactiveSessions?: number; } export class TerminalManagerRuntime extends EventEmitter<TerminalManagerEvents> {