Update files
This commit is contained in:
@@ -34,6 +34,21 @@ export interface CellChange {
|
||||
cell: Cell;
|
||||
}
|
||||
|
||||
export interface TerminalRawChunk {
|
||||
data: string;
|
||||
startOffset: number;
|
||||
endOffset: number;
|
||||
replay: boolean;
|
||||
}
|
||||
|
||||
export interface TerminalRawSubscriptionResult {
|
||||
unsubscribe: () => void;
|
||||
replayedFrom: number;
|
||||
currentOffset: number;
|
||||
earliestAvailableOffset: number;
|
||||
reset: boolean;
|
||||
}
|
||||
|
||||
export type ClientMessage =
|
||||
| { type: "input"; data: string }
|
||||
| { type: "resize"; rows: number; cols: number }
|
||||
@@ -49,6 +64,11 @@ export interface TerminalSession {
|
||||
cwd: string;
|
||||
send(msg: ClientMessage): void;
|
||||
subscribe(listener: (msg: ServerMessage) => void): () => void;
|
||||
subscribeRaw(
|
||||
listener: (chunk: TerminalRawChunk) => void,
|
||||
options?: { fromOffset?: number }
|
||||
): TerminalRawSubscriptionResult;
|
||||
getOutputOffset(): number;
|
||||
getState(): TerminalState;
|
||||
kill(): void;
|
||||
}
|
||||
@@ -169,6 +189,15 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
|
||||
|
||||
const id = randomUUID();
|
||||
const listeners = new Set<(msg: ServerMessage) => void>();
|
||||
const rawListeners = new Set<(chunk: TerminalRawChunk) => void>();
|
||||
const rawOutputChunks: Array<{
|
||||
startOffset: number;
|
||||
endOffset: number;
|
||||
data: string;
|
||||
}> = [];
|
||||
const maxRawBufferBytes = 8 * 1024 * 1024;
|
||||
let rawBufferBytes = 0;
|
||||
let outputOffset = 0;
|
||||
let killed = false;
|
||||
|
||||
// Create xterm.js headless terminal
|
||||
@@ -192,9 +221,66 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
|
||||
},
|
||||
});
|
||||
|
||||
function appendRawOutputChunk(data: string): void {
|
||||
const chunkBytes = Buffer.byteLength(data);
|
||||
if (chunkBytes <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startOffset = outputOffset;
|
||||
const endOffset = startOffset + chunkBytes;
|
||||
outputOffset = endOffset;
|
||||
|
||||
rawOutputChunks.push({
|
||||
startOffset,
|
||||
endOffset,
|
||||
data,
|
||||
});
|
||||
rawBufferBytes += chunkBytes;
|
||||
|
||||
while (rawBufferBytes > maxRawBufferBytes && rawOutputChunks.length > 0) {
|
||||
const removed = rawOutputChunks.shift();
|
||||
if (!removed) {
|
||||
break;
|
||||
}
|
||||
rawBufferBytes -= removed.endOffset - removed.startOffset;
|
||||
}
|
||||
|
||||
const chunk: TerminalRawChunk = {
|
||||
data,
|
||||
startOffset,
|
||||
endOffset,
|
||||
replay: false,
|
||||
};
|
||||
for (const listener of rawListeners) {
|
||||
listener(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
function getEarliestAvailableOffset(): number {
|
||||
const earliest = rawOutputChunks[0];
|
||||
return earliest ? earliest.startOffset : outputOffset;
|
||||
}
|
||||
|
||||
function alignOffsetToChunkBoundary(offset: number): number {
|
||||
if (offset === outputOffset) {
|
||||
return offset;
|
||||
}
|
||||
for (const chunk of rawOutputChunks) {
|
||||
if (offset === chunk.startOffset || offset === chunk.endOffset) {
|
||||
return offset;
|
||||
}
|
||||
if (offset > chunk.startOffset && offset < chunk.endOffset) {
|
||||
return chunk.startOffset;
|
||||
}
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
// Pipe PTY output to terminal emulator
|
||||
ptyProcess.onData((data) => {
|
||||
if (killed) return;
|
||||
appendRawOutputChunk(data);
|
||||
terminal.write(data, () => {
|
||||
// Notify listeners of changes
|
||||
const state = getState();
|
||||
@@ -254,11 +340,51 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
|
||||
};
|
||||
}
|
||||
|
||||
function subscribeRaw(
|
||||
listener: (chunk: TerminalRawChunk) => void,
|
||||
options?: { fromOffset?: number }
|
||||
): TerminalRawSubscriptionResult {
|
||||
const requestedOffset = Math.max(0, Math.floor(options?.fromOffset ?? 0));
|
||||
const earliestAvailableOffset = getEarliestAvailableOffset();
|
||||
const clampedOffset = Math.max(requestedOffset, earliestAvailableOffset);
|
||||
const replayedFrom = alignOffsetToChunkBoundary(clampedOffset);
|
||||
const reset = replayedFrom !== requestedOffset;
|
||||
|
||||
for (const chunk of rawOutputChunks) {
|
||||
if (chunk.endOffset <= replayedFrom) {
|
||||
continue;
|
||||
}
|
||||
listener({
|
||||
data: chunk.data,
|
||||
startOffset: chunk.startOffset,
|
||||
endOffset: chunk.endOffset,
|
||||
replay: true,
|
||||
});
|
||||
}
|
||||
|
||||
rawListeners.add(listener);
|
||||
|
||||
return {
|
||||
unsubscribe: () => {
|
||||
rawListeners.delete(listener);
|
||||
},
|
||||
replayedFrom,
|
||||
currentOffset: outputOffset,
|
||||
earliestAvailableOffset,
|
||||
reset,
|
||||
};
|
||||
}
|
||||
|
||||
function getOutputOffset(): number {
|
||||
return outputOffset;
|
||||
}
|
||||
|
||||
function kill(): void {
|
||||
if (killed) return;
|
||||
killed = true;
|
||||
ptyProcess.kill();
|
||||
terminal.dispose();
|
||||
rawListeners.clear();
|
||||
}
|
||||
|
||||
// Small delay to let shell initialize
|
||||
@@ -268,9 +394,11 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
|
||||
id,
|
||||
name,
|
||||
cwd,
|
||||
send,
|
||||
subscribe,
|
||||
getState,
|
||||
kill,
|
||||
send,
|
||||
subscribe,
|
||||
subscribeRaw,
|
||||
getOutputOffset,
|
||||
getState,
|
||||
kill,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user