feat(tui): add real-time terminal output polling

- Poll screen hardcopy every 500ms for live output
- Clean up temp files after reading
- Update CLAUDE.md with TUI documentation
- Add TUI command examples to Commands section

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-21 06:21:25 +01:00
co-authored by Claude Opus 4.5
parent 84dab25f38
commit f6819729aa
2 changed files with 54 additions and 22 deletions
+6
View File
@@ -42,6 +42,10 @@ npm run web # After npm run build (shorthand)
node dist/index.js web # After npm run build
claudeman web # After npm link
# Start TUI (terminal user interface):
npx tsx src/index.ts tui # Dev mode - full-screen TUI
claudeman tui # After npm link
# Testing (vitest)
# Note: globals: true configured - no imports needed for describe/it/expect
npm run test # Run all tests once
@@ -96,6 +100,8 @@ pkill -f "SCREEN.*claudeman" # Force kill all claudeman screens
| `src/state-store.ts` | JSON persistence to `~/.claudeman/` with debounced writes |
| `src/web/server.ts` | Fastify REST API + SSE at `/api/events` |
| `src/web/public/app.js` | Frontend: SSE handling, xterm.js, tab management |
| `src/tui/App.tsx` | TUI main component: tabs, terminal viewport, status bar (Ink/React) |
| `src/tui/hooks/useSessionManager.ts` | TUI session state, screen polling, input handling |
| `src/types.ts` | All TypeScript interfaces |
### Data Flow
+48 -22
View File
@@ -8,13 +8,14 @@
*/
import { useState, useEffect, useCallback, useRef } from 'react';
import { existsSync, readFileSync, watchFile, unwatchFile } from 'fs';
import { existsSync, readFileSync, writeFileSync, watchFile, unwatchFile, unlinkSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { execSync, spawn } from 'child_process';
import { execSync } from 'child_process';
import type { ScreenSession } from '../../types.js';
const SCREENS_FILE = join(homedir(), '.claudeman', 'screens.json');
const OUTPUT_POLL_INTERVAL = 500; // Poll terminal output every 500ms
interface SessionManagerState {
sessions: ScreenSession[];
@@ -101,6 +102,49 @@ export function useSessionManager(): SessionManagerState {
// Get active session
const activeSession = sessions.find((s) => s.sessionId === activeSessionId) || null;
// Poll terminal output for active session
useEffect(() => {
if (!activeSessionId || !activeSession) return;
const pollOutput = () => {
if (!isScreenAlive(activeSession.screenName)) return;
try {
const hardcopyFile = `/tmp/claudeman-${activeSessionId}-hardcopy`;
execSync(`screen -S ${activeSession.screenName} -X hardcopy ${hardcopyFile}`, {
encoding: 'utf-8',
timeout: 1000,
});
if (existsSync(hardcopyFile)) {
const content = readFileSync(hardcopyFile, 'utf-8');
// Only update if content changed
if (content !== outputBufferRef.current) {
outputBufferRef.current = content;
setTerminalOutput(content);
}
// Clean up temp file
try {
unlinkSync(hardcopyFile);
} catch {
// Ignore cleanup errors
}
}
} catch {
// Hardcopy may fail, that's ok
}
};
// Initial poll
pollOutput();
// Set up polling interval
const intervalId = setInterval(pollOutput, OUTPUT_POLL_INTERVAL);
return () => {
clearInterval(intervalId);
};
}, [activeSessionId, activeSession]);
// Refresh sessions manually
const refreshSessions = useCallback(() => {
setSessions(loadSessions());
@@ -111,26 +155,8 @@ export function useSessionManager(): SessionManagerState {
setActiveSessionId(sessionId);
setTerminalOutput('');
outputBufferRef.current = '';
// Start capturing output from the screen session
const session = sessions.find((s) => s.sessionId === sessionId);
if (session && isScreenAlive(session.screenName)) {
// Use screen -X hardcopy to capture current screen content
try {
const hardcopyFile = `/tmp/claudeman-${sessionId}-hardcopy`;
execSync(`screen -S ${session.screenName} -X hardcopy ${hardcopyFile}`, {
encoding: 'utf-8',
});
if (existsSync(hardcopyFile)) {
const content = readFileSync(hardcopyFile, 'utf-8');
setTerminalOutput(content);
outputBufferRef.current = content;
}
} catch {
// Hardcopy may fail, that's ok
}
}
}, [sessions]);
// Polling effect will handle fetching output
}, []);
// Create new session
const createSession = useCallback(async (): Promise<string | null> => {