Skip to content

Commit 7080f00

Browse files
committed
store the timings and log rule execution to console
1 parent 5c43d39 commit 7080f00

File tree

3 files changed

+251
-25
lines changed

3 files changed

+251
-25
lines changed

src/core/Cline.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,6 +1115,7 @@ export class Cline {
11151115
experiments,
11161116
enableMcpServerCreation,
11171117
rooIgnoreInstructions,
1118+
provider.getSchedulableRulesManager(),
11181119
)
11191120
})()
11201121

src/core/prompts/sections/schedulable-rules.ts

Lines changed: 211 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* CHANGELOG:
66
* - 2025-03-11 03:16:11: (Schedulable Rules - Implementation) Initial implementation with parseTimeInterval, SchedulableRule interface, and SchedulableRulesManager class
77
* - 2025-03-11 03:16:58: (Schedulable Rules - Implementation) Fixed TypeScript errors related to imports and type annotations
8+
* - 2025-03-11 06:30:00: (Schedulable Rules - Persistence) Added persistence for rule execution times using VSCode global state
9+
* - 2025-03-11 06:36:30: (Schedulable Rules - Async) Fixed async handling for non-awaited markRuleAsExecuted calls
810
*
911
* PURPOSE:
1012
* This file handles loading and management of time-based rule files (.clinerules-5m, etc.)
@@ -13,6 +15,9 @@
1315
* METHODS:
1416
* - parseTimeInterval(): Parses time intervals from filenames
1517
* - SchedulableRulesManager.constructor(): Initializes a new rules manager
18+
* - SchedulableRulesManager.setContext(): Sets the VSCode extension context for state persistence
19+
* - SchedulableRulesManager.loadExecutionTimes(): Loads execution times from global state
20+
* - SchedulableRulesManager.saveExecutionTimes(): Saves execution times to global state
1621
* - SchedulableRulesManager.resetAllRules(): Resets all rule execution times
1722
* - SchedulableRulesManager.loadSchedulableRules(): Loads all schedulable rules from a directory
1823
* - SchedulableRulesManager.shouldExecuteRule(): Checks if a rule should be executed
@@ -23,6 +28,7 @@
2328

2429
import * as path from "path"
2530
import * as fs from "fs/promises"
31+
import * as vscode from "vscode"
2632
import { logger } from "../../../utils/logging"
2733

2834
/**
@@ -82,21 +88,129 @@ export function parseTimeInterval(timeStr: string): { interval: number; unit: st
8288
*/
8389
export class SchedulableRulesManager {
8490
private lastExecutionTimes: Map<string, number> = new Map()
91+
private context: vscode.ExtensionContext | null = null
92+
private outputChannel: vscode.OutputChannel | null = null
93+
private readonly STORAGE_KEY = "schedulableRules.lastExecutionTimes"
8594

8695
/**
8796
* Create a new SchedulableRulesManager
8897
*/
8998
constructor() {
90-
this.resetAllRules()
9199
logger.info("SchedulableRulesManager initialized")
92100
}
93101

102+
/**
103+
* Set the extension context for persistence
104+
* @param context - VSCode extension context
105+
*/
106+
public setContext(context: vscode.ExtensionContext): void {
107+
this.context = context
108+
this.loadExecutionTimes()
109+
logger.debug("SchedulableRulesManager context set")
110+
this.log("debug", "SchedulableRulesManager context set")
111+
}
112+
113+
/**
114+
* Set the output channel for logging
115+
* @param outputChannel - VSCode output channel
116+
*/
117+
public setOutputChannel(outputChannel: vscode.OutputChannel): void {
118+
this.outputChannel = outputChannel
119+
this.log("info", "SchedulableRulesManager output channel set")
120+
}
121+
122+
/**
123+
* Log a message to both the outputChannel (if available) and the logger
124+
* @param level - Log level
125+
* @param message - Message to log
126+
*/
127+
private log(level: "debug" | "info" | "warn" | "error", message: string): void {
128+
// Add timestamp for better time tracking
129+
const timestamp = new Date().toLocaleTimeString()
130+
const formattedMessage = `[${timestamp}] [SchedulableRules] ${message}`
131+
132+
// Always show output channel when logging
133+
if (this.outputChannel) {
134+
this.outputChannel.appendLine(formattedMessage)
135+
136+
// Show the output panel for important messages
137+
if (level === "info" || level === "warn" || level === "error") {
138+
this.outputChannel.show(true)
139+
}
140+
}
141+
142+
// Also log to the regular logger for completeness
143+
switch (level) {
144+
case "debug":
145+
logger.debug(message)
146+
break
147+
case "info":
148+
logger.info(message)
149+
break
150+
case "warn":
151+
logger.warn(message)
152+
break
153+
case "error":
154+
logger.error(message)
155+
break
156+
}
157+
}
158+
159+
/**
160+
* Load execution times from global state
161+
*/
162+
private loadExecutionTimes(): void {
163+
if (!this.context) {
164+
this.log("warn", "Cannot load execution times: context not set")
165+
return
166+
}
167+
168+
try {
169+
const savedTimes = this.context.globalState.get<Record<string, number>>(this.STORAGE_KEY)
170+
if (savedTimes) {
171+
this.lastExecutionTimes = new Map(Object.entries(savedTimes))
172+
this.log("debug", `Loaded ${this.lastExecutionTimes.size} rule execution times from storage`)
173+
}
174+
} catch (err) {
175+
this.log("error", `Failed to load execution times: ${err instanceof Error ? err.message : String(err)}`)
176+
}
177+
}
178+
179+
/**
180+
* Save execution times to global state
181+
*/
182+
private saveExecutionTimes(): Promise<void> {
183+
if (!this.context) {
184+
this.log("warn", "Cannot save execution times: context not set")
185+
return Promise.resolve()
186+
}
187+
188+
try {
189+
const timesObject = Object.fromEntries(this.lastExecutionTimes.entries())
190+
// Convert Thenable to Promise and handle errors
191+
return Promise.resolve(this.context.globalState.update(this.STORAGE_KEY, timesObject))
192+
.then(() => {
193+
this.log("debug", `Saved ${this.lastExecutionTimes.size} rule execution times to storage`)
194+
})
195+
.catch((err: unknown) => {
196+
this.log(
197+
"error",
198+
`Failed to save execution times: ${err instanceof Error ? err.message : String(err)}`,
199+
)
200+
})
201+
} catch (err: unknown) {
202+
this.log("error", `Failed to save execution times: ${err instanceof Error ? err.message : String(err)}`)
203+
return Promise.resolve()
204+
}
205+
}
206+
94207
/**
95208
* Reset all rule execution times
96209
*/
97-
public resetAllRules(): void {
210+
public resetAllRules(): Promise<void> {
98211
this.lastExecutionTimes.clear()
99-
logger.debug("All schedulable rules reset")
212+
this.log("debug", "All schedulable rules reset")
213+
return this.saveExecutionTimes()
100214
}
101215

102216
/**
@@ -106,12 +220,12 @@ export class SchedulableRulesManager {
106220
*/
107221
public async loadSchedulableRules(cwd: string): Promise<SchedulableRule[]> {
108222
try {
109-
logger.debug(`Loading schedulable rules from: ${cwd}`)
223+
this.log("debug", `Loading schedulable rules from: ${cwd}`)
110224
const files = await fs.readdir(cwd)
111225

112226
// Filter for files matching the pattern .clinerules-\d+[smhd]
113227
const ruleFiles = files.filter((file: string) => /^\.clinerules-\d+[smhd]$/.test(file))
114-
logger.debug(`Found ${ruleFiles.length} schedulable rule files`)
228+
this.log("debug", `Found ${ruleFiles.length} schedulable rule files`)
115229

116230
const rules: SchedulableRule[] = []
117231

@@ -134,48 +248,89 @@ export class SchedulableRulesManager {
134248
lastExecuted: this.lastExecutionTimes.get(file) || 0,
135249
})
136250

137-
logger.debug(`Loaded rule file: ${file}, interval: ${display}`)
251+
this.log("debug", `Loaded rule file: ${file}, interval: ${display}`)
138252
} catch (err) {
139-
logger.error(
253+
this.log(
254+
"error",
140255
`Failed to parse schedulable rule file ${file}: ${err instanceof Error ? err.message : String(err)}`,
141256
)
142257
}
143258
}
144259

145260
return rules
146261
} catch (err) {
147-
logger.error(`Failed to load schedulable rules: ${err instanceof Error ? err.message : String(err)}`)
262+
this.log("error", `Failed to load schedulable rules: ${err instanceof Error ? err.message : String(err)}`)
148263
return []
149264
}
150265
}
151266

152267
/**
153268
* Check if a rule should be executed based on its interval
154269
* @param rule - The rule to check
155-
* @returns True if the rule should be executed, false otherwise
156-
*/
270+
/**
271+
* Check if a rule should be executed based on its interval
272+
* @param rule - The rule to check
273+
* @returns True if the rule should be executed, false otherwise
274+
*/
157275
public shouldExecuteRule(rule: SchedulableRule): boolean {
158276
const now = Date.now()
159277
const lastExecution = this.lastExecutionTimes.get(rule.fileName) || 0
160-
const shouldExecute = now - lastExecution >= rule.interval
278+
const timeElapsed = now - lastExecution
279+
const timeRemaining = Math.max(0, rule.interval - timeElapsed)
280+
const shouldExecute = timeElapsed >= rule.interval
281+
282+
// Format time remaining in a human-readable format
283+
const formatTimeRemaining = (): string => {
284+
if (timeRemaining === 0) return "ready now"
285+
286+
const seconds = Math.floor(timeRemaining / 1000) % 60
287+
const minutes = Math.floor(timeRemaining / (1000 * 60)) % 60
288+
const hours = Math.floor(timeRemaining / (1000 * 60 * 60))
289+
290+
const parts = []
291+
if (hours > 0) parts.push(`${hours}h`)
292+
if (minutes > 0) parts.push(`${minutes}m`)
293+
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`)
161294

295+
return parts.join(" ")
296+
}
297+
298+
// Always log at "info" level for better visibility in the output panel
162299
if (shouldExecute) {
163-
logger.debug(
164-
`Rule ${rule.fileName} should be executed (last executed: ${new Date(lastExecution).toISOString()})`,
300+
this.log(
301+
"info",
302+
`Rule ${rule.fileName} is ready to execute (last executed: ${lastExecution > 0 ? new Date(lastExecution).toISOString() : "never"})`,
303+
)
304+
} else {
305+
const nextRunTimeFormatted = new Date(lastExecution + rule.interval).toLocaleTimeString()
306+
this.log(
307+
"info", // Changed from debug to info for visibility
308+
`Rule ${rule.fileName} will execute in ${formatTimeRemaining()} at ${nextRunTimeFormatted} (last executed: ${new Date(lastExecution).toISOString()})`,
165309
)
166310
}
167311

168312
return shouldExecute
169313
}
170-
171-
/**
172-
* Mark a rule as executed
314+
/*
315+
* Non-blocking method that saves the execution time to storage
316+
* without requiring the caller to await
317+
*
173318
* @param rule - The rule to mark as executed
174319
*/
175320
public markRuleAsExecuted(rule: SchedulableRule): void {
176321
const now = Date.now()
177322
this.lastExecutionTimes.set(rule.fileName, now)
178-
logger.info(`Rule ${rule.fileName} marked as executed at ${new Date(now).toISOString()}`)
323+
this.log("info", `Rule ${rule.fileName} marked as executed at ${new Date(now).toISOString()}`)
324+
325+
// Save to persistent storage without blocking
326+
// This ensures that even if the caller doesn't await the promise,
327+
// the execution times will still be saved to global state
328+
Promise.resolve(this.saveExecutionTimes()).catch((err: unknown) => {
329+
this.log(
330+
"error",
331+
`Failed to save execution times for ${rule.fileName}: ${err instanceof Error ? err.message : String(err)}`,
332+
)
333+
})
179334
}
180335

181336
/**
@@ -186,7 +341,7 @@ export class SchedulableRulesManager {
186341
public async getExecutableRules(cwd: string): Promise<SchedulableRule[]> {
187342
const rules = await this.loadSchedulableRules(cwd)
188343
const executableRules = rules.filter((rule) => this.shouldExecuteRule(rule))
189-
logger.debug(`Found ${executableRules.length} executable rules out of ${rules.length} total rules`)
344+
this.log("debug", `Found ${executableRules.length} executable rules out of ${rules.length} total rules`)
190345
return executableRules
191346
}
192347

@@ -195,20 +350,55 @@ export class SchedulableRulesManager {
195350
* @param cwd - The current working directory
196351
* @returns Promise resolving to an array of rules with next execution time
197352
*/
198-
public async getAllRules(cwd: string): Promise<Array<SchedulableRule & { nextExecution: number }>> {
353+
public async getAllRules(
354+
cwd: string,
355+
): Promise<Array<SchedulableRule & { nextExecution: number; nextExecutionTimestamp: number }>> {
199356
const rules = await this.loadSchedulableRules(cwd)
200357
const now = Date.now()
201358

202359
return rules.map((rule) => {
203360
const lastExecution = this.lastExecutionTimes.get(rule.fileName) || 0
204-
const nextExecution = lastExecution + rule.interval
205-
const timeRemaining = Math.max(0, nextExecution - now)
361+
const nextExecutionTimestamp = lastExecution + rule.interval
362+
const timeRemaining = Math.max(0, nextExecutionTimestamp - now)
363+
364+
// Format time remaining in a human-readable format
365+
const timeUntilNextRun = this.formatTimeRemaining(timeRemaining)
366+
367+
// Format next run time as a nice clock time
368+
const nextRunTime = new Date(nextExecutionTimestamp).toLocaleTimeString(undefined, {
369+
hour: "2-digit",
370+
minute: "2-digit",
371+
second: "2-digit",
372+
})
206373

207374
return {
208375
...rule,
209376
lastExecuted: lastExecution,
210377
nextExecution: timeRemaining,
378+
nextExecutionTimestamp: nextExecutionTimestamp, // Add absolute timestamp to enable UI countdown
379+
timeUntilNextRun: timeUntilNextRun, // Human-readable time remaining
380+
nextRunTime: nextRunTime, // Clock time of next execution
211381
}
212382
})
213383
}
384+
385+
/**
386+
* Format milliseconds into a human-readable time format
387+
* @param milliseconds - Time in milliseconds
388+
* @returns Human-readable time string
389+
*/
390+
private formatTimeRemaining(milliseconds: number): string {
391+
if (milliseconds === 0) return "ready now"
392+
393+
const seconds = Math.floor(milliseconds / 1000) % 60
394+
const minutes = Math.floor(milliseconds / (1000 * 60)) % 60
395+
const hours = Math.floor(milliseconds / (1000 * 60 * 60))
396+
397+
const parts = []
398+
if (hours > 0) parts.push(`${hours}h`)
399+
if (minutes > 0) parts.push(`${minutes}m`)
400+
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`)
401+
402+
return parts.join(" ")
403+
}
214404
}

0 commit comments

Comments
 (0)