Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
215 lines
6.1 KiB
TypeScript
215 lines
6.1 KiB
TypeScript
export type TerminalContextReadRange = "viewport" | "tail" | "head" | "lines";
|
|
|
|
export type TerminalContextReadSource = "live" | "snapshot";
|
|
|
|
export const TERMINAL_CONTEXT_DEFAULT_MAX_LINES = 80;
|
|
export const TERMINAL_CONTEXT_MAX_LINES = 300;
|
|
|
|
export interface TerminalContextReadRequest {
|
|
sessionId?: string;
|
|
range?: TerminalContextReadRange;
|
|
startLine?: number;
|
|
maxLines?: number;
|
|
}
|
|
|
|
export interface TerminalContextLineWindowInput {
|
|
range?: TerminalContextReadRange;
|
|
totalLines: number;
|
|
viewportStartLine?: number;
|
|
viewportEndLine?: number;
|
|
startLine?: number;
|
|
maxLines?: number;
|
|
}
|
|
|
|
export interface TerminalContextLineWindow {
|
|
startLine: number;
|
|
endLine: number;
|
|
maxLines: number;
|
|
}
|
|
|
|
export interface TerminalContextReadResult {
|
|
ok: true;
|
|
sessionId: string;
|
|
label?: string;
|
|
range: TerminalContextReadRange;
|
|
content: string;
|
|
totalLines: number;
|
|
startLine: number;
|
|
endLine: number;
|
|
returnedLines: number;
|
|
hasMoreBefore: boolean;
|
|
hasMoreAfter: boolean;
|
|
source: TerminalContextReadSource;
|
|
alternateScreen?: boolean;
|
|
}
|
|
|
|
export type TerminalContextReader = (
|
|
request: TerminalContextReadRequest & { sessionId: string },
|
|
) => Promise<TerminalContextReadResult | { ok: false; error: string }>;
|
|
|
|
export interface BuildTerminalContextReadResultInput {
|
|
sessionId: string;
|
|
label?: string;
|
|
fullText: string;
|
|
range?: TerminalContextReadRange;
|
|
startLine?: number;
|
|
maxLines?: number;
|
|
source: TerminalContextReadSource;
|
|
alternateScreen?: boolean;
|
|
viewportStartLine?: number;
|
|
viewportEndLine?: number;
|
|
}
|
|
|
|
export interface TerminalContextSnapshotTextInput {
|
|
scrollbackText?: string;
|
|
viewportText?: string;
|
|
pendingText?: string;
|
|
}
|
|
|
|
export interface TerminalContextSnapshotText {
|
|
fullText: string;
|
|
viewportStartLine?: number;
|
|
viewportEndLine?: number;
|
|
}
|
|
|
|
export function normalizeTerminalContextMaxLines(value: unknown): number {
|
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
return TERMINAL_CONTEXT_DEFAULT_MAX_LINES;
|
|
}
|
|
return Math.min(
|
|
TERMINAL_CONTEXT_MAX_LINES,
|
|
Math.max(1, Math.round(value)),
|
|
);
|
|
}
|
|
|
|
export function normalizeTerminalContextRange(value: unknown): TerminalContextReadRange {
|
|
if (
|
|
value === "viewport" ||
|
|
value === "tail" ||
|
|
value === "head" ||
|
|
value === "lines"
|
|
) {
|
|
return value;
|
|
}
|
|
return "viewport";
|
|
}
|
|
|
|
export function resolveTerminalContextLineWindow(
|
|
input: TerminalContextLineWindowInput,
|
|
): TerminalContextLineWindow {
|
|
const range = normalizeTerminalContextRange(input.range);
|
|
const maxLines = normalizeTerminalContextMaxLines(input.maxLines);
|
|
const totalLines = Math.max(0, Math.floor(input.totalLines));
|
|
|
|
if (totalLines === 0) {
|
|
return { startLine: 0, endLine: -1, maxLines };
|
|
}
|
|
|
|
if (range === "head") {
|
|
return {
|
|
startLine: 0,
|
|
endLine: Math.min(totalLines - 1, maxLines - 1),
|
|
maxLines,
|
|
};
|
|
}
|
|
|
|
if (range === "tail") {
|
|
return {
|
|
startLine: Math.max(0, totalLines - maxLines),
|
|
endLine: totalLines - 1,
|
|
maxLines,
|
|
};
|
|
}
|
|
|
|
if (range === "lines") {
|
|
const start = clampLine(input.startLine, totalLines);
|
|
return {
|
|
startLine: start,
|
|
endLine: Math.min(totalLines - 1, start + maxLines - 1),
|
|
maxLines,
|
|
};
|
|
}
|
|
|
|
const viewportStart = clampLine(input.viewportStartLine, totalLines);
|
|
const viewportEnd = clampLine(input.viewportEndLine, totalLines);
|
|
const rawStart = Math.min(viewportStart, viewportEnd);
|
|
const end = Math.max(viewportStart, viewportEnd);
|
|
const start = Math.max(rawStart, end - maxLines + 1);
|
|
return { startLine: start, endLine: end, maxLines };
|
|
}
|
|
|
|
export function buildTerminalContextReadResult(
|
|
input: BuildTerminalContextReadResultInput,
|
|
): TerminalContextReadResult {
|
|
const range = normalizeTerminalContextRange(input.range);
|
|
const lines = splitTerminalContextLines(input.fullText);
|
|
const totalLines = lines.length;
|
|
const window = resolveTerminalContextLineWindow({
|
|
range,
|
|
totalLines,
|
|
viewportStartLine: input.viewportStartLine,
|
|
viewportEndLine: input.viewportEndLine,
|
|
startLine: input.startLine,
|
|
maxLines: input.maxLines,
|
|
});
|
|
const selectedLines = window.endLine >= window.startLine
|
|
? lines.slice(window.startLine, window.endLine + 1)
|
|
: [];
|
|
|
|
return {
|
|
ok: true,
|
|
sessionId: input.sessionId,
|
|
label: input.label,
|
|
range,
|
|
content: selectedLines.join("\n"),
|
|
totalLines,
|
|
startLine: window.startLine,
|
|
endLine: window.endLine,
|
|
returnedLines: selectedLines.length,
|
|
hasMoreBefore: window.startLine > 0,
|
|
hasMoreAfter: window.endLine >= 0 && window.endLine < totalLines - 1,
|
|
source: input.source,
|
|
alternateScreen: input.alternateScreen,
|
|
};
|
|
}
|
|
|
|
export function buildTerminalContextSnapshotText(
|
|
input: TerminalContextSnapshotTextInput,
|
|
): TerminalContextSnapshotText {
|
|
const parts: Array<{ kind: "scrollback" | "viewport" | "pending"; text: string }> = [];
|
|
if (input.scrollbackText) parts.push({ kind: "scrollback", text: input.scrollbackText });
|
|
if (input.viewportText) parts.push({ kind: "viewport", text: input.viewportText });
|
|
if (input.pendingText) parts.push({ kind: "pending", text: input.pendingText });
|
|
|
|
let lineOffset = 0;
|
|
let viewportStartLine: number | undefined;
|
|
let viewportEndLine: number | undefined;
|
|
for (const part of parts) {
|
|
const lineCount = splitTerminalContextLines(part.text).length;
|
|
if (part.kind === "viewport") {
|
|
viewportStartLine = lineOffset;
|
|
viewportEndLine = lineOffset + lineCount - 1;
|
|
} else if (part.kind === "pending" && viewportStartLine !== undefined) {
|
|
viewportEndLine = lineOffset + lineCount - 1;
|
|
}
|
|
lineOffset += lineCount;
|
|
}
|
|
|
|
return {
|
|
fullText: parts.map((part) => part.text).join("\n"),
|
|
viewportStartLine,
|
|
viewportEndLine,
|
|
};
|
|
}
|
|
|
|
function splitTerminalContextLines(text: string): string[] {
|
|
if (!text) return [];
|
|
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
}
|
|
|
|
function clampLine(value: unknown, totalLines: number): number {
|
|
if (totalLines <= 0) return 0;
|
|
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
return Math.min(totalLines - 1, Math.max(0, Math.floor(value)));
|
|
}
|