Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions src/_live-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { LoadedUsageEntry, SessionBlock } from './_session-blocks.ts';
import type { CostMode, SortOrder } from './_types.ts';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { Result } from '@praha/byethrow';
import { glob } from 'tinyglobby';
import { CLAUDE_PROJECTS_DIR_NAME, USAGE_DATA_GLOB_PATTERN } from './_consts.ts';
import { identifySessionBlocks } from './_session-blocks.ts';
Expand All @@ -23,6 +24,7 @@ import {
sortFilesByTimestamp,
usageDataSchema,
} from './data-loader.ts';
import { logger } from './logger.ts';
import { PricingFetcher } from './pricing-fetcher.ts';

/**
Expand Down Expand Up @@ -93,11 +95,22 @@ export class LiveMonitor implements Disposable {

for (const file of sortedFiles) {
const content = await readFile(file, 'utf-8')
.catch(() => {
.catch((error) => {
// Handle file access errors gracefully with specific logging for ENOENT
const isEnoent = error instanceof Error &&
(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);
}

// Skip files that can't be read
return '';
});

const lines = content
.trim()
.split('\n')
Expand Down
7 changes: 4 additions & 3 deletions src/_live-rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down
38 changes: 35 additions & 3 deletions src/commands/_blocks.live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -91,13 +122,14 @@ 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);
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(() => {});
}
}
16 changes: 14 additions & 2 deletions src/data-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,9 +520,21 @@ export async function getEarliestTimestamp(filePath: string): Promise<Date | nul
return earliestDate;
}
catch (error) {
// Log file access errors for diagnostics, but continue processing
// Handle file access errors gracefully
// This ensures files without timestamps or with access issues are sorted to the end
logger.debug(`Failed to get earliest timestamp for ${filePath}:`, error);

// Check if this is a file sync related error (ENOENT)
const isEnoent = error instanceof Error &&
(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 to avoid spam
logger.debug(`File temporarily unavailable (likely syncing): ${path.basename(filePath)}`);
} else {
// For other errors, log with more detail for debugging
logger.debug(`Failed to get earliest timestamp for ${filePath}:`, error);
}

return null;
}
}
Expand Down