mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 05:29:42 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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> => {
|
||||
|
||||
Reference in New Issue
Block a user