chore(app): add terminal input/output debug logging

This commit is contained in:
Mohamed Boudra
2026-02-15 15:43:07 +07:00
parent adb511d2be
commit 75dc802a3a
5 changed files with 327 additions and 1 deletions
@@ -29,6 +29,10 @@ import {
TerminalStreamController,
type TerminalStreamControllerStatus,
} from "@/terminal/runtime/terminal-stream-controller";
import {
summarizeTerminalText,
terminalDebugLog,
} from "@/terminal/runtime/terminal-debug";
import TerminalEmulator from "./terminal-emulator";
interface TerminalPaneProps {
@@ -459,10 +463,32 @@ export function TerminalPane({ serverId, cwd }: TerminalPaneProps) {
}
): boolean => {
if (!client || activeStreamId === null) {
terminalDebugLog({
scope: "terminal-pane",
event: "input:key:drop-no-stream",
details: {
key: input.key,
ctrl: input.ctrl,
shift: input.shift,
alt: input.alt,
activeStreamId,
},
});
return false;
}
const normalizedKey = normalizeTerminalTransportKey(input.key);
terminalDebugLog({
scope: "terminal-pane",
event: "input:key:send",
details: {
key: normalizedKey,
ctrl: input.ctrl,
shift: input.shift,
alt: input.alt,
activeStreamId,
},
});
client.sendTerminalStreamKey(activeStreamId, {
key: normalizedKey,
ctrl: input.ctrl,
@@ -480,6 +506,15 @@ export function TerminalPane({ serverId, cwd }: TerminalPaneProps) {
if (data.length === 0) {
return;
}
terminalDebugLog({
scope: "terminal-pane",
event: "input:data:received",
details: {
length: data.length,
preview: summarizeTerminalText({ text: data, maxChars: 80 }),
activeStreamId,
},
});
if (hasPendingTerminalModifiers(modifiers)) {
const pendingResolution = resolvePendingModifierDataInput({
@@ -507,8 +542,25 @@ export function TerminalPane({ serverId, cwd }: TerminalPaneProps) {
}
if (!client || activeStreamId === null) {
terminalDebugLog({
scope: "terminal-pane",
event: "input:data:drop-no-stream",
details: {
length: data.length,
preview: summarizeTerminalText({ text: data, maxChars: 80 }),
},
});
return;
}
terminalDebugLog({
scope: "terminal-pane",
event: "input:data:send",
details: {
length: data.length,
preview: summarizeTerminalText({ text: data, maxChars: 80 }),
activeStreamId,
},
});
client.sendTerminalStreamInput(activeStreamId, data);
},
[
@@ -539,6 +591,15 @@ export function TerminalPane({ serverId, cwd }: TerminalPaneProps) {
return;
}
lastReportedSizeRef.current = { rows: normalizedRows, cols: normalizedCols };
terminalDebugLog({
scope: "terminal-pane",
event: "display:resize:send",
details: {
terminalId: selectedTerminalId,
rows: normalizedRows,
cols: normalizedCols,
},
});
client.sendTerminalInput(selectedTerminalId, {
type: "resize",
rows: normalizedRows,
@@ -0,0 +1,77 @@
type TerminalDebugGlobal = {
__PASEO_TERMINAL_DEBUG?: boolean;
};
type TerminalDebugLogInput = {
scope: string;
event: string;
details?: Record<string, unknown>;
};
function resolveGlobalDebugFlag(): boolean | null {
if (typeof globalThis === "undefined") {
return null;
}
const value = (globalThis as TerminalDebugGlobal).__PASEO_TERMINAL_DEBUG;
if (typeof value === "boolean") {
return value;
}
return null;
}
export function isTerminalDebugEnabled(): boolean {
const globalFlag = resolveGlobalDebugFlag();
if (globalFlag !== null) {
return globalFlag;
}
return process.env.NODE_ENV === "development";
}
export function terminalDebugLog(input: TerminalDebugLogInput): void {
if (!isTerminalDebugEnabled()) {
return;
}
const payload = input.details
? { ...input.details, ts: Date.now() }
: { ts: Date.now() };
console.log(`[terminal][${input.scope}] ${input.event}`, payload);
}
function escapeControlBytes(input: { text: string }): string {
let output = "";
for (const char of input.text) {
const code = char.charCodeAt(0);
if (code === 10) {
output += "\\n";
continue;
}
if (code === 13) {
output += "\\r";
continue;
}
if (code === 9) {
output += "\\t";
continue;
}
if (code < 32 || code === 127) {
output += `\\x${code.toString(16).padStart(2, "0")}`;
continue;
}
output += char;
}
return output;
}
export function summarizeTerminalText(input: {
text: string;
maxChars?: number;
}): string {
const maxChars = input.maxChars ?? 80;
const escaped = escapeControlBytes({ text: input.text });
if (escaped.length <= maxChars) {
return escaped;
}
return `${escaped.slice(0, maxChars)}…`;
}
@@ -8,6 +8,7 @@ import {
normalizeTerminalTransportKey,
shouldInterceptDomTerminalKey,
} from "@/utils/terminal-keys";
import { summarizeTerminalText, terminalDebugLog } from "./terminal-debug";
export type TerminalEmulatorRuntimeTheme = {
backgroundColor: string;
@@ -82,6 +83,13 @@ export class TerminalEmulatorRuntime {
}
mount(input: TerminalEmulatorRuntimeMountInput): void {
terminalDebugLog({
scope: "emulator-runtime",
event: "mount:start",
details: {
initialOutputLength: input.initialOutputText.length,
},
});
this.unmount();
input.host.innerHTML = "";
@@ -138,6 +146,15 @@ export class TerminalEmulatorRuntime {
}
this.lastSize = { rows: nextRows, cols: nextCols };
terminalDebugLog({
scope: "emulator-runtime",
event: "resize:emit",
details: {
rows: nextRows,
cols: nextCols,
force,
},
});
this.callbacks.onResize?.({
rows: nextRows,
cols: nextCols,
@@ -147,6 +164,14 @@ export class TerminalEmulatorRuntime {
fitAndEmitResize(true);
const inputDisposable = terminal.onData((data) => {
terminalDebugLog({
scope: "emulator-runtime",
event: "input:onData",
details: {
length: data.length,
preview: summarizeTerminalText({ text: data, maxChars: 64 }),
},
});
this.callbacks.onInput?.(data);
});
@@ -182,6 +207,17 @@ export class TerminalEmulatorRuntime {
key: normalizeTerminalTransportKey(normalizedKey),
...modifiers,
});
terminalDebugLog({
scope: "emulator-runtime",
event: "key:intercepted",
details: {
key: normalizedKey,
ctrl: modifiers.ctrl,
shift: modifiers.shift,
alt: modifiers.alt,
meta: modifiers.meta,
},
});
if (this.pendingModifiers.ctrl || this.pendingModifiers.shift || this.pendingModifiers.alt) {
this.callbacks.onPendingModifiersConsumed?.();
@@ -238,6 +274,17 @@ export class TerminalEmulatorRuntime {
if (input.initialOutputText.length > 0) {
terminal.write(input.initialOutputText);
terminalDebugLog({
scope: "emulator-runtime",
event: "output:initial-write",
details: {
length: input.initialOutputText.length,
preview: summarizeTerminalText({
text: input.initialOutputText,
maxChars: 96,
}),
},
});
}
const disposables: TerminalEmulatorRuntimeDisposables = {
@@ -296,11 +343,24 @@ export class TerminalEmulatorRuntime {
return;
}
this.pendingWriteText += input.text;
terminalDebugLog({
scope: "emulator-runtime",
event: "output:enqueue",
details: {
chunkLength: input.text.length,
queueLength: this.pendingWriteText.length,
preview: summarizeTerminalText({ text: input.text, maxChars: 64 }),
},
});
this.scheduleWriteFlush();
}
clear(): void {
this.pendingWriteText = "";
terminalDebugLog({
scope: "emulator-runtime",
event: "output:clear",
});
this.terminal?.reset();
}
@@ -309,6 +369,13 @@ export class TerminalEmulatorRuntime {
}
unmount(): void {
terminalDebugLog({
scope: "emulator-runtime",
event: "mount:unmount",
details: {
pendingWriteLength: this.pendingWriteText.length,
},
});
this.pendingWriteText = "";
this.isWriteFlushQueued = false;
@@ -340,6 +407,14 @@ export class TerminalEmulatorRuntime {
}
const text = this.pendingWriteText;
this.pendingWriteText = "";
terminalDebugLog({
scope: "emulator-runtime",
event: "output:flush",
details: {
length: text.length,
preview: summarizeTerminalText({ text, maxChars: 96 }),
},
});
this.terminal.write(text);
}
@@ -4,6 +4,7 @@ import {
readTerminalOutputBuffer,
type TerminalOutputBuffer,
} from "@/utils/terminal-output-buffer";
import { summarizeTerminalText, terminalDebugLog } from "./terminal-debug";
export type TerminalOutputChunk = {
sequence: number;
@@ -50,6 +51,14 @@ export class TerminalOutputPump {
return;
}
terminalDebugLog({
scope: "output-pump",
event: "selected-terminal:set",
details: {
previousTerminalId: this.selectedTerminalId,
nextTerminalId: input.terminalId,
},
});
this.clearSelectedChunkFlushTimer();
this.selectedChunkAccumulator = "";
this.selectedTerminalId = input.terminalId;
@@ -77,6 +86,16 @@ export class TerminalOutputPump {
}
this.selectedChunkAccumulator += input.text;
terminalDebugLog({
scope: "output-pump",
event: "selected-terminal:accumulate",
details: {
terminalId: input.terminalId,
appendedLength: input.text.length,
accumulatorLength: this.selectedChunkAccumulator.length,
preview: summarizeTerminalText({ text: input.text, maxChars: 80 }),
},
});
this.scheduleSelectedChunkFlush();
}
@@ -134,6 +153,15 @@ export class TerminalOutputPump {
const text = this.selectedChunkAccumulator;
this.selectedChunkAccumulator = "";
terminalDebugLog({
scope: "output-pump",
event: "selected-terminal:flush",
details: {
terminalId: this.selectedTerminalId,
textLength: text.length,
preview: summarizeTerminalText({ text, maxChars: 96 }),
},
});
this.emitSelectedChunk({ text });
}
@@ -6,6 +6,7 @@ import {
waitForDuration,
withPromiseTimeout,
} from "@/utils/terminal-attach";
import { summarizeTerminalText, terminalDebugLog } from "./terminal-debug";
export type TerminalStreamControllerAttachPayload = {
streamId: number | null;
@@ -100,6 +101,14 @@ export class TerminalStreamController {
if (this.isDisposed) {
return;
}
terminalDebugLog({
scope: "stream-controller",
event: "terminal:set",
details: {
previousTerminalId: this.selectedTerminalId,
nextTerminalId: input.terminalId,
},
});
const nextTerminalId = input.terminalId;
const previousTerminalId = this.selectedTerminalId;
@@ -144,6 +153,14 @@ export class TerminalStreamController {
if (this.isDisposed) {
return;
}
terminalDebugLog({
scope: "stream-controller",
event: "stream:exit",
details: {
terminalId: input.terminalId,
streamId: input.streamId,
},
});
const activeStream = this.activeStream;
if (!activeStream) {
@@ -217,6 +234,16 @@ export class TerminalStreamController {
if (!this.isAttachGenerationCurrent({ generation: input.generation, terminalId: input.terminalId })) {
return;
}
terminalDebugLog({
scope: "stream-controller",
event: "attach:attempt",
details: {
terminalId: input.terminalId,
generation: input.generation,
attempt,
maxAttachAttempts,
},
});
try {
const preferredSize = this.options.getPreferredSize();
@@ -244,6 +271,15 @@ export class TerminalStreamController {
if (attachPayload.error || typeof attachPayload.streamId !== "number") {
lastErrorMessage = attachPayload.error ?? "Unable to attach terminal stream";
terminalDebugLog({
scope: "stream-controller",
event: "attach:response-error",
details: {
terminalId: input.terminalId,
attempt,
error: lastErrorMessage,
},
});
const hasRemainingAttempts = attempt < maxAttachAttempts - 1;
if (hasRemainingAttempts && isRetryableError({ message: lastErrorMessage })) {
await waitForDelay({ durationMs: getRetryDelayMs({ attempt }) });
@@ -287,6 +323,16 @@ export class TerminalStreamController {
decoder,
unsubscribe,
};
terminalDebugLog({
scope: "stream-controller",
event: "attach:success",
details: {
terminalId: input.terminalId,
streamId,
currentOffset: attachPayload.currentOffset,
reset: attachPayload.reset,
},
});
this.updateStatus({
terminalId: input.terminalId,
streamId,
@@ -297,6 +343,15 @@ export class TerminalStreamController {
} catch (error) {
lastErrorMessage =
error instanceof Error ? error.message : "Unable to attach terminal stream";
terminalDebugLog({
scope: "stream-controller",
event: "attach:exception",
details: {
terminalId: input.terminalId,
attempt,
error: lastErrorMessage,
},
});
const hasRemainingAttempts = attempt < maxAttachAttempts - 1;
if (hasRemainingAttempts && isRetryableError({ message: lastErrorMessage })) {
await waitForDelay({ durationMs: getRetryDelayMs({ attempt }) });
@@ -345,6 +400,18 @@ export class TerminalStreamController {
if (text.length === 0) {
return;
}
terminalDebugLog({
scope: "stream-controller",
event: "stream:chunk",
details: {
terminalId: input.terminalId,
streamId: input.streamId,
endOffset: input.chunk.endOffset,
byteLength: input.chunk.data.byteLength,
textLength: text.length,
preview: summarizeTerminalText({ text, maxChars: 96 }),
},
});
this.options.onChunk({
terminalId: input.terminalId,
@@ -357,6 +424,15 @@ export class TerminalStreamController {
if (!activeStream) {
return;
}
terminalDebugLog({
scope: "stream-controller",
event: "stream:detach",
details: {
terminalId: activeStream.terminalId,
streamId: activeStream.streamId,
shouldDetach: input.shouldDetach,
},
});
this.activeStream = null;
try {
@@ -403,7 +479,16 @@ export class TerminalStreamController {
private updateStatus(status: TerminalStreamControllerStatus): void {
this.status = status;
terminalDebugLog({
scope: "stream-controller",
event: "status:update",
details: {
terminalId: status.terminalId,
streamId: status.streamId,
isAttaching: status.isAttaching,
error: status.error,
},
});
this.options.onStatusChange?.(status);
}
}