From 386fe0c8854d55364f16148ed1f0049628876213 Mon Sep 17 00:00:00 2001 From: Tony <2420242@qq.com> Date: Sat, 5 Jul 2025 03:52:11 +0800 Subject: [PATCH 1/3] fix: improve error handling for cloud sync ENOENT errors in live monitoring - Add specific handling for ENOENT errors in live monitoring - Show user-friendly warning instead of error for sync issues - Use debug level logging for temporary file unavailability - Preserve original error information for better debugging Fixes #246 --- src/_live-monitor.ts | 21 +++++++++++++++++++-- src/commands/_blocks.live.ts | 27 ++++++++++++++++++++++----- src/data-loader.ts | 16 ++++++++++++++-- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/_live-monitor.ts b/src/_live-monitor.ts index fd7af213..fda700a7 100644 --- a/src/_live-monitor.ts +++ b/src/_live-monitor.ts @@ -23,6 +23,7 @@ import { sortFilesByTimestamp, usageDataSchema, } from './data-loader.ts'; +import { logger } from './logger.ts'; import { PricingFetcher } from './pricing-fetcher.ts'; /** @@ -94,12 +95,28 @@ export class LiveMonitor implements Disposable { for (const file of sortedFiles) { const fileReader = Result.try({ try: async () => readFile(file, 'utf-8'), - catch: () => new Error('File read failed'), + catch: (error) => { + // Preserve original error information for better debugging + if (error instanceof Error) { + return error; + } + return new Error(`File read failed: ${String(error)}`); + }, }); const fileResult = await fileReader(); if (Result.isFailure(fileResult)) { - // Skip files that can't be read + // Log file access issues for debugging, but continue processing + const error = fileResult.error; + const isEnoent = error.message.includes('ENOENT') || error.message.includes('no such file or directory'); + + if (isEnoent) { + // For ENOENT errors (likely due to cloud sync), use debug level + logger.debug(`File temporarily unavailable (likely syncing): ${path.basename(file)}`); + } else { + // For other read errors, use debug level but with more detail + logger.debug(`Failed to read usage file ${path.basename(file)}:`, error.message); + } continue; } diff --git a/src/commands/_blocks.live.ts b/src/commands/_blocks.live.ts index e09d8485..316e98eb 100644 --- a/src/commands/_blocks.live.ts +++ b/src/commands/_blocks.live.ts @@ -93,11 +93,28 @@ export async function startLiveMonitoring(config: LiveMonitoringConfig): Promise // Handle and display errors const errorMessage = error instanceof Error ? error.message : String(error); - terminal.startBuffering(); - terminal.clearScreen(); - terminal.write(pc.red(`Error: ${errorMessage}\n`)); - terminal.flush(); - logger.error(`Live monitoring error: ${errorMessage}`); + + // Check if this is a file synchronization related error (ENOENT) + const isSyncError = errorMessage.includes('ENOENT') || errorMessage.includes('no such file or directory'); + + if (isSyncError) { + // For sync-related errors, show a friendlier message and use warning level + const friendlyMessage = 'File temporarily unavailable (likely due to cloud sync)'; + terminal.startBuffering(); + terminal.clearScreen(); + terminal.write(pc.yellow(`Warning: ${friendlyMessage}\n`)); + terminal.write(pc.dim('Waiting for file sync to complete...\n')); + terminal.flush(); + logger.warn(`File sync issue detected: ${errorMessage}`); + } else { + // For other errors, use the original error handling + terminal.startBuffering(); + terminal.clearScreen(); + terminal.write(pc.red(`Error: ${errorMessage}\n`)); + terminal.flush(); + logger.error(`Live monitoring error: ${errorMessage}`); + } + await delayWithAbort(config.refreshInterval, abortController.signal).catch(() => {}); } } diff --git a/src/data-loader.ts b/src/data-loader.ts index 5f92db4e..1b3dcd0c 100644 --- a/src/data-loader.ts +++ b/src/data-loader.ts @@ -516,9 +516,21 @@ export async function getEarliestTimestamp(filePath: string): Promise Date: Sat, 5 Jul 2025 04:12:59 +0800 Subject: [PATCH 2/3] fix: prevent ENOENT errors from terminating live monitoring - Move ENOENT error handling inside the monitoring loop - Continue monitoring after showing sync warning instead of exiting - Preserve program termination for non-sync errors - Improve user experience during cloud sync scenarios --- src/commands/_blocks.live.ts | 63 ++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/src/commands/_blocks.live.ts b/src/commands/_blocks.live.ts index 316e98eb..38cb5670 100644 --- a/src/commands/_blocks.live.ts +++ b/src/commands/_blocks.live.ts @@ -65,8 +65,39 @@ export async function startLiveMonitoring(config: LiveMonitoringConfig): Promise continue; } - // Get latest data - const activeBlock = await monitor.getActiveBlock(); + // Get latest data with error handling + const blockResult = await Result.try({ + try: async () => monitor.getActiveBlock(), + catch: (error) => error, + })(); + + if (Result.isFailure(blockResult)) { + const error = blockResult.error; + const errorMessage = error instanceof Error ? error.message : String(error); + + // Check if this is a file synchronization related error (ENOENT) + const isSyncError = errorMessage.includes('ENOENT') || errorMessage.includes('no such file or directory'); + + if (isSyncError) { + // For sync-related errors, show a friendlier message and continue monitoring + const friendlyMessage = 'File temporarily unavailable (likely due to cloud sync)'; + terminal.startBuffering(); + terminal.clearScreen(); + terminal.write(pc.yellow(`Warning: ${friendlyMessage}\n`)); + terminal.write(pc.dim('Waiting for file sync to complete...\n')); + terminal.flush(); + logger.warn(`File sync issue detected: ${errorMessage}`); + + // Continue monitoring after a delay + await delayWithAbort(config.refreshInterval, abortController.signal); + continue; + } else { + // For other errors, re-throw to be handled by outer try-catch + throw error; + } + } + + const activeBlock = blockResult.value; monitor.clearCache(); // TODO: debug LiveMonitor.getActiveBlock() efficiency if (activeBlock == null) { @@ -91,29 +122,13 @@ export async function startLiveMonitoring(config: LiveMonitoringConfig): Promise return; // Normal graceful shutdown } - // Handle and display errors + // Handle non-sync errors that caused the monitoring loop to exit const errorMessage = error instanceof Error ? error.message : String(error); - - // Check if this is a file synchronization related error (ENOENT) - const isSyncError = errorMessage.includes('ENOENT') || errorMessage.includes('no such file or directory'); - - if (isSyncError) { - // For sync-related errors, show a friendlier message and use warning level - const friendlyMessage = 'File temporarily unavailable (likely due to cloud sync)'; - terminal.startBuffering(); - terminal.clearScreen(); - terminal.write(pc.yellow(`Warning: ${friendlyMessage}\n`)); - terminal.write(pc.dim('Waiting for file sync to complete...\n')); - terminal.flush(); - logger.warn(`File sync issue detected: ${errorMessage}`); - } else { - // For other errors, use the original error handling - terminal.startBuffering(); - terminal.clearScreen(); - terminal.write(pc.red(`Error: ${errorMessage}\n`)); - terminal.flush(); - logger.error(`Live monitoring error: ${errorMessage}`); - } + terminal.startBuffering(); + terminal.clearScreen(); + terminal.write(pc.red(`Error: ${errorMessage}\n`)); + terminal.flush(); + logger.error(`Live monitoring error: ${errorMessage}`); await delayWithAbort(config.refreshInterval, abortController.signal).catch(() => {}); } From 4304422e505f4e95219b30ed94ee3fc80883c4d0 Mon Sep 17 00:00:00 2001 From: Tony <2420242@qq.com> Date: Sat, 5 Jul 2025 05:26:31 +0800 Subject: [PATCH 3/3] feat: improve time precision in live monitoring display - Replace prettyMs with explicit hours/minutes format for remaining time - Add separate remaining time display in compact mode - Ensure minute-level precision in all time displays --- src/_live-rendering.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/_live-rendering.ts b/src/_live-rendering.ts index 855bfbf0..7fcea947 100644 --- a/src/_live-rendering.ts +++ b/src/_live-rendering.ts @@ -149,8 +149,8 @@ export function renderLiveDisplay(terminal: TerminalManager, block: SessionBlock // Session details (indented) const col1 = `${pc.gray('Started:')} ${startTime}`; - const col2 = `${pc.gray('Elapsed:')} ${prettyMs(elapsed * 60 * 1000, { compact: true })}`; - const col3 = `${pc.gray('Remaining:')} ${prettyMs(remaining * 60 * 1000, { compact: true })} (${endTime})`; + const col2 = `${pc.gray('Elapsed:')} ${Math.floor(elapsed / 60)}h ${Math.floor(elapsed % 60)}m`; + const col3 = `${pc.gray('Remaining:')} ${Math.floor(remaining / 60)}h ${Math.floor(remaining % 60)}m (${endTime})`; // Calculate actual visible lengths without ANSI codes const col1Visible = stringWidth(col1); const col2Visible = stringWidth(col2); @@ -370,7 +370,8 @@ export function renderCompactLiveDisplay( // Session info const sessionPercent = (elapsed / (elapsed + remaining)) * 100; - terminal.write(`Session: ${sessionPercent.toFixed(1)}% (${Math.floor(elapsed / 60)}h ${Math.floor(elapsed % 60)}m)\n`); + terminal.write(`Session: ${sessionPercent.toFixed(1)}% (${Math.floor(elapsed / 60)}h ${Math.floor(elapsed % 60)}m elapsed)\n`); + terminal.write(`Remaining: ${Math.floor(remaining / 60)}h ${Math.floor(remaining % 60)}m\n`); // Token usage if (config.tokenLimit != null && config.tokenLimit > 0) {