π File detail
utils/activityManager.ts
π§© .tsπ 165 linesπΎ 4,973 bytesπ text
β Back to All Filesπ― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes ActivityManager and activityManager β mainly functions, hooks, or classes. It composes internal code from bootstrap (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import { getActiveTimeCounter as getActiveTimeCounterImpl } from '../bootstrap/state.js' type ActivityManagerOptions = { getNow?: () => number getActiveTimeCounter?: typeof getActiveTimeCounterImpl
π€ Exports (heuristic)
ActivityManageractivityManager
π₯οΈ Source preview
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()