Skip to content

Commit 9fe17a4

Browse files
committed
fix: migrate task history to global state for cross-workspace access (#5315)
1 parent a1ae29c commit 9fe17a4

File tree

12 files changed

+549
-166
lines changed

12 files changed

+549
-166
lines changed

packages/types/src/global-settings.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
providerSettingsEntrySchema,
88
providerSettingsSchema,
99
} from "./provider-settings.js"
10-
import { historyItemSchema } from "./history.js"
1110
import { codebaseIndexModelsSchema, codebaseIndexConfigSchema } from "./codebase-index.js"
1211
import { experimentsSchema } from "./experiment.js"
1312
import { telemetrySettingsSchema } from "./telemetry.js"
@@ -26,7 +25,6 @@ export const globalSettingsSchema = z.object({
2625

2726
lastShownAnnouncementId: z.string().optional(),
2827
customInstructions: z.string().optional(),
29-
taskHistory: z.array(historyItemSchema).optional(),
3028

3129
condensingApiConfigId: z.string().optional(),
3230
customCondensingPrompt: z.string().optional(),

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ export * from "./terminal.js"
2020
export * from "./tool.js"
2121
export * from "./type-fu.js"
2222
export * from "./vscode.js"
23+
export * from "./workspace-settings.js"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { z } from "zod"
2+
import { historyItemSchema } from "./history.js"
3+
4+
/**
5+
* WorkspaceSettings - Settings that are specific to a workspace
6+
*/
7+
export const workspaceSettingsSchema = z.object({
8+
taskHistory: z.array(historyItemSchema).optional(),
9+
})
10+
11+
export type WorkspaceSettings = z.infer<typeof workspaceSettingsSchema>
12+
13+
export const WORKSPACE_SETTINGS_KEYS = workspaceSettingsSchema.keyof().options
14+
15+
export type WorkspaceSettingsKey = keyof WorkspaceSettings

src/core/config/ContextProxy.ts

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import {
1414
providerSettingsSchema,
1515
globalSettingsSchema,
1616
isSecretStateKey,
17+
type WorkspaceSettings,
18+
type WorkspaceSettingsKey,
19+
WORKSPACE_SETTINGS_KEYS,
20+
workspaceSettingsSchema,
1721
} from "@roo-code/types"
1822
import { TelemetryService } from "@roo-code/telemetry"
1923

@@ -23,12 +27,11 @@ type GlobalStateKey = keyof GlobalState
2327
type SecretStateKey = keyof SecretState
2428
type RooCodeSettingsKey = keyof RooCodeSettings
2529

26-
const PASS_THROUGH_STATE_KEYS = ["taskHistory"]
30+
const PASS_THROUGH_STATE_KEYS: string[] = []
2731

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

3034
const globalSettingsExportSchema = globalSettingsSchema.omit({
31-
taskHistory: true,
3235
listApiConfigMeta: true,
3336
currentApiConfigName: true,
3437
})
@@ -38,12 +41,14 @@ export class ContextProxy {
3841

3942
private stateCache: GlobalState
4043
private secretCache: SecretState
44+
private workspaceStateCache: WorkspaceSettings
4145
private _isInitialized = false
4246

4347
constructor(context: vscode.ExtensionContext) {
4448
this.originalContext = context
4549
this.stateCache = {}
4650
this.secretCache = {}
51+
this.workspaceStateCache = {}
4752
this._isInitialized = false
4853
}
4954

@@ -71,6 +76,17 @@ export class ContextProxy {
7176

7277
await Promise.all(promises)
7378

79+
// Initialize workspace state cache
80+
for (const key of WORKSPACE_SETTINGS_KEYS) {
81+
try {
82+
this.workspaceStateCache[key] = this.originalContext.workspaceState.get(key)
83+
} catch (error) {
84+
logger.error(
85+
`Error loading workspace ${key}: ${error instanceof Error ? error.message : String(error)}`,
86+
)
87+
}
88+
}
89+
7490
this._isInitialized = true
7591
}
7692

@@ -151,6 +167,51 @@ export class ContextProxy {
151167
return Object.fromEntries(SECRET_STATE_KEYS.map((key) => [key, this.getSecret(key)]))
152168
}
153169

170+
/**
171+
* ExtensionContext.workspaceState
172+
* https://code.visualstudio.com/api/references/vscode-api#ExtensionContext.workspaceState
173+
*/
174+
175+
getWorkspaceState<K extends WorkspaceSettingsKey>(key: K): WorkspaceSettings[K]
176+
getWorkspaceState<K extends WorkspaceSettingsKey>(key: K, defaultValue: WorkspaceSettings[K]): WorkspaceSettings[K]
177+
getWorkspaceState<K extends WorkspaceSettingsKey>(
178+
key: K,
179+
defaultValue?: WorkspaceSettings[K],
180+
): WorkspaceSettings[K] {
181+
const value = this.workspaceStateCache[key]
182+
return value !== undefined ? value : defaultValue
183+
}
184+
185+
updateWorkspaceState<K extends WorkspaceSettingsKey>(key: K, value: WorkspaceSettings[K]) {
186+
this.workspaceStateCache[key] = value
187+
return this.originalContext.workspaceState.update(key, value)
188+
}
189+
190+
private getAllWorkspaceState(): WorkspaceSettings {
191+
return Object.fromEntries(WORKSPACE_SETTINGS_KEYS.map((key) => [key, this.getWorkspaceState(key)]))
192+
}
193+
194+
/**
195+
* WorkspaceSettings
196+
*/
197+
198+
public getWorkspaceSettings(): WorkspaceSettings {
199+
const values = this.getAllWorkspaceState()
200+
201+
try {
202+
return workspaceSettingsSchema.parse(values)
203+
} catch (error) {
204+
if (error instanceof ZodError) {
205+
TelemetryService.instance.captureSchemaValidationError({ schemaName: "WorkspaceSettings", error })
206+
}
207+
208+
return WORKSPACE_SETTINGS_KEYS.reduce(
209+
(acc, key) => ({ ...acc, [key]: values[key] }),
210+
{} as WorkspaceSettings,
211+
)
212+
}
213+
}
214+
154215
/**
155216
* GlobalSettings
156217
*/
@@ -264,10 +325,12 @@ export class ContextProxy {
264325
// Clear in-memory caches
265326
this.stateCache = {}
266327
this.secretCache = {}
328+
this.workspaceStateCache = {}
267329

268330
await Promise.all([
269331
...GLOBAL_STATE_KEYS.map((key) => this.originalContext.globalState.update(key, undefined)),
270332
...SECRET_STATE_KEYS.map((key) => this.originalContext.secrets.delete(key)),
333+
...WORKSPACE_SETTINGS_KEYS.map((key) => this.originalContext.workspaceState.update(key, undefined)),
271334
])
272335

273336
await this.initialize()

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

Lines changed: 26 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import * as vscode from "vscode"
44

5-
import { GLOBAL_STATE_KEYS, SECRET_STATE_KEYS } from "@roo-code/types"
5+
import { GLOBAL_STATE_KEYS, SECRET_STATE_KEYS, WORKSPACE_SETTINGS_KEYS } from "@roo-code/types"
66

77
import { ContextProxy } from "../ContextProxy"
88

@@ -22,6 +22,7 @@ describe("ContextProxy", () => {
2222
let mockContext: any
2323
let mockGlobalState: any
2424
let mockSecrets: any
25+
let mockWorkspaceState: any
2526

2627
beforeEach(async () => {
2728
// Reset mocks
@@ -40,10 +41,17 @@ describe("ContextProxy", () => {
4041
delete: vi.fn().mockResolvedValue(undefined),
4142
}
4243

44+
// Mock workspaceState
45+
mockWorkspaceState = {
46+
get: vi.fn(),
47+
update: vi.fn().mockResolvedValue(undefined),
48+
}
49+
4350
// Mock the extension context
4451
mockContext = {
4552
globalState: mockGlobalState,
4653
secrets: mockSecrets,
54+
workspaceState: mockWorkspaceState,
4755
extensionUri: { path: "/test/extension" },
4856
extensionPath: "/test/extension",
4957
globalStorageUri: { path: "/test/storage" },
@@ -82,6 +90,13 @@ describe("ContextProxy", () => {
8290
expect(mockSecrets.get).toHaveBeenCalledWith(key)
8391
}
8492
})
93+
94+
it("should initialize workspace state cache with all workspace settings keys", () => {
95+
expect(mockWorkspaceState.get).toHaveBeenCalledTimes(WORKSPACE_SETTINGS_KEYS.length)
96+
for (const key of WORKSPACE_SETTINGS_KEYS) {
97+
expect(mockWorkspaceState.get).toHaveBeenCalledWith(key)
98+
}
99+
})
85100
})
86101

87102
describe("getGlobalState", () => {
@@ -102,41 +117,6 @@ describe("ContextProxy", () => {
102117
const result = proxy.getGlobalState("apiProvider", "deepseek")
103118
expect(result).toBe("deepseek")
104119
})
105-
106-
it("should bypass cache for pass-through state keys", async () => {
107-
// Setup mock return value
108-
mockGlobalState.get.mockReturnValue("pass-through-value")
109-
110-
// Use a pass-through key (taskHistory)
111-
const result = proxy.getGlobalState("taskHistory")
112-
113-
// Should get value directly from original context
114-
expect(result).toBe("pass-through-value")
115-
expect(mockGlobalState.get).toHaveBeenCalledWith("taskHistory")
116-
})
117-
118-
it("should respect default values for pass-through state keys", async () => {
119-
// Setup mock to return undefined
120-
mockGlobalState.get.mockReturnValue(undefined)
121-
122-
// Use a pass-through key with default value
123-
const historyItems = [
124-
{
125-
id: "1",
126-
number: 1,
127-
ts: 1,
128-
task: "test",
129-
tokensIn: 1,
130-
tokensOut: 1,
131-
totalCost: 1,
132-
},
133-
]
134-
135-
const result = proxy.getGlobalState("taskHistory", historyItems)
136-
137-
// Should return default value when original context returns undefined
138-
expect(result).toBe(historyItems)
139-
})
140120
})
141121

142122
describe("updateGlobalState", () => {
@@ -150,33 +130,6 @@ describe("ContextProxy", () => {
150130
const storedValue = await proxy.getGlobalState("apiProvider")
151131
expect(storedValue).toBe("deepseek")
152132
})
153-
154-
it("should bypass cache for pass-through state keys", async () => {
155-
const historyItems = [
156-
{
157-
id: "1",
158-
number: 1,
159-
ts: 1,
160-
task: "test",
161-
tokensIn: 1,
162-
tokensOut: 1,
163-
totalCost: 1,
164-
},
165-
]
166-
167-
await proxy.updateGlobalState("taskHistory", historyItems)
168-
169-
// Should update original context
170-
expect(mockGlobalState.update).toHaveBeenCalledWith("taskHistory", historyItems)
171-
172-
// Setup mock for subsequent get
173-
mockGlobalState.get.mockReturnValue(historyItems)
174-
175-
// Should get fresh value from original context
176-
const storedValue = proxy.getGlobalState("taskHistory")
177-
expect(storedValue).toBe(historyItems)
178-
expect(mockGlobalState.get).toHaveBeenCalledWith("taskHistory")
179-
})
180133
})
181134

182135
describe("getSecret", () => {
@@ -391,6 +344,16 @@ describe("ContextProxy", () => {
391344
expect(mockGlobalState.update).toHaveBeenCalledTimes(expectedUpdateCalls)
392345
})
393346

347+
it("should update all workspace state keys to undefined", async () => {
348+
// Reset all state
349+
await proxy.resetAllState()
350+
351+
// Should have called update with undefined for each workspace key
352+
for (const key of WORKSPACE_SETTINGS_KEYS) {
353+
expect(mockWorkspaceState.update).toHaveBeenCalledWith(key, undefined)
354+
}
355+
})
356+
394357
it("should delete all secrets", async () => {
395358
// Setup initial secrets
396359
await proxy.storeSecret("apiKey", "test-api-key")

src/core/webview/ClineProvider.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,7 +1130,7 @@ export class ClineProvider
11301130
uiMessagesFilePath: string
11311131
apiConversationHistory: Anthropic.MessageParam[]
11321132
}> {
1133-
const history = this.getGlobalState("taskHistory") ?? []
1133+
const history = this.contextProxy.getWorkspaceState("taskHistory") ?? []
11341134
const historyItem = history.find((item) => item.id === id)
11351135

11361136
if (historyItem) {
@@ -1240,9 +1240,9 @@ export class ClineProvider
12401240
}
12411241

12421242
async deleteTaskFromState(id: string) {
1243-
const taskHistory = this.getGlobalState("taskHistory") ?? []
1243+
const taskHistory = this.contextProxy.getWorkspaceState("taskHistory") ?? []
12441244
const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
1245-
await this.updateGlobalState("taskHistory", updatedTaskHistory)
1245+
await this.contextProxy.updateWorkspaceState("taskHistory", updatedTaskHistory)
12461246
await this.postStateToWebview()
12471247
}
12481248

@@ -1443,10 +1443,12 @@ export class ClineProvider
14431443
autoCondenseContextPercent: autoCondenseContextPercent ?? 100,
14441444
uriScheme: vscode.env.uriScheme,
14451445
currentTaskItem: this.getCurrentCline()?.taskId
1446-
? (taskHistory || []).find((item: HistoryItem) => item.id === this.getCurrentCline()?.taskId)
1446+
? (this.contextProxy.getWorkspaceState("taskHistory") || []).find(
1447+
(item: HistoryItem) => item.id === this.getCurrentCline()?.taskId,
1448+
)
14471449
: undefined,
14481450
clineMessages: this.getCurrentCline()?.clineMessages || [],
1449-
taskHistory: (taskHistory || [])
1451+
taskHistory: (this.contextProxy.getWorkspaceState("taskHistory") || [])
14501452
.filter((item: HistoryItem) => item.ts && item.task)
14511453
.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
14521454
soundEnabled: soundEnabled ?? false,
@@ -1610,7 +1612,7 @@ export class ClineProvider
16101612
allowedMaxRequests: stateValues.allowedMaxRequests,
16111613
autoCondenseContext: stateValues.autoCondenseContext ?? true,
16121614
autoCondenseContextPercent: stateValues.autoCondenseContextPercent ?? 100,
1613-
taskHistory: stateValues.taskHistory,
1615+
taskHistory: this.contextProxy.getWorkspaceState("taskHistory"),
16141616
allowedCommands: stateValues.allowedCommands,
16151617
soundEnabled: stateValues.soundEnabled ?? false,
16161618
ttsEnabled: stateValues.ttsEnabled ?? false,
@@ -1681,7 +1683,7 @@ export class ClineProvider
16811683
}
16821684

16831685
async updateTaskHistory(item: HistoryItem): Promise<HistoryItem[]> {
1684-
const history = (this.getGlobalState("taskHistory") as HistoryItem[] | undefined) || []
1686+
const history = this.contextProxy.getWorkspaceState("taskHistory") || []
16851687
const existingItemIndex = history.findIndex((h) => h.id === item.id)
16861688

16871689
if (existingItemIndex !== -1) {
@@ -1690,7 +1692,7 @@ export class ClineProvider
16901692
history.push(item)
16911693
}
16921694

1693-
await this.updateGlobalState("taskHistory", history)
1695+
await this.contextProxy.updateWorkspaceState("taskHistory", history)
16941696
return history
16951697
}
16961698

0 commit comments

Comments
 (0)