Skip to content

Commit 432cfed

Browse files
committed
refactor(history): store task history in external file\
1 parent 97f9686 commit 432cfed

File tree

2 files changed

+111
-36
lines changed

2 files changed

+111
-36
lines changed

src/core/config/ContextProxy.ts

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type GlobalStateKey = keyof GlobalState
2424
type SecretStateKey = keyof SecretState
2525
type RooCodeSettingsKey = keyof RooCodeSettings
2626

27-
const PASS_THROUGH_STATE_KEYS = ["taskHistory"]
27+
const PASS_THROUGH_STATE_KEYS: string[] = []
2828

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

@@ -52,6 +52,31 @@ export class ContextProxy {
5252
return this._isInitialized
5353
}
5454

55+
private async readTaskHistoryFile(): Promise<any[]> {
56+
try {
57+
const taskHistoryUri = vscode.Uri.joinPath(this.globalStorageUri, "taskHistory.jsonl")
58+
const fileContent = await vscode.workspace.fs.readFile(taskHistoryUri)
59+
const lines = fileContent.toString().split("\n").filter(Boolean)
60+
return lines.map((line) => JSON.parse(line))
61+
} catch (error) {
62+
if (error instanceof vscode.FileSystemError && error.code === "FileNotFound") {
63+
return []
64+
}
65+
logger.error(`Error reading task history file: ${error instanceof Error ? error.message : String(error)}`)
66+
return []
67+
}
68+
}
69+
70+
private async writeTaskHistoryFile(tasks: any[]): Promise<void> {
71+
try {
72+
const taskHistoryUri = vscode.Uri.joinPath(this.globalStorageUri, "taskHistory.jsonl")
73+
const content = tasks.map((task) => JSON.stringify(task)).join("\n") + "\n"
74+
await vscode.workspace.fs.writeFile(taskHistoryUri, Buffer.from(content))
75+
} catch (error) {
76+
logger.error(`Error writing task history file: ${error instanceof Error ? error.message : String(error)}`)
77+
}
78+
}
79+
5580
public async initialize() {
5681
for (const key of GLOBAL_STATE_KEYS) {
5782
try {
@@ -62,6 +87,31 @@ export class ContextProxy {
6287
}
6388
}
6489

90+
// Load task history from file
91+
if (GLOBAL_STATE_KEYS.includes("taskHistory")) {
92+
const tasks = await this.readTaskHistoryFile()
93+
this.stateCache.taskHistory = tasks
94+
95+
// Migrate task history from global state if global state has data
96+
const globalStateTasks = this.originalContext.globalState.get("taskHistory")
97+
if (Array.isArray(globalStateTasks) && globalStateTasks.length > 0) {
98+
try {
99+
// Append global state tasks to existing file content
100+
const combinedTasks = [...tasks, ...globalStateTasks]
101+
await this.writeTaskHistoryFile(combinedTasks)
102+
this.stateCache.taskHistory = combinedTasks
103+
await this.originalContext.globalState.update("taskHistory", undefined)
104+
vscode.window.showInformationMessage(
105+
"Task history has been migrated using an append strategy to preserve existing entries.",
106+
)
107+
} catch (error) {
108+
logger.error(
109+
`Error migrating task history: ${error instanceof Error ? error.message : String(error)}`,
110+
)
111+
}
112+
}
113+
}
114+
65115
const promises = [
66116
...SECRET_STATE_KEYS.map(async (key) => {
67117
try {
@@ -165,18 +215,17 @@ export class ContextProxy {
165215
getGlobalState<K extends GlobalStateKey>(key: K): GlobalState[K]
166216
getGlobalState<K extends GlobalStateKey>(key: K, defaultValue: GlobalState[K]): GlobalState[K]
167217
getGlobalState<K extends GlobalStateKey>(key: K, defaultValue?: GlobalState[K]): GlobalState[K] {
168-
if (isPassThroughStateKey(key)) {
169-
const value = this.originalContext.globalState.get<GlobalState[K]>(key)
170-
return value === undefined || value === null ? defaultValue : value
171-
}
172-
173218
const value = this.stateCache[key]
174219
return value !== undefined ? value : defaultValue
175220
}
176221

177-
updateGlobalState<K extends GlobalStateKey>(key: K, value: GlobalState[K]) {
178-
if (isPassThroughStateKey(key)) {
179-
return this.originalContext.globalState.update(key, value)
222+
async updateGlobalState<K extends GlobalStateKey>(key: K, value: GlobalState[K]) {
223+
if (key === "taskHistory") {
224+
this.stateCache[key] = value
225+
if (Array.isArray(value)) {
226+
await this.writeTaskHistoryFile(value)
227+
}
228+
return
180229
}
181230

182231
this.stateCache[key] = value
@@ -361,6 +410,18 @@ export class ContextProxy {
361410
this.stateCache = {}
362411
this.secretCache = {}
363412

413+
// Delete task history file
414+
try {
415+
const taskHistoryUri = vscode.Uri.joinPath(this.globalStorageUri, "taskHistory.jsonl")
416+
await vscode.workspace.fs.delete(taskHistoryUri)
417+
} catch (error) {
418+
if (!(error instanceof vscode.FileSystemError && error.code === "FileNotFound")) {
419+
logger.error(
420+
`Error deleting task history file: ${error instanceof Error ? error.message : String(error)}`,
421+
)
422+
}
423+
}
424+
364425
await Promise.all([
365426
...GLOBAL_STATE_KEYS.map((key) => this.originalContext.globalState.update(key, undefined)),
366427
...SECRET_STATE_KEYS.map((key) => this.originalContext.secrets.delete(key)),

src/core/config/__tests__/ContextProxy.spec.ts

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,30 @@ import { ContextProxy } from "../ContextProxy"
99
vi.mock("vscode", () => ({
1010
Uri: {
1111
file: vi.fn((path) => ({ path })),
12+
joinPath: vi.fn((base, ...paths) => ({ path: `${base.path}/${paths.join("/")}` })),
1213
},
1314
ExtensionMode: {
1415
Development: 1,
1516
Production: 2,
1617
Test: 3,
1718
},
19+
FileSystemError: class FileSystemError extends Error {
20+
code: string
21+
constructor(message: string) {
22+
super(message)
23+
this.code = "FileNotFound"
24+
}
25+
},
26+
workspace: {
27+
fs: {
28+
readFile: vi.fn().mockRejectedValue(Object.assign(new Error("FileNotFound"), { code: "FileNotFound" })),
29+
writeFile: vi.fn().mockResolvedValue(undefined),
30+
delete: vi.fn().mockResolvedValue(undefined),
31+
},
32+
},
33+
window: {
34+
showInformationMessage: vi.fn(),
35+
},
1836
}))
1937

2038
describe("ContextProxy", () => {
@@ -71,12 +89,14 @@ describe("ContextProxy", () => {
7189
describe("constructor", () => {
7290
it("should initialize state cache with all global state keys", () => {
7391
// +1 for the migration check of old nested settings
74-
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 1)
92+
// +1 for the taskHistory migration check
93+
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 2)
7594
for (const key of GLOBAL_STATE_KEYS) {
7695
expect(mockGlobalState.get).toHaveBeenCalledWith(key)
7796
}
78-
// Also check for migration call
97+
// Also check for migration calls
7998
expect(mockGlobalState.get).toHaveBeenCalledWith("openRouterImageGenerationSettings")
99+
expect(mockGlobalState.get).toHaveBeenCalledWith("taskHistory")
80100
})
81101

82102
it("should initialize secret cache with all secret keys", () => {
@@ -99,8 +119,10 @@ describe("ContextProxy", () => {
99119
const result = proxy.getGlobalState("apiProvider")
100120
expect(result).toBe("deepseek")
101121

102-
// Original context should be called once during updateGlobalState (+1 for migration check)
103-
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 1) // From initialization + migration check
122+
// Original context should be called during initialization
123+
// +1 for openRouterImageGenerationSettings migration check
124+
// +1 for taskHistory migration check
125+
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 2)
104126
})
105127

106128
it("should handle default values correctly", async () => {
@@ -109,23 +131,16 @@ describe("ContextProxy", () => {
109131
expect(result).toBe("deepseek")
110132
})
111133

112-
it("should bypass cache for pass-through state keys", async () => {
113-
// Setup mock return value
114-
mockGlobalState.get.mockReturnValue("pass-through-value")
115-
116-
// Use a pass-through key (taskHistory)
134+
it("should return value from cache for taskHistory", async () => {
135+
// taskHistory is now loaded from file and stored in cache
117136
const result = proxy.getGlobalState("taskHistory")
118137

119-
// Should get value directly from original context
120-
expect(result).toBe("pass-through-value")
121-
expect(mockGlobalState.get).toHaveBeenCalledWith("taskHistory")
138+
// Should return the cached value (empty array from file read)
139+
expect(result).toEqual([])
122140
})
123141

124-
it("should respect default values for pass-through state keys", async () => {
125-
// Setup mock to return undefined
126-
mockGlobalState.get.mockReturnValue(undefined)
127-
128-
// Use a pass-through key with default value
142+
it("should respect default values for taskHistory", async () => {
143+
// Use taskHistory with default value
129144
const historyItems = [
130145
{
131146
id: "1",
@@ -140,8 +155,8 @@ describe("ContextProxy", () => {
140155

141156
const result = proxy.getGlobalState("taskHistory", historyItems)
142157

143-
// Should return default value when original context returns undefined
144-
expect(result).toBe(historyItems)
158+
// Should return cached value (empty array) since it exists
159+
expect(result).toEqual([])
145160
})
146161
})
147162

@@ -157,7 +172,7 @@ describe("ContextProxy", () => {
157172
expect(storedValue).toBe("deepseek")
158173
})
159174

160-
it("should bypass cache for pass-through state keys", async () => {
175+
it("should write taskHistory to file instead of global state", async () => {
161176
const historyItems = [
162177
{
163178
id: "1",
@@ -172,16 +187,15 @@ describe("ContextProxy", () => {
172187

173188
await proxy.updateGlobalState("taskHistory", historyItems)
174189

175-
// Should update original context
176-
expect(mockGlobalState.update).toHaveBeenCalledWith("taskHistory", historyItems)
190+
// Should NOT update original context for taskHistory
191+
expect(mockGlobalState.update).not.toHaveBeenCalledWith("taskHistory", historyItems)
177192

178-
// Setup mock for subsequent get
179-
mockGlobalState.get.mockReturnValue(historyItems)
193+
// Should write to file (mocked in vscode.workspace.fs.writeFile)
194+
expect(vscode.workspace.fs.writeFile).toHaveBeenCalled()
180195

181-
// Should get fresh value from original context
196+
// Should get value from cache
182197
const storedValue = proxy.getGlobalState("taskHistory")
183-
expect(storedValue).toBe(historyItems)
184-
expect(mockGlobalState.get).toHaveBeenCalledWith("taskHistory")
198+
expect(storedValue).toEqual(historyItems)
185199
})
186200
})
187201

0 commit comments

Comments
 (0)