source dump of claude code
at main 164 lines 5.0 kB view raw
1import { getActiveTimeCounter as getActiveTimeCounterImpl } from '../bootstrap/state.js' 2 3type ActivityManagerOptions = { 4 getNow?: () => number 5 getActiveTimeCounter?: typeof getActiveTimeCounterImpl 6} 7 8/** 9 * ActivityManager handles generic activity tracking for both user and CLI operations. 10 * It automatically deduplicates overlapping activities and provides separate metrics 11 * for user vs CLI active time. 12 */ 13export class ActivityManager { 14 private activeOperations = new Set<string>() 15 16 private lastUserActivityTime: number = 0 // Start with 0 to indicate no activity yet 17 private lastCLIRecordedTime: number 18 19 private isCLIActive: boolean = false 20 21 private readonly USER_ACTIVITY_TIMEOUT_MS = 5000 // 5 seconds 22 23 private readonly getNow: () => number 24 private readonly getActiveTimeCounter: typeof getActiveTimeCounterImpl 25 26 private static instance: ActivityManager | null = null 27 28 constructor(options?: ActivityManagerOptions) { 29 this.getNow = options?.getNow ?? (() => Date.now()) 30 this.getActiveTimeCounter = 31 options?.getActiveTimeCounter ?? getActiveTimeCounterImpl 32 this.lastCLIRecordedTime = this.getNow() 33 } 34 35 static getInstance(): ActivityManager { 36 if (!ActivityManager.instance) { 37 ActivityManager.instance = new ActivityManager() 38 } 39 return ActivityManager.instance 40 } 41 42 /** 43 * Reset the singleton instance (for testing purposes) 44 */ 45 static resetInstance(): void { 46 ActivityManager.instance = null 47 } 48 49 /** 50 * Create a new instance with custom options (for testing purposes) 51 */ 52 static createInstance(options?: ActivityManagerOptions): ActivityManager { 53 ActivityManager.instance = new ActivityManager(options) 54 return ActivityManager.instance 55 } 56 57 /** 58 * Called when user interacts with the CLI (typing, commands, etc.) 59 */ 60 recordUserActivity(): void { 61 // Don't record user time if CLI is active (CLI takes precedence) 62 if (!this.isCLIActive && this.lastUserActivityTime !== 0) { 63 const now = this.getNow() 64 const timeSinceLastActivity = (now - this.lastUserActivityTime) / 1000 65 66 if (timeSinceLastActivity > 0) { 67 const activeTimeCounter = this.getActiveTimeCounter() 68 if (activeTimeCounter) { 69 const timeoutSeconds = this.USER_ACTIVITY_TIMEOUT_MS / 1000 70 71 // Only record time if within the timeout window 72 if (timeSinceLastActivity < timeoutSeconds) { 73 activeTimeCounter.add(timeSinceLastActivity, { type: 'user' }) 74 } 75 } 76 } 77 } 78 79 // Update the last user activity timestamp 80 this.lastUserActivityTime = this.getNow() 81 } 82 83 /** 84 * Starts tracking CLI activity (tool execution, AI response, etc.) 85 */ 86 startCLIActivity(operationId: string): void { 87 // If operation already exists, it likely means the previous one didn't clean up 88 // properly (e.g., component crashed/unmounted without calling end). Force cleanup 89 // to avoid overestimating time - better to underestimate than overestimate. 90 if (this.activeOperations.has(operationId)) { 91 this.endCLIActivity(operationId) 92 } 93 94 const wasEmpty = this.activeOperations.size === 0 95 this.activeOperations.add(operationId) 96 97 if (wasEmpty) { 98 this.isCLIActive = true 99 this.lastCLIRecordedTime = this.getNow() 100 } 101 } 102 103 /** 104 * Stops tracking CLI activity 105 */ 106 endCLIActivity(operationId: string): void { 107 this.activeOperations.delete(operationId) 108 109 if (this.activeOperations.size === 0) { 110 // Last operation ended - CLI becoming inactive 111 // Record the CLI time before switching to inactive 112 const now = this.getNow() 113 const timeSinceLastRecord = (now - this.lastCLIRecordedTime) / 1000 114 115 if (timeSinceLastRecord > 0) { 116 const activeTimeCounter = this.getActiveTimeCounter() 117 if (activeTimeCounter) { 118 activeTimeCounter.add(timeSinceLastRecord, { type: 'cli' }) 119 } 120 } 121 122 this.lastCLIRecordedTime = now 123 this.isCLIActive = false 124 } 125 } 126 127 /** 128 * Convenience method to track an async operation automatically (mainly for testing/debugging) 129 */ 130 async trackOperation<T>( 131 operationId: string, 132 fn: () => Promise<T>, 133 ): Promise<T> { 134 this.startCLIActivity(operationId) 135 try { 136 return await fn() 137 } finally { 138 this.endCLIActivity(operationId) 139 } 140 } 141 142 /** 143 * Gets current activity states (mainly for testing/debugging) 144 */ 145 getActivityStates(): { 146 isUserActive: boolean 147 isCLIActive: boolean 148 activeOperationCount: number 149 } { 150 const now = this.getNow() 151 const timeSinceUserActivity = (now - this.lastUserActivityTime) / 1000 152 const isUserActive = 153 timeSinceUserActivity < this.USER_ACTIVITY_TIMEOUT_MS / 1000 154 155 return { 156 isUserActive, 157 isCLIActive: this.isCLIActive, 158 activeOperationCount: this.activeOperations.size, 159 } 160 } 161} 162 163// Export singleton instance 164export const activityManager = ActivityManager.getInstance()