chore(app): add terminal input/output debug logging
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user