mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-25 14:57:37 +00:00
Add terminal size reset feature and fix source maps (#72)
- Add POST /api/sessions/:sessionId/reset-size endpoint to reset terminal size when clients disconnect - Implement reset-size control pipe command in PTY manager - Update session-view component to call reset-size on unmount - Add terminal resize event listener in fwd.ts to track terminal size changes - Fix source maps configuration for development mode: - Set inline source maps with embedded sources in esbuild config - Add source map settings to TypeScript configs - Set NODE_ENV to development for dev builds This ensures external terminals resize back to their actual size when the last web client disconnects. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d37f813b83
commit
f339e69f9a
9 changed files with 206 additions and 11 deletions
|
|
@ -28,14 +28,22 @@ const commonOptions = {
|
|||
compilerOptions: {
|
||||
experimentalDecorators: true,
|
||||
useDefineForClassFields: false,
|
||||
sourceMap: true,
|
||||
inlineSourceMap: true,
|
||||
inlineSources: true,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const devOptions = {
|
||||
...commonOptions,
|
||||
sourcemap: true,
|
||||
sourcemap: 'inline',
|
||||
sourcesContent: true,
|
||||
minify: false,
|
||||
define: {
|
||||
...commonOptions.define,
|
||||
'process.env.NODE_ENV': '"development"',
|
||||
},
|
||||
};
|
||||
|
||||
const prodOptions = {
|
||||
|
|
|
|||
|
|
@ -222,6 +222,17 @@ export class SessionView extends LitElement {
|
|||
super.disconnectedCallback();
|
||||
this.connected = false;
|
||||
|
||||
logger.log('SessionView disconnectedCallback called', {
|
||||
sessionId: this.session?.id,
|
||||
sessionStatus: this.session?.status,
|
||||
});
|
||||
|
||||
// Reset terminal size for external terminals when leaving session view
|
||||
if (this.session && this.session.status !== 'exited') {
|
||||
logger.log('Calling resetTerminalSize for session', this.session.id);
|
||||
this.resetTerminalSize();
|
||||
}
|
||||
|
||||
// Remove click outside handler
|
||||
document.removeEventListener('click', this.handleClickOutside);
|
||||
|
||||
|
|
@ -1012,6 +1023,39 @@ export class SessionView extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
private async resetTerminalSize() {
|
||||
if (!this.session) {
|
||||
logger.warn('resetTerminalSize called but no session available');
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log('Sending reset-size request for session', this.session.id);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/sessions/${this.session.id}/reset-size`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...this.authClient.getAuthHeader(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
logger.error('failed to reset terminal size', {
|
||||
status: response.status,
|
||||
sessionId: this.session.id,
|
||||
});
|
||||
} else {
|
||||
logger.log('terminal size reset successfully for session', this.session.id);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('error resetting terminal size', {
|
||||
error,
|
||||
sessionId: this.session.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private refreshTerminalAfterMobileInput() {
|
||||
// After closing mobile input, the viewport changes and the terminal
|
||||
// needs to recalculate its scroll position to avoid getting stuck
|
||||
|
|
|
|||
|
|
@ -80,6 +80,11 @@ export async function startVibeTunnelForward(args: string[]) {
|
|||
logger.debug(`Control path: ${controlPath}`);
|
||||
const ptyManager = new PtyManager(controlPath);
|
||||
|
||||
// Store original terminal dimensions
|
||||
const originalCols = process.stdout.columns || 80;
|
||||
const originalRows = process.stdout.rows || 24;
|
||||
logger.debug(`Original terminal size: ${originalCols}x${originalRows}`);
|
||||
|
||||
try {
|
||||
// Create a human-readable session name
|
||||
const sessionName = generateSessionName(command, cwd);
|
||||
|
|
@ -94,8 +99,8 @@ export async function startVibeTunnelForward(args: string[]) {
|
|||
sessionId: finalSessionId,
|
||||
name: sessionName,
|
||||
workingDir: cwd,
|
||||
cols: process.stdout.columns || 80,
|
||||
rows: process.stdout.rows || 24,
|
||||
cols: originalCols,
|
||||
rows: originalRows,
|
||||
forwardToStdout: true,
|
||||
onExit: async (exitCode: number) => {
|
||||
// Show exit message
|
||||
|
|
@ -103,6 +108,9 @@ export async function startVibeTunnelForward(args: string[]) {
|
|||
chalk.yellow(`\n✓ VibeTunnel session ended`) + chalk.gray(` (exit code: ${exitCode})`)
|
||||
);
|
||||
|
||||
// Remove resize listener
|
||||
process.stdout.removeListener('resize', resizeHandler);
|
||||
|
||||
// Restore terminal settings and clean up stdin
|
||||
if (process.stdin.isTTY) {
|
||||
logger.debug('Restoring terminal to normal mode');
|
||||
|
|
@ -137,6 +145,23 @@ export async function startVibeTunnelForward(args: string[]) {
|
|||
logger.log(chalk.gray('Control directory:'), path.join(controlPath, result.sessionId));
|
||||
logger.log(chalk.gray('Build:'), `${BUILD_DATE} | Commit: ${GIT_COMMIT}`);
|
||||
|
||||
// Set up terminal resize handler
|
||||
const resizeHandler = () => {
|
||||
const cols = process.stdout.columns || 80;
|
||||
const rows = process.stdout.rows || 24;
|
||||
logger.debug(`Terminal resized to ${cols}x${rows}`);
|
||||
|
||||
// Send resize command through PTY manager
|
||||
try {
|
||||
ptyManager.resizeSession(result.sessionId, cols, rows);
|
||||
} catch (error) {
|
||||
logger.error('Failed to resize session:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Listen for terminal resize events
|
||||
process.stdout.on('resize', resizeHandler);
|
||||
|
||||
// Set up raw mode for terminal input
|
||||
if (process.stdin.isTTY) {
|
||||
logger.debug('Setting terminal to raw mode for input forwarding');
|
||||
|
|
|
|||
|
|
@ -8,9 +8,6 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { AsciinemaHeader, AsciinemaEvent, PtyError } from './types.js';
|
||||
import { createLogger } from '../utils/logger.js';
|
||||
|
||||
const logger = createLogger('AsciinemaWriter');
|
||||
|
||||
export class AsciinemaWriter {
|
||||
private writeStream: fs.WriteStream;
|
||||
|
|
@ -165,11 +162,12 @@ export class AsciinemaWriter {
|
|||
// Force immediate disk write to trigger file watchers
|
||||
if (this.fd !== null) {
|
||||
try {
|
||||
fs.fsync(this.fd, (err) => {
|
||||
/*fs.fsync(this.fd, (err) => {
|
||||
if (err) {
|
||||
logger.error(`Failed to fsync asciinema file: ${err.message}`);
|
||||
}
|
||||
});
|
||||
});*/
|
||||
fs.fsyncSync(this.fd);
|
||||
} catch (_e) {
|
||||
// Ignore sync errors
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {
|
|||
PtyError,
|
||||
ResizeControlMessage,
|
||||
KillControlMessage,
|
||||
ResetSizeControlMessage,
|
||||
} from './types.js';
|
||||
import { AsciinemaWriter } from './asciinema-writer.js';
|
||||
import { SessionManager } from './session-manager.js';
|
||||
|
|
@ -575,6 +576,19 @@ export class PtyManager extends EventEmitter {
|
|||
} catch (error) {
|
||||
logger.warn(`Failed to kill session ${session.id} with signal ${signal}:`, error);
|
||||
}
|
||||
} else if (message.cmd === 'reset-size') {
|
||||
try {
|
||||
if (session.ptyProcess) {
|
||||
// Get current terminal size from process.stdout
|
||||
const cols = process.stdout.columns || 80;
|
||||
const rows = process.stdout.rows || 24;
|
||||
session.ptyProcess.resize(cols, rows);
|
||||
session.asciinemaWriter?.writeResize(cols, rows);
|
||||
logger.debug(`Reset session ${session.id} size to terminal size: ${cols}x${rows}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to reset session ${session.id} size to terminal size:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -661,7 +675,7 @@ export class PtyManager extends EventEmitter {
|
|||
*/
|
||||
private sendControlMessage(
|
||||
sessionId: string,
|
||||
message: ResizeControlMessage | KillControlMessage
|
||||
message: ResizeControlMessage | KillControlMessage | ResetSizeControlMessage
|
||||
): boolean {
|
||||
const sessionPaths = this.sessionManager.getSessionPaths(sessionId);
|
||||
if (!sessionPaths) {
|
||||
|
|
@ -749,6 +763,46 @@ export class PtyManager extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset session size to terminal size (for external terminals)
|
||||
*/
|
||||
resetSessionSize(sessionId: string): void {
|
||||
const memorySession = this.sessions.get(sessionId);
|
||||
|
||||
try {
|
||||
// For in-memory sessions, we can't reset to terminal size since we don't know it
|
||||
if (memorySession?.ptyProcess) {
|
||||
throw new PtyError(
|
||||
`Cannot reset size for in-memory session ${sessionId}`,
|
||||
'INVALID_OPERATION',
|
||||
sessionId
|
||||
);
|
||||
}
|
||||
|
||||
// For external sessions, send reset-size command via control pipe
|
||||
const resetSizeMessage: ResetSizeControlMessage = {
|
||||
cmd: 'reset-size',
|
||||
};
|
||||
|
||||
const sent = this.sendControlMessage(sessionId, resetSizeMessage);
|
||||
if (!sent) {
|
||||
throw new PtyError(
|
||||
`Failed to send reset-size command to session ${sessionId}`,
|
||||
'CONTROL_MESSAGE_FAILED',
|
||||
sessionId
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(`Sent reset-size command to session ${sessionId}`);
|
||||
} catch (error) {
|
||||
throw new PtyError(
|
||||
`Failed to reset session size for ${sessionId}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
'RESET_SIZE_FAILED',
|
||||
sessionId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill a session with proper SIGTERM -> SIGKILL escalation
|
||||
* Returns a promise that resolves when the process is actually terminated
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ export interface KillControlMessage extends ControlMessage {
|
|||
signal?: string | number;
|
||||
}
|
||||
|
||||
export interface ResetSizeControlMessage extends ControlMessage {
|
||||
cmd: 'reset-size';
|
||||
}
|
||||
|
||||
export type AsciinemaEvent = {
|
||||
time: number;
|
||||
type: 'o' | 'i' | 'r' | 'm';
|
||||
|
|
|
|||
|
|
@ -1015,6 +1015,64 @@ export function createSessionRoutes(config: SessionRoutesConfig): Router {
|
|||
}
|
||||
});
|
||||
|
||||
// Reset terminal size (for external terminals)
|
||||
router.post('/sessions/:sessionId/reset-size', async (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
|
||||
try {
|
||||
// In HQ mode, forward to remote if session belongs to one
|
||||
if (remoteRegistry) {
|
||||
const remote = remoteRegistry.getRemoteBySessionId(sessionId);
|
||||
if (remote) {
|
||||
logger.debug(`forwarding reset-size to remote ${remote.id}`);
|
||||
const response = await fetch(`${remote.url}/api/sessions/${sessionId}/reset-size`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${remote.token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
return res.status(response.status).json(error);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return res.json(result);
|
||||
}
|
||||
}
|
||||
|
||||
logger.log(chalk.cyan(`resetting terminal size for session ${sessionId}`));
|
||||
|
||||
// Check if session exists
|
||||
const session = ptyManager.getSession(sessionId);
|
||||
if (!session) {
|
||||
logger.error(`session ${sessionId} not found for reset-size`);
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
}
|
||||
|
||||
// Check if session is running
|
||||
if (session.status !== 'running') {
|
||||
logger.error(`session ${sessionId} is not running (status: ${session.status})`);
|
||||
return res.status(400).json({ error: 'Session is not running' });
|
||||
}
|
||||
|
||||
// Reset the session size
|
||||
ptyManager.resetSessionSize(sessionId);
|
||||
logger.log(chalk.green(`session ${sessionId} size reset to terminal size`));
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logger.error('error resetting session size via PTY service:', error);
|
||||
if (error instanceof PtyError) {
|
||||
res.status(500).json({ error: 'Failed to reset session size', details: error.message });
|
||||
} else {
|
||||
res.status(500).json({ error: 'Failed to reset session size' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"useDefineForClassFields": false,
|
||||
"typeRoots": ["./node_modules/@types"]
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,9 @@
|
|||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"types": ["node"],
|
||||
"composite": true
|
||||
"composite": true,
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true
|
||||
},
|
||||
"include": [
|
||||
"src/server/**/*",
|
||||
|
|
|
|||
Loading…
Reference in a new issue