Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/__mocks__/vscode.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
// Define historyItems for test mock data
const historyItems = [
{
id: "1",
number: 1,
ts: Date.now(),
task: "test",
tokensIn: 100,
tokensOut: 50,
totalCost: 0.001,
cacheWrites: 0,
cacheReads: 0,
},
]

const vscode = {
env: {
language: "en", // Default language for tests
Expand All @@ -23,6 +38,11 @@ const vscode = {
all: [],
},
},
FileSystemError: class {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a code property to the FileSystemError mock so that error.code comparisons in ContextProxy.ts work as expected.

This comment was generated because it violated a code review rule: mrule_oAUXVfj5l9XxF01R.

constructor(message) {
this.message = message
}
},
workspace: {
onDidSaveTextDocument: jest.fn(),
createFileSystemWatcher: jest.fn().mockReturnValue({
Expand All @@ -32,6 +52,16 @@ const vscode = {
}),
fs: {
stat: jest.fn(),
readFile: jest.fn().mockImplementation((uri) => {
if (uri.path.includes("taskHistory.jsonl")) {
// Return stringified historyItems with each item on a new line
const content = historyItems.map((item) => JSON.stringify(item)).join("\n")
return Promise.resolve(Buffer.from(content))
}
return Promise.reject(new vscode.FileSystemError("File not found"))
}),
writeFile: jest.fn(),
delete: jest.fn(),
},
},
Disposable: class {
Expand All @@ -48,6 +78,19 @@ const vscode = {
with: jest.fn(),
toJSON: jest.fn(),
}),
joinPath: jest.fn().mockImplementation((uri, ...pathSegments) => {
const path = [uri.path, ...pathSegments].join("/")
return {
fsPath: path,
scheme: "file",
authority: "",
path: path,
query: "",
fragment: "",
with: jest.fn(),
toJSON: jest.fn(),
}
}),
},
EventEmitter: class {
constructor() {
Expand Down Expand Up @@ -102,3 +145,5 @@ const vscode = {
}

module.exports = vscode
// Export historyItems for use in tests
module.exports.historyItems = historyItems
79 changes: 70 additions & 9 deletions src/core/config/ContextProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type GlobalStateKey = keyof GlobalState
type SecretStateKey = keyof SecretState
type RooCodeSettingsKey = keyof RooCodeSettings

const PASS_THROUGH_STATE_KEYS = ["taskHistory"]
const PASS_THROUGH_STATE_KEYS: string[] = []

export const isPassThroughStateKey = (key: string) => PASS_THROUGH_STATE_KEYS.includes(key)

Expand Down Expand Up @@ -50,6 +50,31 @@ export class ContextProxy {
return this._isInitialized
}

private async readTaskHistoryFile(): Promise<any[]> {
try {
const taskHistoryUri = vscode.Uri.joinPath(this.globalStorageUri, "taskHistory.jsonl")
const fileContent = await vscode.workspace.fs.readFile(taskHistoryUri)
const lines = fileContent.toString().split("\n").filter(Boolean)
return lines.map((line) => JSON.parse(line))
} catch (error) {
if (error instanceof vscode.FileSystemError && error.code === "FileNotFound") {
return []
}
logger.error(`Error reading task history file: ${error instanceof Error ? error.message : String(error)}`)
return []
}
}

private async writeTaskHistoryFile(tasks: any[]): Promise<void> {
try {
const taskHistoryUri = vscode.Uri.joinPath(this.globalStorageUri, "taskHistory.jsonl")
const content = tasks.map((task) => JSON.stringify(task)).join("\n") + "\n"
await vscode.workspace.fs.writeFile(taskHistoryUri, Buffer.from(content))
} catch (error) {
logger.error(`Error writing task history file: ${error instanceof Error ? error.message : String(error)}`)
}
}

public async initialize() {
for (const key of GLOBAL_STATE_KEYS) {
try {
Expand All @@ -60,6 +85,31 @@ export class ContextProxy {
}
}

// Load task history from file
if (GLOBAL_STATE_KEYS.includes("taskHistory")) {
const tasks = await this.readTaskHistoryFile()
this.stateCache.taskHistory = tasks

// Migrate task history from global state if global state has data
const globalStateTasks = this.originalContext.globalState.get("taskHistory")
if (Array.isArray(globalStateTasks) && globalStateTasks.length > 0) {
try {
// Append global state tasks to existing file content
const combinedTasks = [...tasks, ...globalStateTasks]
await this.writeTaskHistoryFile(combinedTasks)
this.stateCache.taskHistory = combinedTasks
await this.originalContext.globalState.update("taskHistory", undefined)
vscode.window.showInformationMessage(
"Task history has been migrated using an append strategy to preserve existing entries.",
)
} catch (error) {
logger.error(
`Error migrating task history: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
}

const promises = SECRET_STATE_KEYS.map(async (key) => {
try {
this.secretCache[key] = await this.originalContext.secrets.get(key)
Expand Down Expand Up @@ -105,18 +155,17 @@ export class ContextProxy {
getGlobalState<K extends GlobalStateKey>(key: K): GlobalState[K]
getGlobalState<K extends GlobalStateKey>(key: K, defaultValue: GlobalState[K]): GlobalState[K]
getGlobalState<K extends GlobalStateKey>(key: K, defaultValue?: GlobalState[K]): GlobalState[K] {
if (isPassThroughStateKey(key)) {
const value = this.originalContext.globalState.get<GlobalState[K]>(key)
return value === undefined || value === null ? defaultValue : value
}

const value = this.stateCache[key]
return value !== undefined ? value : defaultValue
}

updateGlobalState<K extends GlobalStateKey>(key: K, value: GlobalState[K]) {
if (isPassThroughStateKey(key)) {
return this.originalContext.globalState.update(key, value)
async updateGlobalState<K extends GlobalStateKey>(key: K, value: GlobalState[K]) {
if (key === "taskHistory") {
this.stateCache[key] = value
if (Array.isArray(value)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In updateGlobalState, the new branch for taskHistory only proceeds if the value is an array. It would be helpful to add a type check (or log a warning) if a non-array is passed, to help catch unexpected types.

This comment was generated because it violated a code review rule: mrule_OR1S8PRRHcvbdFib.

await this.writeTaskHistoryFile(value)
}
return
}

this.stateCache[key] = value
Expand Down Expand Up @@ -264,6 +313,18 @@ export class ContextProxy {
this.stateCache = {}
this.secretCache = {}

// Delete task history file
try {
const taskHistoryUri = vscode.Uri.joinPath(this.globalStorageUri, "taskHistory.jsonl")
await vscode.workspace.fs.delete(taskHistoryUri)
} catch (error) {
if (!(error instanceof vscode.FileSystemError && error.code === "FileNotFound")) {
logger.error(
`Error deleting task history file: ${error instanceof Error ? error.message : String(error)}`,
)
}
}

await Promise.all([
...GLOBAL_STATE_KEYS.map((key) => this.originalContext.globalState.update(key, undefined)),
...SECRET_STATE_KEYS.map((key) => this.originalContext.secrets.delete(key)),
Expand Down
Loading