Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
2 changes: 0 additions & 2 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
providerSettingsEntrySchema,
providerSettingsSchema,
} from "./provider-settings.js"
import { historyItemSchema } from "./history.js"
import { codebaseIndexModelsSchema, codebaseIndexConfigSchema } from "./codebase-index.js"
import { experimentsSchema } from "./experiment.js"
import { telemetrySettingsSchema } from "./telemetry.js"
Expand All @@ -26,7 +25,6 @@ export const globalSettingsSchema = z.object({

lastShownAnnouncementId: z.string().optional(),
customInstructions: z.string().optional(),
taskHistory: z.array(historyItemSchema).optional(),

condensingApiConfigId: z.string().optional(),
customCondensingPrompt: z.string().optional(),
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export * from "./terminal.js"
export * from "./tool.js"
export * from "./type-fu.js"
export * from "./vscode.js"
export * from "./workspace-settings.js"
15 changes: 15 additions & 0 deletions packages/types/src/workspace-settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { z } from "zod"
import { historyItemSchema } from "./history.js"

/**
* WorkspaceSettings - Settings that are specific to a workspace
*/
export const workspaceSettingsSchema = z.object({
taskHistory: z.array(historyItemSchema).optional(),
})

export type WorkspaceSettings = z.infer<typeof workspaceSettingsSchema>

export const WORKSPACE_SETTINGS_KEYS = workspaceSettingsSchema.keyof().options

export type WorkspaceSettingsKey = keyof WorkspaceSettings
67 changes: 65 additions & 2 deletions src/core/config/ContextProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import {
providerSettingsSchema,
globalSettingsSchema,
isSecretStateKey,
type WorkspaceSettings,
type WorkspaceSettingsKey,
WORKSPACE_SETTINGS_KEYS,
workspaceSettingsSchema,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"

Expand All @@ -23,12 +27,11 @@ 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)

const globalSettingsExportSchema = globalSettingsSchema.omit({
taskHistory: true,
listApiConfigMeta: true,
currentApiConfigName: true,
})
Expand All @@ -38,12 +41,14 @@ export class ContextProxy {

private stateCache: GlobalState
private secretCache: SecretState
private workspaceStateCache: WorkspaceSettings
private _isInitialized = false

constructor(context: vscode.ExtensionContext) {
this.originalContext = context
this.stateCache = {}
this.secretCache = {}
this.workspaceStateCache = {}
this._isInitialized = false
}

Expand Down Expand Up @@ -71,6 +76,17 @@ export class ContextProxy {

await Promise.all(promises)

// Initialize workspace state cache
for (const key of WORKSPACE_SETTINGS_KEYS) {
try {
this.workspaceStateCache[key] = this.originalContext.workspaceState.get(key)
} catch (error) {
logger.error(
`Error loading workspace ${key}: ${error instanceof Error ? error.message : String(error)}`,
)
}
}

this._isInitialized = true
}

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

/**
* ExtensionContext.workspaceState
* https://code.visualstudio.com/api/references/vscode-api#ExtensionContext.workspaceState
*/

getWorkspaceState<K extends WorkspaceSettingsKey>(key: K): WorkspaceSettings[K]
getWorkspaceState<K extends WorkspaceSettingsKey>(key: K, defaultValue: WorkspaceSettings[K]): WorkspaceSettings[K]
getWorkspaceState<K extends WorkspaceSettingsKey>(
key: K,
defaultValue?: WorkspaceSettings[K],
): WorkspaceSettings[K] {
const value = this.workspaceStateCache[key]
return value !== undefined ? value : defaultValue
}

updateWorkspaceState<K extends WorkspaceSettingsKey>(key: K, value: WorkspaceSettings[K]) {
this.workspaceStateCache[key] = value
return this.originalContext.workspaceState.update(key, value)
}

private getAllWorkspaceState(): WorkspaceSettings {
return Object.fromEntries(WORKSPACE_SETTINGS_KEYS.map((key) => [key, this.getWorkspaceState(key)]))
}

/**
* WorkspaceSettings
*/

public getWorkspaceSettings(): WorkspaceSettings {
const values = this.getAllWorkspaceState()

try {
return workspaceSettingsSchema.parse(values)
} catch (error) {
if (error instanceof ZodError) {
TelemetryService.instance.captureSchemaValidationError({ schemaName: "WorkspaceSettings", error })
}

return WORKSPACE_SETTINGS_KEYS.reduce(
(acc, key) => ({ ...acc, [key]: values[key] }),
{} as WorkspaceSettings,
)
}
}

/**
* GlobalSettings
*/
Expand Down Expand Up @@ -264,10 +325,12 @@ export class ContextProxy {
// Clear in-memory caches
this.stateCache = {}
this.secretCache = {}
this.workspaceStateCache = {}

await Promise.all([
...GLOBAL_STATE_KEYS.map((key) => this.originalContext.globalState.update(key, undefined)),
...SECRET_STATE_KEYS.map((key) => this.originalContext.secrets.delete(key)),
...WORKSPACE_SETTINGS_KEYS.map((key) => this.originalContext.workspaceState.update(key, undefined)),
])

await this.initialize()
Expand Down
89 changes: 26 additions & 63 deletions src/core/config/__tests__/ContextProxy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import * as vscode from "vscode"

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

import { ContextProxy } from "../ContextProxy"

Expand All @@ -22,6 +22,7 @@ describe("ContextProxy", () => {
let mockContext: any
let mockGlobalState: any
let mockSecrets: any
let mockWorkspaceState: any

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

// Mock workspaceState
mockWorkspaceState = {
get: vi.fn(),
update: vi.fn().mockResolvedValue(undefined),
}

// Mock the extension context
mockContext = {
globalState: mockGlobalState,
secrets: mockSecrets,
workspaceState: mockWorkspaceState,
extensionUri: { path: "/test/extension" },
extensionPath: "/test/extension",
globalStorageUri: { path: "/test/storage" },
Expand Down Expand Up @@ -82,6 +90,13 @@ describe("ContextProxy", () => {
expect(mockSecrets.get).toHaveBeenCalledWith(key)
}
})

it("should initialize workspace state cache with all workspace settings keys", () => {
expect(mockWorkspaceState.get).toHaveBeenCalledTimes(WORKSPACE_SETTINGS_KEYS.length)
for (const key of WORKSPACE_SETTINGS_KEYS) {
expect(mockWorkspaceState.get).toHaveBeenCalledWith(key)
}
})
})

describe("getGlobalState", () => {
Expand All @@ -102,41 +117,6 @@ describe("ContextProxy", () => {
const result = proxy.getGlobalState("apiProvider", "deepseek")
expect(result).toBe("deepseek")
})

it("should bypass cache for pass-through state keys", async () => {
// Setup mock return value
mockGlobalState.get.mockReturnValue("pass-through-value")

// Use a pass-through key (taskHistory)
const result = proxy.getGlobalState("taskHistory")

// Should get value directly from original context
expect(result).toBe("pass-through-value")
expect(mockGlobalState.get).toHaveBeenCalledWith("taskHistory")
})

it("should respect default values for pass-through state keys", async () => {
// Setup mock to return undefined
mockGlobalState.get.mockReturnValue(undefined)

// Use a pass-through key with default value
const historyItems = [
{
id: "1",
number: 1,
ts: 1,
task: "test",
tokensIn: 1,
tokensOut: 1,
totalCost: 1,
},
]

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

// Should return default value when original context returns undefined
expect(result).toBe(historyItems)
})
})

describe("updateGlobalState", () => {
Expand All @@ -150,33 +130,6 @@ describe("ContextProxy", () => {
const storedValue = await proxy.getGlobalState("apiProvider")
expect(storedValue).toBe("deepseek")
})

it("should bypass cache for pass-through state keys", async () => {
const historyItems = [
{
id: "1",
number: 1,
ts: 1,
task: "test",
tokensIn: 1,
tokensOut: 1,
totalCost: 1,
},
]

await proxy.updateGlobalState("taskHistory", historyItems)

// Should update original context
expect(mockGlobalState.update).toHaveBeenCalledWith("taskHistory", historyItems)

// Setup mock for subsequent get
mockGlobalState.get.mockReturnValue(historyItems)

// Should get fresh value from original context
const storedValue = proxy.getGlobalState("taskHistory")
expect(storedValue).toBe(historyItems)
expect(mockGlobalState.get).toHaveBeenCalledWith("taskHistory")
})
})

describe("getSecret", () => {
Expand Down Expand Up @@ -391,6 +344,16 @@ describe("ContextProxy", () => {
expect(mockGlobalState.update).toHaveBeenCalledTimes(expectedUpdateCalls)
})

it("should update all workspace state keys to undefined", async () => {
// Reset all state
await proxy.resetAllState()

// Should have called update with undefined for each workspace key
for (const key of WORKSPACE_SETTINGS_KEYS) {
expect(mockWorkspaceState.update).toHaveBeenCalledWith(key, undefined)
}
})

it("should delete all secrets", async () => {
// Setup initial secrets
await proxy.storeSecret("apiKey", "test-api-key")
Expand Down
2 changes: 1 addition & 1 deletion src/core/task/__tests__/Task.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ describe("Cline", () => {

mockExtensionContext = {
globalState: {
get: vi.fn().mockImplementation((key: keyof GlobalState) => {
get: vi.fn().mockImplementation((key: string) => {
if (key === "taskHistory") {
return [
{
Expand Down
18 changes: 10 additions & 8 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1130,7 +1130,7 @@ export class ClineProvider
uiMessagesFilePath: string
apiConversationHistory: Anthropic.MessageParam[]
}> {
const history = this.getGlobalState("taskHistory") ?? []
const history = this.contextProxy.getWorkspaceState("taskHistory") ?? []
const historyItem = history.find((item) => item.id === id)

if (historyItem) {
Expand Down Expand Up @@ -1240,9 +1240,9 @@ export class ClineProvider
}

async deleteTaskFromState(id: string) {
const taskHistory = this.getGlobalState("taskHistory") ?? []
const taskHistory = this.contextProxy.getWorkspaceState("taskHistory") ?? []
const updatedTaskHistory = taskHistory.filter((task) => task.id !== id)
await this.updateGlobalState("taskHistory", updatedTaskHistory)
await this.contextProxy.updateWorkspaceState("taskHistory", updatedTaskHistory)
await this.postStateToWebview()
}

Expand Down Expand Up @@ -1443,10 +1443,12 @@ export class ClineProvider
autoCondenseContextPercent: autoCondenseContextPercent ?? 100,
uriScheme: vscode.env.uriScheme,
currentTaskItem: this.getCurrentCline()?.taskId
? (taskHistory || []).find((item: HistoryItem) => item.id === this.getCurrentCline()?.taskId)
? (this.contextProxy.getWorkspaceState("taskHistory") || []).find(
(item: HistoryItem) => item.id === this.getCurrentCline()?.taskId,
)
: undefined,
clineMessages: this.getCurrentCline()?.clineMessages || [],
taskHistory: (taskHistory || [])
taskHistory: (this.contextProxy.getWorkspaceState("taskHistory") || [])
.filter((item: HistoryItem) => item.ts && item.task)
.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
soundEnabled: soundEnabled ?? false,
Expand Down Expand Up @@ -1610,7 +1612,7 @@ export class ClineProvider
allowedMaxRequests: stateValues.allowedMaxRequests,
autoCondenseContext: stateValues.autoCondenseContext ?? true,
autoCondenseContextPercent: stateValues.autoCondenseContextPercent ?? 100,
taskHistory: stateValues.taskHistory,
taskHistory: this.contextProxy.getWorkspaceState("taskHistory"),
allowedCommands: stateValues.allowedCommands,
soundEnabled: stateValues.soundEnabled ?? false,
ttsEnabled: stateValues.ttsEnabled ?? false,
Expand Down Expand Up @@ -1681,7 +1683,7 @@ export class ClineProvider
}

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

if (existingItemIndex !== -1) {
Expand All @@ -1690,7 +1692,7 @@ export class ClineProvider
history.push(item)
}

await this.updateGlobalState("taskHistory", history)
await this.contextProxy.updateWorkspaceState("taskHistory", history)
return history
}

Expand Down
Loading
Loading