Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
152 changes: 152 additions & 0 deletions packages/types/src/__tests__/defaults.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { describe, expect, it } from "vitest"
import { settingDefaults, getSettingWithDefault } from "../defaults.js"
import { DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, DEFAULT_WRITE_DELAY_MS } from "../global-settings.js"

describe("settingDefaults", () => {
it("should have all expected default values", () => {
// Auto-approval settings (all default to false for safety)
expect(settingDefaults.autoApprovalEnabled).toBe(false)
expect(settingDefaults.alwaysAllowReadOnly).toBe(false)
expect(settingDefaults.alwaysAllowReadOnlyOutsideWorkspace).toBe(false)
expect(settingDefaults.alwaysAllowWrite).toBe(false)
expect(settingDefaults.alwaysAllowWriteOutsideWorkspace).toBe(false)
expect(settingDefaults.alwaysAllowWriteProtected).toBe(false)
expect(settingDefaults.alwaysAllowBrowser).toBe(false)
expect(settingDefaults.alwaysAllowMcp).toBe(false)
expect(settingDefaults.alwaysAllowModeSwitch).toBe(false)
expect(settingDefaults.alwaysAllowSubtasks).toBe(false)
expect(settingDefaults.alwaysAllowExecute).toBe(false)
expect(settingDefaults.alwaysAllowFollowupQuestions).toBe(false)
expect(settingDefaults.requestDelaySeconds).toBe(0)
expect(settingDefaults.followupAutoApproveTimeoutMs).toBe(0)
expect(settingDefaults.commandExecutionTimeout).toBe(0)
expect(settingDefaults.preventCompletionWithOpenTodos).toBe(false)
expect(settingDefaults.autoCondenseContext).toBe(false)
expect(settingDefaults.autoCondenseContextPercent).toBe(50)

// Browser settings
expect(settingDefaults.browserToolEnabled).toBe(true)
expect(settingDefaults.browserViewportSize).toBe("900x600")
expect(settingDefaults.remoteBrowserEnabled).toBe(false)
expect(settingDefaults.screenshotQuality).toBe(75)

// Audio/TTS settings
expect(settingDefaults.soundEnabled).toBe(true)
expect(settingDefaults.soundVolume).toBe(0.5)
expect(settingDefaults.ttsEnabled).toBe(true)
expect(settingDefaults.ttsSpeed).toBe(1.0)

// Checkpoint settings
expect(settingDefaults.enableCheckpoints).toBe(false)
expect(settingDefaults.checkpointTimeout).toBe(DEFAULT_CHECKPOINT_TIMEOUT_SECONDS)

// Terminal settings
expect(settingDefaults.terminalOutputLineLimit).toBe(500)
expect(settingDefaults.terminalOutputCharacterLimit).toBe(50_000)
expect(settingDefaults.terminalShellIntegrationTimeout).toBe(30_000)
expect(settingDefaults.terminalShellIntegrationDisabled).toBe(false)
expect(settingDefaults.terminalCommandDelay).toBe(0)
expect(settingDefaults.terminalPowershellCounter).toBe(false)
expect(settingDefaults.terminalZshClearEolMark).toBe(false)
expect(settingDefaults.terminalZshOhMy).toBe(false)
expect(settingDefaults.terminalZshP10k).toBe(false)
expect(settingDefaults.terminalZdotdir).toBe(false)
expect(settingDefaults.terminalCompressProgressBar).toBe(false)

// Context management settings
expect(settingDefaults.maxOpenTabsContext).toBe(20)
expect(settingDefaults.maxWorkspaceFiles).toBe(200)
expect(settingDefaults.showRooIgnoredFiles).toBe(false)
expect(settingDefaults.enableSubfolderRules).toBe(false)
expect(settingDefaults.maxReadFileLine).toBe(-1)
expect(settingDefaults.maxImageFileSize).toBe(5)
expect(settingDefaults.maxTotalImageSize).toBe(20)
expect(settingDefaults.maxConcurrentFileReads).toBe(5)

// Diagnostic settings
expect(settingDefaults.diagnosticsEnabled).toBe(false)
expect(settingDefaults.includeDiagnosticMessages).toBe(true)
expect(settingDefaults.maxDiagnosticMessages).toBe(50)
expect(settingDefaults.writeDelayMs).toBe(DEFAULT_WRITE_DELAY_MS)

// Prompt enhancement settings
expect(settingDefaults.includeTaskHistoryInEnhance).toBe(true)

// UI settings
expect(settingDefaults.reasoningBlockCollapsed).toBe(true)
expect(settingDefaults.historyPreviewCollapsed).toBe(false)
expect(settingDefaults.enterBehavior).toBe("send")
expect(settingDefaults.hasOpenedModeSelector).toBe(false)

// Environment details settings
expect(settingDefaults.includeCurrentTime).toBe(true)
expect(settingDefaults.includeCurrentCost).toBe(true)
expect(settingDefaults.maxGitStatusFiles).toBe(0)

// Language settings
expect(settingDefaults.language).toBe("en")

// MCP settings
expect(settingDefaults.mcpEnabled).toBe(true)
expect(settingDefaults.enableMcpServerCreation).toBe(false)

// Rate limiting
expect(settingDefaults.rateLimitSeconds).toBe(0)

// Indexing settings
expect(settingDefaults.codebaseIndexEnabled).toBe(false)
expect(settingDefaults.codebaseIndexQdrantUrl).toBe("http://localhost:6333")
expect(settingDefaults.codebaseIndexEmbedderProvider).toBe("openai")
expect(settingDefaults.codebaseIndexEmbedderBaseUrl).toBe("")
expect(settingDefaults.codebaseIndexEmbedderModelId).toBe("")
expect(settingDefaults.codebaseIndexEmbedderModelDimension).toBe(1536)
expect(settingDefaults.codebaseIndexOpenAiCompatibleBaseUrl).toBe("")
expect(settingDefaults.codebaseIndexBedrockRegion).toBe("us-east-1")
expect(settingDefaults.codebaseIndexBedrockProfile).toBe("")
expect(settingDefaults.codebaseIndexSearchMaxResults).toBe(100)
expect(settingDefaults.codebaseIndexSearchMinScore).toBe(0.4)
expect(settingDefaults.codebaseIndexOpenRouterSpecificProvider).toBe("")
})

it("should be immutable (readonly)", () => {
// TypeScript should prevent this at compile time, but we can verify the type
const defaultsCopy = { ...settingDefaults }
expect(defaultsCopy.browserToolEnabled).toBe(settingDefaults.browserToolEnabled)
})
})

describe("getSettingWithDefault", () => {
it("should return the value when defined (matching type)", () => {
// Test with values that match the default type
expect(getSettingWithDefault("browserToolEnabled", true)).toBe(true)
expect(getSettingWithDefault("soundVolume", 0.5)).toBe(0.5)
expect(getSettingWithDefault("maxOpenTabsContext", 20)).toBe(20)
expect(getSettingWithDefault("enterBehavior", "send")).toBe("send")
})

it("should return the default when value is undefined", () => {
expect(getSettingWithDefault("browserToolEnabled", undefined)).toBe(true)
expect(getSettingWithDefault("soundVolume", undefined)).toBe(0.5)
expect(getSettingWithDefault("maxOpenTabsContext", undefined)).toBe(20)
expect(getSettingWithDefault("enterBehavior", undefined)).toBe("send")
expect(getSettingWithDefault("mcpEnabled", undefined)).toBe(true)
expect(getSettingWithDefault("showRooIgnoredFiles", undefined)).toBe(false)
})

it("should demonstrate reset-to-default pattern", () => {
// This test demonstrates the ideal "reset to default" pattern:
// When a user resets a setting, we store `undefined` (not the default value)
// When reading, we apply the default at consumption time

// Simulating reading from storage where value is undefined (reset state)
const storedValue = undefined
const effectiveValue = getSettingWithDefault("browserToolEnabled", storedValue)

// User sees the default value
expect(effectiveValue).toBe(true)

// If the default changes in the future (e.g., to false),
// users who reset their setting would automatically get the new default
// because they stored `undefined`, not `true`
})
})
176 changes: 176 additions & 0 deletions packages/types/src/defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* Centralized defaults registry for Roo Code settings.
*
* IMPORTANT: These defaults should be applied at READ time (when consuming state),
* NOT at WRITE time (when saving settings). This ensures:
* - Users who haven't customized a setting inherit future default improvements
* - Storage only contains intentional user customizations, not copies of defaults
* - "Reset to Default" properly removes settings from storage (sets to undefined)
*
* Pattern:
* - On save: pass `undefined` to remove a setting from storage (reset to default)
* - On read: apply defaults using `value ?? settingDefaults.settingName`
*/

import { DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, DEFAULT_WRITE_DELAY_MS } from "./global-settings.js"

/**
* Default values for all settings that can be reset to default.
*
* These values are the source of truth for defaults throughout the application.
* When a setting is undefined in storage, these defaults should be applied
* at consumption time.
*
* IMPORTANT: Every setting that has a default value MUST be listed here.
* The clearDefaultSettings() function uses this registry to remove default
* values from storage on every startup.
*/
export const settingDefaults = {
// ===== Auto-approval settings =====
// All auto-approval settings default to false for safety
autoApprovalEnabled: false,
alwaysAllowReadOnly: false,
alwaysAllowReadOnlyOutsideWorkspace: false,
alwaysAllowWrite: false,
alwaysAllowWriteOutsideWorkspace: false,
alwaysAllowWriteProtected: false,
alwaysAllowBrowser: false,
alwaysAllowMcp: false,
alwaysAllowModeSwitch: false,
alwaysAllowSubtasks: false,
alwaysAllowExecute: false,
alwaysAllowFollowupQuestions: false,
requestDelaySeconds: 0,
followupAutoApproveTimeoutMs: 0,
commandExecutionTimeout: 0,
preventCompletionWithOpenTodos: false,
autoCondenseContext: false,
autoCondenseContextPercent: 50,

// ===== Browser settings =====
browserToolEnabled: true,
browserViewportSize: "900x600",
remoteBrowserEnabled: false,
screenshotQuality: 75,

// ===== Audio/TTS settings =====
soundEnabled: true,
soundVolume: 0.5,
ttsEnabled: true,
ttsSpeed: 1.0,

// ===== Checkpoint settings =====
enableCheckpoints: false,
checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,

// ===== Terminal settings =====
terminalOutputLineLimit: 500,
terminalOutputCharacterLimit: 50_000,
terminalShellIntegrationTimeout: 30_000,
terminalShellIntegrationDisabled: false,
terminalCommandDelay: 0,
terminalPowershellCounter: false,
terminalZshClearEolMark: false,
terminalZshOhMy: false,
terminalZshP10k: false,
terminalZdotdir: false,
terminalCompressProgressBar: false,

// ===== Context management settings =====
maxOpenTabsContext: 20,
maxWorkspaceFiles: 200,
showRooIgnoredFiles: false,
enableSubfolderRules: false,
maxReadFileLine: -1,
maxImageFileSize: 5,
maxTotalImageSize: 20,
maxConcurrentFileReads: 5,

// ===== Diagnostic settings =====
diagnosticsEnabled: false,
includeDiagnosticMessages: true,
maxDiagnosticMessages: 50,
writeDelayMs: DEFAULT_WRITE_DELAY_MS,

// ===== Prompt enhancement settings =====
includeTaskHistoryInEnhance: true,

// ===== UI settings =====
reasoningBlockCollapsed: true,
historyPreviewCollapsed: false,
enterBehavior: "send" as const,
hasOpenedModeSelector: false,

// ===== Environment details settings =====
includeCurrentTime: true,
includeCurrentCost: true,
maxGitStatusFiles: 0,

// ===== Language settings =====
language: "en" as const,

// ===== MCP settings =====
mcpEnabled: true,
enableMcpServerCreation: false,

// ===== Rate limiting =====
rateLimitSeconds: 0,

// ===== Indexing settings =====
codebaseIndexEnabled: false,
codebaseIndexQdrantUrl: "http://localhost:6333",
codebaseIndexEmbedderProvider: "openai" as const,
codebaseIndexEmbedderBaseUrl: "",
codebaseIndexEmbedderModelId: "",
codebaseIndexEmbedderModelDimension: 1536,
codebaseIndexOpenAiCompatibleBaseUrl: "",
codebaseIndexBedrockRegion: "us-east-1",
codebaseIndexBedrockProfile: "",
codebaseIndexSearchMaxResults: 100,
codebaseIndexSearchMinScore: 0.4,
codebaseIndexOpenRouterSpecificProvider: "",
} as const

/**
* Type representing all setting keys that have defaults.
*/
export type SettingWithDefault = keyof typeof settingDefaults

/**
* Helper function to get a setting value with its default applied.
* Use this when reading settings from storage.
*
* @param key - The setting key
* @param value - The value from storage (may be undefined)
* @returns The value if defined, otherwise the default
*
* @example
* const browserToolEnabled = getSettingWithDefault('browserToolEnabled', storedValue)
*/
export function getSettingWithDefault<K extends SettingWithDefault>(
key: K,
value: (typeof settingDefaults)[K] | undefined,
): (typeof settingDefaults)[K] {
return value ?? settingDefaults[key]
}

/**
* Applies defaults to a partial settings object.
* Only applies defaults for settings that are undefined.
*
* @param settings - Partial settings object
* @returns Settings object with defaults applied for undefined values
*/
export function applySettingDefaults<T extends Partial<Record<SettingWithDefault, unknown>>>(
settings: T,
): T & typeof settingDefaults {
Comment on lines +164 to +166
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor API mismatch: the PR summary mentions an applySettingsDefaults() helper, but the exported helper is applySettingDefaults() (singular) and currently unused. If any callers import the plural name, it will fail at build time; consider renaming or adding a compat alias.

Suggested change
export function applySettingDefaults<T extends Partial<Record<SettingWithDefault, unknown>>>(
settings: T,
): T & typeof settingDefaults {
export function applySettingsDefaults<T extends Partial<Record<SettingWithDefault, unknown>>>(
settings: T,
): T & typeof settingDefaults {

Fix it with Roo Code or mention @roomote and request a fix.

const result = { ...settings } as T & typeof settingDefaults

for (const key of Object.keys(settingDefaults) as SettingWithDefault[]) {
if (result[key] === undefined) {
;(result as Record<SettingWithDefault, unknown>)[key] = settingDefaults[key]
}
}

return result
}
Comment on lines +164 to +176
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor naming mismatch: defaults.ts exports applySettingDefaults() (singular) but the PR summary and the existing top-level TODO refer to applySettingsDefaults() (plural). Consider renaming to plural or adding a compat alias to avoid a future import typo.

Suggested change
export function applySettingDefaults<T extends Partial<Record<SettingWithDefault, unknown>>>(
settings: T,
): T & typeof settingDefaults {
const result = { ...settings } as T & typeof settingDefaults
for (const key of Object.keys(settingDefaults) as SettingWithDefault[]) {
if (result[key] === undefined) {
;(result as Record<SettingWithDefault, unknown>)[key] = settingDefaults[key]
}
}
return result
}
export function applySettingsDefaults<T extends Partial<Record<SettingWithDefault, unknown>>>(
settings: T,
): T & typeof settingDefaults {
const result = { ...settings } as T & typeof settingDefaults
for (const key of Object.keys(settingDefaults) as SettingWithDefault[]) {
if (result[key] === undefined) {
;(result as Record<SettingWithDefault, unknown>)[key] = settingDefaults[key]
}
}
return result
}

Fix it with Roo Code or mention @roomote and request a fix.

32 changes: 32 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,31 @@ export const globalSettingsSchema = z.object({
codebaseIndexModels: codebaseIndexModelsSchema.optional(),
codebaseIndexConfig: codebaseIndexConfigSchema.optional(),

// Indexing settings (flattened from codebaseIndexConfig for reset-to-default pattern)
codebaseIndexEnabled: z.boolean().optional(),
codebaseIndexQdrantUrl: z.string().optional(),
codebaseIndexEmbedderProvider: z
.enum([
"openai",
"ollama",
"openai-compatible",
"gemini",
"mistral",
"vercel-ai-gateway",
"bedrock",
"openrouter",
])
.optional(),
codebaseIndexEmbedderBaseUrl: z.string().optional(),
codebaseIndexEmbedderModelId: z.string().optional(),
codebaseIndexEmbedderModelDimension: z.number().optional(),
codebaseIndexOpenAiCompatibleBaseUrl: z.string().optional(),
codebaseIndexBedrockRegion: z.string().optional(),
codebaseIndexBedrockProfile: z.string().optional(),
codebaseIndexSearchMaxResults: z.number().optional(),
codebaseIndexSearchMinScore: z.number().optional(),
codebaseIndexOpenRouterSpecificProvider: z.string().optional(),

language: languagesSchema.optional(),

telemetrySetting: telemetrySettingsSchema.optional(),
Expand Down Expand Up @@ -203,6 +228,13 @@ export const globalSettingsSchema = z.object({
* Used by the worktree feature to open the Roo Code sidebar in a new window.
*/
worktreeAutoOpenPath: z.string().optional(),

/**
* Version of settings migrations that have been applied.
* Used to track which migrations have run to avoid re-running them.
* @internal
*/
settingsMigrationVersion: z.number().optional(),
})

export type GlobalSettings = z.infer<typeof globalSettingsSchema>
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 @@ -7,6 +7,7 @@ export * from "./custom-tool.js"
export * from "./embedding.js"
export * from "./events.js"
export * from "./experiment.js"
export * from "./defaults.js"
export * from "./followup.js"
export * from "./git.js"
export * from "./global-settings.js"
Expand Down
Loading
Loading