fix: Chroma connection errors and remove dead last_user_message code (#525)
* fix: distinguish connection errors from collection-not-found in ChromaSync Previously, ensureCollection() caught ALL errors from chroma_get_collection_info and assumed they meant "collection doesn't exist". This caused connection errors like "Not connected" to trigger unnecessary collection creation attempts. Now connection-related errors are re-thrown immediately instead of being misinterpreted as missing collections. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: improve error handling for Chroma connection and collection creation * fix: remove dead last_user_message from summarize flow The last_user_message field was extracted from transcripts but never used. In Claude Code transcripts, "user" type messages are mostly tool_results, not actual user input. The user's original request is already stored in user_prompts table. This removes the false warning "Missing last_user_message when queueing summary" which was complaining about missing data that didn't exist and wasn't needed. Changes: - summary-hook: Only extract last_assistant_message - SessionRoutes: Remove last_user_message from request body handling - SessionManager.queueSummarize: Remove lastUserMessage parameter - PendingMessage interface: Remove last_user_message field - SDKSession interface: Remove last_user_message field - All agents: Remove last_user_message from buildSummaryPrompt calls 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * build artifacts for plugin * Enhance error handling across multiple services - Improved logging in `BranchManager.ts` to capture recovery checkout failures. - Updated `PaginationHelper.ts` to log when file paths are plain strings instead of valid JSON. - Enhanced error logging in `SDKAgent.ts` for Claude executable detection failures. - Added logging for plain string handling in `SearchManager.ts` for files read and edited. - Improved logging in `paths.ts` for git root detection failures. - Enhanced JSON parsing error handling in `timeline-formatting.ts` with previews of failed inputs. - Updated `transcript-parser.ts` to log summary of parse errors after processing transcript lines. - Established a baseline for error handling practices in `error-handling-baseline.txt`. - Documented error handling anti-pattern rules in `CLAUDE.md` to prevent silent failures and improve code quality. * Add error handling anti-pattern detection script and guidelines - Introduced `detect-error-handling-antipatterns.ts` to identify common error handling issues in TypeScript code. - Created comprehensive documentation in `CLAUDE.md` outlining forbidden patterns, allowed patterns, and critical path protection rules. - Implemented checks for empty catch blocks, logging practices, and try-catch block sizes to prevent silent failures and improve debugging. - Established a reporting mechanism to summarize detected anti-patterns with severity levels. * feat: add console filter bar and log line parsing with filtering capabilities - Introduced a console filter bar with options to filter logs by level and component. - Implemented parsing of log lines to extract structured data including timestamp, level, component, and correlation ID. - Added functionality to toggle individual and all levels/components for filtering. - Enhanced log line rendering with color coding based on log level and special message types. - Improved responsiveness of the filter bar for smaller screens. --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -42,13 +42,13 @@ async function summaryHook(input?: StopInput): Promise<void> {
|
||||
throw new Error(`Missing transcript_path in Stop hook input for session ${session_id}`);
|
||||
}
|
||||
|
||||
// Extract last user AND assistant messages from transcript
|
||||
const lastUserMessage = extractLastMessage(input.transcript_path, 'user');
|
||||
// Extract last assistant message from transcript (the work Claude did)
|
||||
// Note: "user" messages in transcripts are mostly tool_results, not actual user input.
|
||||
// The user's original request is already stored in user_prompts table.
|
||||
const lastAssistantMessage = extractLastMessage(input.transcript_path, 'assistant', true);
|
||||
|
||||
logger.dataIn('HOOK', 'Stop: Requesting summary', {
|
||||
workerPort: port,
|
||||
hasLastUserMessage: !!lastUserMessage,
|
||||
hasLastAssistantMessage: !!lastAssistantMessage
|
||||
});
|
||||
|
||||
@@ -58,7 +58,6 @@ async function summaryHook(input?: StopInput): Promise<void> {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
contentSessionId: session_id,
|
||||
last_user_message: lastUserMessage,
|
||||
last_assistant_message: lastAssistantMessage
|
||||
})
|
||||
// Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion)
|
||||
|
||||
+8
-7
@@ -20,7 +20,6 @@ export interface SDKSession {
|
||||
memory_session_id: string | null;
|
||||
project: string;
|
||||
user_prompt: string;
|
||||
last_user_message?: string;
|
||||
last_assistant_message?: string;
|
||||
}
|
||||
|
||||
@@ -97,17 +96,19 @@ export function buildObservationPrompt(obs: Observation): string {
|
||||
try {
|
||||
toolInput = typeof obs.tool_input === 'string' ? JSON.parse(obs.tool_input) : obs.tool_input;
|
||||
} catch (error) {
|
||||
// Expected: tool_input may not be valid JSON (e.g., plain strings)
|
||||
// Not logging - this is a normal fallback for non-JSON tool inputs
|
||||
toolInput = obs.tool_input; // If parse fails, use raw value
|
||||
logger.debug('SDK', 'Tool input is plain string, using as-is', {
|
||||
toolName: obs.tool_name
|
||||
}, error as Error);
|
||||
toolInput = obs.tool_input;
|
||||
}
|
||||
|
||||
try {
|
||||
toolOutput = typeof obs.tool_output === 'string' ? JSON.parse(obs.tool_output) : obs.tool_output;
|
||||
} catch (error) {
|
||||
// Expected: tool_output may not be valid JSON (e.g., plain strings)
|
||||
// Not logging - this is a normal fallback for non-JSON tool outputs
|
||||
toolOutput = obs.tool_output; // If parse fails, use raw value
|
||||
logger.debug('SDK', 'Tool output is plain string, using as-is', {
|
||||
toolName: obs.tool_name
|
||||
}, error as Error);
|
||||
toolOutput = obs.tool_output;
|
||||
}
|
||||
|
||||
return `<observed_from_primary_session>
|
||||
|
||||
@@ -200,8 +200,7 @@ function extractPriorMessages(transcriptPath: string): { userMessage: string; as
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
// Expected: malformed JSON lines in transcript
|
||||
// Not logging - this loops through many lines, logging each would be excessive
|
||||
logger.debug('PARSER', 'Skipping malformed transcript line', { lineIndex: i }, parseError as Error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -229,8 +228,7 @@ export async function generateContext(input?: ContextInput, useColors: boolean =
|
||||
try {
|
||||
unlinkSync(VERSION_MARKER_PATH);
|
||||
} catch (unlinkError) {
|
||||
// Marker might not exist - expected during first run
|
||||
// Not logging - this is a normal case during initial setup
|
||||
logger.debug('SYSTEM', 'Marker file cleanup failed (may not exist)', {}, unlinkError as Error);
|
||||
}
|
||||
logger.error('SYSTEM', 'Native module rebuild needed - restart Claude Code to auto-fix');
|
||||
return '';
|
||||
|
||||
@@ -14,7 +14,6 @@ export interface PersistentPendingMessage {
|
||||
tool_input: string | null;
|
||||
tool_response: string | null;
|
||||
cwd: string | null;
|
||||
last_user_message: string | null;
|
||||
last_assistant_message: string | null;
|
||||
prompt_number: number | null;
|
||||
status: 'pending' | 'processing' | 'processed' | 'failed';
|
||||
@@ -59,9 +58,9 @@ export class PendingMessageStore {
|
||||
INSERT INTO pending_messages (
|
||||
session_db_id, content_session_id, message_type,
|
||||
tool_name, tool_input, tool_response, cwd,
|
||||
last_user_message, last_assistant_message,
|
||||
last_assistant_message,
|
||||
prompt_number, status, retry_count, created_at_epoch
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
@@ -72,7 +71,6 @@ export class PendingMessageStore {
|
||||
message.tool_input ? JSON.stringify(message.tool_input) : null,
|
||||
message.tool_response ? JSON.stringify(message.tool_response) : null,
|
||||
message.cwd || null,
|
||||
message.last_user_message || null,
|
||||
message.last_assistant_message || null,
|
||||
message.prompt_number || null,
|
||||
now
|
||||
@@ -422,7 +420,6 @@ export class PendingMessageStore {
|
||||
tool_response: persistent.tool_response ? JSON.parse(persistent.tool_response) : undefined,
|
||||
prompt_number: persistent.prompt_number || undefined,
|
||||
cwd: persistent.cwd || undefined,
|
||||
last_user_message: persistent.last_user_message || undefined,
|
||||
last_assistant_message: persistent.last_assistant_message || undefined
|
||||
};
|
||||
}
|
||||
|
||||
@@ -165,7 +165,10 @@ export class ChromaSync {
|
||||
|
||||
logger.debug('CHROMA_SYNC', 'Collection exists', { collection: this.collectionName });
|
||||
} catch (error) {
|
||||
// Collection doesn't exist, create it
|
||||
// Log the FULL error - don't try to guess what type it is
|
||||
logger.warn('CHROMA_SYNC', 'Collection check failed, attempting to create', { collection: this.collectionName }, error as Error);
|
||||
|
||||
// Try to create collection - if this also fails, we'll see that error too
|
||||
logger.info('CHROMA_SYNC', 'Creating collection', { collection: this.collectionName });
|
||||
|
||||
try {
|
||||
|
||||
@@ -66,8 +66,8 @@ function removePidFile(): void {
|
||||
try {
|
||||
if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
|
||||
} catch (error) {
|
||||
// PID file removal is cleanup - log but don't fail shutdown
|
||||
logger.warn('SYSTEM', 'Failed to remove PID file', { path: PID_FILE, error: (error as Error).message });
|
||||
logger.warn('SYSTEM', 'Failed to remove PID file', { path: PID_FILE }, error as Error);
|
||||
return; // Non-critical cleanup, OK to fail
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,8 +129,8 @@ export async function updateCursorContextForProject(projectName: string, port: n
|
||||
writeContextFile(entry.workspacePath, context);
|
||||
logger.debug('CURSOR', 'Updated context file', { projectName, workspacePath: entry.workspacePath });
|
||||
} catch (error) {
|
||||
// Context update is non-critical - log and continue
|
||||
logger.warn('CURSOR', 'Failed to update context file', { projectName, error: (error as Error).message });
|
||||
logger.warn('CURSOR', 'Failed to update context file', { projectName }, error as Error);
|
||||
return; // Non-critical context update, OK to fail
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,8 +150,7 @@ async function isPortInUse(port: number): Promise<boolean> {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/api/health`);
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
// Expected: port is free or service not responding
|
||||
// Not logging - this is called frequently for health checks
|
||||
// [ANTI-PATTERN IGNORED]: Health check polls every 500ms, logging would flood
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -164,10 +163,8 @@ async function waitForHealth(port: number, timeoutMs: number = 30000): Promise<b
|
||||
const response = await fetch(`http://127.0.0.1:${port}/api/readiness`);
|
||||
if (response.ok) return true;
|
||||
} catch (error) {
|
||||
logger.debug('SYSTEM', 'Service not ready yet, will retry', {
|
||||
port,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
// [ANTI-PATTERN IGNORED]: Retry loop - expected failures during startup, will retry
|
||||
logger.debug('SYSTEM', 'Service not ready yet, will retry', { port }, error as Error);
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
@@ -370,7 +367,7 @@ export class WorkerService {
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
logger.error('SYSTEM', 'Error during shutdown', {}, error as Error);
|
||||
process.exit(1);
|
||||
process.exit(1); // Exit with error code - this terminates execution
|
||||
}
|
||||
};
|
||||
|
||||
@@ -459,6 +456,7 @@ export class WorkerService {
|
||||
}]
|
||||
});
|
||||
} catch (error) {
|
||||
// [POSSIBLY RELEVANT]: API must respond even on error, log full error and return error response
|
||||
logger.error('WORKER', 'Failed to load instructions', { topic, operation }, error as Error);
|
||||
res.status(500).json({
|
||||
content: [{
|
||||
@@ -543,6 +541,7 @@ export class WorkerService {
|
||||
// This avoids code duplication and "headers already sent" errors
|
||||
next();
|
||||
} catch (error) {
|
||||
// [POSSIBLY RELEVANT]: API must respond even on error, log full error and return error response
|
||||
logger.error('WORKER', 'Context inject handler failed', {}, error as Error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Internal server error' });
|
||||
@@ -621,19 +620,17 @@ export class WorkerService {
|
||||
try {
|
||||
execSync(`taskkill /PID ${pid} /T /F`, { timeout: 60000, stdio: 'ignore' });
|
||||
} catch (error) {
|
||||
logger.debug('SYSTEM', 'Failed to kill process, may have already exited', {
|
||||
pid,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
// [ANTI-PATTERN IGNORED]: Cleanup loop - process may have exited, continue to next PID
|
||||
logger.debug('SYSTEM', 'Failed to kill process, may have already exited', { pid }, error as Error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const pid of pids) {
|
||||
try {
|
||||
process.kill(pid, 'SIGKILL');
|
||||
} catch {
|
||||
// Process already exited - expected during cleanup
|
||||
logger.debug('SYSTEM', 'Process already exited', { pid });
|
||||
} catch (error) {
|
||||
// [ANTI-PATTERN IGNORED]: Cleanup loop - process may have exited, continue to next PID
|
||||
logger.debug('SYSTEM', 'Process already exited', { pid }, error as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -838,7 +835,7 @@ export class WorkerService {
|
||||
// Small delay between sessions to avoid rate limiting
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
} catch (error) {
|
||||
// Recovery is best-effort - skip failed sessions and continue with others
|
||||
// [ANTI-PATTERN IGNORED]: Recovery loop - skip failed session, continue to next
|
||||
logger.warn('SYSTEM', `Failed to process session ${sessionDbId}`, {}, error as Error);
|
||||
result.sessionsSkipped++;
|
||||
}
|
||||
@@ -986,9 +983,9 @@ export class WorkerService {
|
||||
process.kill(pid, 'SIGKILL');
|
||||
}
|
||||
logger.info('SYSTEM', 'Killed process', { pid });
|
||||
} catch {
|
||||
// Process may have already exited - continue shutdown
|
||||
logger.debug('SYSTEM', 'Process already exited during force kill', { pid });
|
||||
} catch (error) {
|
||||
// [ANTI-PATTERN IGNORED]: Shutdown cleanup - process already exited, continue
|
||||
logger.debug('SYSTEM', 'Process already exited during force kill', { pid }, error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1004,8 +1001,7 @@ export class WorkerService {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Expected: process has exited
|
||||
// Not logging - this is called in a tight loop during cleanup
|
||||
// [ANTI-PATTERN IGNORED]: Tight loop checking 100s of PIDs every 100ms during cleanup
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@@ -1094,8 +1090,9 @@ async function runInteractiveSetup(): Promise<number> {
|
||||
if (existsSync(settingsPath)) {
|
||||
try {
|
||||
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
||||
} catch {
|
||||
// Start fresh if corrupt
|
||||
} catch (error) {
|
||||
// [ANTI-PATTERN IGNORED]: Fallback behavior - corrupt settings, continue with defaults
|
||||
logger.debug('SETUP', 'Corrupt settings file, starting fresh', { path: settingsPath }, error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1300,8 +1297,9 @@ async function detectClaudeCode(): Promise<boolean> {
|
||||
if (stdout.trim()) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// CLI not found
|
||||
} catch (error) {
|
||||
// [ANTI-PATTERN IGNORED]: Fallback behavior - CLI not found, continue to directory check
|
||||
logger.debug('SYSTEM', 'Claude CLI not in PATH', {}, error as Error);
|
||||
}
|
||||
|
||||
// Check for Claude Code plugin directory
|
||||
@@ -1413,8 +1411,8 @@ function configureCursorMcp(target: string): number {
|
||||
config.mcpServers = {};
|
||||
}
|
||||
} catch (error) {
|
||||
// Start fresh if corrupt
|
||||
logger.warn('SYSTEM', 'Corrupt mcp.json, creating new config', { path: mcpJsonPath, error: error instanceof Error ? error.message : String(error) });
|
||||
// [ANTI-PATTERN IGNORED]: Fallback behavior - corrupt config, continue with empty
|
||||
logger.warn('SYSTEM', 'Corrupt mcp.json, creating new config', { path: mcpJsonPath }, error as Error);
|
||||
config = { mcpServers: {} };
|
||||
}
|
||||
}
|
||||
@@ -1669,8 +1667,9 @@ ${context}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Worker not running - that's ok, context will be generated after first session
|
||||
} catch (error) {
|
||||
// [ANTI-PATTERN IGNORED]: Fallback behavior - worker not running, use placeholder
|
||||
logger.debug('CURSOR', 'Worker not running during install', {}, error as Error);
|
||||
}
|
||||
|
||||
if (!contextGenerated) {
|
||||
|
||||
@@ -43,7 +43,6 @@ export interface PendingMessage {
|
||||
tool_response?: any;
|
||||
prompt_number?: number;
|
||||
cwd?: string;
|
||||
last_user_message?: string;
|
||||
last_assistant_message?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -240,8 +240,9 @@ export async function switchBranch(targetBranch: string): Promise<SwitchResult>
|
||||
if (info.branch && isValidBranchName(info.branch)) {
|
||||
execGit(['checkout', info.branch]);
|
||||
}
|
||||
} catch {
|
||||
// Recovery failed, user needs manual intervention
|
||||
} catch (recoveryError) {
|
||||
// [POSSIBLY RELEVANT]: Recovery checkout failed, user needs manual intervention - already logging main error above
|
||||
logger.warn('BRANCH', 'Recovery checkout also failed', { originalBranch: info.branch }, recoveryError as Error);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -229,7 +229,6 @@ export class GeminiAgent {
|
||||
memory_session_id: session.memorySessionId,
|
||||
project: session.project,
|
||||
user_prompt: session.userPrompt,
|
||||
last_user_message: message.last_user_message || '',
|
||||
last_assistant_message: message.last_assistant_message || ''
|
||||
}, mode);
|
||||
|
||||
|
||||
@@ -188,7 +188,6 @@ export class OpenRouterAgent {
|
||||
memory_session_id: session.memorySessionId,
|
||||
project: session.project,
|
||||
user_prompt: session.userPrompt,
|
||||
last_user_message: message.last_user_message || '',
|
||||
last_assistant_message: message.last_assistant_message || ''
|
||||
}, mode);
|
||||
|
||||
|
||||
@@ -52,8 +52,7 @@ export class PaginationHelper {
|
||||
// Return as JSON string
|
||||
return JSON.stringify(strippedPaths);
|
||||
} catch (err) {
|
||||
// Expected: file paths may not be valid JSON (plain string)
|
||||
// Not logging - normal fallback for non-JSON file path strings
|
||||
logger.debug('WORKER', 'File paths is plain string, using as-is', {}, err as Error);
|
||||
return filePathsStr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,7 +289,6 @@ export class SDKAgent {
|
||||
memory_session_id: session.memorySessionId,
|
||||
project: session.project,
|
||||
user_prompt: session.userPrompt,
|
||||
last_user_message: message.last_user_message || '',
|
||||
last_assistant_message: message.last_assistant_message || ''
|
||||
}, mode);
|
||||
|
||||
@@ -544,7 +543,8 @@ export class SDKAgent {
|
||||
|
||||
if (claudePath) return claudePath;
|
||||
} catch (error) {
|
||||
logger.debug('SDK', 'Claude executable auto-detection failed', error);
|
||||
// [ANTI-PATTERN IGNORED]: Fallback behavior - which/where failed, continue to throw clear error
|
||||
logger.debug('SDK', 'Claude executable auto-detection failed', {}, error as Error);
|
||||
}
|
||||
|
||||
throw new Error('Claude executable not found. Please either:\n1. Add "claude" to your system PATH, or\n2. Set CLAUDE_CODE_PATH in ~/.claude-mem/settings.json');
|
||||
|
||||
@@ -1401,8 +1401,7 @@ export class SearchManager {
|
||||
lines.push(`**Files Read:** ${filesRead.join(', ')}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// Expected: files_read may not be valid JSON (plain string)
|
||||
// Not logging - normal fallback for plain text file lists
|
||||
logger.debug('WORKER', 'files_read is plain string, using as-is', {}, error as Error);
|
||||
if (summary.files_read.trim()) {
|
||||
lines.push(`**Files Read:** ${summary.files_read}`);
|
||||
}
|
||||
@@ -1417,8 +1416,7 @@ export class SearchManager {
|
||||
lines.push(`**Files Edited:** ${filesEdited.join(', ')}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// Expected: files_edited may not be valid JSON (plain string)
|
||||
// Not logging - normal fallback for plain text file lists
|
||||
logger.debug('WORKER', 'files_edited is plain string, using as-is', {}, error as Error);
|
||||
if (summary.files_edited.trim()) {
|
||||
lines.push(`**Files Edited:** ${summary.files_edited}`);
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ export class SessionManager {
|
||||
* CRITICAL: Persists to database FIRST before adding to in-memory queue.
|
||||
* This ensures summarize requests survive worker crashes.
|
||||
*/
|
||||
queueSummarize(sessionDbId: number, lastUserMessage: string, lastAssistantMessage?: string): void {
|
||||
queueSummarize(sessionDbId: number, lastAssistantMessage?: string): void {
|
||||
// Auto-initialize from database if needed (handles worker restarts)
|
||||
let session = this.sessions.get(sessionDbId);
|
||||
if (!session) {
|
||||
@@ -244,7 +244,6 @@ export class SessionManager {
|
||||
// CRITICAL: Persist to database FIRST
|
||||
const message: PendingMessage = {
|
||||
type: 'summarize',
|
||||
last_user_message: lastUserMessage,
|
||||
last_assistant_message: lastAssistantMessage
|
||||
};
|
||||
|
||||
|
||||
@@ -338,9 +338,9 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
const sessionDbId = this.parseIntParam(req, res, 'sessionDbId');
|
||||
if (sessionDbId === null) return;
|
||||
|
||||
const { last_user_message, last_assistant_message } = req.body;
|
||||
const { last_assistant_message } = req.body;
|
||||
|
||||
this.sessionManager.queueSummarize(sessionDbId, last_user_message, last_assistant_message);
|
||||
this.sessionManager.queueSummarize(sessionDbId, last_assistant_message);
|
||||
|
||||
// CRITICAL: Ensure SDK agent is running to consume the queue
|
||||
this.ensureGeneratorRunning(sessionDbId, 'summarize');
|
||||
@@ -492,12 +492,12 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
/**
|
||||
* Queue summarize by contentSessionId (summary-hook uses this)
|
||||
* POST /api/sessions/summarize
|
||||
* Body: { contentSessionId, last_user_message, last_assistant_message }
|
||||
* Body: { contentSessionId, last_assistant_message }
|
||||
*
|
||||
* Checks privacy, queues summarize request for SDK agent
|
||||
*/
|
||||
private handleSummarizeByClaudeId = this.wrapHandler((req: Request, res: Response): void => {
|
||||
const { contentSessionId, last_user_message, last_assistant_message } = req.body;
|
||||
const { contentSessionId, last_assistant_message } = req.body;
|
||||
|
||||
if (!contentSessionId) {
|
||||
return this.badRequest(res, 'Missing contentSessionId');
|
||||
@@ -523,17 +523,7 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
}
|
||||
|
||||
// Queue summarize
|
||||
this.sessionManager.queueSummarize(
|
||||
sessionDbId,
|
||||
last_user_message || logger.happyPathError(
|
||||
'SESSION',
|
||||
'Missing last_user_message when queueing summary in SessionRoutes',
|
||||
{ sessionId: sessionDbId },
|
||||
undefined,
|
||||
''
|
||||
),
|
||||
last_assistant_message
|
||||
);
|
||||
this.sessionManager.queueSummarize(sessionDbId, last_assistant_message);
|
||||
|
||||
// Ensure SDK agent is running
|
||||
this.ensureGeneratorRunning(sessionDbId, 'summarize');
|
||||
|
||||
+4
-2
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync } from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { SettingsDefaultsManager } from './SettingsDefaultsManager.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
// Get __dirname that works in both ESM (hooks) and CJS (worker) contexts
|
||||
function getDirname(): string {
|
||||
@@ -103,8 +104,9 @@ export function getCurrentProjectName(): string {
|
||||
}).trim();
|
||||
return basename(gitRoot);
|
||||
} catch (error) {
|
||||
// Expected: not a git repo or git not available
|
||||
// Not logging - this is a common fallback path
|
||||
logger.debug('SYSTEM', 'Git root detection failed, using cwd basename', {
|
||||
cwd: process.cwd()
|
||||
}, error as Error);
|
||||
return basename(process.cwd());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
/**
|
||||
* Parse JSON array string, returning empty array on failure
|
||||
@@ -16,7 +17,9 @@ export function parseJsonArray(json: string | null): string[] {
|
||||
const parsed = JSON.parse(json);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (err) {
|
||||
// [APPROVED OVERRIDE]: Expected JSON parse failures for malformed data fields, too frequent to log
|
||||
logger.debug('PARSER', 'Failed to parse JSON array, using empty fallback', {
|
||||
preview: json?.substring(0, 50)
|
||||
}, err as Error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2643,6 +2643,161 @@
|
||||
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
/* Console Filter Bar */
|
||||
.console-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
background: var(--color-bg-secondary);
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
.console-filter-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.console-filter-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.console-filter-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.console-filter-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 3px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: 4px;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.console-filter-chip:hover {
|
||||
background: var(--color-bg-card-hover);
|
||||
border-color: var(--chip-color, var(--color-border-hover));
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.console-filter-chip.active {
|
||||
background: var(--chip-color, var(--color-accent-primary));
|
||||
border-color: var(--chip-color, var(--color-accent-primary));
|
||||
color: white;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.console-filter-chip.active:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.console-filter-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: 4px;
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.console-filter-action:hover {
|
||||
background: var(--color-bg-card-hover);
|
||||
border-color: var(--color-border-hover);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
/* Log Line Styles */
|
||||
.log-line {
|
||||
display: block;
|
||||
padding: 2px 0;
|
||||
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.log-line-raw {
|
||||
color: var(--color-text-secondary);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.log-line-empty {
|
||||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
padding: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
color: var(--color-text-muted);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.log-level {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.log-component {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.log-correlation {
|
||||
color: var(--color-accent-primary);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Log Level Colors in Dark Mode */
|
||||
[data-theme="dark"] .log-line-raw {
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for filter bar */
|
||||
@media (max-width: 600px) {
|
||||
.console-filters {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.console-filter-section {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.console-filter-chip {
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive Modal */
|
||||
@media (max-width: 900px) {
|
||||
.modal-body {
|
||||
|
||||
@@ -1,4 +1,72 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||
|
||||
// Log levels and components matching the logger.ts definitions
|
||||
type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
|
||||
type LogComponent = 'HOOK' | 'WORKER' | 'SDK' | 'PARSER' | 'DB' | 'SYSTEM' | 'HTTP' | 'SESSION' | 'CHROMA';
|
||||
|
||||
interface ParsedLogLine {
|
||||
raw: string;
|
||||
timestamp?: string;
|
||||
level?: LogLevel;
|
||||
component?: LogComponent;
|
||||
correlationId?: string;
|
||||
message?: string;
|
||||
isSpecial?: 'dataIn' | 'dataOut' | 'success' | 'failure' | 'timing' | 'happyPath';
|
||||
}
|
||||
|
||||
// Configuration for log levels
|
||||
const LOG_LEVELS: { key: LogLevel; label: string; icon: string; color: string }[] = [
|
||||
{ key: 'DEBUG', label: 'Debug', icon: '🔍', color: '#8b8b8b' },
|
||||
{ key: 'INFO', label: 'Info', icon: 'ℹ️', color: '#58a6ff' },
|
||||
{ key: 'WARN', label: 'Warn', icon: '⚠️', color: '#d29922' },
|
||||
{ key: 'ERROR', label: 'Error', icon: '❌', color: '#f85149' },
|
||||
];
|
||||
|
||||
// Configuration for log components
|
||||
const LOG_COMPONENTS: { key: LogComponent; label: string; icon: string; color: string }[] = [
|
||||
{ key: 'HOOK', label: 'Hook', icon: '🪝', color: '#a371f7' },
|
||||
{ key: 'WORKER', label: 'Worker', icon: '⚙️', color: '#58a6ff' },
|
||||
{ key: 'SDK', label: 'SDK', icon: '📦', color: '#3fb950' },
|
||||
{ key: 'PARSER', label: 'Parser', icon: '📄', color: '#79c0ff' },
|
||||
{ key: 'DB', label: 'DB', icon: '🗄️', color: '#f0883e' },
|
||||
{ key: 'SYSTEM', label: 'System', icon: '💻', color: '#8b949e' },
|
||||
{ key: 'HTTP', label: 'HTTP', icon: '🌐', color: '#39d353' },
|
||||
{ key: 'SESSION', label: 'Session', icon: '📋', color: '#db61a2' },
|
||||
{ key: 'CHROMA', label: 'Chroma', icon: '🔮', color: '#a855f7' },
|
||||
];
|
||||
|
||||
// Parse a single log line into structured data
|
||||
function parseLogLine(line: string): ParsedLogLine {
|
||||
// Pattern: [timestamp] [LEVEL] [COMPONENT] [correlation?] message
|
||||
// Example: [2025-01-02 14:30:45.123] [INFO ] [WORKER] [session-123] → message
|
||||
const pattern = /^\[([^\]]+)\]\s+\[(\w+)\s*\]\s+\[(\w+)\s*\]\s+(?:\[([^\]]+)\]\s+)?(.*)$/;
|
||||
const match = line.match(pattern);
|
||||
|
||||
if (!match) {
|
||||
return { raw: line };
|
||||
}
|
||||
|
||||
const [, timestamp, level, component, correlationId, message] = match;
|
||||
|
||||
// Detect special message types
|
||||
let isSpecial: ParsedLogLine['isSpecial'] = undefined;
|
||||
if (message.startsWith('→')) isSpecial = 'dataIn';
|
||||
else if (message.startsWith('←')) isSpecial = 'dataOut';
|
||||
else if (message.startsWith('✓')) isSpecial = 'success';
|
||||
else if (message.startsWith('✗')) isSpecial = 'failure';
|
||||
else if (message.startsWith('⏱')) isSpecial = 'timing';
|
||||
else if (message.includes('[HAPPY-PATH]')) isSpecial = 'happyPath';
|
||||
|
||||
return {
|
||||
raw: line,
|
||||
timestamp,
|
||||
level: level?.trim() as LogLevel,
|
||||
component: component?.trim() as LogComponent,
|
||||
correlationId: correlationId || undefined,
|
||||
message,
|
||||
isSpecial,
|
||||
};
|
||||
}
|
||||
|
||||
interface LogsDrawerProps {
|
||||
isOpen: boolean;
|
||||
@@ -10,12 +78,53 @@ export function LogsDrawer({ isOpen, onClose }: LogsDrawerProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [autoRefresh, setAutoRefresh] = useState(false);
|
||||
const [height, setHeight] = useState(300); // Default height
|
||||
const [height, setHeight] = useState(350);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const startYRef = useRef(0);
|
||||
const startHeightRef = useRef(0);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const wasAtBottomRef = useRef(true);
|
||||
|
||||
// Filter state
|
||||
const [activeLevels, setActiveLevels] = useState<Set<LogLevel>>(
|
||||
new Set(['DEBUG', 'INFO', 'WARN', 'ERROR'])
|
||||
);
|
||||
const [activeComponents, setActiveComponents] = useState<Set<LogComponent>>(
|
||||
new Set(['HOOK', 'WORKER', 'SDK', 'PARSER', 'DB', 'SYSTEM', 'HTTP', 'SESSION', 'CHROMA'])
|
||||
);
|
||||
|
||||
// Parse and filter log lines
|
||||
const parsedLines = useMemo(() => {
|
||||
if (!logs) return [];
|
||||
return logs.split('\n').map(parseLogLine);
|
||||
}, [logs]);
|
||||
|
||||
const filteredLines = useMemo(() => {
|
||||
return parsedLines.filter(line => {
|
||||
// Always show unparsed lines
|
||||
if (!line.level || !line.component) return true;
|
||||
return activeLevels.has(line.level) && activeComponents.has(line.component);
|
||||
});
|
||||
}, [parsedLines, activeLevels, activeComponents]);
|
||||
|
||||
// Check if user is at bottom before updating
|
||||
const checkIfAtBottom = useCallback(() => {
|
||||
if (!contentRef.current) return true;
|
||||
const { scrollTop, scrollHeight, clientHeight } = contentRef.current;
|
||||
return scrollHeight - scrollTop - clientHeight < 50;
|
||||
}, []);
|
||||
|
||||
// Auto-scroll to bottom
|
||||
const scrollToBottom = useCallback(() => {
|
||||
if (contentRef.current && wasAtBottomRef.current) {
|
||||
contentRef.current.scrollTop = contentRef.current.scrollHeight;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchLogs = useCallback(async () => {
|
||||
// Save scroll position before fetch
|
||||
wasAtBottomRef.current = checkIfAtBottom();
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
@@ -30,7 +139,12 @@ export function LogsDrawer({ isOpen, onClose }: LogsDrawerProps) {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [checkIfAtBottom]);
|
||||
|
||||
// Scroll to bottom after logs update
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [logs, scrollToBottom]);
|
||||
|
||||
const handleClearLogs = useCallback(async () => {
|
||||
if (!confirm('Are you sure you want to clear all logs?')) {
|
||||
@@ -84,6 +198,7 @@ export function LogsDrawer({ isOpen, onClose }: LogsDrawerProps) {
|
||||
// Fetch logs when drawer opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
wasAtBottomRef.current = true; // Start at bottom on open
|
||||
fetchLogs();
|
||||
}
|
||||
}, [isOpen, fetchLogs]);
|
||||
@@ -98,10 +213,119 @@ export function LogsDrawer({ isOpen, onClose }: LogsDrawerProps) {
|
||||
return () => clearInterval(interval);
|
||||
}, [isOpen, autoRefresh, fetchLogs]);
|
||||
|
||||
// Toggle level filter
|
||||
const toggleLevel = useCallback((level: LogLevel) => {
|
||||
setActiveLevels(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(level)) {
|
||||
next.delete(level);
|
||||
} else {
|
||||
next.add(level);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Toggle component filter
|
||||
const toggleComponent = useCallback((component: LogComponent) => {
|
||||
setActiveComponents(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(component)) {
|
||||
next.delete(component);
|
||||
} else {
|
||||
next.add(component);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Select all / none for levels
|
||||
const setAllLevels = useCallback((enabled: boolean) => {
|
||||
if (enabled) {
|
||||
setActiveLevels(new Set(['DEBUG', 'INFO', 'WARN', 'ERROR']));
|
||||
} else {
|
||||
setActiveLevels(new Set());
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Select all / none for components
|
||||
const setAllComponents = useCallback((enabled: boolean) => {
|
||||
if (enabled) {
|
||||
setActiveComponents(new Set(['HOOK', 'WORKER', 'SDK', 'PARSER', 'DB', 'SYSTEM', 'HTTP', 'SESSION', 'CHROMA']));
|
||||
} else {
|
||||
setActiveComponents(new Set());
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get style for a parsed log line
|
||||
const getLineStyle = (line: ParsedLogLine): React.CSSProperties => {
|
||||
const levelConfig = LOG_LEVELS.find(l => l.key === line.level);
|
||||
const componentConfig = LOG_COMPONENTS.find(c => c.key === line.component);
|
||||
|
||||
let color = 'var(--color-text-primary)';
|
||||
let fontWeight = 'normal';
|
||||
let backgroundColor = 'transparent';
|
||||
|
||||
if (line.level === 'ERROR') {
|
||||
color = '#f85149';
|
||||
backgroundColor = 'rgba(248, 81, 73, 0.1)';
|
||||
} else if (line.level === 'WARN') {
|
||||
color = '#d29922';
|
||||
backgroundColor = 'rgba(210, 153, 34, 0.05)';
|
||||
} else if (line.isSpecial === 'success') {
|
||||
color = '#3fb950';
|
||||
} else if (line.isSpecial === 'failure') {
|
||||
color = '#f85149';
|
||||
} else if (line.isSpecial === 'happyPath') {
|
||||
color = '#d29922';
|
||||
} else if (levelConfig) {
|
||||
color = levelConfig.color;
|
||||
}
|
||||
|
||||
return { color, fontWeight, backgroundColor, padding: '1px 0', borderRadius: '2px' };
|
||||
};
|
||||
|
||||
// Render a single log line with syntax highlighting
|
||||
const renderLogLine = (line: ParsedLogLine, index: number) => {
|
||||
if (!line.timestamp) {
|
||||
// Unparsed line - render as-is
|
||||
return (
|
||||
<div key={index} className="log-line log-line-raw">
|
||||
{line.raw}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const levelConfig = LOG_LEVELS.find(l => l.key === line.level);
|
||||
const componentConfig = LOG_COMPONENTS.find(c => c.key === line.component);
|
||||
|
||||
return (
|
||||
<div key={index} className="log-line" style={getLineStyle(line)}>
|
||||
<span className="log-timestamp">[{line.timestamp}]</span>
|
||||
{' '}
|
||||
<span className="log-level" style={{ color: levelConfig?.color }} title={line.level}>
|
||||
[{levelConfig?.icon || ''} {line.level?.padEnd(5)}]
|
||||
</span>
|
||||
{' '}
|
||||
<span className="log-component" style={{ color: componentConfig?.color }} title={line.component}>
|
||||
[{componentConfig?.icon || ''} {line.component?.padEnd(7)}]
|
||||
</span>
|
||||
{' '}
|
||||
{line.correlationId && (
|
||||
<>
|
||||
<span className="log-correlation">[{line.correlationId}]</span>
|
||||
{' '}
|
||||
</>
|
||||
)}
|
||||
<span className="log-message">{line.message}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="console-drawer" style={{ height: `${height}px` }}>
|
||||
<div
|
||||
@@ -132,6 +356,16 @@ export function LogsDrawer({ isOpen, onClose }: LogsDrawerProps) {
|
||||
>
|
||||
↻
|
||||
</button>
|
||||
<button
|
||||
className="console-control-btn"
|
||||
onClick={() => {
|
||||
wasAtBottomRef.current = true;
|
||||
scrollToBottom();
|
||||
}}
|
||||
title="Scroll to bottom"
|
||||
>
|
||||
⬇
|
||||
</button>
|
||||
<button
|
||||
className="console-control-btn console-clear-btn"
|
||||
onClick={handleClearLogs}
|
||||
@@ -150,16 +384,74 @@ export function LogsDrawer({ isOpen, onClose }: LogsDrawerProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filter Bar */}
|
||||
<div className="console-filters">
|
||||
<div className="console-filter-section">
|
||||
<span className="console-filter-label">Levels:</span>
|
||||
<div className="console-filter-chips">
|
||||
{LOG_LEVELS.map(level => (
|
||||
<button
|
||||
key={level.key}
|
||||
className={`console-filter-chip ${activeLevels.has(level.key) ? 'active' : ''}`}
|
||||
onClick={() => toggleLevel(level.key)}
|
||||
style={{
|
||||
'--chip-color': level.color,
|
||||
} as React.CSSProperties}
|
||||
title={level.label}
|
||||
>
|
||||
{level.icon} {level.label}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
className="console-filter-action"
|
||||
onClick={() => setAllLevels(activeLevels.size === 0)}
|
||||
title={activeLevels.size === LOG_LEVELS.length ? 'Select none' : 'Select all'}
|
||||
>
|
||||
{activeLevels.size === LOG_LEVELS.length ? '○' : '●'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="console-filter-section">
|
||||
<span className="console-filter-label">Components:</span>
|
||||
<div className="console-filter-chips">
|
||||
{LOG_COMPONENTS.map(comp => (
|
||||
<button
|
||||
key={comp.key}
|
||||
className={`console-filter-chip ${activeComponents.has(comp.key) ? 'active' : ''}`}
|
||||
onClick={() => toggleComponent(comp.key)}
|
||||
style={{
|
||||
'--chip-color': comp.color,
|
||||
} as React.CSSProperties}
|
||||
title={comp.label}
|
||||
>
|
||||
{comp.icon} {comp.label}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
className="console-filter-action"
|
||||
onClick={() => setAllComponents(activeComponents.size === 0)}
|
||||
title={activeComponents.size === LOG_COMPONENTS.length ? 'Select none' : 'Select all'}
|
||||
>
|
||||
{activeComponents.size === LOG_COMPONENTS.length ? '○' : '●'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="console-error">
|
||||
⚠ {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="console-content">
|
||||
<pre className="console-logs">
|
||||
{logs || 'No logs available'}
|
||||
</pre>
|
||||
<div className="console-content" ref={contentRef}>
|
||||
<div className="console-logs">
|
||||
{filteredLines.length === 0 ? (
|
||||
<div className="log-line log-line-empty">No logs available</div>
|
||||
) : (
|
||||
filteredLines.map((line, index) => renderLogLine(line, index))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { logger } from './logger.js';
|
||||
import type {
|
||||
TranscriptEntry,
|
||||
UserTranscriptEntry,
|
||||
@@ -42,14 +43,22 @@ export class TranscriptParser {
|
||||
const entry = JSON.parse(line) as TranscriptEntry;
|
||||
this.entries.push(entry);
|
||||
} catch (error) {
|
||||
// Note: Parse errors are accumulated and accessible via getParseErrors()
|
||||
// Not logging each individual line failure - would be too verbose for large transcripts
|
||||
logger.debug('PARSER', 'Failed to parse transcript line', { lineNumber: index + 1 }, error as Error);
|
||||
this.parseErrors.push({
|
||||
lineNumber: index + 1,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Log summary if there were parse errors
|
||||
if (this.parseErrors.length > 0) {
|
||||
logger.warn('PARSER', `Failed to parse ${this.parseErrors.length} lines`, {
|
||||
path: transcriptPath,
|
||||
totalLines: lines.length,
|
||||
errorCount: this.parseErrors.length
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user