fix: session restoration now properly attaches to existing screens

- Pass existing screenSession to Session constructor for restored sessions
- Skip terminal buffer clearing when attaching to restored (existing) screens
- Frontend auto-calls /interactive endpoint when selecting a restored session
- Fixes issue where restored sessions appeared broken after server restart

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-19 19:56:28 +01:00
co-authored by Claude Opus 4.5
parent 611815628e
commit 3e26a6a18e
3 changed files with 60 additions and 26 deletions
+44 -24
View File
@@ -129,6 +129,7 @@ export class Session extends EventEmitter {
name?: string; name?: string;
screenManager?: ScreenManager; screenManager?: ScreenManager;
useScreen?: boolean; useScreen?: boolean;
screenSession?: ScreenSession; // For restored sessions - pass the existing screen
}) { }) {
super(); super();
this.id = config.id || uuidv4(); this.id = config.id || uuidv4();
@@ -139,6 +140,7 @@ export class Session extends EventEmitter {
this._lastActivityAt = this.createdAt; this._lastActivityAt = this.createdAt;
this._screenManager = config.screenManager || null; this._screenManager = config.screenManager || null;
this._useScreen = config.useScreen ?? (this._screenManager !== null && ScreenManager.isScreenAvailable()); this._useScreen = config.useScreen ?? (this._screenManager !== null && ScreenManager.isScreenAvailable());
this._screenSession = config.screenSession || null; // Use existing screen if provided
// Initialize task tracker and forward events // Initialize task tracker and forward events
this._taskTracker = new TaskTracker(); this._taskTracker = new TaskTracker();
@@ -379,14 +381,21 @@ export class Session extends EventEmitter {
console.log('[Session] Starting interactive Claude session' + (this._useScreen ? ' (with screen)' : '')); console.log('[Session] Starting interactive Claude session' + (this._useScreen ? ' (with screen)' : ''));
// If screen wrapping is enabled, create a screen session first // If screen wrapping is enabled, create or attach to a screen session
if (this._useScreen && this._screenManager) { if (this._useScreen && this._screenManager) {
try { try {
this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'claude', this._name); // Check if we already have a screen session (restored session)
console.log('[Session] Created screen session:', this._screenSession.screenName); const isRestoredSession = this._screenSession !== null;
if (isRestoredSession) {
console.log('[Session] Attaching to existing screen session:', this._screenSession.screenName);
} else {
// Create a new screen session
this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'claude', this._name);
console.log('[Session] Created screen session:', this._screenSession.screenName);
// Wait a moment for screen to fully start // Wait a moment for screen to fully start
await new Promise(resolve => setTimeout(resolve, 300)); await new Promise(resolve => setTimeout(resolve, 300));
}
// Attach to the screen session via PTY // Attach to the screen session via PTY
this.ptyProcess = pty.spawn('screen', [ this.ptyProcess = pty.spawn('screen', [
@@ -399,12 +408,14 @@ export class Session extends EventEmitter {
env: { ...process.env, TERM: 'xterm-256color' }, env: { ...process.env, TERM: 'xterm-256color' },
}); });
// Screen creates blank space when initializing. After attaching, wait for // For NEW screens: clear buffer after initial burst (screen initialization noise)
// the initial burst then clear the buffer and tell clients to clear their terminal. // For RESTORED screens: don't clear - we want to see the existing output
setTimeout(() => { if (!isRestoredSession) {
this._terminalBuffer = ''; setTimeout(() => {
this.emit('clearTerminal'); this._terminalBuffer = '';
}, 100); this.emit('clearTerminal');
}, 100);
}
} catch (err) { } catch (err) {
console.error('[Session] Failed to create screen session, falling back to direct PTY:', err); console.error('[Session] Failed to create screen session, falling back to direct PTY:', err);
this._useScreen = false; this._useScreen = false;
@@ -510,14 +521,21 @@ export class Session extends EventEmitter {
const shell = process.env.SHELL || '/bin/bash'; const shell = process.env.SHELL || '/bin/bash';
console.log('[Session] Starting shell session with:', shell + (this._useScreen ? ' (with screen)' : '')); console.log('[Session] Starting shell session with:', shell + (this._useScreen ? ' (with screen)' : ''));
// If screen wrapping is enabled, create a screen session first // If screen wrapping is enabled, create or attach to a screen session
if (this._useScreen && this._screenManager) { if (this._useScreen && this._screenManager) {
try { try {
this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'shell', this._name); // Check if we already have a screen session (restored session)
console.log('[Session] Created screen session:', this._screenSession.screenName); const isRestoredSession = this._screenSession !== null;
if (isRestoredSession) {
console.log('[Session] Attaching to existing screen session:', this._screenSession.screenName);
} else {
// Create a new screen session
this._screenSession = await this._screenManager.createScreen(this.id, this.workingDir, 'shell', this._name);
console.log('[Session] Created screen session:', this._screenSession.screenName);
// Wait a moment for screen to fully start // Wait a moment for screen to fully start
await new Promise(resolve => setTimeout(resolve, 300)); await new Promise(resolve => setTimeout(resolve, 300));
}
// Attach to the screen session via PTY // Attach to the screen session via PTY
this.ptyProcess = pty.spawn('screen', [ this.ptyProcess = pty.spawn('screen', [
@@ -530,14 +548,16 @@ export class Session extends EventEmitter {
env: { ...process.env, TERM: 'xterm-256color' }, env: { ...process.env, TERM: 'xterm-256color' },
}); });
// Screen creates blank space when initializing. After attaching, wait for // For NEW screens: clear by sending 'clear' command to the shell
// the initial burst then clear by sending 'clear' command to the shell. // For RESTORED screens: don't clear - we want to see the existing output
setTimeout(() => { if (!isRestoredSession) {
if (this.ptyProcess) { setTimeout(() => {
this._terminalBuffer = ''; if (this.ptyProcess) {
this.ptyProcess.write('clear\n'); this._terminalBuffer = '';
} this.ptyProcess.write('clear\n');
}, 100); }
}, 100);
}
} catch (err) { } catch (err) {
console.error('[Session] Failed to create screen session, falling back to direct PTY:', err); console.error('[Session] Failed to create screen session, falling back to direct PTY:', err);
this._useScreen = false; this._useScreen = false;
+13
View File
@@ -633,6 +633,19 @@ class ClaudemanApp {
this.activeSessionId = sessionId; this.activeSessionId = sessionId;
this.renderSessionTabs(); this.renderSessionTabs();
// Check if this is a restored session that needs to be attached
const session = this.sessions.get(sessionId);
if (session && session.pid === null && session.status === 'idle') {
// This is a restored session - attach to the existing screen
try {
await fetch(`/api/sessions/${sessionId}/interactive`, { method: 'POST' });
// Update local session state
session.status = 'busy';
} catch (err) {
console.error('Failed to attach to restored session:', err);
}
}
// Load terminal buffer for this session // Load terminal buffer for this session
try { try {
const res = await fetch(`/api/sessions/${sessionId}/terminal`); const res = await fetch(`/api/sessions/${sessionId}/terminal`);
+3 -2
View File
@@ -1393,14 +1393,15 @@ export class WebServer extends EventEmitter {
const screens = this.screenManager.getScreens(); const screens = this.screenManager.getScreens();
for (const screen of screens) { for (const screen of screens) {
if (!this.sessions.has(screen.sessionId)) { if (!this.sessions.has(screen.sessionId)) {
// Create a session object for this screen // Create a session object for this screen with the existing screenSession
const session = new Session({ const session = new Session({
id: screen.sessionId, // Preserve the original session ID id: screen.sessionId, // Preserve the original session ID
workingDir: screen.workingDir, workingDir: screen.workingDir,
mode: screen.mode, mode: screen.mode,
name: screen.name || screen.screenName, name: screen.name || screen.screenName,
screenManager: this.screenManager, screenManager: this.screenManager,
useScreen: true useScreen: true,
screenSession: screen // Pass the existing screen so startInteractive() can attach to it
}); });
this.sessions.set(session.id, session); this.sessions.set(session.id, session);