diff --git a/src/ccstatusline.ts b/src/ccstatusline.ts index 2c87ea6..7e09139 100644 --- a/src/ccstatusline.ts +++ b/src/ccstatusline.ts @@ -2,6 +2,10 @@ import chalk from 'chalk'; import { runTUI } from './tui'; +import type { + BlockMetrics, + TokenMetrics +} from './types'; import { StatusJSONSchema } from './types/StatusJSON'; import { updateColorMap } from './utils/colors'; import { @@ -68,15 +72,15 @@ async function renderMultipleLines(data: StatusJSON) { // Check if block timer is needed const hasBlockTimer = lines.some(line => line.some(item => item.type === 'block-timer')); - let tokenMetrics = null; + let tokenMetrics: TokenMetrics | null = null; if (hasTokenItems && data.transcript_path) tokenMetrics = await getTokenMetrics(data.transcript_path); - let sessionDuration = null; + let sessionDuration: string | null = null; if (hasSessionClock && data.transcript_path) sessionDuration = await getSessionDuration(data.transcript_path); - let blockMetrics = null; + let blockMetrics: BlockMetrics | null = null; if (hasBlockTimer && data.transcript_path) blockMetrics = getBlockMetrics(data.transcript_path); diff --git a/src/types/BlockMetrics.ts b/src/types/BlockMetrics.ts new file mode 100644 index 0000000..b634e51 --- /dev/null +++ b/src/types/BlockMetrics.ts @@ -0,0 +1,5 @@ +export interface BlockMetrics { + startTime: Date; + lastActivity: Date; + isActive: boolean; +} \ No newline at end of file diff --git a/src/types/RenderContext.ts b/src/types/RenderContext.ts index 7d84431..9e088c9 100644 --- a/src/types/RenderContext.ts +++ b/src/types/RenderContext.ts @@ -1,4 +1,4 @@ -import type { BlockInfo } from '../utils/jsonl'; +import type { BlockMetrics } from '../types'; import type { StatusJSON } from './StatusJSON'; import type { TokenMetrics } from './TokenMetrics'; @@ -7,7 +7,7 @@ export interface RenderContext { data?: StatusJSON; tokenMetrics?: TokenMetrics | null; sessionDuration?: string | null; - blockMetrics?: BlockInfo | null; + blockMetrics?: BlockMetrics | null; terminalWidth?: number | null; isPreview?: boolean; lineIndex?: number; // Index of the current line being rendered (for theme cycling) diff --git a/src/types/index.ts b/src/types/index.ts index 71dfb2d..916ded0 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -16,4 +16,5 @@ export type { TokenMetrics, TokenUsage, TranscriptLine } from './TokenMetrics'; export type { RenderContext } from './RenderContext'; export type { PowerlineFontStatus } from './PowerlineFontStatus'; export type { ClaudeSettings } from './ClaudeSettings'; -export type { ColorEntry } from './ColorEntry'; \ No newline at end of file +export type { ColorEntry } from './ColorEntry'; +export type { BlockMetrics } from './BlockMetrics'; \ No newline at end of file diff --git a/src/utils/jsonl.ts b/src/utils/jsonl.ts index 41ddc72..fd10070 100644 --- a/src/utils/jsonl.ts +++ b/src/utils/jsonl.ts @@ -4,6 +4,7 @@ import { globSync } from 'tinyglobby'; import { promisify } from 'util'; import type { + BlockMetrics, TokenMetrics, TranscriptLine } from '../types'; @@ -13,12 +14,6 @@ const readFile = promisify(fs.readFile); const readFileSync = fs.readFileSync; const statSync = fs.statSync; -export interface BlockInfo { - startTime: Date; - lastActivity: Date; - isActive: boolean; -} - export async function getSessionDuration(transcriptPath: string): Promise { try { if (!fs.existsSync(transcriptPath)) { @@ -151,7 +146,7 @@ export async function getTokenMetrics(transcriptPath: string): Promise { @@ -207,58 +200,84 @@ function findMostRecentBlockStartTime( filesWithStats.sort((a, b) => b.mtime.getTime() - a.mtime.getTime()); - // Step 3: Collect recent timestamps to properly identify blocks - // We need to look back far enough to find the start of the current/recent block - const timestamps: Date[] = []; - // Look back 20 hours to find the current session - const cutoffTime = new Date(now.getTime() - 20 * 60 * 60 * 1000); + // Step 3: Progressive lookback - start small and expand if needed + // Start with 2x session duration (10 hours), expand to 48 hours if needed + const lookbackChunks = [ + 10, // 2x session duration - catches most cases + 20, // 4x session duration - catches longer sessions + 48 // Maximum lookback for marathon sessions + ]; - for (const { file, mtime } of filesWithStats) { - // Skip files that are too old (optimization) - if (mtime.getTime() < cutoffTime.getTime()) { + let timestamps: Date[] = []; + let mostRecentTimestamp: Date | null = null; + let continuousWorkStart: Date | null = null; + let foundSessionGap = false; + + for (const lookbackHours of lookbackChunks) { + const cutoffTime = new Date(now.getTime() - lookbackHours * 60 * 60 * 1000); + timestamps = []; + + // Collect timestamps for this lookback period + for (const { file, mtime } of filesWithStats) { + if (mtime.getTime() < cutoffTime.getTime()) { + break; + } + const fileTimestamps = getAllTimestampsFromFile(file); + timestamps.push(...fileTimestamps); + } + + if (timestamps.length === 0) { + continue; // Try next chunk + } + + // Sort timestamps (most recent first) + timestamps.sort((a, b) => b.getTime() - a.getTime()); + + // Get most recent timestamp (only set once) + if (!mostRecentTimestamp && timestamps[0]) { + mostRecentTimestamp = timestamps[0]; + + // Check if the most recent activity is within the current session period + const timeSinceLastActivity = now.getTime() - mostRecentTimestamp.getTime(); + if (timeSinceLastActivity > sessionDurationMs) { + // No activity within the current session period + return null; + } + } + + // Look for a session gap in this chunk + continuousWorkStart = mostRecentTimestamp; + for (let i = 1; i < timestamps.length; i++) { + const currentTimestamp = timestamps[i]; + const previousTimestamp = timestamps[i - 1]; + + if (!currentTimestamp || !previousTimestamp) + continue; + + const gap = previousTimestamp.getTime() - currentTimestamp.getTime(); + + if (gap >= sessionDurationMs) { + // Found a true session boundary + foundSessionGap = true; + break; + } + + continuousWorkStart = currentTimestamp; + } + + // If we found a gap, we're done + if (foundSessionGap) { break; } - // Get all timestamps from this file (not just the latest) - const fileTimestamps = getAllTimestampsFromFile(file); - timestamps.push(...fileTimestamps); - } - - if (timestamps.length === 0) { - return null; - } - - // Step 4: Sort timestamps and identify the most recent block - timestamps.sort((a, b) => b.getTime() - a.getTime()); // Most recent first - - // Find the most recent timestamp - const mostRecentTimestamp = timestamps[0]; - if (!mostRecentTimestamp) { - return null; - } - - // Now work backwards to find when continuous work started - let continuousWorkStart: Date = mostRecentTimestamp; - - for (let i = 1; i < timestamps.length; i++) { - const currentTimestamp = timestamps[i]; - const previousTimestamp = timestamps[i - 1]; - - if (!currentTimestamp || !previousTimestamp) { - continue; - } - - // Check if there's a gap larger than 1 hour (consider it a break) - const gap = previousTimestamp.getTime() - currentTimestamp.getTime(); - const oneHourMs = 60 * 60 * 1000; - - if (gap > oneHourMs) { - // We found a gap - continuous work starts with previousTimestamp + // If this was our last chunk, use what we have + if (lookbackHours === lookbackChunks[lookbackChunks.length - 1]) { break; } + } - // This timestamp is still part of continuous work - continuousWorkStart = currentTimestamp; + if (!mostRecentTimestamp || !continuousWorkStart) { + return null; } // Floor the continuous work start to the hour diff --git a/src/widgets/BlockTimer.ts b/src/widgets/BlockTimer.ts index dea0d4a..c0c399f 100644 --- a/src/widgets/BlockTimer.ts +++ b/src/widgets/BlockTimer.ts @@ -68,15 +68,22 @@ export class BlockTimerWidget implements Widget { } // Check if we have block metrics in context - const blockInfo = context.blockMetrics; - if (!blockInfo) { - return null; + const blockMetrics = context.blockMetrics; + if (!blockMetrics) { + // No active session - show empty progress bar or 0hr 0m + if (displayMode === 'progress' || displayMode === 'progress-short') { + const barWidth = displayMode === 'progress' ? 32 : 16; + const emptyBar = '░'.repeat(barWidth); + return item.rawValue ? `[${emptyBar}] 0%` : `Block [${emptyBar}] 0%`; + } else { + return item.rawValue ? '0hr 0m' : 'Block: 0hr 0m'; + } } try { // Calculate elapsed time and progress const now = new Date(); - const elapsedMs = now.getTime() - blockInfo.startTime.getTime(); + const elapsedMs = now.getTime() - blockMetrics.startTime.getTime(); const sessionDurationMs = 5 * 60 * 60 * 1000; // 5 hours const progress = Math.min(elapsedMs / sessionDurationMs, 1.0); const percentage = (progress * 100).toFixed(1);