feat: add ensure_pty for deferred PTY creation with client dimensions

New ensure_pty message: creates PTY if not running, or resizes and
sends replay buffer if already active. This allows the frontend to
request PTY creation with actual terminal dimensions after xterm.js
FitAddon calculates the correct cols/rows for the container.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 01:41:47 +03:00
parent c127faccef
commit 5b07dfe323
2 changed files with 23 additions and 11 deletions

View File

@ -29,6 +29,8 @@ export type BackendMessageType =
| 'create_pty' | 'create_pty'
| 'kill_pty' | 'kill_pty'
| 'resize' | 'resize'
| 'ensure_pty'
| 'request_replay'
| 'list_directories' | 'list_directories'
| 'list_sessions'; | 'list_sessions';

View File

@ -81,13 +81,16 @@ class ClaudeCliProxy {
case 'create_pty': case 'create_pty':
this.handleCreatePty(msg.payload as CreatePtyPayload); this.handleCreatePty(msg.payload as CreatePtyPayload);
break; break;
case 'ensure_pty':
this.handleEnsurePty(msg.payload as CreatePtyPayload);
break;
case 'kill_pty': case 'kill_pty':
this.handleKillPty(msg.payload as KillPtyPayload); this.handleKillPty(msg.payload as KillPtyPayload);
break; break;
case 'resize': case 'resize':
this.handleResize(msg.payload as ResizePayload); this.handleResize(msg.payload as ResizePayload);
break; break;
case 'request_replay' as any: { case 'request_replay': {
const { chatId } = msg.payload as { chatId: string }; const { chatId } = msg.payload as { chatId: string };
const replay = this.pty.getReplayBuffer(chatId); const replay = this.pty.getReplayBuffer(chatId);
if (replay) { if (replay) {
@ -116,23 +119,15 @@ class ClaudeCliProxy {
const activePtys = new Set(this.pty.listPtys()); const activePtys = new Set(this.pty.listPtys());
const chatIds = new Set(payload.chats.map((c) => c.id)); const chatIds = new Set(payload.chats.map((c) => c.id));
// Send replay + pty_ready for already running PTYs
// PTYs for new chats will be created via ensure_pty from frontend
for (const chat of payload.chats) { for (const chat of payload.chats) {
if (activePtys.has(chat.id)) { if (activePtys.has(chat.id)) {
// PTY exists — send replay + ready
const replay = this.pty.getReplayBuffer(chat.id); const replay = this.pty.getReplayBuffer(chat.id);
if (replay) { if (replay) {
this.ws.sendBinary(encodeBinaryFrame(DIR_PTY_OUTPUT, chat.id, replay)); this.ws.sendBinary(encodeBinaryFrame(DIR_PTY_OUTPUT, chat.id, replay));
} }
this.ws.send({ type: 'pty_ready', payload: { chatId: chat.id } }); this.ws.send({ type: 'pty_ready', payload: { chatId: chat.id } });
} else if (chat.workDir) {
// PTY not running — create it
this.handleCreatePty({
chatId: chat.id,
dir: chat.workDir,
resumeSessionId: chat.sessionId ?? undefined,
cols: chat.cols ?? 120,
rows: chat.rows ?? 40,
});
} }
} }
@ -166,6 +161,21 @@ class ClaudeCliProxy {
this.ws.send({ type: 'pty_ready', payload: { chatId: payload.chatId } }); this.ws.send({ type: 'pty_ready', payload: { chatId: payload.chatId } });
} }
private handleEnsurePty(payload: CreatePtyPayload): void {
if (this.pty.hasPty(payload.chatId)) {
// PTY already exists — resize to match client, send replay + ready
this.pty.resizePty(payload.chatId, payload.cols, payload.rows);
const replay = this.pty.getReplayBuffer(payload.chatId);
if (replay) {
this.ws.sendBinary(encodeBinaryFrame(DIR_PTY_OUTPUT, payload.chatId, replay));
}
this.ws.send({ type: 'pty_ready', payload: { chatId: payload.chatId } });
} else {
// PTY doesn't exist — create with client's dimensions
this.handleCreatePty(payload);
}
}
private handleKillPty(payload: KillPtyPayload): void { private handleKillPty(payload: KillPtyPayload): void {
this.pty.killPty(payload.chatId); this.pty.killPty(payload.chatId);
this.ws.send({ type: 'pty_closed', payload: { chatId: payload.chatId, exitCode: 0 } }); this.ws.send({ type: 'pty_closed', payload: { chatId: payload.chatId, exitCode: 0 } });