213 lines · 5.9 KB · TypeScript
RawDownload
import type {  EnvironmentId,  TerminalAttachStreamEvent,  TerminalMetadataStreamEvent,  TerminalSessionSnapshot,  TerminalSummary,  ThreadId,} from "@t3tools/contracts";import {  appendOutput,  DEFAULT_MAX_TERMINAL_BUFFER_BYTES,  EMPTY_TERMINAL_OUTPUT_STATE,  resetOutput,  type TerminalOutputState,} from "./terminalOutput.ts"; export {  DEFAULT_MAX_TERMINAL_BUFFER_BYTES,  INITIAL_TERMINAL_OUTPUT_CURSOR,  readTerminalOutputUpdate,  terminalOutputText,  type TerminalOutputCursor,  type TerminalOutputState,  type TerminalOutputUpdate,} from "./terminalOutput.ts"; export interface TerminalSessionState {  readonly summary: TerminalSummary | null;  readonly output: TerminalOutputState;  readonly status: TerminalSessionSnapshot["status"] | "closed";  readonly error: string | null;  readonly hasRunningSubprocess: boolean;  readonly updatedAt: string | null;  readonly version: number;  readonly lifecycleVersion: number;} export interface TerminalBufferState {  readonly output: TerminalOutputState;  readonly status: TerminalSessionSnapshot["status"] | "closed";  readonly error: string | null;  readonly updatedAt: string | null;  readonly version: number;  readonly lifecycleVersion: number;} export interface KnownTerminalSessionTarget {  readonly environmentId: EnvironmentId;  readonly threadId: ThreadId;  readonly terminalId: string;} export interface KnownTerminalSession {  readonly target: KnownTerminalSessionTarget;  readonly state: TerminalSessionState;} export function selectRunningSubprocessTerminalIds(  sessions: ReadonlyArray<KnownTerminalSession>,): ReadonlyArray<string> {  return sessions    .filter((session) => session.state.hasRunningSubprocess)    .map((session) => session.target.terminalId);} export const EMPTY_TERMINAL_BUFFER_STATE = Object.freeze<TerminalBufferState>({  output: EMPTY_TERMINAL_OUTPUT_STATE,  status: "closed",  error: null,  updatedAt: null,  version: 0,  lifecycleVersion: 0,}); export const EMPTY_TERMINAL_SESSION_STATE = Object.freeze<TerminalSessionState>({  summary: null,  output: EMPTY_TERMINAL_OUTPUT_STATE,  status: "closed",  error: null,  hasRunningSubprocess: false,  updatedAt: null,  version: 0,  lifecycleVersion: 0,}); let terminalAttachGeneration = 0; /** A reinstalled attach stream must not reuse an old renderer's output cursor. */export function nextTerminalAttachSeedState(): TerminalBufferState {  return {    ...EMPTY_TERMINAL_BUFFER_STATE,    output: {      ...EMPTY_TERMINAL_OUTPUT_STATE,      generation: ++terminalAttachGeneration,    },  };} function terminalBufferStateFromSnapshot(  snapshot: TerminalSessionSnapshot,  maxBufferBytes: number,  current: TerminalBufferState = EMPTY_TERMINAL_BUFFER_STATE,): TerminalBufferState {  return {    output: resetOutput(current.output, snapshot.history, maxBufferBytes),    status: snapshot.status,    error: null,    updatedAt: snapshot.updatedAt,    version: current.version + 1,    lifecycleVersion: current.lifecycleVersion,  };} function latestTimestamp(left: string | null, right: string | null): string | null {  if (left === null) return right;  if (right === null) return left;  return Date.parse(left) >= Date.parse(right) ? left : right;} export function combineTerminalSessionState(  summary: TerminalSummary | null,  buffer: TerminalBufferState,): TerminalSessionState {  return {    summary,    output: buffer.output,    status: buffer.version > 0 ? buffer.status : (summary?.status ?? buffer.status),    error: buffer.error,    hasRunningSubprocess: summary?.hasRunningSubprocess ?? false,    updatedAt: latestTimestamp(summary?.updatedAt ?? null, buffer.updatedAt),    version: buffer.version,    lifecycleVersion: buffer.lifecycleVersion,  };} export function applyTerminalAttachStreamEvent(  current: TerminalBufferState,  event: TerminalAttachStreamEvent,  maxBufferBytes = DEFAULT_MAX_TERMINAL_BUFFER_BYTES,): TerminalBufferState {  switch (event.type) {    case "snapshot":      return {        ...terminalBufferStateFromSnapshot(event.snapshot, maxBufferBytes, current),        lifecycleVersion:          current.version === 0 ? current.lifecycleVersion : current.lifecycleVersion + 1,      };    case "restarted":      return {        ...terminalBufferStateFromSnapshot(event.snapshot, maxBufferBytes, current),        lifecycleVersion: current.lifecycleVersion + 1,      };    case "output":      return {        ...current,        output: appendOutput(current.output, event.data, maxBufferBytes),        status: current.status === "closed" ? "running" : current.status,        error: null,        version: current.version + 1,      };    case "cleared":      return {        ...current,        output: resetOutput(current.output, "", maxBufferBytes),        error: null,        version: current.version + 1,      };    case "exited":      return {        ...current,        status: "exited",        error: null,        version: current.version + 1,      };    case "closed":      return {        ...current,        status: "closed",        error: null,        version: current.version + 1,      };    case "error":      return {        ...current,        status: "error",        error: event.message,        version: current.version + 1,      };    case "activity":      return current;  }} export function applyTerminalMetadataStreamEvent(  current: ReadonlyArray<TerminalSummary>,  event: TerminalMetadataStreamEvent,): ReadonlyArray<TerminalSummary> {  if (event.type === "snapshot") {    return event.terminals;  }  if (event.type === "remove") {    return current.filter(      (terminal) =>        terminal.threadId !== event.threadId || terminal.terminalId !== event.terminalId,    );  }  const next = current.filter(    (terminal) =>      terminal.threadId !== event.terminal.threadId ||      terminal.terminalId !== event.terminal.terminalId,  );  return [...next, event.terminal];}