mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +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
|
node dist/index.js web # After npm run build
|
||||||
claudeman web # After npm link
|
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)
|
# Testing (vitest)
|
||||||
# Note: globals: true configured - no imports needed for describe/it/expect
|
# Note: globals: true configured - no imports needed for describe/it/expect
|
||||||
npm run test # Run all tests once
|
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/state-store.ts` | JSON persistence to `~/.claudeman/` with debounced writes |
|
||||||
| `src/web/server.ts` | Fastify REST API + SSE at `/api/events` |
|
| `src/web/server.ts` | Fastify REST API + SSE at `/api/events` |
|
||||||
| `src/web/public/app.js` | Frontend: SSE handling, xterm.js, tab management |
|
| `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 |
|
| `src/types.ts` | All TypeScript interfaces |
|
||||||
|
|
||||||
### Data Flow
|
### Data Flow
|
||||||
|
|||||||
@@ -8,13 +8,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
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 { homedir } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { execSync, spawn } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
import type { ScreenSession } from '../../types.js';
|
import type { ScreenSession } from '../../types.js';
|
||||||
|
|
||||||
const SCREENS_FILE = join(homedir(), '.claudeman', 'screens.json');
|
const SCREENS_FILE = join(homedir(), '.claudeman', 'screens.json');
|
||||||
|
const OUTPUT_POLL_INTERVAL = 500; // Poll terminal output every 500ms
|
||||||
|
|
||||||
interface SessionManagerState {
|
interface SessionManagerState {
|
||||||
sessions: ScreenSession[];
|
sessions: ScreenSession[];
|
||||||
@@ -101,6 +102,49 @@ export function useSessionManager(): SessionManagerState {
|
|||||||
// Get active session
|
// Get active session
|
||||||
const activeSession = sessions.find((s) => s.sessionId === activeSessionId) || null;
|
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
|
// Refresh sessions manually
|
||||||
const refreshSessions = useCallback(() => {
|
const refreshSessions = useCallback(() => {
|
||||||
setSessions(loadSessions());
|
setSessions(loadSessions());
|
||||||
@@ -111,26 +155,8 @@ export function useSessionManager(): SessionManagerState {
|
|||||||
setActiveSessionId(sessionId);
|
setActiveSessionId(sessionId);
|
||||||
setTerminalOutput('');
|
setTerminalOutput('');
|
||||||
outputBufferRef.current = '';
|
outputBufferRef.current = '';
|
||||||
|
// Polling effect will handle fetching output
|
||||||
// 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]);
|
|
||||||
|
|
||||||
// Create new session
|
// Create new session
|
||||||
const createSession = useCallback(async (): Promise<string | null> => {
|
const createSession = useCallback(async (): Promise<string | null> => {
|
||||||
|
|||||||
Reference in New Issue
Block a user