diff --git a/AGENTS.md b/AGENTS.md index ce50b1b4..9fb1583c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,3 +115,26 @@ In `agentic/agents/`: 1. Create agent file 2. Define prompt in `prompts/` 3. Register in `registry.rs` + +## Frontend Debugging + +A local log receiver server is available at `scripts/debug-log-server.mjs`. + +**Start the server:** +```bash +node scripts/debug-log-server.mjs +# Listens on http://127.0.0.1:7469, writes logs to debug-agent.log +``` + +**Instrument code (one-liner fetch):** +```typescript +fetch('http://127.0.0.1:7469/log',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'file.ts:LINE',message:'desc',data:{k:v},timestamp:Date.now()})}).catch(()=>{}); +``` + +**Clear logs between runs:** +```bash +# Via HTTP +curl -X POST http://127.0.0.1:7469/clear +``` + +Logs are written to `debug-agent.log` in project root as NDJSON. The agent reads this file directly — no copy-paste needed. diff --git a/scripts/debug-log-server.mjs b/scripts/debug-log-server.mjs new file mode 100644 index 00000000..cf69d043 --- /dev/null +++ b/scripts/debug-log-server.mjs @@ -0,0 +1,86 @@ +/** + * Debug Log Receiver Server + * Receives POST requests from fetch-based instrumentation and writes to a log file. + * + * Usage: + * node scripts/debug-log-server.mjs [port] [logfile] + * + * Defaults: + * port = 7469 + * logfile = debug-agent.log (in project root) + * + * Frontend fetch template (copy into your code): + * fetch('http://127.0.0.1:7469/log', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({location:'file.ts:LINE', message:'desc', data:{k:v}, timestamp:Date.now()})}).catch(()=>{}); + */ + +import http from 'http'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT = path.resolve(__dirname, '..'); + +const PORT = parseInt(process.argv[2] ?? '7469', 10); +const LOG_FILE = path.resolve(ROOT, process.argv[3] ?? 'debug-agent.log'); + +// Clear log file on start +fs.writeFileSync(LOG_FILE, '', 'utf8'); +console.log(`[debug-log-server] started on http://127.0.0.1:${PORT}`); +console.log(`[debug-log-server] writing logs to ${LOG_FILE}`); +console.log(`[debug-log-server] fetch template:`); +console.log(` fetch('http://127.0.0.1:${PORT}/log', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({location:'file.ts:LINE', message:'desc', data:{}, timestamp:Date.now()})}).catch(()=>{});\n`); + +const server = http.createServer((req, res) => { + // CORS headers so browser can POST from any origin + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.writeHead(204); + res.end(); + return; + } + + if (req.method === 'POST' && req.url === '/log') { + let body = ''; + req.on('data', chunk => { body += chunk; }); + req.on('end', () => { + try { + const payload = JSON.parse(body); + const entry = JSON.stringify({ ...payload, _receivedAt: new Date().toISOString() }); + fs.appendFileSync(LOG_FILE, entry + '\n', 'utf8'); + + // Pretty print to console for quick monitoring + const loc = payload.location ?? '?'; + const msg = payload.message ?? ''; + const data = payload.data ? JSON.stringify(payload.data) : ''; + console.log(`[LOG] ${loc} | ${msg} | ${data}`); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true })); + } catch (e) { + res.writeHead(400); + res.end(JSON.stringify({ error: 'invalid json' })); + } + }); + return; + } + + // /clear - clear log file + if (req.method === 'POST' && req.url === '/clear') { + fs.writeFileSync(LOG_FILE, '', 'utf8'); + console.log('[debug-log-server] log file cleared'); + res.writeHead(200); + res.end(JSON.stringify({ ok: true })); + return; + } + + res.writeHead(404); + res.end(); +}); + +server.listen(PORT, '127.0.0.1', () => { + console.log('[debug-log-server] ready\n'); +}); diff --git a/src/web-ui/src/flow_chat/services/flow-chat-manager/SessionModule.ts b/src/web-ui/src/flow_chat/services/flow-chat-manager/SessionModule.ts index 841500e6..6c39b1a5 100644 --- a/src/web-ui/src/flow_chat/services/flow-chat-manager/SessionModule.ts +++ b/src/web-ui/src/flow_chat/services/flow-chat-manager/SessionModule.ts @@ -258,73 +258,8 @@ export async function switchChatSession( ): Promise { try { const session = context.flowChatStore.getState().sessions.get(sessionId); - - if (session?.isHistorical) { - try { - const workspacePath = requireSessionWorkspacePath(session.workspacePath, sessionId); - - await context.flowChatStore.loadSessionHistory( - sessionId, - workspacePath, - undefined, - session.remoteConnectionId, - session.remoteSshHost - ); - - try { - await agentAPI.restoreSession( - sessionId, - workspacePath, - session.remoteConnectionId, - session.remoteSshHost - ); - - context.flowChatStore.setState(prev => { - const newSessions = new Map(prev.sessions); - const sess = newSessions.get(sessionId); - if (sess) { - newSessions.set(sessionId, { ...sess, isHistorical: false }); - } - return { ...prev, sessions: newSessions }; - }); - } catch (restoreError: any) { - log.warn('Historical session restore failed, creating new session', { sessionId, error: restoreError }); - const currentSession = context.flowChatStore.getState().sessions.get(sessionId); - if (currentSession) { - await agentAPI.createSession({ - sessionId: sessionId, - sessionName: currentSession.title || `Session ${sessionId.slice(0, 8)}`, - agentType: currentSession.mode || 'agentic', - workspacePath, - remoteConnectionId: currentSession.remoteConnectionId, - remoteSshHost: currentSession.remoteSshHost, - config: { - modelName: currentSession.config.modelName || 'auto', - enableTools: true, - safeMode: true, - remoteConnectionId: currentSession.remoteConnectionId, - remoteSshHost: currentSession.remoteSshHost, - } - }); - - context.flowChatStore.setState(prev => { - const newSessions = new Map(prev.sessions); - const sess = newSessions.get(sessionId); - if (sess) { - newSessions.set(sessionId, { ...sess, isHistorical: false }); - } - return { ...prev, sessions: newSessions }; - }); - } - } - } catch (error) { - log.error('Failed to load session history', { sessionId, error }); - notificationService.warning('Failed to load session history, showing empty session', { - duration: 3000 - }); - } - } - + + // Switch UI immediately so the user sees the new session without waiting for history load. context.flowChatStore.switchSession(sessionId); touchSessionActivity( @@ -335,6 +270,29 @@ export async function switchChatSession( ).catch(error => { log.debug('Failed to touch session activity', { sessionId, error }); }); + + if (session?.isHistorical) { + // Load history in the background — do not block the UI. + (async () => { + try { + const workspacePath = requireSessionWorkspacePath(session.workspacePath, sessionId); + + // loadSessionHistory internally calls restoreSession + loadSessionTurns. + await context.flowChatStore.loadSessionHistory( + sessionId, + workspacePath, + undefined, + session.remoteConnectionId, + session.remoteSshHost + ); + } catch (error) { + log.error('Failed to load session history', { sessionId, error }); + notificationService.warning('Failed to load session history, showing empty session', { + duration: 3000 + }); + } + })(); + } } catch (error) { log.error('Failed to switch chat session', { sessionId, error }); notificationService.error('Failed to switch session', {