| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164 |
- import { 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 instance
- export const activityManager = ActivityManager.getInstance()
|