Skip to content

Commit 230c7ed

Browse files
committed
Strongly type our settings use composition where possible
1 parent 0fd0800 commit 230c7ed

26 files changed

+1622
-769
lines changed

src/core/Cline.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -994,7 +994,7 @@ export class Cline extends EventEmitter<ClineEvents> {
994994
}
995995
}
996996

997-
const { terminalOutputLineLimit } = (await this.providerRef.deref()?.getState()) ?? {}
997+
const { terminalOutputLineLimit = 500 } = (await this.providerRef.deref()?.getState()) ?? {}
998998

999999
process.on("line", (line) => {
10001000
if (!didContinue) {
@@ -2339,7 +2339,7 @@ export class Cline extends EventEmitter<ClineEvents> {
23392339
}
23402340

23412341
// Get the maxReadFileLine setting
2342-
const { maxReadFileLine } = (await this.providerRef.deref()?.getState()) ?? {}
2342+
const { maxReadFileLine = 500 } = (await this.providerRef.deref()?.getState()) ?? {}
23432343

23442344
// Count total lines in the file
23452345
let totalLines = 0
@@ -2480,13 +2480,14 @@ export class Cline extends EventEmitter<ClineEvents> {
24802480
this.consecutiveMistakeCount = 0
24812481
const absolutePath = path.resolve(this.cwd, relDirPath)
24822482
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
2483-
const { showRooIgnoredFiles } = (await this.providerRef.deref()?.getState()) ?? {}
2483+
const { showRooIgnoredFiles = true } =
2484+
(await this.providerRef.deref()?.getState()) ?? {}
24842485
const result = formatResponse.formatFilesList(
24852486
absolutePath,
24862487
files,
24872488
didHitLimit,
24882489
this.rooIgnoreController,
2489-
showRooIgnoredFiles ?? true,
2490+
showRooIgnoredFiles,
24902491
)
24912492
const completeMessage = JSON.stringify({
24922493
...sharedMessageProps,
@@ -3759,15 +3760,16 @@ export class Cline extends EventEmitter<ClineEvents> {
37593760
async getEnvironmentDetails(includeFileDetails: boolean = false) {
37603761
let details = ""
37613762

3762-
const { terminalOutputLineLimit, maxWorkspaceFiles } = (await this.providerRef.deref()?.getState()) ?? {}
3763+
const { terminalOutputLineLimit = 500, maxWorkspaceFiles = 200 } =
3764+
(await this.providerRef.deref()?.getState()) ?? {}
37633765

37643766
// It could be useful for cline to know if the user went from one or no file to another between messages, so we always include this context
37653767
details += "\n\n# VSCode Visible Files"
37663768
const visibleFilePaths = vscode.window.visibleTextEditors
37673769
?.map((editor) => editor.document?.uri?.fsPath)
37683770
.filter(Boolean)
37693771
.map((absolutePath) => path.relative(this.cwd, absolutePath))
3770-
.slice(0, maxWorkspaceFiles ?? 200)
3772+
.slice(0, maxWorkspaceFiles)
37713773

37723774
// Filter paths through rooIgnoreController
37733775
const allowedVisibleFiles = this.rooIgnoreController
@@ -3979,7 +3981,7 @@ export class Cline extends EventEmitter<ClineEvents> {
39793981
} else {
39803982
const maxFiles = maxWorkspaceFiles ?? 200
39813983
const [files, didHitLimit] = await listFiles(this.cwd, true, maxFiles)
3982-
const { showRooIgnoredFiles } = (await this.providerRef.deref()?.getState()) ?? {}
3984+
const { showRooIgnoredFiles = true } = (await this.providerRef.deref()?.getState()) ?? {}
39833985
const result = formatResponse.formatFilesList(
39843986
this.cwd,
39853987
files,

src/core/__tests__/contextProxy.test.ts

Lines changed: 135 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
// npx jest src/core/__tests__/contextProxy.test.ts
22

3+
import fs from "fs/promises"
4+
35
import * as vscode from "vscode"
46
import { ContextProxy } from "../contextProxy"
57

68
import { logger } from "../../utils/logging"
7-
import { GLOBAL_STATE_KEYS, SECRET_KEYS, ConfigurationKey, GlobalStateKey } from "../../shared/globalState"
9+
import { GLOBAL_STATE_KEYS, SECRET_STATE_KEYS } from "../../shared/globalState"
810

911
jest.mock("vscode", () => ({
1012
Uri: {
@@ -77,8 +79,8 @@ describe("ContextProxy", () => {
7779
})
7880

7981
it("should initialize secret cache with all secret keys", () => {
80-
expect(mockSecrets.get).toHaveBeenCalledTimes(SECRET_KEYS.length)
81-
for (const key of SECRET_KEYS) {
82+
expect(mockSecrets.get).toHaveBeenCalledTimes(SECRET_STATE_KEYS.length)
83+
for (const key of SECRET_STATE_KEYS) {
8284
expect(mockSecrets.get).toHaveBeenCalledWith(key)
8385
}
8486
})
@@ -87,28 +89,28 @@ describe("ContextProxy", () => {
8789
describe("getGlobalState", () => {
8890
it("should return value from cache when it exists", async () => {
8991
// Manually set a value in the cache
90-
await proxy.updateGlobalState("apiProvider", "cached-value")
92+
await proxy.updateGlobalState("apiProvider", "deepseek")
9193

9294
// Should return the cached value
9395
const result = proxy.getGlobalState("apiProvider")
94-
expect(result).toBe("cached-value")
96+
expect(result).toBe("deepseek")
9597

9698
// Original context should be called once during updateGlobalState
9799
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length) // Only from initialization
98100
})
99101

100102
it("should handle default values correctly", async () => {
101103
// No value in cache
102-
const result = proxy.getGlobalState("apiProvider", "default-value")
103-
expect(result).toBe("default-value")
104+
const result = proxy.getGlobalState("apiProvider", "deepseek")
105+
expect(result).toBe("deepseek")
104106
})
105107

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

110112
// Use a pass-through key (taskHistory)
111-
const result = proxy.getGlobalState("taskHistory" as GlobalStateKey)
113+
const result = proxy.getGlobalState("taskHistory")
112114

113115
// Should get value directly from original context
114116
expect(result).toBe("pass-through-value")
@@ -120,37 +122,61 @@ describe("ContextProxy", () => {
120122
mockGlobalState.get.mockReturnValue(undefined)
121123

122124
// Use a pass-through key with default value
123-
const result = proxy.getGlobalState("taskHistory" as GlobalStateKey, "default-value")
125+
const historyItems = [
126+
{
127+
id: "1",
128+
number: 1,
129+
ts: 1,
130+
task: "test",
131+
tokensIn: 1,
132+
tokensOut: 1,
133+
totalCost: 1,
134+
},
135+
]
136+
137+
const result = proxy.getGlobalState("taskHistory", historyItems)
124138

125139
// Should return default value when original context returns undefined
126-
expect(result).toBe("default-value")
140+
expect(result).toBe(historyItems)
127141
})
128142
})
129143

130144
describe("updateGlobalState", () => {
131145
it("should update state directly in original context", async () => {
132-
await proxy.updateGlobalState("apiProvider", "new-value")
146+
await proxy.updateGlobalState("apiProvider", "deepseek")
133147

134148
// Should have called original context
135-
expect(mockGlobalState.update).toHaveBeenCalledWith("apiProvider", "new-value")
149+
expect(mockGlobalState.update).toHaveBeenCalledWith("apiProvider", "deepseek")
136150

137151
// Should have stored the value in cache
138152
const storedValue = await proxy.getGlobalState("apiProvider")
139-
expect(storedValue).toBe("new-value")
153+
expect(storedValue).toBe("deepseek")
140154
})
141155

142156
it("should bypass cache for pass-through state keys", async () => {
143-
await proxy.updateGlobalState("taskHistory" as GlobalStateKey, "new-value")
157+
const historyItems = [
158+
{
159+
id: "1",
160+
number: 1,
161+
ts: 1,
162+
task: "test",
163+
tokensIn: 1,
164+
tokensOut: 1,
165+
totalCost: 1,
166+
},
167+
]
168+
169+
await proxy.updateGlobalState("taskHistory", historyItems)
144170

145171
// Should update original context
146-
expect(mockGlobalState.update).toHaveBeenCalledWith("taskHistory", "new-value")
172+
expect(mockGlobalState.update).toHaveBeenCalledWith("taskHistory", historyItems)
147173

148174
// Setup mock for subsequent get
149-
mockGlobalState.get.mockReturnValue("new-value")
175+
mockGlobalState.get.mockReturnValue(historyItems)
150176

151177
// Should get fresh value from original context
152-
const storedValue = proxy.getGlobalState("taskHistory" as GlobalStateKey)
153-
expect(storedValue).toBe("new-value")
178+
const storedValue = proxy.getGlobalState("taskHistory")
179+
expect(storedValue).toBe(historyItems)
154180
expect(mockGlobalState.get).toHaveBeenCalledWith("taskHistory")
155181
})
156182
})
@@ -220,27 +246,6 @@ describe("ContextProxy", () => {
220246
const storedValue = proxy.getGlobalState("apiModelId")
221247
expect(storedValue).toBe("gpt-4")
222248
})
223-
224-
it("should handle unknown keys as global state with warning", async () => {
225-
// Spy on the logger
226-
const warnSpy = jest.spyOn(logger, "warn")
227-
228-
// Spy on updateGlobalState
229-
const updateGlobalStateSpy = jest.spyOn(proxy, "updateGlobalState")
230-
231-
// Test with an unknown key
232-
await proxy.setValue("unknownKey" as ConfigurationKey, "some-value")
233-
234-
// Should have logged a warning
235-
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Unknown key: unknownKey"))
236-
237-
// Should have called updateGlobalState
238-
expect(updateGlobalStateSpy).toHaveBeenCalledWith("unknownKey", "some-value")
239-
240-
// Should have stored the value in state cache
241-
const storedValue = proxy.getGlobalState("unknownKey" as GlobalStateKey)
242-
expect(storedValue).toBe("some-value")
243-
})
244249
})
245250

246251
describe("setValues", () => {
@@ -288,7 +293,7 @@ describe("ContextProxy", () => {
288293
})
289294
})
290295

291-
describe("setApiConfiguration", () => {
296+
describe("setProviderSettings", () => {
292297
it("should clear old API configuration values and set new ones", async () => {
293298
// Set up initial API configuration values
294299
await proxy.updateGlobalState("apiModelId", "old-model")
@@ -298,8 +303,8 @@ describe("ContextProxy", () => {
298303
// Spy on setValues
299304
const setValuesSpy = jest.spyOn(proxy, "setValues")
300305

301-
// Call setApiConfiguration with new configuration
302-
await proxy.setApiConfiguration({
306+
// Call setProviderSettings with new configuration
307+
await proxy.setProviderSettings({
303308
apiModelId: "new-model",
304309
apiProvider: "anthropic",
305310
// Note: openAiBaseUrl is not included in the new config
@@ -332,8 +337,8 @@ describe("ContextProxy", () => {
332337
// Spy on setValues
333338
const setValuesSpy = jest.spyOn(proxy, "setValues")
334339

335-
// Call setApiConfiguration with empty configuration
336-
await proxy.setApiConfiguration({})
340+
// Call setProviderSettings with empty configuration
341+
await proxy.setProviderSettings({})
337342

338343
// Verify setValues was called with undefined for all existing API config keys
339344
expect(setValuesSpy).toHaveBeenCalledWith(
@@ -397,12 +402,12 @@ describe("ContextProxy", () => {
397402
await proxy.resetAllState()
398403

399404
// Should have called delete for each key
400-
for (const key of SECRET_KEYS) {
405+
for (const key of SECRET_STATE_KEYS) {
401406
expect(mockSecrets.delete).toHaveBeenCalledWith(key)
402407
}
403408

404409
// Total calls should equal the number of secret keys
405-
expect(mockSecrets.delete).toHaveBeenCalledTimes(SECRET_KEYS.length)
410+
expect(mockSecrets.delete).toHaveBeenCalledTimes(SECRET_STATE_KEYS.length)
406411
})
407412

408413
it("should reinitialize caches after reset", async () => {
@@ -416,4 +421,88 @@ describe("ContextProxy", () => {
416421
expect(initializeSpy).toHaveBeenCalledTimes(1)
417422
})
418423
})
424+
425+
describe("exportGlobalSettings", () => {
426+
it("should write global settings to a file when filePath is provided", async () => {
427+
await proxy.setValues({
428+
apiModelId: "gpt-4",
429+
apiProvider: "openai",
430+
openAiApiKey: "test-api-key",
431+
autoApprovalEnabled: true,
432+
})
433+
434+
const filePath = `/tmp/roo-global-config-${Date.now()}.json`
435+
const result = await proxy.exportGlobalSettings(filePath)
436+
expect(result).toEqual({ autoApprovalEnabled: true })
437+
const fileContent = await fs.readFile(filePath, "utf-8")
438+
expect(fileContent).toContain('"autoApprovalEnabled": true')
439+
440+
await proxy.setValue("autoApprovalEnabled", false)
441+
expect(proxy.getValue("autoApprovalEnabled")).toBe(false)
442+
443+
const importedConfig = await proxy.importGlobalSettings(filePath)
444+
expect(importedConfig).toEqual({ autoApprovalEnabled: true })
445+
expect(proxy.getValue("autoApprovalEnabled")).toBe(true)
446+
447+
await fs.unlink(filePath)
448+
})
449+
})
450+
451+
describe("exportProviderSettings", () => {
452+
it("should write provider settings to a file when filePath is provided", async () => {
453+
await proxy.setValues({
454+
apiModelId: "gpt-4",
455+
apiProvider: "openai",
456+
openAiApiKey: "test-api-key",
457+
autoApprovalEnabled: true,
458+
})
459+
460+
const filePath = `/tmp/roo-api-config-${Date.now()}.json`
461+
const result = await proxy.exportProviderSettings(filePath)
462+
expect(result).toEqual({
463+
apiModelId: "gpt-4",
464+
apiProvider: "openai",
465+
openAiApiKey: "test-api-key",
466+
apiKey: "test-secret",
467+
awsAccessKey: "test-secret",
468+
awsSecretKey: "test-secret",
469+
awsSessionToken: "test-secret",
470+
deepSeekApiKey: "test-secret",
471+
geminiApiKey: "test-secret",
472+
glamaApiKey: "test-secret",
473+
mistralApiKey: "test-secret",
474+
openAiNativeApiKey: "test-secret",
475+
openRouterApiKey: "test-secret",
476+
requestyApiKey: "test-secret",
477+
unboundApiKey: "test-secret",
478+
})
479+
const fileContent = await fs.readFile(filePath, "utf-8")
480+
expect(fileContent).toContain('"openAiApiKey": "test-api-key"')
481+
482+
await proxy.setValue("openAiApiKey", "new-test-api-key")
483+
expect(proxy.getValue("openAiApiKey")).toBe("new-test-api-key")
484+
485+
const importedConfig = await proxy.importProviderSettings(filePath)
486+
expect(importedConfig).toEqual({
487+
apiModelId: "gpt-4",
488+
apiProvider: "openai",
489+
openAiApiKey: "test-api-key",
490+
apiKey: "test-secret",
491+
awsAccessKey: "test-secret",
492+
awsSecretKey: "test-secret",
493+
awsSessionToken: "test-secret",
494+
deepSeekApiKey: "test-secret",
495+
geminiApiKey: "test-secret",
496+
glamaApiKey: "test-secret",
497+
mistralApiKey: "test-secret",
498+
openAiNativeApiKey: "test-secret",
499+
openRouterApiKey: "test-secret",
500+
requestyApiKey: "test-secret",
501+
unboundApiKey: "test-secret",
502+
})
503+
expect(proxy.getValue("openAiApiKey")).toBe("test-api-key")
504+
505+
await fs.unlink(filePath)
506+
})
507+
})
419508
})

src/core/config/ConfigManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ApiConfigMeta } from "../../shared/ExtensionMessage"
66
export interface ApiConfigData {
77
currentApiConfigName: string
88
apiConfigs: {
9-
[key: string]: ApiConfiguration
9+
[key: string]: ApiConfiguration & { id?: string }
1010
}
1111
modeApiConfigs?: Partial<Record<Mode, string>>
1212
}

0 commit comments

Comments
 (0)