feat: add GNU screen wrapping with process monitor

Add screen session management infrastructure:
- ScreenManager class to create, track, and kill GNU screen sessions
- Persistence of screen sessions to ~/.claudeman/screens.json
- Process stats collection (memory, CPU, child count)
- Reconciliation to detect dead/orphan screens

New API endpoints:
- GET /api/screens - list screens with stats
- DELETE /api/screens/:sessionId - kill screen
- POST /api/screens/reconcile - find dead screens
- POST /api/screens/stats/start|stop - control stats polling

New Process Monitor panel:
- Slide-up panel with real-time stats (2s updates)
- Shows memory, CPU, child processes per screen
- Kill button for each screen session
- Reconcile button to clean up dead screens

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-19 02:18:51 +01:00
co-authored by Claude Opus 4.5
parent b30657a36f
commit 56c9f670e6
6 changed files with 606 additions and 0 deletions
+56
View File
@@ -7,6 +7,7 @@ import { homedir } from 'node:os';
import { EventEmitter } from 'node:events';
import { Session, ClaudeMessage, type BackgroundTask } from '../session.js';
import { RespawnController, RespawnConfig, RespawnState } from '../respawn-controller.js';
import { ScreenManager } from '../screen-manager.js';
import { getStore } from '../state-store.js';
import { generateClaudeMd } from '../templates/claude-md.js';
import { v4 as uuidv4 } from 'uuid';
@@ -53,6 +54,7 @@ export class WebServer extends EventEmitter {
private sseClients: Set<FastifyReply> = new Set();
private store = getStore();
private port: number;
private screenManager: ScreenManager;
// Terminal batching for performance
private terminalBatches: Map<string, string> = new Map();
private terminalBatchTimer: NodeJS.Timeout | null = null;
@@ -61,6 +63,21 @@ export class WebServer extends EventEmitter {
super();
this.port = port;
this.app = Fastify({ logger: false });
this.screenManager = new ScreenManager();
// Set up screen manager event listeners
this.screenManager.on('screenCreated', (screen) => {
this.broadcast('screen:created', screen);
});
this.screenManager.on('screenKilled', (data) => {
this.broadcast('screen:killed', data);
});
this.screenManager.on('screenDied', (data) => {
this.broadcast('screen:died', data);
});
this.screenManager.on('statsUpdated', (screens) => {
this.broadcast('screen:statsUpdated', screens);
});
}
private async setupRoutes(): Promise<void> {
@@ -684,6 +701,42 @@ export class WebServer extends EventEmitter {
return { error: (err as Error).message };
}
});
// ============ Screen Management Endpoints ============
// Get all tracked screens with stats
this.app.get('/api/screens', async () => {
const screens = await this.screenManager.getScreensWithStats();
return {
screens,
screenAvailable: ScreenManager.isScreenAvailable()
};
});
// Kill a screen session
this.app.delete('/api/screens/:sessionId', async (req) => {
const { sessionId } = req.params as { sessionId: string };
const success = await this.screenManager.killScreen(sessionId);
return { success };
});
// Reconcile screens (find dead ones)
this.app.post('/api/screens/reconcile', async () => {
const result = await this.screenManager.reconcileScreens();
return result;
});
// Start stats collection
this.app.post('/api/screens/stats/start', async () => {
this.screenManager.startStatsCollection(2000);
return { success: true };
});
// Stop stats collection
this.app.post('/api/screens/stats/stop', async () => {
this.screenManager.stopStatsCollection();
return { success: true };
});
}
private setupSessionListeners(session: Session): void {
@@ -988,6 +1041,9 @@ export class WebServer extends EventEmitter {
}
this.terminalBatches.clear();
// Stop screen stats collection
this.screenManager.stopStatsCollection();
// Stop all respawn controllers
for (const controller of this.respawnControllers.values()) {
controller.stop();