mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-20 13:45:54 +00:00
Remove lint_output.txt and add to gitignore
- Removed lint_output.txt from git tracking - Added lint_output.txt to .gitignore to prevent future commits 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0f26328940
commit
1a08d4603a
16 changed files with 319 additions and 520 deletions
|
|
@ -2,4 +2,5 @@
|
|||
- You do not need to manually build the web project, the user has npm run dev running in a separate terminal
|
||||
- Never screenshot via puppeteer. always query the DOM to see what's what.
|
||||
- NEVER EVER USE SETTIMEOUT FOR ANYTHING IN THE FRONTEND UNLESS EXPLICITELY PERMITTED
|
||||
- npm run lint in web/ before commit and fix the issues.
|
||||
- npm run lint in web/ before commit and fix the issues.
|
||||
- Always fix import issues, always fix all lint issues, always typecheck and fix type issues even in unrelated code
|
||||
|
|
@ -15,6 +15,8 @@ export interface Session {
|
|||
lastModified: string;
|
||||
pid?: number;
|
||||
waiting?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
@customElement('session-card')
|
||||
|
|
@ -141,13 +143,40 @@ export class SessionCard extends LitElement {
|
|||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to kill session');
|
||||
const errorData = await response.text();
|
||||
console.error('Failed to kill session:', errorData);
|
||||
throw new Error(`Kill failed: ${response.status}`);
|
||||
}
|
||||
// Note: We don't stop the animation on success - let the session list refresh handle it
|
||||
|
||||
// Kill succeeded - dispatch event to notify parent components
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('session-killed', {
|
||||
detail: {
|
||||
sessionId: this.session.id,
|
||||
session: this.session,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
|
||||
console.log(`Session ${this.session.id} killed successfully`);
|
||||
} catch (error) {
|
||||
console.error('Error killing session:', error);
|
||||
// Stop animation on error
|
||||
|
||||
// Show error to user (keep animation to indicate something went wrong)
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('session-kill-error', {
|
||||
detail: {
|
||||
sessionId: this.session.id,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
// Stop animation in all cases
|
||||
this.stopKillingAnimation();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ export interface Session {
|
|||
lastModified: string;
|
||||
pid?: number;
|
||||
waiting?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
@customElement('session-list')
|
||||
|
|
@ -41,6 +43,29 @@ export class SessionList extends LitElement {
|
|||
window.location.search = `?session=${session.id}`;
|
||||
}
|
||||
|
||||
private handleSessionKilled(e: CustomEvent) {
|
||||
const { sessionId } = e.detail;
|
||||
console.log(`Session ${sessionId} killed, updating session list...`);
|
||||
|
||||
// Immediately remove the session from the local state for instant UI feedback
|
||||
this.sessions = this.sessions.filter((session) => session.id !== sessionId);
|
||||
|
||||
// Then trigger a refresh to get the latest server state
|
||||
this.dispatchEvent(new CustomEvent('refresh'));
|
||||
}
|
||||
|
||||
private handleSessionKillError(e: CustomEvent) {
|
||||
const { sessionId, error } = e.detail;
|
||||
console.error(`Failed to kill session ${sessionId}:`, error);
|
||||
|
||||
// Dispatch error event to parent for user notification
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('error', {
|
||||
detail: `Failed to kill session: ${error}`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public async handleCleanupExited() {
|
||||
if (this.cleaningExited) return;
|
||||
|
||||
|
|
@ -91,7 +116,12 @@ export class SessionList extends LitElement {
|
|||
filteredSessions,
|
||||
(session) => session.id,
|
||||
(session) => html`
|
||||
<session-card .session=${session} @session-select=${this.handleSessionSelect}>
|
||||
<session-card
|
||||
.session=${session}
|
||||
@session-select=${this.handleSessionSelect}
|
||||
@session-killed=${this.handleSessionKilled}
|
||||
@session-kill-error=${this.handleSessionKillError}
|
||||
>
|
||||
</session-card>
|
||||
`
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ export class SessionView extends LitElement {
|
|||
// Listen for terminal resize events to capture dimensions
|
||||
this.terminal.addEventListener(
|
||||
'terminal-resize',
|
||||
this.handleTerminalResize.bind(this) as EventListener
|
||||
this.handleTerminalResize.bind(this) as unknown as EventListener
|
||||
);
|
||||
|
||||
// Wait a moment for freshly created sessions before connecting
|
||||
|
|
@ -306,10 +306,12 @@ export class SessionView extends LitElement {
|
|||
// and avoid syncing the default 80x24 dimensions
|
||||
const colsDiff = Math.abs(cols - (this.session.width || 120));
|
||||
const rowsDiff = Math.abs(rows - (this.session.height || 30));
|
||||
|
||||
|
||||
if ((colsDiff > 5 || rowsDiff > 5) && !(cols === 80 && rows === 24)) {
|
||||
console.log(`Syncing terminal dimensions: ${cols}x${rows} (was ${this.session.width}x${this.session.height})`);
|
||||
|
||||
console.log(
|
||||
`Syncing terminal dimensions: ${cols}x${rows} (was ${this.session.width}x${this.session.height})`
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/sessions/${this.session.id}/resize`, {
|
||||
method: 'POST',
|
||||
|
|
@ -521,7 +523,7 @@ export class SessionView extends LitElement {
|
|||
clearTimeout(this.resizeTimeout);
|
||||
}
|
||||
|
||||
this.resizeTimeout = setTimeout(async () => {
|
||||
this.resizeTimeout = window.setTimeout(async () => {
|
||||
// Only send resize request if dimensions actually changed
|
||||
if (cols === this.lastResizeWidth && rows === this.lastResizeHeight) {
|
||||
console.log(`Skipping redundant resize request: ${cols}x${rows}`);
|
||||
|
|
@ -531,8 +533,10 @@ export class SessionView extends LitElement {
|
|||
// Send resize request to backend if session is active
|
||||
if (this.session && this.session.status !== 'exited') {
|
||||
try {
|
||||
console.log(`Sending resize request: ${cols}x${rows} (was ${this.lastResizeWidth}x${this.lastResizeHeight})`);
|
||||
|
||||
console.log(
|
||||
`Sending resize request: ${cols}x${rows} (was ${this.lastResizeWidth}x${this.lastResizeHeight})`
|
||||
);
|
||||
|
||||
const response = await fetch(`/api/sessions/${this.session.id}/resize`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
|
@ -987,7 +991,7 @@ export class SessionView extends LitElement {
|
|||
</button>
|
||||
<div class="text-vs-text min-w-0 flex-1 overflow-hidden">
|
||||
<div
|
||||
class="text-vs-accent text-xs sm:text-sm overflow-x-auto scrollbar-thin scrollbar-thumb-vs-border scrollbar-track-transparent whitespace-nowrap"
|
||||
class="text-vs-accent text-xs sm:text-sm overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
title="${this.session.name || this.session.command}"
|
||||
>
|
||||
${this.session.name || this.session.command}
|
||||
|
|
|
|||
|
|
@ -182,7 +182,6 @@ export class Terminal extends LitElement {
|
|||
allowTransparency: false,
|
||||
convertEol: true,
|
||||
drawBoldTextInBrightColors: true,
|
||||
fontWeightBold: 'bold',
|
||||
minimumContrastRatio: 1,
|
||||
macOptionIsMeta: true,
|
||||
altClickMovesCursor: true,
|
||||
|
|
@ -193,13 +192,10 @@ export class Terminal extends LitElement {
|
|||
foreground: '#d4d4d4',
|
||||
cursor: '#00ff00',
|
||||
cursorAccent: '#1e1e1e',
|
||||
selectionBackground: '#264f78',
|
||||
selectionForeground: '#ffffff',
|
||||
selectionInactiveBackground: '#3a3a3a',
|
||||
// Standard 16 colors (0-15) - using proper xterm colors
|
||||
black: '#000000',
|
||||
red: '#cd0000',
|
||||
green: '#00cd00',
|
||||
green: '#00cd00',
|
||||
yellow: '#cdcd00',
|
||||
blue: '#0000ee',
|
||||
magenta: '#cd00cd',
|
||||
|
|
@ -304,23 +300,23 @@ export class Terminal extends LitElement {
|
|||
const containerHeight = this.container.clientHeight;
|
||||
const lineHeight = this.fontSize * 1.2;
|
||||
const charWidth = this.measureCharacterWidth();
|
||||
|
||||
|
||||
const newCols = Math.max(20, Math.floor(containerWidth / charWidth));
|
||||
const newRows = Math.max(6, Math.floor(containerHeight / lineHeight));
|
||||
|
||||
|
||||
// Update logical dimensions if they changed significantly
|
||||
const colsChanged = Math.abs(newCols - this.cols) > 3;
|
||||
const rowsChanged = Math.abs(newRows - this.rows) > 2;
|
||||
|
||||
|
||||
if (colsChanged || rowsChanged) {
|
||||
this.cols = newCols;
|
||||
this.rows = newRows;
|
||||
this.actualRows = newRows;
|
||||
|
||||
|
||||
// Resize the terminal to the new dimensions
|
||||
if (this.terminal) {
|
||||
this.terminal.resize(this.cols, this.rows);
|
||||
|
||||
|
||||
// Dispatch resize event for backend synchronization
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('terminal-resize', {
|
||||
|
|
@ -362,12 +358,12 @@ export class Terminal extends LitElement {
|
|||
// Only listen to window resize events to avoid pixel-level jitter
|
||||
// Use debounced handling to prevent resize spam
|
||||
let windowResizeTimeout: number | null = null;
|
||||
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
if (windowResizeTimeout) {
|
||||
clearTimeout(windowResizeTimeout);
|
||||
}
|
||||
windowResizeTimeout = setTimeout(() => {
|
||||
windowResizeTimeout = window.setTimeout(() => {
|
||||
this.fitTerminal();
|
||||
}, 150); // Debounce window resize events
|
||||
});
|
||||
|
|
@ -807,7 +803,7 @@ export class Terminal extends LitElement {
|
|||
if (isUnderline) classes += ' underline';
|
||||
if (isDim) classes += ' dim';
|
||||
if (isStrikethrough) classes += ' strikethrough';
|
||||
|
||||
|
||||
// Handle inverse colors
|
||||
if (isInverse) {
|
||||
// Swap foreground and background colors
|
||||
|
|
@ -821,7 +817,7 @@ export class Terminal extends LitElement {
|
|||
style += `background-color: ${tempFg};`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle invisible text
|
||||
if (isInvisible) {
|
||||
style += 'opacity: 0;';
|
||||
|
|
|
|||
|
|
@ -354,7 +354,21 @@ export class CastConverter {
|
|||
if (Array.isArray(data) && data.length >= 3) {
|
||||
const [_timestamp, type, eventData] = data;
|
||||
|
||||
if (type === 'o') {
|
||||
if (_timestamp === 'exit') {
|
||||
disconnect();
|
||||
|
||||
if (terminal.dispatchEvent) {
|
||||
terminal.dispatchEvent(
|
||||
new CustomEvent('session-exit', {
|
||||
detail: {
|
||||
exitCode: data[1],
|
||||
sessionId: data[2] || null,
|
||||
},
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
} else if (type === 'o') {
|
||||
// Output event - add to batch buffer
|
||||
addToOutputBuffer(eventData);
|
||||
} else if (type === 'r') {
|
||||
|
|
@ -383,20 +397,6 @@ export class CastConverter {
|
|||
}
|
||||
} else if (type === 'i') {
|
||||
// Ignore 'i' (input) events - those are for sending to server, not displaying
|
||||
} else if (_timestamp === 'exit') {
|
||||
disconnect();
|
||||
|
||||
if (terminal.dispatchEvent) {
|
||||
terminal.dispatchEvent(
|
||||
new CustomEvent('session-exit', {
|
||||
detail: {
|
||||
exitCode: data[1],
|
||||
sessionId: data[2] || null,
|
||||
},
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.error('Unknown stream message format:', data);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,31 @@
|
|||
// XTerm 256-color palette generator
|
||||
export function generateXTermColorCSS(): string {
|
||||
const colors: string[] = [];
|
||||
|
||||
|
||||
// Standard 16 colors (0-15)
|
||||
const standard16 = [
|
||||
'#000000', '#800000', '#008000', '#808000', '#000080', '#800080', '#008080', '#c0c0c0',
|
||||
'#808080', '#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff'
|
||||
'#000000',
|
||||
'#800000',
|
||||
'#008000',
|
||||
'#808000',
|
||||
'#000080',
|
||||
'#800080',
|
||||
'#008080',
|
||||
'#c0c0c0',
|
||||
'#808080',
|
||||
'#ff0000',
|
||||
'#00ff00',
|
||||
'#ffff00',
|
||||
'#0000ff',
|
||||
'#ff00ff',
|
||||
'#00ffff',
|
||||
'#ffffff',
|
||||
];
|
||||
|
||||
|
||||
standard16.forEach((color, i) => {
|
||||
colors.push(` --terminal-color-${i}: ${color};`);
|
||||
});
|
||||
|
||||
|
||||
// 216 color cube (16-231)
|
||||
const cube = [0, 95, 135, 175, 215, 255];
|
||||
for (let r = 0; r < 6; r++) {
|
||||
|
|
@ -25,13 +39,13 @@ export function generateXTermColorCSS(): string {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Grayscale (232-255)
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const gray = Math.round(8 + i * 10);
|
||||
const hex = gray.toString(16).padStart(2, '0');
|
||||
colors.push(` --terminal-color-${232 + i}: #${hex}${hex}${hex};`);
|
||||
}
|
||||
|
||||
|
||||
return `:root {\n${colors.join('\n')}\n}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@
|
|||
/* Override Tailwind's font-mono to use Hack Nerd Font */
|
||||
.font-mono {
|
||||
font-family:
|
||||
'Hack Nerd Font Mono', 'Fira Code', ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo,
|
||||
monospace !important;
|
||||
'Hack Nerd Font Mono', 'Fira Code', ui-monospace, SFMono-Regular, 'SF Mono', Consolas,
|
||||
'Liberation Mono', Menlo, monospace !important;
|
||||
}
|
||||
|
||||
/* Mobile scroll and touch behavior fixes */
|
||||
|
|
@ -98,10 +98,13 @@ body {
|
|||
.xterm {
|
||||
padding: 0 !important;
|
||||
font-family:
|
||||
'Hack Nerd Font Mono', 'Fira Code', ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo,
|
||||
monospace !important;
|
||||
'Hack Nerd Font Mono', 'Fira Code', ui-monospace, SFMono-Regular, 'SF Mono', Consolas,
|
||||
'Liberation Mono', Menlo, monospace !important;
|
||||
font-variant-ligatures: none;
|
||||
font-feature-settings: "liga" 0, "clig" 0, "calt" 0;
|
||||
font-feature-settings:
|
||||
'liga' 0,
|
||||
'clig' 0,
|
||||
'calt' 0;
|
||||
text-rendering: optimizeSpeed;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
|
@ -110,7 +113,7 @@ body {
|
|||
/* Terminal character specific styling */
|
||||
.terminal-char {
|
||||
font-variant-ligatures: none;
|
||||
font-feature-settings: "liga" 0;
|
||||
font-feature-settings: 'liga' 0;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
|
|
@ -145,8 +148,14 @@ body {
|
|||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 50% { opacity: 1; }
|
||||
51%, 100% { opacity: 0.3; }
|
||||
0%,
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
51%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.xterm .xterm-viewport {
|
||||
|
|
|
|||
|
|
@ -348,8 +348,9 @@ export class PtyManager {
|
|||
|
||||
/**
|
||||
* Kill a session with proper SIGTERM -> SIGKILL escalation
|
||||
* Returns a promise that resolves when the process is actually terminated
|
||||
*/
|
||||
killSession(sessionId: string, signal: string | number = 'SIGTERM'): void {
|
||||
async killSession(sessionId: string, signal: string | number = 'SIGTERM'): Promise<void> {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
throw new PtyError(`Session ${sessionId} not found`, 'SESSION_NOT_FOUND', sessionId);
|
||||
|
|
@ -357,17 +358,17 @@ export class PtyManager {
|
|||
|
||||
try {
|
||||
if (session.ptyProcess) {
|
||||
// If signal is already SIGKILL, send it immediately
|
||||
// If signal is already SIGKILL, send it immediately and wait briefly
|
||||
if (signal === 'SIGKILL' || signal === 9) {
|
||||
session.ptyProcess.kill('SIGKILL');
|
||||
this.sessions.delete(sessionId);
|
||||
// Wait a bit for SIGKILL to take effect
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
return;
|
||||
}
|
||||
|
||||
// Start with SIGTERM and escalate if needed (don't await to keep method sync)
|
||||
this.killSessionWithEscalation(sessionId, session).catch((error) => {
|
||||
console.error(`Kill escalation failed for session ${sessionId}:`, error);
|
||||
});
|
||||
// Start with SIGTERM and escalate if needed
|
||||
await this.killSessionWithEscalation(sessionId, session);
|
||||
} else {
|
||||
// No PTY process, just remove from sessions
|
||||
this.sessions.delete(sessionId);
|
||||
|
|
@ -468,9 +469,11 @@ export class PtyManager {
|
|||
* Cleanup a specific session
|
||||
*/
|
||||
cleanupSession(sessionId: string): void {
|
||||
// Kill active session if exists
|
||||
// Kill active session if exists (fire-and-forget for cleanup)
|
||||
if (this.sessions.has(sessionId)) {
|
||||
this.killSession(sessionId);
|
||||
this.killSession(sessionId).catch((error) => {
|
||||
console.error(`Error killing session ${sessionId} during cleanup:`, error);
|
||||
});
|
||||
}
|
||||
|
||||
// Remove from storage
|
||||
|
|
|
|||
|
|
@ -239,31 +239,25 @@ export class PtyService {
|
|||
|
||||
/**
|
||||
* Kill a session
|
||||
* Returns a promise that resolves when the process is actually terminated
|
||||
*/
|
||||
killSession(sessionId: string, signal: string | number = 'SIGTERM'): void {
|
||||
async killSession(sessionId: string, signal: string | number = 'SIGTERM'): Promise<void> {
|
||||
if (this.ptyManager) {
|
||||
return this.ptyManager.killSession(sessionId, signal);
|
||||
return await this.ptyManager.killSession(sessionId, signal);
|
||||
} else {
|
||||
return this.killSessionTtyFwd(sessionId, signal);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill session using tty-fwd binary
|
||||
* Kill session using tty-fwd binary with proper escalation
|
||||
*/
|
||||
private killSessionTtyFwd(sessionId: string, signal: string | number): void {
|
||||
private async killSessionTtyFwd(sessionId: string, signal: string | number): Promise<void> {
|
||||
const ttyFwdPath = this.findTtyFwdBinary();
|
||||
const args = ['--control-path', this.config.controlPath, '--session', sessionId];
|
||||
|
||||
if (signal === 'SIGTERM' || signal === 15) {
|
||||
args.push('--stop');
|
||||
} else if (signal === 'SIGKILL' || signal === 9) {
|
||||
args.push('--kill');
|
||||
} else {
|
||||
args.push('--signal', String(signal));
|
||||
}
|
||||
|
||||
try {
|
||||
// If signal is already SIGKILL, send it immediately
|
||||
if (signal === 'SIGKILL' || signal === 9) {
|
||||
const args = ['--control-path', this.config.controlPath, '--session', sessionId, '--kill'];
|
||||
const result = spawnSync(ttyFwdPath, args, {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000,
|
||||
|
|
@ -272,6 +266,82 @@ export class PtyService {
|
|||
if (result.status !== 0) {
|
||||
throw new PtyError(`tty-fwd kill failed with code ${result.status}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get session info to find PID for monitoring
|
||||
const session = this.getSession(sessionId);
|
||||
if (!session || !session.pid) {
|
||||
throw new PtyError(
|
||||
`Session ${sessionId} not found or has no PID`,
|
||||
'SESSION_NOT_FOUND',
|
||||
sessionId
|
||||
);
|
||||
}
|
||||
|
||||
const pid = session.pid;
|
||||
console.log(`Terminating session ${sessionId} (PID: ${pid}) with tty-fwd SIGTERM...`);
|
||||
|
||||
try {
|
||||
// Send SIGTERM first via tty-fwd
|
||||
const args = ['--control-path', this.config.controlPath, '--session', sessionId, '--stop'];
|
||||
const result = spawnSync(ttyFwdPath, args, {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new PtyError(`tty-fwd stop failed with code ${result.status}`);
|
||||
}
|
||||
|
||||
// Wait up to 3 seconds for graceful termination (check every 500ms)
|
||||
const maxWaitTime = 3000;
|
||||
const checkInterval = 500;
|
||||
const maxChecks = maxWaitTime / checkInterval;
|
||||
|
||||
for (let i = 0; i < maxChecks; i++) {
|
||||
// Wait for check interval
|
||||
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
||||
|
||||
// Check if process is still alive
|
||||
try {
|
||||
process.kill(pid, 0); // Signal 0 just checks if process exists
|
||||
// Process still exists, continue waiting
|
||||
console.log(`Session ${sessionId} still alive after ${(i + 1) * checkInterval}ms...`);
|
||||
} catch (_error) {
|
||||
// Process no longer exists - it terminated gracefully
|
||||
console.log(
|
||||
`Session ${sessionId} terminated gracefully after ${(i + 1) * checkInterval}ms`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Process didn't terminate gracefully within 3 seconds, force kill
|
||||
console.log(
|
||||
`Session ${sessionId} didn't terminate gracefully, sending SIGKILL via tty-fwd...`
|
||||
);
|
||||
const killArgs = [
|
||||
'--control-path',
|
||||
this.config.controlPath,
|
||||
'--session',
|
||||
sessionId,
|
||||
'--kill',
|
||||
];
|
||||
const killResult = spawnSync(ttyFwdPath, killArgs, {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
if (killResult.status !== 0) {
|
||||
console.warn(
|
||||
`tty-fwd SIGKILL failed with code ${killResult.status}, process may already be dead`
|
||||
);
|
||||
}
|
||||
|
||||
// Wait a bit more for SIGKILL to take effect
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
console.log(`Session ${sessionId} forcefully terminated with SIGKILL`);
|
||||
} catch (error) {
|
||||
throw new PtyError(
|
||||
`Failed to kill session via tty-fwd: ${error instanceof Error ? error.message : String(error)}`
|
||||
|
|
|
|||
|
|
@ -125,15 +125,24 @@ Lists all sessions with metadata.
|
|||
|
||||
Gets specific session by ID.
|
||||
|
||||
##### `killSession(sessionId: string, signal?: string | number): void`
|
||||
##### `killSession(sessionId: string, signal?: string | number): Promise<void>`
|
||||
|
||||
Terminates a session.
|
||||
Terminates a session and waits for the process to actually be killed.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `sessionId`: Session to terminate
|
||||
- `signal`: Signal to send (default: 'SIGTERM')
|
||||
|
||||
**Returns:** Promise that resolves when the process is actually terminated
|
||||
|
||||
**Process:**
|
||||
|
||||
1. Sends SIGTERM initially
|
||||
2. Waits up to 3 seconds (checking every 500ms)
|
||||
3. Sends SIGKILL if process doesn't terminate gracefully
|
||||
4. Resolves when process is confirmed dead
|
||||
|
||||
##### `cleanupSession(sessionId: string): void`
|
||||
|
||||
Removes session and cleans up files.
|
||||
|
|
|
|||
|
|
@ -365,7 +365,12 @@ export class SessionManager {
|
|||
// D = Uninterruptible sleep (usually IO)
|
||||
// T = Stopped (on a signal or being traced)
|
||||
// Z = Zombie (terminated but not reaped)
|
||||
const isWaiting = stat.includes('S') || stat.includes('D') || stat.includes('T');
|
||||
|
||||
// For terminal sessions, only consider these as truly "waiting":
|
||||
// D = Uninterruptible sleep (usually blocked on I/O)
|
||||
// T = Stopped/traced (paused by signal)
|
||||
// Note: 'S' state is normal for interactive shells waiting for input
|
||||
const isWaiting = stat.includes('D') || stat.includes('T');
|
||||
|
||||
return { isAlive, isWaiting };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,97 +0,0 @@
|
|||
/**
|
||||
* Basic tests for PtyService
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { PtyService } from '../PtyService.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
describe('PtyService', () => {
|
||||
let tempDir: string;
|
||||
let ptyService: PtyService;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create temporary directory for testing
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ptyservice-test-'));
|
||||
|
||||
ptyService = new PtyService({
|
||||
controlPath: tempDir,
|
||||
implementation: 'auto',
|
||||
fallbackToTtyFwd: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Cleanup temporary directory
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should initialize successfully', () => {
|
||||
expect(ptyService).toBeDefined();
|
||||
expect(ptyService.getControlPath()).toBe(tempDir);
|
||||
});
|
||||
|
||||
it('should detect implementation', () => {
|
||||
const implementation = ptyService.getCurrentImplementation();
|
||||
expect(implementation).toMatch(/^(node-pty|tty-fwd)$/);
|
||||
});
|
||||
|
||||
it('should list sessions (empty initially)', () => {
|
||||
const sessions = ptyService.listSessions();
|
||||
expect(Array.isArray(sessions)).toBe(true);
|
||||
expect(sessions.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should get active session count', () => {
|
||||
const count = ptyService.getActiveSessionCount();
|
||||
expect(typeof count).toBe('number');
|
||||
expect(count).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('should return configuration', () => {
|
||||
const config = ptyService.getConfig();
|
||||
expect(config.controlPath).toBe(tempDir);
|
||||
expect(config.fallbackToTtyFwd).toBe(true);
|
||||
});
|
||||
|
||||
// Only run session creation test if node-pty is available
|
||||
it.skipIf(!process.env.CI)('should create and manage a session', async () => {
|
||||
if (!ptyService.isUsingNodePty()) {
|
||||
// Skip if using tty-fwd (requires actual binary)
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a simple echo session
|
||||
const result = await ptyService.createSession(['echo', 'hello'], {
|
||||
sessionName: 'test-session',
|
||||
});
|
||||
|
||||
expect(result.sessionId).toBeDefined();
|
||||
expect(result.sessionInfo.name).toBe('test-session');
|
||||
expect(result.sessionInfo.cmdline).toEqual(['echo', 'hello']);
|
||||
|
||||
// List sessions should now include our session
|
||||
const sessions = ptyService.listSessions();
|
||||
expect(sessions.length).toBe(1);
|
||||
expect(sessions[0].session_id).toBe(result.sessionId);
|
||||
|
||||
// Get specific session
|
||||
const session = ptyService.getSession(result.sessionId);
|
||||
expect(session).toBeDefined();
|
||||
expect(session?.session_id).toBe(result.sessionId);
|
||||
|
||||
// Wait a bit for echo to complete
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Cleanup
|
||||
ptyService.cleanupSession(result.sessionId);
|
||||
|
||||
// Should be cleaned up
|
||||
const sessionsAfterCleanup = ptyService.listSessions();
|
||||
expect(sessionsAfterCleanup.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
/**
|
||||
* Demo of Enhanced PTY Features
|
||||
*
|
||||
* This demonstrates the new waiting state detection and proper process termination
|
||||
* features that match tty-fwd behavior.
|
||||
*/
|
||||
|
||||
import { PtyService } from './PtyService.js';
|
||||
|
||||
async function demonstrateEnhancedFeatures() {
|
||||
const ptyService = new PtyService();
|
||||
|
||||
console.log('=== Enhanced PTY Features Demo ===');
|
||||
console.log('');
|
||||
|
||||
// 1. Demonstrate waiting state detection
|
||||
console.log('1. Creating a session that will be in "waiting" state...');
|
||||
const result = await ptyService.createSession(['sleep', '10'], {
|
||||
sessionName: 'waiting-demo',
|
||||
workingDir: process.cwd(),
|
||||
});
|
||||
|
||||
console.log(`Created session: ${result.sessionId}`);
|
||||
console.log('');
|
||||
|
||||
// 2. Check session states
|
||||
console.log('2. Checking session states...');
|
||||
const sessions = ptyService.listSessions();
|
||||
for (const session of sessions) {
|
||||
console.log(`Session ${session.session_id}:`);
|
||||
console.log(` Status: ${session.status}`);
|
||||
console.log(` Waiting: ${session.waiting}`);
|
||||
console.log(` PID: ${session.pid}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// 3. Demonstrate proper kill escalation
|
||||
console.log('3. Demonstrating SIGTERM -> SIGKILL escalation...');
|
||||
console.log('This will:');
|
||||
console.log(' - Send SIGTERM first');
|
||||
console.log(' - Wait up to 3 seconds (checking every 500ms)');
|
||||
console.log(" - Send SIGKILL if process doesn't terminate gracefully");
|
||||
console.log('');
|
||||
|
||||
// Kill the session to demonstrate escalation
|
||||
ptyService.killSession(result.sessionId);
|
||||
|
||||
console.log('Kill command sent. Check the console output above for escalation details.');
|
||||
console.log('');
|
||||
|
||||
// 4. Show final session state
|
||||
setTimeout(() => {
|
||||
console.log('4. Final session states:');
|
||||
const finalSessions = ptyService.listSessions();
|
||||
console.log(`Active sessions: ${finalSessions.length}`);
|
||||
|
||||
for (const session of finalSessions) {
|
||||
console.log(`Session ${session.session_id}: ${session.status}`);
|
||||
}
|
||||
}, 4000); // Wait 4 seconds to see full escalation process
|
||||
}
|
||||
|
||||
// Export the demo function
|
||||
export { demonstrateEnhancedFeatures };
|
||||
|
||||
// Run demo if this file is executed directly
|
||||
if (process.argv[1] && process.argv[1].endsWith('enhanced-features-demo.ts')) {
|
||||
demonstrateEnhancedFeatures().catch(console.error);
|
||||
}
|
||||
|
|
@ -1,262 +0,0 @@
|
|||
/**
|
||||
* Integration Example - How to integrate PTY service with existing server
|
||||
*
|
||||
* This example shows how to replace tty-fwd calls with the new PTY service
|
||||
* while maintaining backward compatibility.
|
||||
*/
|
||||
|
||||
import { PtyService, SessionEntryWithId, PtyError } from './index.js';
|
||||
|
||||
// Configuration - can be set via environment variables
|
||||
const PTY_CONFIG = {
|
||||
implementation: (process.env.PTY_IMPLEMENTATION as any) || 'auto',
|
||||
controlPath: process.env.TTY_FWD_CONTROL_DIR || undefined,
|
||||
fallbackToTtyFwd: process.env.PTY_FALLBACK_TTY_FWD !== 'false',
|
||||
ttyFwdPath: process.env.TTY_FWD_PATH || undefined,
|
||||
};
|
||||
|
||||
// Create global PTY service instance
|
||||
export const ptyService = new PtyService(PTY_CONFIG);
|
||||
|
||||
console.log(`PTY Service initialized with ${ptyService.getCurrentImplementation()} implementation`);
|
||||
|
||||
// Example: Replace existing session creation code
|
||||
export async function createSession(
|
||||
command: string[],
|
||||
sessionName?: string,
|
||||
workingDir?: string
|
||||
): Promise<{ sessionId: string; sessionInfo: any }> {
|
||||
try {
|
||||
const result = await ptyService.createSession(command, {
|
||||
sessionName,
|
||||
workingDir,
|
||||
term: 'xterm-256color',
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Created session ${result.sessionId} using ${ptyService.getCurrentImplementation()}`
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new PtyError(`Failed to create session: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Replace existing session listing code
|
||||
export function listSessions(): SessionEntryWithId[] {
|
||||
try {
|
||||
return ptyService.listSessions();
|
||||
} catch (error) {
|
||||
console.error('Failed to list sessions:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Replace existing session input handling
|
||||
export function sendSessionInput(sessionId: string, text?: string, key?: string): void {
|
||||
try {
|
||||
if (text !== undefined) {
|
||||
ptyService.sendInput(sessionId, { text });
|
||||
} else if (key !== undefined) {
|
||||
ptyService.sendInput(sessionId, { key: key as any });
|
||||
} else {
|
||||
throw new Error('Either text or key must be provided');
|
||||
}
|
||||
} catch (error) {
|
||||
throw new PtyError(`Failed to send input to session ${sessionId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Replace existing session termination code
|
||||
export function killSession(sessionId: string, signal: string | number = 'SIGTERM'): void {
|
||||
try {
|
||||
ptyService.killSession(sessionId, signal);
|
||||
console.log(`Killed session ${sessionId}`);
|
||||
} catch (error) {
|
||||
throw new PtyError(`Failed to kill session ${sessionId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Replace existing session cleanup code
|
||||
export function cleanupSession(sessionId: string): void {
|
||||
try {
|
||||
ptyService.cleanupSession(sessionId);
|
||||
console.log(`Cleaned up session ${sessionId}`);
|
||||
} catch (error) {
|
||||
throw new PtyError(`Failed to cleanup session ${sessionId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Get session info (compatible with existing code)
|
||||
export function getSession(sessionId: string): SessionEntryWithId | null {
|
||||
try {
|
||||
return ptyService.getSession(sessionId);
|
||||
} catch (error) {
|
||||
console.error(`Failed to get session ${sessionId}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Example: Health check function
|
||||
export function getPtyServiceStatus() {
|
||||
return {
|
||||
implementation: ptyService.getCurrentImplementation(),
|
||||
usingNodePty: ptyService.isUsingNodePty(),
|
||||
usingTtyFwd: ptyService.isUsingTtyFwd(),
|
||||
activeSessionCount: ptyService.getActiveSessionCount(),
|
||||
controlPath: ptyService.getControlPath(),
|
||||
config: ptyService.getConfig(),
|
||||
};
|
||||
}
|
||||
|
||||
// Example Express.js route handlers (drop-in replacements)
|
||||
export const routeHandlers = {
|
||||
// POST /api/sessions
|
||||
async createSessionHandler(req: any, res: any) {
|
||||
try {
|
||||
const { command, workingDir } = req.body;
|
||||
|
||||
if (!command || !Array.isArray(command)) {
|
||||
return res.status(400).json({ error: 'Invalid command' });
|
||||
}
|
||||
|
||||
const result = await createSession(command, undefined, workingDir);
|
||||
res.json({ sessionId: result.sessionId });
|
||||
} catch (error) {
|
||||
console.error('Failed to create session:', error);
|
||||
res.status(500).json({ error: 'Failed to create session' });
|
||||
}
|
||||
},
|
||||
|
||||
// GET /api/sessions
|
||||
listSessionsHandler(req: any, res: any) {
|
||||
try {
|
||||
const sessions = listSessions();
|
||||
res.json(sessions);
|
||||
} catch (error) {
|
||||
console.error('Failed to list sessions:', error);
|
||||
res.status(500).json({ error: 'Failed to list sessions' });
|
||||
}
|
||||
},
|
||||
|
||||
// POST /api/sessions/:sessionId/input
|
||||
sendInputHandler(req: any, res: any) {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const { text, key } = req.body;
|
||||
|
||||
sendSessionInput(sessionId, text, key);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(`Failed to send input to session ${req.params.sessionId}:`, error);
|
||||
res.status(500).json({ error: 'Failed to send input' });
|
||||
}
|
||||
},
|
||||
|
||||
// DELETE /api/sessions/:sessionId
|
||||
killSessionHandler(req: any, res: any) {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
killSession(sessionId);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(`Failed to kill session ${req.params.sessionId}:`, error);
|
||||
res.status(500).json({ error: 'Failed to kill session' });
|
||||
}
|
||||
},
|
||||
|
||||
// DELETE /api/sessions/:sessionId/cleanup
|
||||
cleanupSessionHandler(req: any, res: any) {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
cleanupSession(sessionId);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(`Failed to cleanup session ${req.params.sessionId}:`, error);
|
||||
res.status(500).json({ error: 'Failed to cleanup session' });
|
||||
}
|
||||
},
|
||||
|
||||
// GET /api/sessions/:sessionId
|
||||
getSessionHandler(req: any, res: any) {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const session = getSession(sessionId);
|
||||
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
}
|
||||
|
||||
res.json(session);
|
||||
} catch (error) {
|
||||
console.error(`Failed to get session ${req.params.sessionId}:`, error);
|
||||
res.status(500).json({ error: 'Failed to get session' });
|
||||
}
|
||||
},
|
||||
|
||||
// GET /api/pty/status (new endpoint for monitoring)
|
||||
statusHandler(req: any, res: any) {
|
||||
try {
|
||||
const status = getPtyServiceStatus();
|
||||
res.json(status);
|
||||
} catch (error) {
|
||||
console.error('Failed to get PTY service status:', error);
|
||||
res.status(500).json({ error: 'Failed to get status' });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Example migration notes for existing code:
|
||||
/*
|
||||
|
||||
MIGRATION GUIDE:
|
||||
|
||||
1. Replace tty-fwd spawn calls:
|
||||
OLD:
|
||||
```typescript
|
||||
const proc = spawn(ttyFwdPath, ['--control-path', controlPath, '--', ...command]);
|
||||
```
|
||||
NEW:
|
||||
```typescript
|
||||
const result = await ptyService.createSession(command, options);
|
||||
```
|
||||
|
||||
2. Replace tty-fwd list calls:
|
||||
OLD:
|
||||
```typescript
|
||||
const result = spawn(ttyFwdPath, ['--control-path', controlPath, '--list-sessions']);
|
||||
```
|
||||
NEW:
|
||||
```typescript
|
||||
const sessions = ptyService.listSessions();
|
||||
```
|
||||
|
||||
3. Replace tty-fwd input calls:
|
||||
OLD:
|
||||
```typescript
|
||||
spawn(ttyFwdPath, ['--control-path', controlPath, '--session', sessionId, '--send-text', text]);
|
||||
```
|
||||
NEW:
|
||||
```typescript
|
||||
ptyService.sendInput(sessionId, { text });
|
||||
```
|
||||
|
||||
4. File paths remain the same:
|
||||
- stream-out files are still in the same location
|
||||
- Session directory structure is identical
|
||||
- Asciinema format is fully compatible
|
||||
|
||||
5. Environment variable support:
|
||||
- PTY_IMPLEMENTATION: 'node-pty' | 'tty-fwd' | 'auto'
|
||||
- PTY_FALLBACK_TTY_FWD: 'true' | 'false'
|
||||
- TTY_FWD_CONTROL_DIR: control directory path
|
||||
- TTY_FWD_PATH: path to tty-fwd binary
|
||||
|
||||
6. Error handling:
|
||||
- All functions throw PtyError for consistent error handling
|
||||
- Automatic fallback to tty-fwd if node-pty fails
|
||||
- Graceful degradation in test environments
|
||||
|
||||
*/
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import express, { Response } from 'express';
|
||||
import { createServer } from 'http';
|
||||
import { WebSocketServer, WebSocket } from 'ws';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { PtyService, PtyError } from './pty/index.js';
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ app.delete('/api/sessions/:sessionId', async (req, res) => {
|
|||
return res.status(404).json({ error: 'Session not found' });
|
||||
}
|
||||
|
||||
ptyService.killSession(sessionId, 'SIGTERM');
|
||||
await ptyService.killSession(sessionId, 'SIGTERM');
|
||||
console.log(`Session ${sessionId} killed`);
|
||||
|
||||
res.json({ success: true, message: 'Session killed' });
|
||||
|
|
@ -272,8 +272,15 @@ app.get('/api/sessions/:sessionId/stream', async (req, res) => {
|
|||
res.write(`data: ${line}\n\n`);
|
||||
headerSent = true;
|
||||
} else if (Array.isArray(parsed) && parsed.length >= 3) {
|
||||
const instantEvent = [0, parsed[1], parsed[2]];
|
||||
res.write(`data: ${JSON.stringify(instantEvent)}\n\n`);
|
||||
// Check if this is an exit event (format: ['exit', exitCode, sessionId])
|
||||
if (parsed[0] === 'exit') {
|
||||
// Exit events should preserve their original format
|
||||
res.write(`data: ${line}\n\n`);
|
||||
} else {
|
||||
// Regular asciinema events get timestamp set to 0 for existing content
|
||||
const instantEvent = [0, parsed[1], parsed[2]];
|
||||
res.write(`data: ${JSON.stringify(instantEvent)}\n\n`);
|
||||
}
|
||||
}
|
||||
} catch (_e) {
|
||||
// Skip invalid lines
|
||||
|
|
@ -329,9 +336,16 @@ app.get('/api/sessions/:sessionId/stream', async (req, res) => {
|
|||
continue; // Skip duplicate headers
|
||||
}
|
||||
if (Array.isArray(parsed) && parsed.length >= 3) {
|
||||
const currentTime = Date.now() / 1000;
|
||||
const realTimeEvent = [currentTime - startTime, parsed[1], parsed[2]];
|
||||
eventData = `data: ${JSON.stringify(realTimeEvent)}\n\n`;
|
||||
// Check if this is an exit event (format: ['exit', exitCode, sessionId])
|
||||
if (parsed[0] === 'exit') {
|
||||
// Exit events should preserve their original format without timestamp modification
|
||||
eventData = `data: ${JSON.stringify(parsed)}\n\n`;
|
||||
} else {
|
||||
// Regular asciinema events get relative timestamp
|
||||
const currentTime = Date.now() / 1000;
|
||||
const realTimeEvent = [currentTime - startTime, parsed[1], parsed[2]];
|
||||
eventData = `data: ${JSON.stringify(realTimeEvent)}\n\n`;
|
||||
}
|
||||
}
|
||||
} catch (_e) {
|
||||
// Handle non-JSON as raw output
|
||||
|
|
@ -606,6 +620,49 @@ app.post('/api/sessions/:sessionId/input', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// Resize session terminal
|
||||
app.post('/api/sessions/:sessionId/resize', async (req, res) => {
|
||||
const sessionId = req.params.sessionId;
|
||||
const { width, height } = req.body;
|
||||
|
||||
if (typeof width !== 'number' || typeof height !== 'number') {
|
||||
return res.status(400).json({ error: 'Width and height must be numbers' });
|
||||
}
|
||||
|
||||
if (width < 1 || height < 1 || width > 1000 || height > 1000) {
|
||||
return res.status(400).json({ error: 'Width and height must be between 1 and 1000' });
|
||||
}
|
||||
|
||||
console.log(`Resizing session ${sessionId} to ${width}x${height}`);
|
||||
|
||||
try {
|
||||
// Validate session exists
|
||||
const session = ptyService.getSession(sessionId);
|
||||
if (!session) {
|
||||
console.error(`Session ${sessionId} not found for resize`);
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
}
|
||||
|
||||
if (session.status !== 'running') {
|
||||
console.error(`Session ${sessionId} is not running (status: ${session.status})`);
|
||||
return res.status(400).json({ error: 'Session is not running' });
|
||||
}
|
||||
|
||||
// Resize the session
|
||||
ptyService.resizeSession(sessionId, width, height);
|
||||
console.log(`Successfully resized session ${sessionId} to ${width}x${height}`);
|
||||
|
||||
res.json({ success: true, width, height });
|
||||
} catch (error) {
|
||||
console.error('Error resizing session via PTY service:', error);
|
||||
if (error instanceof PtyError) {
|
||||
res.status(500).json({ error: 'Failed to resize session', details: error.message });
|
||||
} else {
|
||||
res.status(500).json({ error: 'Failed to resize session' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// PTY service status endpoint
|
||||
app.get('/api/pty/status', (req, res) => {
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue