mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
Import / export
This commit is contained in:
parent
8f64109894
commit
edafcf04ba
12 changed files with 485 additions and 73 deletions
|
|
@ -1,5 +1,7 @@
|
|||
// npx jest src/core/__tests__/contextProxy.test.ts
|
||||
|
||||
import fs from "fs/promises"
|
||||
|
||||
import * as vscode from "vscode"
|
||||
import { ContextProxy } from "../contextProxy"
|
||||
|
||||
|
|
@ -416,4 +418,76 @@ describe("ContextProxy", () => {
|
|||
expect(initializeSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("exportGlobalConfiguration", () => {
|
||||
it("should write configuration to a file when filePath is provided", async () => {
|
||||
await proxy.updateGlobalState("apiModelId", "gpt-4")
|
||||
await proxy.updateGlobalState("apiProvider", "openai")
|
||||
await proxy.storeSecret("openAiApiKey", "test-api-key")
|
||||
|
||||
const filePath = `/tmp/roo-global-config-${Date.now()}.json`
|
||||
const result = await proxy.exportGlobalConfiguration(filePath)
|
||||
expect(result).toEqual({ apiProvider: "openai" })
|
||||
const fileContent = await fs.readFile(filePath, "utf-8")
|
||||
expect(fileContent).toContain('"apiProvider": "openai"')
|
||||
|
||||
await proxy.updateGlobalState("apiProvider", "openrouter")
|
||||
|
||||
const importedConfig = await proxy.importGlobalConfiguration(filePath)
|
||||
expect(importedConfig).toEqual({ apiProvider: "openai" })
|
||||
|
||||
await fs.unlink(filePath)
|
||||
})
|
||||
})
|
||||
|
||||
describe("exportApiConfiguration", () => {
|
||||
it("should write configuration to a file when filePath is provided", async () => {
|
||||
await proxy.updateGlobalState("apiModelId", "gpt-4")
|
||||
await proxy.updateGlobalState("apiProvider", "openai")
|
||||
await proxy.storeSecret("openAiApiKey", "test-api-key")
|
||||
|
||||
const filePath = `/tmp/roo-api-config-${Date.now()}.json`
|
||||
const result = await proxy.exportApiConfiguration(filePath)
|
||||
expect(result).toEqual({
|
||||
apiModelId: "gpt-4",
|
||||
openAiApiKey: "test-api-key",
|
||||
apiKey: "test-secret",
|
||||
awsAccessKey: "test-secret",
|
||||
awsSecretKey: "test-secret",
|
||||
awsSessionToken: "test-secret",
|
||||
deepSeekApiKey: "test-secret",
|
||||
geminiApiKey: "test-secret",
|
||||
glamaApiKey: "test-secret",
|
||||
mistralApiKey: "test-secret",
|
||||
openAiNativeApiKey: "test-secret",
|
||||
openRouterApiKey: "test-secret",
|
||||
requestyApiKey: "test-secret",
|
||||
unboundApiKey: "test-secret",
|
||||
})
|
||||
const fileContent = await fs.readFile(filePath, "utf-8")
|
||||
expect(fileContent).toContain('"openAiApiKey": "test-api-key"')
|
||||
|
||||
await proxy.storeSecret("openAiApiKey", "new-text-api-key")
|
||||
|
||||
const importedConfig = await proxy.importApiConfiguration(filePath)
|
||||
expect(importedConfig).toEqual({
|
||||
apiModelId: "gpt-4",
|
||||
openAiApiKey: "test-api-key",
|
||||
apiKey: "test-secret",
|
||||
awsAccessKey: "test-secret",
|
||||
awsSecretKey: "test-secret",
|
||||
awsSessionToken: "test-secret",
|
||||
deepSeekApiKey: "test-secret",
|
||||
geminiApiKey: "test-secret",
|
||||
glamaApiKey: "test-secret",
|
||||
mistralApiKey: "test-secret",
|
||||
openAiNativeApiKey: "test-secret",
|
||||
openRouterApiKey: "test-secret",
|
||||
requestyApiKey: "test-secret",
|
||||
unboundApiKey: "test-secret",
|
||||
})
|
||||
|
||||
await fs.unlink(filePath)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import * as vscode from "vscode"
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
|
||||
import { logger } from "../utils/logging"
|
||||
import {
|
||||
|
|
@ -11,8 +13,22 @@ import {
|
|||
isSecretKey,
|
||||
isGlobalStateKey,
|
||||
isPassThroughStateKey,
|
||||
globalStateSchema,
|
||||
} from "../shared/globalState"
|
||||
import { API_CONFIG_KEYS, ApiConfiguration } from "../shared/api"
|
||||
import { API_CONFIG_KEYS, ApiConfiguration, apiHandlerOptionsSchema, ApiHandlerOptionsKey } from "../shared/api"
|
||||
|
||||
const NON_EXPORTABLE_GLOBAL_CONFIGURATION: GlobalStateKey[] = [
|
||||
"taskHistory",
|
||||
"listApiConfigMeta",
|
||||
"currentApiConfigName",
|
||||
]
|
||||
|
||||
const NON_EXPORTABLE_API_CONFIGURATION: ApiHandlerOptionsKey[] = [
|
||||
"glamaModelInfo",
|
||||
"openRouterModelInfo",
|
||||
"unboundModelInfo",
|
||||
"requestyModelInfo",
|
||||
]
|
||||
|
||||
export class ContextProxy {
|
||||
private readonly originalContext: vscode.ExtensionContext
|
||||
|
|
@ -155,14 +171,125 @@ export class ContextProxy {
|
|||
// that the setting's value should be `undefined` and therefore we
|
||||
// need to remove it from the state cache if it exists.
|
||||
await this.setValues({
|
||||
...API_CONFIG_KEYS.filter((key) => !!this.stateCache.get(key)).reduce(
|
||||
(acc, key) => ({ ...acc, [key]: undefined }),
|
||||
{} as Partial<ConfigurationValues>,
|
||||
),
|
||||
...API_CONFIG_KEYS.filter((key) => isGlobalStateKey(key))
|
||||
.filter((key) => !!this.stateCache.get(key))
|
||||
.reduce((acc, key) => ({ ...acc, [key]: undefined }), {} as Partial<ConfigurationValues>),
|
||||
...apiConfiguration,
|
||||
})
|
||||
}
|
||||
|
||||
private getAllGlobalStateValues() {
|
||||
const values: Partial<Record<GlobalStateKey, any>> = {}
|
||||
|
||||
for (const key of GLOBAL_STATE_KEYS) {
|
||||
const value = this.getGlobalState(key)
|
||||
|
||||
if (value !== undefined) {
|
||||
values[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
private getAllSecretValues() {
|
||||
const values: Partial<Record<SecretKey, string>> = {}
|
||||
|
||||
for (const key of SECRET_KEYS) {
|
||||
const value = this.getSecret(key)
|
||||
|
||||
if (value !== undefined) {
|
||||
values[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
async exportGlobalConfiguration(filePath: string) {
|
||||
try {
|
||||
const values = this.getAllGlobalStateValues()
|
||||
const configuration = globalStateSchema.parse(values)
|
||||
const omit = new Set<string>([...API_CONFIG_KEYS, ...NON_EXPORTABLE_GLOBAL_CONFIGURATION])
|
||||
const entries = Object.entries(configuration).filter(([key]) => !omit.has(key))
|
||||
|
||||
if (entries.length === 0) {
|
||||
throw new Error("No configuration values to export.")
|
||||
}
|
||||
|
||||
const globalConfiguration = Object.fromEntries(entries)
|
||||
|
||||
const dirname = path.dirname(filePath)
|
||||
await fs.mkdir(dirname, { recursive: true })
|
||||
await fs.writeFile(filePath, JSON.stringify(globalConfiguration, null, 2), "utf-8")
|
||||
return globalConfiguration
|
||||
} catch (error) {
|
||||
console.log(error.message)
|
||||
logger.error(
|
||||
`Error exporting global configuration to ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async importGlobalConfiguration(filePath: string) {
|
||||
try {
|
||||
const configuration = globalStateSchema.parse(JSON.parse(await fs.readFile(filePath, "utf-8")))
|
||||
const omit = new Set<string>([...API_CONFIG_KEYS, ...NON_EXPORTABLE_GLOBAL_CONFIGURATION])
|
||||
const entries = Object.entries(configuration).filter(([key]) => !omit.has(key))
|
||||
|
||||
if (entries.length === 0) {
|
||||
throw new Error("No configuration values to import.")
|
||||
}
|
||||
|
||||
const globalConfiguration = Object.fromEntries(entries)
|
||||
await this.setValues(globalConfiguration)
|
||||
return globalConfiguration
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error importing global configuration from ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async exportApiConfiguration(filePath: string) {
|
||||
try {
|
||||
const apiConfiguration = apiHandlerOptionsSchema
|
||||
.omit(NON_EXPORTABLE_API_CONFIGURATION.reduce((acc, key) => ({ ...acc, [key]: true }), {}))
|
||||
.parse({
|
||||
...this.getAllGlobalStateValues(),
|
||||
...this.getAllSecretValues(),
|
||||
})
|
||||
|
||||
const dirname = path.dirname(filePath)
|
||||
await fs.mkdir(dirname, { recursive: true })
|
||||
await fs.writeFile(filePath, JSON.stringify(apiConfiguration, null, 2), "utf-8")
|
||||
return apiConfiguration
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error exporting API configuration to ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
async importApiConfiguration(filePath: string) {
|
||||
try {
|
||||
const apiConfiguration = apiHandlerOptionsSchema
|
||||
.omit(NON_EXPORTABLE_API_CONFIGURATION.reduce((acc, key) => ({ ...acc, [key]: true }), {}))
|
||||
.parse(JSON.parse(await fs.readFile(filePath, "utf-8")))
|
||||
|
||||
await this.setApiConfiguration(apiConfiguration)
|
||||
return apiConfiguration
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error importing API configuration from ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all global state, secrets, and in-memory caches.
|
||||
* This clears all data from both the in-memory caches and the VSCode storage.
|
||||
|
|
@ -184,6 +311,6 @@ export class ContextProxy {
|
|||
// Wait for all reset operations to complete.
|
||||
await Promise.all([...stateResetPromises, ...secretResetPromises])
|
||||
|
||||
this.initialize()
|
||||
await this.initialize()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
SECRET_KEYS,
|
||||
GLOBAL_STATE_KEYS,
|
||||
ConfigurationValues,
|
||||
isGlobalStateKey,
|
||||
} from "../../shared/globalState"
|
||||
import { HistoryItem } from "../../shared/HistoryItem"
|
||||
import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
|
||||
|
|
@ -99,6 +100,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
private workspaceTracker?: WorkspaceTracker
|
||||
protected mcpHub?: McpHub // Change from private to protected
|
||||
private latestAnnouncementId = "mar-20-2025-3-10" // update to some unique identifier when we add a new announcement
|
||||
private settingsImportedAt?: number
|
||||
private contextProxy: ContextProxy
|
||||
configManager: ConfigManager
|
||||
customModesManager: CustomModesManager
|
||||
|
|
@ -1070,6 +1072,41 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
}
|
||||
case "exportTaskWithId":
|
||||
this.exportTaskWithId(message.text!)
|
||||
break
|
||||
case "importSettings":
|
||||
const uris = await vscode.window.showOpenDialog({
|
||||
filters: { JSON: ["json"] },
|
||||
canSelectMany: false,
|
||||
})
|
||||
|
||||
if (uris) {
|
||||
if (message.text === "global") {
|
||||
await this.contextProxy.importGlobalConfiguration(uris[0].fsPath)
|
||||
} else {
|
||||
await this.contextProxy.importApiConfiguration(uris[0].fsPath)
|
||||
}
|
||||
|
||||
this.settingsImportedAt = Date.now()
|
||||
await this.postStateToWebview()
|
||||
await vscode.window.showInformationMessage(t("common:info.settings_imported"))
|
||||
}
|
||||
break
|
||||
case "exportSettings":
|
||||
const uri = await vscode.window.showSaveDialog({
|
||||
filters: { JSON: ["json"] },
|
||||
defaultUri: vscode.Uri.file(
|
||||
path.join(os.homedir(), "Documents", `roo-code-${message.text}.json`),
|
||||
),
|
||||
})
|
||||
|
||||
if (uri) {
|
||||
if (message.text === "global") {
|
||||
await this.contextProxy.exportGlobalConfiguration(uri.fsPath)
|
||||
} else {
|
||||
await this.contextProxy.exportApiConfiguration(uri.fsPath)
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
case "resetState":
|
||||
await this.resetState()
|
||||
|
|
@ -2582,6 +2619,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
language,
|
||||
renderContext: this.renderContext,
|
||||
maxReadFileLine: maxReadFileLine ?? 500,
|
||||
settingsImportedAt: this.settingsImportedAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2671,7 +2709,9 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
|
|||
// Using the dynamic approach with API_CONFIG_KEYS
|
||||
const apiConfiguration: ApiConfiguration = {
|
||||
// Dynamically add all API-related keys from stateValues
|
||||
...Object.fromEntries(API_CONFIG_KEYS.map((key) => [key, stateValues[key]])),
|
||||
...Object.fromEntries(
|
||||
API_CONFIG_KEYS.filter((key) => isGlobalStateKey(key)).map((key) => [key, stateValues[key]]),
|
||||
),
|
||||
// Add all secrets
|
||||
...secretValues,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,8 @@
|
|||
"mcp_server_restarting": "Restarting {{serverName}} MCP server...",
|
||||
"mcp_server_connected": "{{serverName}} MCP server connected",
|
||||
"mcp_server_deleted": "Deleted MCP server: {{serverName}}",
|
||||
"mcp_server_not_found": "Server \"{{serverName}}\" not found in configuration"
|
||||
"mcp_server_not_found": "Server \"{{serverName}}\" not found in configuration",
|
||||
"settings_imported": "Settings imported successfully."
|
||||
},
|
||||
"answers": {
|
||||
"yes": "Yes",
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ export interface ExtensionState {
|
|||
showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings
|
||||
renderContext: "sidebar" | "editor"
|
||||
maxReadFileLine: number // Maximum number of lines to read from a file before truncating
|
||||
settingsImportedAt?: number
|
||||
}
|
||||
|
||||
export type { ClineMessage, ClineAsk, ClineSay }
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ export interface WebviewMessage {
|
|||
| "showTaskWithId"
|
||||
| "deleteTaskWithId"
|
||||
| "exportTaskWithId"
|
||||
| "importSettings"
|
||||
| "exportSettings"
|
||||
| "resetState"
|
||||
| "requestOllamaModels"
|
||||
| "requestLmStudioModels"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,45 @@
|
|||
import * as vscode from "vscode"
|
||||
import { z } from "zod"
|
||||
|
||||
import { AssertEqual, Equals } from "../utils/type-fu"
|
||||
|
||||
// Models
|
||||
|
||||
export const modelInfoSchema = z.object({
|
||||
maxTokens: z.number().optional(),
|
||||
contextWindow: z.number(),
|
||||
supportsImages: z.boolean().optional(),
|
||||
supportsComputerUse: z.boolean().optional(),
|
||||
supportsPromptCache: z.boolean(),
|
||||
inputPrice: z.number().optional(),
|
||||
outputPrice: z.number().optional(),
|
||||
cacheWritesPrice: z.number().optional(),
|
||||
cacheReadsPrice: z.number().optional(),
|
||||
description: z.string().optional(),
|
||||
reasoningEffort: z.enum(["low", "medium", "high"]).optional(),
|
||||
thinking: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export interface ModelInfo {
|
||||
maxTokens?: number
|
||||
contextWindow: number
|
||||
supportsImages?: boolean
|
||||
supportsComputerUse?: boolean
|
||||
supportsPromptCache: boolean // This value is hardcoded for now.
|
||||
inputPrice?: number
|
||||
outputPrice?: number
|
||||
cacheWritesPrice?: number
|
||||
cacheReadsPrice?: number
|
||||
description?: string
|
||||
reasoningEffort?: "low" | "medium" | "high"
|
||||
thinking?: boolean
|
||||
}
|
||||
|
||||
// Throws a type error if the inferred type of the schema is not equal to the
|
||||
// type of the ModelInfo.
|
||||
type _AssertModelInfoMatchesSchema = AssertEqual<Equals<ModelInfo, z.infer<typeof modelInfoSchema>>>
|
||||
|
||||
// Providers
|
||||
|
||||
export type ApiProvider =
|
||||
| "anthropic"
|
||||
|
|
@ -34,7 +75,7 @@ export interface ApiHandlerOptions {
|
|||
openRouterModelInfo?: ModelInfo
|
||||
openRouterBaseUrl?: string
|
||||
openRouterSpecificProvider?: string
|
||||
// AWS Bedrok
|
||||
// AWS Bedrock
|
||||
awsAccessKey?: string
|
||||
awsSecretKey?: string
|
||||
awsSessionToken?: string
|
||||
|
|
@ -74,7 +115,7 @@ export interface ApiHandlerOptions {
|
|||
openAiNativeApiKey?: string
|
||||
// Mistral
|
||||
mistralApiKey?: string
|
||||
mistralCodestralUrl?: string // New option for Codestral URL
|
||||
mistralCodestralUrl?: string // New option for Codestral URL.
|
||||
// Azure
|
||||
azureApiVersion?: string
|
||||
// OpenRouter
|
||||
|
|
@ -100,18 +141,109 @@ export interface ApiHandlerOptions {
|
|||
fakeAi?: unknown
|
||||
}
|
||||
|
||||
export type ApiHandlerOptionsKey = keyof ApiHandlerOptions
|
||||
|
||||
export const apiHandlerOptionsSchema = z.object({
|
||||
// Anthropic
|
||||
apiModelId: z.string().optional(),
|
||||
apiKey: z.string().optional(),
|
||||
anthropicBaseUrl: z.string().optional(),
|
||||
// Glama
|
||||
glamaModelId: z.string().optional(),
|
||||
glamaModelInfo: modelInfoSchema.optional(),
|
||||
glamaApiKey: z.string().optional(),
|
||||
// OpenRouter
|
||||
openRouterApiKey: z.string().optional(),
|
||||
openRouterModelId: z.string().optional(),
|
||||
openRouterModelInfo: modelInfoSchema.optional(),
|
||||
openRouterBaseUrl: z.string().optional(),
|
||||
openRouterSpecificProvider: z.string().optional(),
|
||||
// AWS Bedrock
|
||||
awsAccessKey: z.string().optional(),
|
||||
awsSecretKey: z.string().optional(),
|
||||
awsSessionToken: z.string().optional(),
|
||||
awsRegion: z.string().optional(),
|
||||
awsUseCrossRegionInference: z.boolean().optional(),
|
||||
awsUsePromptCache: z.boolean().optional(),
|
||||
awspromptCacheId: z.string().optional(),
|
||||
awsProfile: z.string().optional(),
|
||||
awsUseProfile: z.boolean().optional(),
|
||||
awsCustomArn: z.string().optional(),
|
||||
// Google Vertex
|
||||
vertexKeyFile: z.string().optional(),
|
||||
vertexJsonCredentials: z.string().optional(),
|
||||
vertexProjectId: z.string().optional(),
|
||||
vertexRegion: z.string().optional(),
|
||||
// OpenAI
|
||||
openAiBaseUrl: z.string().optional(),
|
||||
openAiApiKey: z.string().optional(),
|
||||
openAiR1FormatEnabled: z.boolean().optional(),
|
||||
openAiModelId: z.string().optional(),
|
||||
openAiCustomModelInfo: modelInfoSchema.optional(),
|
||||
openAiUseAzure: z.boolean().optional(),
|
||||
// Ollama
|
||||
ollamaModelId: z.string().optional(),
|
||||
ollamaBaseUrl: z.string().optional(),
|
||||
// VS Code LM
|
||||
vsCodeLmModelSelector: z
|
||||
.object({
|
||||
vendor: z.string().optional(),
|
||||
family: z.string().optional(),
|
||||
version: z.string().optional(),
|
||||
id: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
// LM Studio
|
||||
lmStudioModelId: z.string().optional(),
|
||||
lmStudioBaseUrl: z.string().optional(),
|
||||
lmStudioDraftModelId: z.string().optional(),
|
||||
lmStudioSpeculativeDecodingEnabled: z.boolean().optional(),
|
||||
// Gemini
|
||||
geminiApiKey: z.string().optional(),
|
||||
googleGeminiBaseUrl: z.string().optional(),
|
||||
// OpenAI Native
|
||||
openAiNativeApiKey: z.string().optional(),
|
||||
// Mistral
|
||||
mistralApiKey: z.string().optional(),
|
||||
mistralCodestralUrl: z.string().optional(),
|
||||
// Azure
|
||||
azureApiVersion: z.string().optional(),
|
||||
// OpenRouter
|
||||
openRouterUseMiddleOutTransform: z.boolean().optional(),
|
||||
openAiStreamingEnabled: z.boolean().optional(),
|
||||
// DeepSeek
|
||||
deepSeekBaseUrl: z.string().optional(),
|
||||
deepSeekApiKey: z.string().optional(),
|
||||
includeMaxTokens: z.boolean().optional(),
|
||||
// Unbound
|
||||
unboundApiKey: z.string().optional(),
|
||||
unboundModelId: z.string().optional(),
|
||||
unboundModelInfo: modelInfoSchema.optional(),
|
||||
// Requesty
|
||||
requestyApiKey: z.string().optional(),
|
||||
requestyModelId: z.string().optional(),
|
||||
requestyModelInfo: modelInfoSchema.optional(),
|
||||
// Claude 3.7 Sonnet Thinking
|
||||
modelTemperature: z.number().nullish(),
|
||||
modelMaxTokens: z.number().optional(),
|
||||
modelMaxThinkingTokens: z.number().optional(),
|
||||
// Fake AI
|
||||
fakeAi: z.unknown().optional(),
|
||||
})
|
||||
|
||||
// Throws a type error if the inferred type of the schema is not equal to the
|
||||
// type of the ApiHandlerOptions.
|
||||
type _AssertApiHandlerOptionsMatchesSchema = AssertEqual<
|
||||
Equals<ApiHandlerOptions, z.infer<typeof apiHandlerOptionsSchema>>
|
||||
>
|
||||
|
||||
export type ApiConfiguration = ApiHandlerOptions & {
|
||||
apiProvider?: ApiProvider
|
||||
id?: string // stable unique identifier
|
||||
id?: string // Stable unique identifier.
|
||||
}
|
||||
|
||||
// Import GlobalStateKey type from globalState.ts
|
||||
import { GlobalStateKey } from "./globalState"
|
||||
|
||||
// Define API configuration keys for dynamic object building.
|
||||
// TODO: This needs actual type safety; a type error should be thrown if
|
||||
// this is not an exhaustive list of all `GlobalStateKey` values.
|
||||
export const API_CONFIG_KEYS: GlobalStateKey[] = [
|
||||
export const API_CONFIG_KEYS: ApiHandlerOptionsKey[] = [
|
||||
"apiModelId",
|
||||
"anthropicBaseUrl",
|
||||
"vsCodeLmModelSelector",
|
||||
|
|
@ -160,23 +292,6 @@ export const API_CONFIG_KEYS: GlobalStateKey[] = [
|
|||
"fakeAi",
|
||||
]
|
||||
|
||||
// Models
|
||||
|
||||
export interface ModelInfo {
|
||||
maxTokens?: number
|
||||
contextWindow: number
|
||||
supportsImages?: boolean
|
||||
supportsComputerUse?: boolean
|
||||
supportsPromptCache: boolean // this value is hardcoded for now
|
||||
inputPrice?: number
|
||||
outputPrice?: number
|
||||
cacheWritesPrice?: number
|
||||
cacheReadsPrice?: number
|
||||
description?: string
|
||||
reasoningEffort?: "low" | "medium" | "high"
|
||||
thinking?: boolean
|
||||
}
|
||||
|
||||
// Anthropic
|
||||
// https://docs.anthropic.com/en/docs/about-claude/models
|
||||
export type AnthropicModelId = keyof typeof anthropicModels
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { z } from "zod"
|
||||
|
||||
import type { SecretKey, GlobalStateKey, ConfigurationKey, ConfigurationValues } from "../exports/roo-code"
|
||||
|
||||
export type { SecretKey, GlobalStateKey, ConfigurationKey, ConfigurationValues }
|
||||
|
|
@ -12,6 +14,10 @@ export type { SecretKey, GlobalStateKey, ConfigurationKey, ConfigurationValues }
|
|||
* keys or a type error will be thrown.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Secret keys.
|
||||
*/
|
||||
|
||||
export const SECRET_KEYS = [
|
||||
"apiKey",
|
||||
"glamaApiKey",
|
||||
|
|
@ -28,10 +34,19 @@ export const SECRET_KEYS = [
|
|||
"requestyApiKey",
|
||||
] as const
|
||||
|
||||
// Throws a type error we're missing a key.
|
||||
type CheckSecretKeysExhaustiveness = Exclude<SecretKey, (typeof SECRET_KEYS)[number]> extends never ? true : false
|
||||
|
||||
const _checkSecretKeysExhaustiveness: CheckSecretKeysExhaustiveness = true
|
||||
|
||||
export const isSecretKey = (key: string): key is SecretKey => SECRET_KEYS.includes(key as SecretKey)
|
||||
|
||||
// TODO: Replace `z.unknown()` with the actual types.
|
||||
export const secretStateSchema = z.record(z.enum(SECRET_KEYS), z.unknown())
|
||||
|
||||
/**
|
||||
* Global state keys.
|
||||
*/
|
||||
|
||||
export const GLOBAL_STATE_KEYS = [
|
||||
"apiProvider",
|
||||
"apiModelId",
|
||||
|
|
@ -127,17 +142,27 @@ export const GLOBAL_STATE_KEYS = [
|
|||
"fakeAi",
|
||||
] as const
|
||||
|
||||
export const PASS_THROUGH_STATE_KEYS = ["taskHistory"] as const
|
||||
|
||||
// Throws a type error we're missing a key.
|
||||
type CheckGlobalStateKeysExhaustiveness =
|
||||
Exclude<GlobalStateKey, (typeof GLOBAL_STATE_KEYS)[number]> extends never ? true : false
|
||||
|
||||
const _checkGlobalStateKeysExhaustiveness: CheckGlobalStateKeysExhaustiveness = true
|
||||
|
||||
export const isSecretKey = (key: string): key is SecretKey => SECRET_KEYS.includes(key as SecretKey)
|
||||
|
||||
export const isGlobalStateKey = (key: string): key is GlobalStateKey =>
|
||||
GLOBAL_STATE_KEYS.includes(key as GlobalStateKey)
|
||||
|
||||
// TODO: Replace `z.unknown()` with the actual types.
|
||||
export const globalStateSchema = z.record(z.enum(GLOBAL_STATE_KEYS), z.unknown())
|
||||
|
||||
/**
|
||||
* Pass-through state keys.
|
||||
* TODO: What are these?
|
||||
*/
|
||||
|
||||
export const PASS_THROUGH_STATE_KEYS = ["taskHistory"] as const
|
||||
|
||||
export const isPassThroughStateKey = (key: string): key is (typeof PASS_THROUGH_STATE_KEYS)[number] =>
|
||||
PASS_THROUGH_STATE_KEYS.includes(key as (typeof PASS_THROUGH_STATE_KEYS)[number])
|
||||
|
||||
// TODO: Replace `z.unknown()` with the actual types.
|
||||
export const passThroughStateSchema = z.record(z.enum(PASS_THROUGH_STATE_KEYS), z.unknown())
|
||||
|
|
|
|||
3
src/utils/type-fu.ts
Normal file
3
src/utils/type-fu.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export type Equals<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false
|
||||
|
||||
export type AssertEqual<T extends true> = T
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
import { HTMLAttributes } from "react"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { Trans } from "react-i18next"
|
||||
import { Info } from "lucide-react"
|
||||
import { Info, Download, Upload, TriangleAlert } from "lucide-react"
|
||||
|
||||
import { VSCodeButton, VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { TelemetrySetting } from "../../../../src/shared/TelemetrySetting"
|
||||
|
||||
import { vscode } from "@/utils/vscode"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui"
|
||||
|
||||
import { SectionHeader } from "./SectionHeader"
|
||||
import { Section } from "./Section"
|
||||
|
|
@ -34,7 +35,6 @@ export const About = ({ version, telemetrySetting, setTelemetrySetting, classNam
|
|||
<Section>
|
||||
<div>
|
||||
<VSCodeCheckbox
|
||||
style={{ marginBottom: "5px" }}
|
||||
checked={telemetrySetting === "enabled"}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
|
|
@ -42,12 +42,7 @@ export const About = ({ version, telemetrySetting, setTelemetrySetting, classNam
|
|||
}}>
|
||||
{t("settings:footer.telemetry.label")}
|
||||
</VSCodeCheckbox>
|
||||
<p
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
marginTop: "5px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<p className="text-vscode-descriptionForeground text-sm mt-0">
|
||||
{t("settings:footer.telemetry.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -63,15 +58,47 @@ export const About = ({ version, telemetrySetting, setTelemetrySetting, classNam
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center gap-3">
|
||||
<p>{t("settings:footer.reset.description")}</p>
|
||||
<VSCodeButton
|
||||
onClick={() => vscode.postMessage({ type: "resetState" })}
|
||||
appearance="secondary"
|
||||
className="shrink-0">
|
||||
<span className="codicon codicon-warning text-vscode-errorForeground mr-1" />
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>
|
||||
<Download className="p-0.5" />
|
||||
Import
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
onClick={() => vscode.postMessage({ type: "importSettings", text: "provider" })}>
|
||||
Current Provider Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => vscode.postMessage({ type: "importSettings", text: "global" })}>
|
||||
Global Settings
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button>
|
||||
<Upload className="p-0.5" />
|
||||
Export
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
onClick={() => vscode.postMessage({ type: "exportSettings", text: "provider" })}>
|
||||
Current Provider Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => vscode.postMessage({ type: "exportSettings", text: "global" })}>
|
||||
Global Settings
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button variant="destructive" onClick={() => vscode.postMessage({ type: "resetState" })}>
|
||||
<TriangleAlert className="p-0.5" />
|
||||
{t("settings:footer.reset.button")}
|
||||
</VSCodeButton>
|
||||
</Button>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
const { t } = useAppTranslation()
|
||||
|
||||
const extensionState = useExtensionState()
|
||||
const { currentApiConfigName, listApiConfigMeta, uriScheme, version } = extensionState
|
||||
const { currentApiConfigName, listApiConfigMeta, uriScheme, version, settingsImportedAt } = extensionState
|
||||
|
||||
const [isDiscardDialogShow, setDiscardDialogShow] = useState(false)
|
||||
const [isChangeDetected, setChangeDetected] = useState(false)
|
||||
|
|
@ -136,6 +136,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
|
||||
// Make sure apiConfiguration is initialized and managed by SettingsView.
|
||||
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
|
||||
|
||||
useEffect(() => {
|
||||
// Update only when currentApiConfigName is changed.
|
||||
// Expected to be triggered by loadApiConfiguration/upsertApiConfiguration.
|
||||
|
|
@ -148,6 +149,13 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
setChangeDetected(false)
|
||||
}, [currentApiConfigName, extensionState, isChangeDetected])
|
||||
|
||||
useEffect(() => {
|
||||
if (settingsImportedAt) {
|
||||
setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
|
||||
setChangeDetected(false)
|
||||
}
|
||||
}, [settingsImportedAt, extensionState])
|
||||
|
||||
const setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType> = useCallback((field, value) => {
|
||||
setCachedState((prevState) => {
|
||||
if (prevState[field] === value) {
|
||||
|
|
@ -180,11 +188,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
}
|
||||
|
||||
setChangeDetected(true)
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
experiments: { ...prevState.experiments, [id]: enabled },
|
||||
}
|
||||
return { ...prevState, experiments: { ...prevState.experiments, [id]: enabled } }
|
||||
})
|
||||
}, [])
|
||||
|
||||
|
|
@ -193,11 +197,9 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
|
|||
if (prevState.telemetrySetting === setting) {
|
||||
return prevState
|
||||
}
|
||||
|
||||
setChangeDetected(true)
|
||||
return {
|
||||
...prevState,
|
||||
telemetrySetting: setting,
|
||||
}
|
||||
return { ...prevState, telemetrySetting: setting }
|
||||
})
|
||||
}, [])
|
||||
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
(value: ApiConfigMeta[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
|
||||
[],
|
||||
)
|
||||
|
||||
const handleMessage = useCallback(
|
||||
(event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
|
|
@ -250,13 +251,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
setExperimentEnabled: (id, enabled) =>
|
||||
setState((prevState) => ({ ...prevState, experiments: { ...prevState.experiments, [id]: enabled } })),
|
||||
setApiConfiguration: (value) =>
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
apiConfiguration: {
|
||||
...prevState.apiConfiguration,
|
||||
...value,
|
||||
},
|
||||
})),
|
||||
setState((prevState) => ({ ...prevState, apiConfiguration: { ...prevState.apiConfiguration, ...value } })),
|
||||
setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
|
||||
setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
|
||||
setAlwaysAllowWrite: (value) => setState((prevState) => ({ ...prevState, alwaysAllowWrite: value })),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue