refactor: extract remote launcher base class for code reuse

This commit is contained in:
weishu
2026-01-05 16:50:22 +08:00
parent 0e360f0a48
commit e8252c601f
3 changed files with 918 additions and 815 deletions
@@ -0,0 +1,129 @@
import { render } from 'ink';
import type { ReactElement } from 'react';
import { MessageBuffer } from '@/ui/ink/messageBuffer';
import { restoreTerminalState } from '@/ui/terminalState';
export type RemoteLauncherExitReason = 'switch' | 'exit';
export type RemoteLauncherDisplayContext = {
messageBuffer: MessageBuffer;
logPath?: string;
onExit: () => void | Promise<void>;
onSwitchToLocal: () => void | Promise<void>;
};
export type RemoteLauncherTerminalHandlers = {
onExit: () => void | Promise<void>;
onSwitchToLocal: () => void | Promise<void>;
};
export type RemoteLauncherAbortHandlers = {
onAbort: () => void | Promise<void>;
onSwitch: () => void | Promise<void>;
};
type RpcHandlerManagerLike = {
registerHandler<TRequest = unknown, TResponse = unknown>(
method: string,
handler: (params: TRequest) => Promise<TResponse> | TResponse
): void;
};
export abstract class RemoteLauncherBase {
protected readonly messageBuffer: MessageBuffer;
protected readonly hasTTY: boolean;
protected readonly logPath?: string;
protected exitReason: RemoteLauncherExitReason | null = null;
protected shouldExit: boolean = false;
private inkInstance: ReturnType<typeof render> | null = null;
protected constructor(logPath?: string) {
this.logPath = logPath;
this.hasTTY = Boolean(process.stdout.isTTY && process.stdin.isTTY);
this.messageBuffer = new MessageBuffer();
}
protected abstract createDisplay(context: RemoteLauncherDisplayContext): ReactElement;
protected abstract runMainLoop(): Promise<void>;
protected abstract cleanup(): Promise<void>;
protected setupTerminal(handlers: RemoteLauncherTerminalHandlers): void {
if (this.hasTTY) {
console.clear();
this.inkInstance = render(this.createDisplay({
messageBuffer: this.messageBuffer,
logPath: this.logPath,
onExit: handlers.onExit,
onSwitchToLocal: handlers.onSwitchToLocal
}), {
exitOnCtrlC: false,
patchConsole: false
});
}
if (this.hasTTY) {
process.stdin.resume();
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
}
process.stdin.setEncoding('utf8');
}
}
protected setupAbortHandlers(
rpcHandlerManager: RpcHandlerManagerLike,
handlers: RemoteLauncherAbortHandlers
): void {
rpcHandlerManager.registerHandler('abort', async () => {
await handlers.onAbort();
});
rpcHandlerManager.registerHandler('switch', async () => {
await handlers.onSwitch();
});
}
protected clearAbortHandlers(rpcHandlerManager: RpcHandlerManagerLike): void {
rpcHandlerManager.registerHandler('abort', async () => {});
rpcHandlerManager.registerHandler('switch', async () => {});
}
protected async requestExit(
reason: RemoteLauncherExitReason,
handler: () => void | Promise<void>
): Promise<void> {
if (!this.exitReason) {
this.exitReason = reason;
}
this.shouldExit = true;
await handler();
}
protected finalizeTerminal(): void {
restoreTerminalState();
if (this.hasTTY) {
try {
process.stdin.pause();
} catch {
}
}
if (this.inkInstance) {
this.inkInstance.unmount();
}
this.messageBuffer.clear();
}
protected async start(handlers: RemoteLauncherTerminalHandlers): Promise<RemoteLauncherExitReason> {
this.setupTerminal(handlers);
try {
await this.runMainLoop();
} finally {
await this.cleanup();
this.finalizeTerminal();
}
return this.exitReason || 'exit';
}
}