Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
5.0 kB · 164 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165import { getActiveTimeCounter as getActiveTimeCounterImpl } from '../bootstrap/state.js'
type ActivityManagerOptions = { getNow?: () => number getActiveTimeCounter?: typeof getActiveTimeCounterImpl}
/** * ActivityManager handles generic activity tracking for both user and CLI operations. * It automatically deduplicates overlapping activities and provides separate metrics * for user vs CLI active time. */export class ActivityManager { private activeOperations = new Set<string>()
private lastUserActivityTime: number = 0 // Start with 0 to indicate no activity yet private lastCLIRecordedTime: number
private isCLIActive: boolean = false
private readonly USER_ACTIVITY_TIMEOUT_MS = 5000 // 5 seconds
private readonly getNow: () => number private readonly getActiveTimeCounter: typeof getActiveTimeCounterImpl
private static instance: ActivityManager | null = null
constructor(options?: ActivityManagerOptions) { this.getNow = options?.getNow ?? (() => Date.now()) this.getActiveTimeCounter = options?.getActiveTimeCounter ?? getActiveTimeCounterImpl this.lastCLIRecordedTime = this.getNow() }
static getInstance(): ActivityManager { if (!ActivityManager.instance) { ActivityManager.instance = new ActivityManager() } return ActivityManager.instance }
/** * Reset the singleton instance (for testing purposes) */ static resetInstance(): void { ActivityManager.instance = null }
/** * Create a new instance with custom options (for testing purposes) */ static createInstance(options?: ActivityManagerOptions): ActivityManager { ActivityManager.instance = new ActivityManager(options) return ActivityManager.instance }
/** * Called when user interacts with the CLI (typing, commands, etc.) */ recordUserActivity(): void { // Don't record user time if CLI is active (CLI takes precedence) if (!this.isCLIActive && this.lastUserActivityTime !== 0) { const now = this.getNow() const timeSinceLastActivity = (now - this.lastUserActivityTime) / 1000
if (timeSinceLastActivity > 0) { const activeTimeCounter = this.getActiveTimeCounter() if (activeTimeCounter) { const timeoutSeconds = this.USER_ACTIVITY_TIMEOUT_MS / 1000
// Only record time if within the timeout window if (timeSinceLastActivity < timeoutSeconds) { activeTimeCounter.add(timeSinceLastActivity, { type: 'user' }) } } } }
// Update the last user activity timestamp this.lastUserActivityTime = this.getNow() }
/** * Starts tracking CLI activity (tool execution, AI response, etc.) */ startCLIActivity(operationId: string): void { // If operation already exists, it likely means the previous one didn't clean up // properly (e.g., component crashed/unmounted without calling end). Force cleanup // to avoid overestimating time - better to underestimate than overestimate. if (this.activeOperations.has(operationId)) { this.endCLIActivity(operationId) }
const wasEmpty = this.activeOperations.size === 0 this.activeOperations.add(operationId)
if (wasEmpty) { this.isCLIActive = true this.lastCLIRecordedTime = this.getNow() } }
/** * Stops tracking CLI activity */ endCLIActivity(operationId: string): void { this.activeOperations.delete(operationId)
if (this.activeOperations.size === 0) { // Last operation ended - CLI becoming inactive // Record the CLI time before switching to inactive const now = this.getNow() const timeSinceLastRecord = (now - this.lastCLIRecordedTime) / 1000
if (timeSinceLastRecord > 0) { const activeTimeCounter = this.getActiveTimeCounter() if (activeTimeCounter) { activeTimeCounter.add(timeSinceLastRecord, { type: 'cli' }) } }
this.lastCLIRecordedTime = now this.isCLIActive = false } }
/** * Convenience method to track an async operation automatically (mainly for testing/debugging) */ async trackOperation<T>( operationId: string, fn: () => Promise<T>, ): Promise<T> { this.startCLIActivity(operationId) try { return await fn() } finally { this.endCLIActivity(operationId) } }
/** * Gets current activity states (mainly for testing/debugging) */ getActivityStates(): { isUserActive: boolean isCLIActive: boolean activeOperationCount: number } { const now = this.getNow() const timeSinceUserActivity = (now - this.lastUserActivityTime) / 1000 const isUserActive = timeSinceUserActivity < this.USER_ACTIVITY_TIMEOUT_MS / 1000
return { isUserActive, isCLIActive: this.isCLIActive, activeOperationCount: this.activeOperations.size, } }}
// Export singleton instanceexport const activityManager = ActivityManager.getInstance()