{"version":3,"file":"settings-manager.d.ts","sourceRoot":"","sources":["../../src/core/settings-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { homedir } from \"os\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../config.js\";\nimport { type ChromeDensity, isChromeDensity } from \"./chrome-density.js\";\nimport { DEFAULT_SETTINGS } from \"./settings-defaults.js\";\nimport {\n\tFileSettingsStorage,\n\tInMemorySettingsStorage,\n\ttype SettingsError,\n\ttype SettingsScope,\n\ttype SettingsStorage,\n} from \"./settings-storage.js\";\nimport type {\n\tBranchSummarySettings,\n\tCompactionSettings,\n\tPackageSource,\n\tSettings,\n\tThinkingBudgetsSettings,\n\tTransportSetting,\n\tWarningSettings,\n} from \"./settings-types.js\";\nimport { isToolOutputView, LEGACY_TOOL_OUTPUT_VIEWS, type ToolOutputView } from \"./tool-output-view.js\";\nimport type { WebtoolsSearchSettings } from \"./tools/webtools-shared.js\";\n\n/** The `/learn` thresholds, which are flat top-level numeric settings. */\nexport type LearnSettingKey =\n\t| \"learnMaxSessions\"\n\t| \"learnMaxAgeDays\"\n\t| \"learnMinRepeats\"\n\t| \"learnMinRequestRepeats\"\n\t| \"learnMaxProposals\";\n\n/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */\nfunction deepMergeSettings(base: Settings, overrides: Settings): Settings {\n\tconst result: Settings = { ...base };\n\n\tfor (const key of Object.keys(overrides) as (keyof Settings)[]) {\n\t\tconst overrideValue = overrides[key];\n\t\tconst baseValue = base[key];\n\n\t\tif (overrideValue === undefined) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// For nested objects, merge recursively\n\t\tif (\n\t\t\ttypeof overrideValue === \"object\" &&\n\t\t\toverrideValue !== null &&\n\t\t\t!Array.isArray(overrideValue) &&\n\t\t\ttypeof baseValue === \"object\" &&\n\t\t\tbaseValue !== null &&\n\t\t\t!Array.isArray(baseValue)\n\t\t) {\n\t\t\t(result as Record<string, unknown>)[key] = { ...baseValue, ...overrideValue };\n\t\t} else {\n\t\t\t// For primitives and arrays, override value wins\n\t\t\t(result as Record<string, unknown>)[key] = overrideValue;\n\t\t}\n\t}\n\n\treturn result;\n}\n\nexport type { SettingsError, SettingsScope, SettingsStorage };\nexport type {\n\tBranchSummarySettings,\n\tCompactionSettings,\n\tImageSettings,\n\tPackageSource,\n\tRetrySettings,\n\tSettings,\n\tThinkingBudgetsSettings,\n\tTransportSetting,\n\tWarningSettings,\n} from \"./settings-types.js\";\n\nexport class SettingsManager {\n\tprivate storage: SettingsStorage;\n\tprivate globalSettings: Settings;\n\tprivate projectSettings: Settings;\n\tprivate settings: Settings;\n\tprivate modifiedFields = new Set<keyof Settings>(); // Track global fields modified during session\n\tprivate modifiedNestedFields = new Map<keyof Settings, Set<string>>(); // Track global nested field modifications\n\tprivate modifiedProjectFields = new Set<keyof Settings>(); // Track project fields modified during session\n\tprivate modifiedProjectNestedFields = new Map<keyof Settings, Set<string>>(); // Track project nested field modifications\n\tprivate globalSettingsLoadError: Error | null = null; // Track if global settings file had parse errors\n\tprivate projectSettingsLoadError: Error | null = null; // Track if project settings file had parse errors\n\tprivate writeQueue: Promise<void> = Promise.resolve();\n\tprivate errors: SettingsError[];\n\n\tprivate constructor(\n\t\tstorage: SettingsStorage,\n\t\tinitialGlobal: Settings,\n\t\tinitialProject: Settings,\n\t\tglobalLoadError: Error | null = null,\n\t\tprojectLoadError: Error | null = null,\n\t\tinitialErrors: SettingsError[] = [],\n\t) {\n\t\tthis.storage = storage;\n\t\tthis.globalSettings = initialGlobal;\n\t\tthis.projectSettings = initialProject;\n\t\tthis.globalSettingsLoadError = globalLoadError;\n\t\tthis.projectSettingsLoadError = projectLoadError;\n\t\tthis.errors = [...initialErrors];\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\t}\n\n\t/** Create a SettingsManager that loads from files */\n\tstatic create(cwd: string, agentDir: string = getAgentDir()): SettingsManager {\n\t\tconst storage = new FileSettingsStorage(cwd, agentDir);\n\t\treturn SettingsManager.fromStorage(storage);\n\t}\n\n\t/** Create a SettingsManager from an arbitrary storage backend */\n\tstatic fromStorage(storage: SettingsStorage): SettingsManager {\n\t\tconst globalLoad = SettingsManager.tryLoadFromStorage(storage, \"global\");\n\t\tconst projectLoad = SettingsManager.tryLoadFromStorage(storage, \"project\");\n\t\tconst initialErrors: SettingsError[] = [];\n\t\tif (globalLoad.error) {\n\t\t\tinitialErrors.push({ scope: \"global\", error: globalLoad.error });\n\t\t}\n\t\tif (projectLoad.error) {\n\t\t\tinitialErrors.push({ scope: \"project\", error: projectLoad.error });\n\t\t}\n\n\t\treturn new SettingsManager(\n\t\t\tstorage,\n\t\t\tglobalLoad.settings,\n\t\t\tprojectLoad.settings,\n\t\t\tglobalLoad.error,\n\t\t\tprojectLoad.error,\n\t\t\tinitialErrors,\n\t\t);\n\t}\n\n\t/** Create an in-memory SettingsManager (no file I/O) */\n\tstatic inMemory(settings: Partial<Settings> = {}): SettingsManager {\n\t\tconst storage = new InMemorySettingsStorage();\n\t\tconst initialSettings = SettingsManager.migrateSettings(structuredClone(settings) as Record<string, unknown>);\n\t\tstorage.withLock(\"global\", () => JSON.stringify(initialSettings, null, 2));\n\t\treturn SettingsManager.fromStorage(storage);\n\t}\n\n\tprivate static loadFromStorage(storage: SettingsStorage, scope: SettingsScope): Settings {\n\t\tlet content: string | undefined;\n\t\tstorage.withLock(scope, (current) => {\n\t\t\tcontent = current;\n\t\t\treturn undefined;\n\t\t});\n\n\t\tif (!content) {\n\t\t\treturn {};\n\t\t}\n\t\tconst settings = JSON.parse(content);\n\t\treturn SettingsManager.migrateSettings(settings);\n\t}\n\n\tprivate static tryLoadFromStorage(\n\t\tstorage: SettingsStorage,\n\t\tscope: SettingsScope,\n\t): { settings: Settings; error: Error | null } {\n\t\ttry {\n\t\t\treturn { settings: SettingsManager.loadFromStorage(storage, scope), error: null };\n\t\t} catch (error) {\n\t\t\treturn { settings: {}, error: error as Error };\n\t\t}\n\t}\n\n\t/** Migrate old settings format to new format */\n\tprivate static migrateSettings(settings: Record<string, unknown>): Settings {\n\t\t// Migrate queueMode -> steeringMode\n\t\tif (\"queueMode\" in settings && !(\"steeringMode\" in settings)) {\n\t\t\tsettings.steeringMode = settings.queueMode;\n\t\t\tdelete settings.queueMode;\n\t\t}\n\n\t\t// Migrate enableEmbsearchTools -> enableSemanticIndex. The tool rename to\n\t\t// `SearchCodebase` was a clean break, but this key is a user's stored\n\t\t// choice, not a tool name: dropping it would silently flip indexing back\n\t\t// on for anyone who had deliberately turned it off.\n\t\tif (\"enableEmbsearchTools\" in settings && !(\"enableSemanticIndex\" in settings)) {\n\t\t\tsettings.enableSemanticIndex = settings.enableEmbsearchTools;\n\t\t\tdelete settings.enableEmbsearchTools;\n\t\t}\n\n\t\t// Migrate legacy websockets boolean -> transport enum\n\t\tif (!(\"transport\" in settings) && typeof settings.websockets === \"boolean\") {\n\t\t\tsettings.transport = settings.websockets ? \"websocket\" : \"sse\";\n\t\t\tdelete settings.websockets;\n\t\t}\n\n\t\t// Migrate old skills object format to new array format\n\t\tif (\n\t\t\t\"skills\" in settings &&\n\t\t\ttypeof settings.skills === \"object\" &&\n\t\t\tsettings.skills !== null &&\n\t\t\t!Array.isArray(settings.skills)\n\t\t) {\n\t\t\tconst skillsSettings = settings.skills as {\n\t\t\t\tenableSkillCommands?: boolean;\n\t\t\t\tcustomDirectories?: unknown;\n\t\t\t};\n\t\t\tif (skillsSettings.enableSkillCommands !== undefined && settings.enableSkillCommands === undefined) {\n\t\t\t\tsettings.enableSkillCommands = skillsSettings.enableSkillCommands;\n\t\t\t}\n\t\t\tif (Array.isArray(skillsSettings.customDirectories) && skillsSettings.customDirectories.length > 0) {\n\t\t\t\tsettings.skills = skillsSettings.customDirectories;\n\t\t\t} else {\n\t\t\t\tdelete settings.skills;\n\t\t\t}\n\t\t}\n\n\t\t// Migrate retry.maxDelayMs -> retry.provider.maxRetryDelayMs\n\t\tif (\n\t\t\t\"retry\" in settings &&\n\t\t\ttypeof settings.retry === \"object\" &&\n\t\t\tsettings.retry !== null &&\n\t\t\t!Array.isArray(settings.retry)\n\t\t) {\n\t\t\tconst retrySettings = settings.retry as Record<string, unknown>;\n\t\t\tconst providerSettings =\n\t\t\t\ttypeof retrySettings.provider === \"object\" && retrySettings.provider !== null\n\t\t\t\t\t? (retrySettings.provider as Record<string, unknown>)\n\t\t\t\t\t: undefined;\n\t\t\tif (\n\t\t\t\ttypeof retrySettings.maxDelayMs === \"number\" &&\n\t\t\t\t(providerSettings?.maxRetryDelayMs === undefined || providerSettings?.maxRetryDelayMs === null)\n\t\t\t) {\n\t\t\t\tretrySettings.provider = {\n\t\t\t\t\t...(providerSettings ?? {}),\n\t\t\t\t\tmaxRetryDelayMs: retrySettings.maxDelayMs,\n\t\t\t\t};\n\t\t\t}\n\t\t\tdelete retrySettings.maxDelayMs;\n\t\t}\n\n\t\treturn settings as Settings;\n\t}\n\n\tgetGlobalSettings(): Settings {\n\t\treturn structuredClone(this.globalSettings);\n\t}\n\n\tgetProjectSettings(): Settings {\n\t\treturn structuredClone(this.projectSettings);\n\t}\n\n\tgetDefaultSettings(): Settings {\n\t\treturn structuredClone(DEFAULT_SETTINGS);\n\t}\n\n\tasync reload(): Promise<void> {\n\t\tawait this.writeQueue;\n\t\tconst globalLoad = SettingsManager.tryLoadFromStorage(this.storage, \"global\");\n\t\tif (!globalLoad.error) {\n\t\t\tthis.globalSettings = globalLoad.settings;\n\t\t\tthis.globalSettingsLoadError = null;\n\t\t} else {\n\t\t\tthis.globalSettingsLoadError = globalLoad.error;\n\t\t\tthis.recordError(\"global\", globalLoad.error);\n\t\t}\n\n\t\tthis.modifiedFields.clear();\n\t\tthis.modifiedNestedFields.clear();\n\t\tthis.modifiedProjectFields.clear();\n\t\tthis.modifiedProjectNestedFields.clear();\n\n\t\tconst projectLoad = SettingsManager.tryLoadFromStorage(this.storage, \"project\");\n\t\tif (!projectLoad.error) {\n\t\t\tthis.projectSettings = projectLoad.settings;\n\t\t\tthis.projectSettingsLoadError = null;\n\t\t} else {\n\t\t\tthis.projectSettingsLoadError = projectLoad.error;\n\t\t\tthis.recordError(\"project\", projectLoad.error);\n\t\t}\n\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\t}\n\n\t/** Apply additional overrides on top of current settings */\n\tapplyOverrides(overrides: Partial<Settings>): void {\n\t\tthis.settings = deepMergeSettings(this.settings, overrides);\n\t}\n\n\t/** Mark a global field as modified during this session */\n\tprivate markModified(field: keyof Settings, nestedKey?: string): void {\n\t\tthis.modifiedFields.add(field);\n\t\tif (nestedKey) {\n\t\t\tif (!this.modifiedNestedFields.has(field)) {\n\t\t\t\tthis.modifiedNestedFields.set(field, new Set());\n\t\t\t}\n\t\t\tthis.modifiedNestedFields.get(field)!.add(nestedKey);\n\t\t}\n\t}\n\n\t/** Mark a project field as modified during this session */\n\tprivate markProjectModified(field: keyof Settings, nestedKey?: string): void {\n\t\tthis.modifiedProjectFields.add(field);\n\t\tif (nestedKey) {\n\t\t\tif (!this.modifiedProjectNestedFields.has(field)) {\n\t\t\t\tthis.modifiedProjectNestedFields.set(field, new Set());\n\t\t\t}\n\t\t\tthis.modifiedProjectNestedFields.get(field)!.add(nestedKey);\n\t\t}\n\t}\n\n\tprivate recordError(scope: SettingsScope, error: unknown): void {\n\t\tconst normalizedError = error instanceof Error ? error : new Error(String(error));\n\t\tthis.errors.push({ scope, error: normalizedError });\n\t}\n\n\tprivate clearModifiedScope(scope: SettingsScope): void {\n\t\tif (scope === \"global\") {\n\t\t\tthis.modifiedFields.clear();\n\t\t\tthis.modifiedNestedFields.clear();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.modifiedProjectFields.clear();\n\t\tthis.modifiedProjectNestedFields.clear();\n\t}\n\n\tprivate enqueueWrite(scope: SettingsScope, task: () => void): void {\n\t\tthis.writeQueue = this.writeQueue\n\t\t\t.then(() => {\n\t\t\t\ttask();\n\t\t\t\tthis.clearModifiedScope(scope);\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tthis.recordError(scope, error);\n\t\t\t});\n\t}\n\n\tprivate cloneModifiedNestedFields(source: Map<keyof Settings, Set<string>>): Map<keyof Settings, Set<string>> {\n\t\tconst snapshot = new Map<keyof Settings, Set<string>>();\n\t\tfor (const [key, value] of source.entries()) {\n\t\t\tsnapshot.set(key, new Set(value));\n\t\t}\n\t\treturn snapshot;\n\t}\n\n\tprivate persistScopedSettings(\n\t\tscope: SettingsScope,\n\t\tsnapshotSettings: Settings,\n\t\tmodifiedFields: Set<keyof Settings>,\n\t\tmodifiedNestedFields: Map<keyof Settings, Set<string>>,\n\t): void {\n\t\tthis.storage.withLock(scope, (current) => {\n\t\t\tconst currentFileSettings = current\n\t\t\t\t? SettingsManager.migrateSettings(JSON.parse(current) as Record<string, unknown>)\n\t\t\t\t: {};\n\t\t\tconst mergedSettings: Settings = { ...currentFileSettings };\n\t\t\tfor (const field of modifiedFields) {\n\t\t\t\tconst value = snapshotSettings[field];\n\t\t\t\tif (modifiedNestedFields.has(field) && typeof value === \"object\" && value !== null) {\n\t\t\t\t\tconst nestedModified = modifiedNestedFields.get(field)!;\n\t\t\t\t\tconst baseNested = (currentFileSettings[field] as Record<string, unknown>) ?? {};\n\t\t\t\t\tconst inMemoryNested = value as Record<string, unknown>;\n\t\t\t\t\tconst mergedNested = { ...baseNested };\n\t\t\t\t\tfor (const nestedKey of nestedModified) {\n\t\t\t\t\t\tmergedNested[nestedKey] = inMemoryNested[nestedKey];\n\t\t\t\t\t}\n\t\t\t\t\t(mergedSettings as Record<string, unknown>)[field] = mergedNested;\n\t\t\t\t} else {\n\t\t\t\t\t(mergedSettings as Record<string, unknown>)[field] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn JSON.stringify(mergedSettings, null, 2);\n\t\t});\n\t}\n\n\tprivate save(): void {\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\n\t\tif (this.globalSettingsLoadError) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst snapshotGlobalSettings = structuredClone(this.globalSettings);\n\t\tconst modifiedFields = new Set(this.modifiedFields);\n\t\tconst modifiedNestedFields = this.cloneModifiedNestedFields(this.modifiedNestedFields);\n\n\t\tthis.enqueueWrite(\"global\", () => {\n\t\t\tthis.persistScopedSettings(\"global\", snapshotGlobalSettings, modifiedFields, modifiedNestedFields);\n\t\t});\n\t}\n\n\tprivate saveProjectSettings(settings: Settings): void {\n\t\tthis.projectSettings = structuredClone(settings);\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\n\t\tif (this.projectSettingsLoadError) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst snapshotProjectSettings = structuredClone(this.projectSettings);\n\t\tconst modifiedFields = new Set(this.modifiedProjectFields);\n\t\tconst modifiedNestedFields = this.cloneModifiedNestedFields(this.modifiedProjectNestedFields);\n\t\tthis.enqueueWrite(\"project\", () => {\n\t\t\tthis.persistScopedSettings(\"project\", snapshotProjectSettings, modifiedFields, modifiedNestedFields);\n\t\t});\n\t}\n\n\tasync flush(): Promise<void> {\n\t\tawait this.writeQueue;\n\t}\n\n\tdrainErrors(): SettingsError[] {\n\t\tconst drained = [...this.errors];\n\t\tthis.errors = [];\n\t\treturn drained;\n\t}\n\n\tgetLastChangelogVersion(): string | undefined {\n\t\treturn this.settings.lastChangelogVersion;\n\t}\n\n\tsetLastChangelogVersion(version: string): void {\n\t\tthis.globalSettings.lastChangelogVersion = version;\n\t\tthis.markModified(\"lastChangelogVersion\");\n\t\tthis.save();\n\t}\n\n\tgetSessionDir(): string | undefined {\n\t\tconst sessionDir = this.settings.sessionDir;\n\t\tif (!sessionDir) {\n\t\t\treturn sessionDir;\n\t\t}\n\t\tif (sessionDir === \"~\") {\n\t\t\treturn homedir();\n\t\t}\n\t\tif (sessionDir.startsWith(\"~/\")) {\n\t\t\treturn join(homedir(), sessionDir.slice(2));\n\t\t}\n\t\treturn sessionDir;\n\t}\n\n\tgetDefaultProvider(): string | undefined {\n\t\treturn this.settings.defaultProvider;\n\t}\n\n\tgetDefaultModel(): string | undefined {\n\t\treturn this.settings.defaultModel;\n\t}\n\n\tsetDefaultProvider(provider: string): void {\n\t\tthis.globalSettings.defaultProvider = provider;\n\t\tthis.markModified(\"defaultProvider\");\n\t\tthis.save();\n\t}\n\n\tsetDefaultModel(modelId: string): void {\n\t\tthis.globalSettings.defaultModel = modelId;\n\t\tthis.markModified(\"defaultModel\");\n\t\tthis.save();\n\t}\n\n\tsetDefaultModelAndProvider(provider: string, modelId: string): void {\n\t\tthis.globalSettings.defaultProvider = provider;\n\t\tthis.globalSettings.defaultModel = modelId;\n\t\tthis.markModified(\"defaultProvider\");\n\t\tthis.markModified(\"defaultModel\");\n\t\tthis.save();\n\t}\n\n\tgetSteeringMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn this.settings.steeringMode || DEFAULT_SETTINGS.steeringMode!;\n\t}\n\n\tsetSteeringMode(mode: \"all\" | \"one-at-a-time\"): void {\n\t\tthis.globalSettings.steeringMode = mode;\n\t\tthis.markModified(\"steeringMode\");\n\t\tthis.save();\n\t}\n\n\tgetFollowUpMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn this.settings.followUpMode || DEFAULT_SETTINGS.followUpMode!;\n\t}\n\n\tsetFollowUpMode(mode: \"all\" | \"one-at-a-time\"): void {\n\t\tthis.globalSettings.followUpMode = mode;\n\t\tthis.markModified(\"followUpMode\");\n\t\tthis.save();\n\t}\n\n\tgetTheme(): string | undefined {\n\t\treturn this.settings.theme;\n\t}\n\n\tsetTheme(theme: string): void {\n\t\tthis.globalSettings.theme = theme;\n\t\tthis.markModified(\"theme\");\n\t\tthis.save();\n\t}\n\n\tgetDefaultThinkingLevel(): \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | undefined {\n\t\treturn this.settings.defaultThinkingLevel;\n\t}\n\n\tsetDefaultThinkingLevel(level: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\"): void {\n\t\tthis.globalSettings.defaultThinkingLevel = level;\n\t\tthis.markModified(\"defaultThinkingLevel\");\n\t\tthis.save();\n\t}\n\n\tgetTransport(): TransportSetting {\n\t\treturn this.settings.transport ?? DEFAULT_SETTINGS.transport!;\n\t}\n\n\tsetTransport(transport: TransportSetting): void {\n\t\tthis.globalSettings.transport = transport;\n\t\tthis.markModified(\"transport\");\n\t\tthis.save();\n\t}\n\n\tgetCompactionEnabled(): boolean {\n\t\treturn this.settings.compaction?.enabled ?? DEFAULT_SETTINGS.compaction!.enabled;\n\t}\n\n\tsetCompactionEnabled(enabled: boolean): void {\n\t\tif (!this.globalSettings.compaction) {\n\t\t\tthis.globalSettings.compaction = {};\n\t\t}\n\t\tthis.globalSettings.compaction.enabled = enabled;\n\t\tthis.markModified(\"compaction\", \"enabled\");\n\t\tthis.save();\n\t}\n\n\tgetCompactionReserveTokens(): number {\n\t\treturn this.settings.compaction?.reserveTokens ?? DEFAULT_SETTINGS.compaction!.reserveTokens;\n\t}\n\n\tgetCompactionKeepRecentTokens(): number {\n\t\treturn this.settings.compaction?.keepRecentTokens ?? DEFAULT_SETTINGS.compaction!.keepRecentTokens;\n\t}\n\n\tgetCompactionSettings(): Required<CompactionSettings> {\n\t\treturn { ...DEFAULT_SETTINGS.compaction, ...this.settings.compaction };\n\t}\n\n\t/**\n\t * Window and threshold `/learn` mines sessions with.\n\t *\n\t * Each value falls back to its default unless it is a finite positive number,\n\t * so a typo (`0`, `-1`, a pasted string) cannot silently reduce the window to\n\t * nothing and make the command report \"no recent sessions\".\n\t */\n\tgetLearnSettings(): {\n\t\tmaxSessions: number;\n\t\tmaxAgeDays: number;\n\t\tminRepeats: number;\n\t\tminRequestRepeats: number;\n\t\tmaxProposals: number;\n\t} {\n\t\tconst positive = (value: unknown, fallback: number): number =>\n\t\t\ttypeof value === \"number\" && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;\n\t\treturn {\n\t\t\tmaxSessions: positive(this.settings.learnMaxSessions, DEFAULT_SETTINGS.learnMaxSessions),\n\t\t\tmaxAgeDays: positive(this.settings.learnMaxAgeDays, DEFAULT_SETTINGS.learnMaxAgeDays),\n\t\t\tminRepeats: positive(this.settings.learnMinRepeats, DEFAULT_SETTINGS.learnMinRepeats),\n\t\t\tminRequestRepeats: positive(this.settings.learnMinRequestRepeats, DEFAULT_SETTINGS.learnMinRequestRepeats),\n\t\t\tmaxProposals: positive(this.settings.learnMaxProposals, DEFAULT_SETTINGS.learnMaxProposals),\n\t\t};\n\t}\n\n\t/** Configured `fast`/`standard`/`capable` tiers, for callers resolving a category. */\n\tgetModelCategories(): Settings[\"modelCategories\"] {\n\t\treturn this.settings.modelCategories ? structuredClone(this.settings.modelCategories) : undefined;\n\t}\n\n\t/**\n\t * Persist one `/learn` threshold to the user scope.\n\t *\n\t * One keyed setter rather than five identical ones: the settings are flat\n\t * top-level numbers that differ only in name, and the settings pane drives\n\t * them from a table. Values are floored to at least 1, matching the read\n\t * side's refusal to let a zero shrink the window to nothing.\n\t */\n\tsetLearnSetting(key: LearnSettingKey, value: number): void {\n\t\tconst safe = Number.isFinite(value) ? Math.max(1, Math.floor(value)) : DEFAULT_SETTINGS[key];\n\t\tthis.globalSettings[key] = safe;\n\t\tthis.markModified(key);\n\t\tthis.save();\n\t}\n\n\t/** Byte cap on a single read/bash tool result before truncation. Clamped to >= 1024. */\n\tgetToolOutputMaxBytes(): number {\n\t\tconst v = this.settings.toolOutput?.maxBytes ?? DEFAULT_SETTINGS.toolOutput!.maxBytes;\n\t\treturn Number.isFinite(v) && v >= 1024 ? Math.floor(v) : DEFAULT_SETTINGS.toolOutput!.maxBytes;\n\t}\n\n\tsetToolOutputMaxBytes(bytes: number): void {\n\t\tif (!this.globalSettings.toolOutput) {\n\t\t\tthis.globalSettings.toolOutput = {};\n\t\t}\n\t\tthis.globalSettings.toolOutput.maxBytes = Math.max(1024, Math.floor(bytes));\n\t\tthis.markModified(\"toolOutput\", \"maxBytes\");\n\t\tthis.save();\n\t}\n\n\t/** Line cap on a single read/bash tool result before truncation. Clamped to >= 1. */\n\tgetToolOutputMaxLines(): number {\n\t\tconst v = this.settings.toolOutput?.maxLines ?? DEFAULT_SETTINGS.toolOutput!.maxLines;\n\t\treturn Number.isFinite(v) && v >= 1 ? Math.floor(v) : DEFAULT_SETTINGS.toolOutput!.maxLines;\n\t}\n\n\tsetToolOutputMaxLines(lines: number): void {\n\t\tif (!this.globalSettings.toolOutput) {\n\t\t\tthis.globalSettings.toolOutput = {};\n\t\t}\n\t\tthis.globalSettings.toolOutput.maxLines = Math.max(1, Math.floor(lines));\n\t\tthis.markModified(\"toolOutput\", \"maxLines\");\n\t\tthis.save();\n\t}\n\n\t/** Whether to stub superseded read results out of the outgoing context. */\n\tgetContextGcEnabled(): boolean {\n\t\treturn this.settings.contextGc?.enabled ?? DEFAULT_SETTINGS.contextGc!.enabled;\n\t}\n\n\tsetContextGcEnabled(enabled: boolean): void {\n\t\tif (!this.globalSettings.contextGc) {\n\t\t\tthis.globalSettings.contextGc = {};\n\t\t}\n\t\tthis.globalSettings.contextGc.enabled = enabled;\n\t\tthis.markModified(\"contextGc\", \"enabled\");\n\t\tthis.save();\n\t}\n\n\t/** Trailing-silence window (ms) before voice capture auto-stops. Clamped to 300-10000. */\n\tgetVoiceSilenceMs(): number {\n\t\tconst fallback = DEFAULT_SETTINGS.voice!.silenceMs;\n\t\tconst v = this.settings.voice?.silenceMs ?? fallback;\n\t\treturn Number.isFinite(v) ? Math.min(10000, Math.max(300, Math.floor(v))) : fallback;\n\t}\n\n\tsetVoiceSilenceMs(ms: number): void {\n\t\tif (!this.globalSettings.voice) {\n\t\t\tthis.globalSettings.voice = {};\n\t\t}\n\t\tthis.globalSettings.voice.silenceMs = Math.min(10000, Math.max(300, Math.floor(ms)));\n\t\tthis.markModified(\"voice\", \"silenceMs\");\n\t\tthis.save();\n\t}\n\n\t/**\n\t * Per-request timeout (seconds) for the webfetch/websearch binary. Settings\n\t * takes precedence, then the `HOOCODE_WEBTOOLS_TIMEOUT` env var, then the\n\t * default 15 (mirrors {@link getClearOnShrink}). Clamped to 1-120; a\n\t * non-finite settings value or malformed env falls through to the next tier.\n\t */\n\tgetWebtoolsTimeoutSecs(): number {\n\t\tconst fallback = DEFAULT_SETTINGS.webtools!.timeoutSecs;\n\t\tconst clamp = (n: number): number => Math.min(120, Math.max(1, Math.floor(n)));\n\t\tconst configured = this.settings.webtools?.timeoutSecs;\n\t\tif (configured !== undefined && Number.isFinite(configured)) {\n\t\t\treturn clamp(configured);\n\t\t}\n\t\tconst envRaw = process.env.HOOCODE_WEBTOOLS_TIMEOUT?.trim();\n\t\tif (envRaw) {\n\t\t\tconst envValue = Number(envRaw);\n\t\t\tif (Number.isFinite(envValue) && envValue > 0) {\n\t\t\t\treturn clamp(envValue);\n\t\t\t}\n\t\t}\n\t\treturn fallback;\n\t}\n\n\t/**\n\t * The `webtools.search` block as the binary would read it (backend choice and\n\t * credentials). hoocode only reads it, to tell whether `websearch` has a keyed\n\t * provider; the binary itself resolves the actual key at call time.\n\t *\n\t * Deliberately the user-level file rather than the merged view: the binary\n\t * reads only `~/.hoocode/settings.json`, so a key in a project's\n\t * `.hoocode/settings.json` never reaches it, and treating one as configured\n\t * would silence the warning for a search that still runs keyless. (Contrast\n\t * {@link getWebtoolsTimeoutSecs}, which is hoocode's own setting forwarded as\n\t * a flag, so there the merged value is the effective one.)\n\t */\n\tgetWebtoolsSearch(): WebtoolsSearchSettings | undefined {\n\t\treturn this.globalSettings.webtools?.search;\n\t}\n\n\tsetWebtoolsTimeoutSecs(secs: number): void {\n\t\tif (!this.globalSettings.webtools) {\n\t\t\tthis.globalSettings.webtools = {};\n\t\t}\n\t\tthis.globalSettings.webtools.timeoutSecs = Math.min(120, Math.max(1, Math.floor(secs)));\n\t\tthis.markModified(\"webtools\", \"timeoutSecs\");\n\t\tthis.save();\n\t}\n\n\t/** Tool names disabled from the TUI; removed from the agent in every session. */\n\tgetDisabledTools(): string[] {\n\t\treturn [...(this.settings.disabledTools ?? DEFAULT_SETTINGS.disabledTools!)];\n\t}\n\n\tsetDisabledTools(names: string[]): void {\n\t\tthis.globalSettings.disabledTools = [...names];\n\t\tthis.markModified(\"disabledTools\");\n\t\tthis.save();\n\t}\n\n\t/**\n\t * How much of a tool call the transcript shows (radar / peek / full).\n\t *\n\t * Settings files written before the dial existed carry the old three-value\n\t * `toolOutputDisplay` key. They are read here rather than rewritten on load,\n\t * so a downgrade keeps working until the user actually changes the setting.\n\t */\n\tgetToolOutputView(): ToolOutputView {\n\t\tconst view = this.settings.toolOutputView;\n\t\tif (isToolOutputView(view)) return view;\n\n\t\tconst legacy = (this.settings as { toolOutputDisplay?: unknown }).toolOutputDisplay;\n\t\tconst migrated = LEGACY_TOOL_OUTPUT_VIEWS[legacy as string];\n\t\treturn migrated ?? DEFAULT_SETTINGS.toolOutputView!;\n\t}\n\n\tsetToolOutputView(view: ToolOutputView): void {\n\t\tthis.globalSettings.toolOutputView = view;\n\t\tthis.markModified(\"toolOutputView\");\n\t\tthis.save();\n\t}\n\n\t/**\n\t * The chrome dial's stop, or undefined when the user has never set one.\n\t *\n\t * Deliberately not defaulted here. \"Unset\" is a real state: it is what lets\n\t * a short terminal open at `compact` without that guess ever overruling a\n\t * choice someone actually made. The caller resolves it (see\n\t * `SMALL_TERMINAL_ROWS`), because only the caller knows the terminal size.\n\t */\n\tgetChromeDensity(): ChromeDensity | undefined {\n\t\tconst density = this.settings.chromeDensity;\n\t\treturn isChromeDensity(density) ? density : undefined;\n\t}\n\n\tsetChromeDensity(density: ChromeDensity): void {\n\t\tthis.globalSettings.chromeDensity = density;\n\t\tthis.markModified(\"chromeDensity\");\n\t\tthis.save();\n\t}\n\n\t/** Persisted overrides for extension-registered flags (name -> value). */\n\tgetFlagOverrides(): Record<string, boolean | string> {\n\t\treturn { ...(this.settings.flags ?? {}) };\n\t}\n\n\tsetFlagOverride(name: string, value: boolean | string): void {\n\t\tif (!this.globalSettings.flags) {\n\t\t\tthis.globalSettings.flags = {};\n\t\t}\n\t\tthis.globalSettings.flags[name] = value;\n\t\tthis.markModified(\"flags\", name);\n\t\tthis.save();\n\t}\n\n\tclearFlagOverride(name: string): void {\n\t\tif (this.globalSettings.flags && name in this.globalSettings.flags) {\n\t\t\tdelete this.globalSettings.flags[name];\n\t\t\tthis.markModified(\"flags\", name);\n\t\t\tthis.save();\n\t\t}\n\t}\n\n\tgetBranchSummarySettings(): Required<BranchSummarySettings> {\n\t\treturn { ...DEFAULT_SETTINGS.branchSummary, ...this.settings.branchSummary };\n\t}\n\n\tgetBranchSummarySkipPrompt(): boolean {\n\t\treturn this.settings.branchSummary?.skipPrompt ?? DEFAULT_SETTINGS.branchSummary!.skipPrompt;\n\t}\n\n\tgetRetryEnabled(): boolean {\n\t\treturn this.settings.retry?.enabled ?? DEFAULT_SETTINGS.retry!.enabled;\n\t}\n\n\tsetRetryEnabled(enabled: boolean): void {\n\t\tif (!this.globalSettings.retry) {\n\t\t\tthis.globalSettings.retry = {};\n\t\t}\n\t\tthis.globalSettings.retry.enabled = enabled;\n\t\tthis.markModified(\"retry\", \"enabled\");\n\t\tthis.save();\n\t}\n\n\tgetRetrySettings(): { enabled: boolean; maxRetries: number; baseDelayMs: number } {\n\t\treturn {\n\t\t\tenabled: this.getRetryEnabled(),\n\t\t\tmaxRetries: this.settings.retry?.maxRetries ?? DEFAULT_SETTINGS.retry!.maxRetries,\n\t\t\tbaseDelayMs: this.settings.retry?.baseDelayMs ?? DEFAULT_SETTINGS.retry!.baseDelayMs,\n\t\t};\n\t}\n\n\tgetProviderRetrySettings(): { timeoutMs?: number; maxRetries?: number; maxRetryDelayMs: number } {\n\t\treturn {\n\t\t\t...DEFAULT_SETTINGS.retry!.provider,\n\t\t\t...this.settings.retry?.provider,\n\t\t};\n\t}\n\n\tgetHideThinkingBlock(): boolean {\n\t\treturn this.settings.hideThinkingBlock ?? DEFAULT_SETTINGS.hideThinkingBlock!;\n\t}\n\n\tsetHideThinkingBlock(hide: boolean): void {\n\t\tthis.globalSettings.hideThinkingBlock = hide;\n\t\tthis.markModified(\"hideThinkingBlock\");\n\t\tthis.save();\n\t}\n\n\tgetShellPath(): string | undefined {\n\t\treturn this.settings.shellPath;\n\t}\n\n\tsetShellPath(path: string | undefined): void {\n\t\tthis.globalSettings.shellPath = path;\n\t\tthis.markModified(\"shellPath\");\n\t\tthis.save();\n\t}\n\n\tgetQuietStartup(): boolean {\n\t\treturn this.settings.quietStartup ?? DEFAULT_SETTINGS.quietStartup!;\n\t}\n\n\tsetQuietStartup(quiet: boolean): void {\n\t\tthis.globalSettings.quietStartup = quiet;\n\t\tthis.markModified(\"quietStartup\");\n\t\tthis.save();\n\t}\n\n\tgetShellCommandPrefix(): string | undefined {\n\t\treturn this.settings.shellCommandPrefix;\n\t}\n\n\tsetShellCommandPrefix(prefix: string | undefined): void {\n\t\tthis.globalSettings.shellCommandPrefix = prefix;\n\t\tthis.markModified(\"shellCommandPrefix\");\n\t\tthis.save();\n\t}\n\n\tgetNpmCommand(): string[] | undefined {\n\t\treturn this.settings.npmCommand ? [...this.settings.npmCommand] : undefined;\n\t}\n\n\tsetNpmCommand(command: string[] | undefined): void {\n\t\tthis.globalSettings.npmCommand = command ? [...command] : undefined;\n\t\tthis.markModified(\"npmCommand\");\n\t\tthis.save();\n\t}\n\n\tgetCollapseChangelog(): boolean {\n\t\treturn this.settings.collapseChangelog ?? DEFAULT_SETTINGS.collapseChangelog!;\n\t}\n\n\tsetCollapseChangelog(collapse: boolean): void {\n\t\tthis.globalSettings.collapseChangelog = collapse;\n\t\tthis.markModified(\"collapseChangelog\");\n\t\tthis.save();\n\t}\n\n\tgetEnableInstallTelemetry(): boolean {\n\t\treturn this.settings.enableInstallTelemetry ?? DEFAULT_SETTINGS.enableInstallTelemetry!;\n\t}\n\n\tsetEnableInstallTelemetry(enabled: boolean): void {\n\t\tthis.globalSettings.enableInstallTelemetry = enabled;\n\t\tthis.markModified(\"enableInstallTelemetry\");\n\t\tthis.save();\n\t}\n\n\tgetPackages(): PackageSource[] {\n\t\treturn [...(this.settings.packages ?? DEFAULT_SETTINGS.packages!)];\n\t}\n\n\tsetPackages(packages: PackageSource[]): void {\n\t\tthis.globalSettings.packages = packages;\n\t\tthis.markModified(\"packages\");\n\t\tthis.save();\n\t}\n\n\tsetProjectPackages(packages: PackageSource[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.packages = packages;\n\t\tthis.markProjectModified(\"packages\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetExtensionPaths(): string[] {\n\t\treturn [...(this.settings.extensions ?? DEFAULT_SETTINGS.extensions!)];\n\t}\n\n\tsetExtensionPaths(paths: string[]): void {\n\t\tthis.globalSettings.extensions = paths;\n\t\tthis.markModified(\"extensions\");\n\t\tthis.save();\n\t}\n\n\tsetProjectExtensionPaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.extensions = paths;\n\t\tthis.markProjectModified(\"extensions\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetSkillPaths(): string[] {\n\t\treturn [...(this.settings.skills ?? DEFAULT_SETTINGS.skills!)];\n\t}\n\n\tsetSkillPaths(paths: string[]): void {\n\t\tthis.globalSettings.skills = paths;\n\t\tthis.markModified(\"skills\");\n\t\tthis.save();\n\t}\n\n\tsetProjectSkillPaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.skills = paths;\n\t\tthis.markProjectModified(\"skills\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetPromptTemplatePaths(): string[] {\n\t\treturn [...(this.settings.prompts ?? DEFAULT_SETTINGS.prompts!)];\n\t}\n\n\tsetPromptTemplatePaths(paths: string[]): void {\n\t\tthis.globalSettings.prompts = paths;\n\t\tthis.markModified(\"prompts\");\n\t\tthis.save();\n\t}\n\n\tsetProjectPromptTemplatePaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.prompts = paths;\n\t\tthis.markProjectModified(\"prompts\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetSlashCommandPaths(): string[] {\n\t\treturn [...(this.settings.slashCommands ?? DEFAULT_SETTINGS.slashCommands!)];\n\t}\n\n\tsetSlashCommandPaths(paths: string[]): void {\n\t\tthis.globalSettings.slashCommands = paths;\n\t\tthis.markModified(\"slashCommands\");\n\t\tthis.save();\n\t}\n\n\tsetProjectSlashCommandPaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.slashCommands = paths;\n\t\tthis.markProjectModified(\"slashCommands\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetThemePaths(): string[] {\n\t\treturn [...(this.settings.themes ?? DEFAULT_SETTINGS.themes!)];\n\t}\n\n\tsetThemePaths(paths: string[]): void {\n\t\tthis.globalSettings.themes = paths;\n\t\tthis.markModified(\"themes\");\n\t\tthis.save();\n\t}\n\n\tsetProjectThemePaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.themes = paths;\n\t\tthis.markProjectModified(\"themes\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetEnableSkillCommands(): boolean {\n\t\treturn this.settings.enableSkillCommands ?? DEFAULT_SETTINGS.enableSkillCommands!;\n\t}\n\n\tsetEnableSkillCommands(enabled: boolean): void {\n\t\tthis.globalSettings.enableSkillCommands = enabled;\n\t\tthis.markModified(\"enableSkillCommands\");\n\t\tthis.save();\n\t}\n\n\tgetEnableSubagent(): boolean {\n\t\treturn this.settings.enableSubagent ?? DEFAULT_SETTINGS.enableSubagent!;\n\t}\n\n\tsetEnableSubagent(enabled: boolean): void {\n\t\tthis.globalSettings.enableSubagent = enabled;\n\t\tthis.markModified(\"enableSubagent\");\n\t\tthis.save();\n\t}\n\n\t/** Whether to dispatch eligible subagents on reused warm workers (experimental). */\n\tgetWarmSubagents(): boolean {\n\t\treturn this.settings.warmSubagents ?? DEFAULT_SETTINGS.warmSubagents!;\n\t}\n\n\tsetWarmSubagents(enabled: boolean): void {\n\t\tthis.globalSettings.warmSubagents = enabled;\n\t\tthis.markModified(\"warmSubagents\");\n\t\tthis.save();\n\t}\n\n\t/** Tree-wide subagent nesting cap. Clamped to >= 1 (delegation can never be disabled here). */\n\tgetMaxSubagentDepth(): number {\n\t\tconst v = this.settings.maxSubagentDepth ?? DEFAULT_SETTINGS.maxSubagentDepth!;\n\t\treturn Number.isFinite(v) && v >= 1 ? Math.floor(v) : 1;\n\t}\n\n\t/** Max concurrent subagents per pool at nesting depth >= 1. Clamped to >= 1. */\n\tgetNestedSubagentConcurrency(): number {\n\t\tconst v = this.settings.nestedSubagentConcurrency ?? DEFAULT_SETTINGS.nestedSubagentConcurrency!;\n\t\treturn Number.isFinite(v) && v >= 1 ? Math.floor(v) : 2;\n\t}\n\n\tgetEnableTodoWrite(): boolean {\n\t\treturn this.settings.enableTodoWrite ?? DEFAULT_SETTINGS.enableTodoWrite!;\n\t}\n\n\tsetEnableTodoWrite(enabled: boolean): void {\n\t\tthis.globalSettings.enableTodoWrite = enabled;\n\t\tthis.markModified(\"enableTodoWrite\");\n\t\tthis.save();\n\t}\n\n\t/** Raw `platform` tokens (string coerced to an array), or undefined when unset. */\n\tgetPlatform(): string[] | undefined {\n\t\tconst value = this.settings.platform;\n\t\tif (value === undefined) return undefined;\n\t\treturn Array.isArray(value) ? value : [value];\n\t}\n\n\t/**\n\t * Persist the artifact platform targets. Always stored as an array (getPlatform\n\t * accepts either shape), and an empty/undefined list removes the key so the\n\t * per-consumer defaults apply again rather than an empty list being read as\n\t * \"target nothing\". Global scope: the layout a machine writes is an\n\t * environment-level choice, not a per-repo one.\n\t */\n\tsetPlatform(platforms: readonly string[] | undefined): void {\n\t\tthis.globalSettings.platform = platforms && platforms.length > 0 ? [...platforms] : undefined;\n\t\tthis.markModified(\"platform\");\n\t\tthis.save();\n\t}\n\n\tgetEnablePluginTools(): boolean {\n\t\treturn this.settings.enablePluginTools ?? DEFAULT_SETTINGS.enablePluginTools!;\n\t}\n\n\tsetEnablePluginTools(enabled: boolean): void {\n\t\tthis.globalSettings.enablePluginTools = enabled;\n\t\tthis.markModified(\"enablePluginTools\");\n\t\tthis.save();\n\t}\n\n\t/**\n\t * Where an autonomous InstallPlugin puts a plugin. `/plugin install` asks the\n\t * human instead of reading this — the setting exists precisely for the path\n\t * where there is nobody to ask.\n\t */\n\tgetPluginInstallScope(): \"user\" | \"project\" {\n\t\tconst value = this.settings.pluginInstallScope;\n\t\treturn value === \"project\" ? \"project\" : DEFAULT_SETTINGS.pluginInstallScope!;\n\t}\n\n\tsetPluginInstallScope(scope: \"user\" | \"project\"): void {\n\t\tthis.globalSettings.pluginInstallScope = scope;\n\t\tthis.markModified(\"pluginInstallScope\");\n\t\tthis.save();\n\t}\n\n\tgetDeferMcpSchemas(): boolean {\n\t\treturn this.settings.deferMcpSchemas ?? DEFAULT_SETTINGS.deferMcpSchemas!;\n\t}\n\n\tsetDeferMcpSchemas(enabled: boolean): void {\n\t\tthis.globalSettings.deferMcpSchemas = enabled;\n\t\tthis.markModified(\"deferMcpSchemas\");\n\t\tthis.save();\n\t}\n\n\tgetEnableWebTools(): boolean {\n\t\treturn this.settings.enableWebTools ?? DEFAULT_SETTINGS.enableWebTools!;\n\t}\n\n\tsetEnableWebTools(enabled: boolean): void {\n\t\tthis.globalSettings.enableWebTools = enabled;\n\t\tthis.markModified(\"enableWebTools\");\n\t\tthis.save();\n\t}\n\n\tgetEnableSemanticIndex(): boolean {\n\t\treturn this.settings.enableSemanticIndex ?? DEFAULT_SETTINGS.enableSemanticIndex!;\n\t}\n\n\tsetEnableSemanticIndex(enabled: boolean): void {\n\t\tthis.globalSettings.enableSemanticIndex = enabled;\n\t\tthis.markModified(\"enableSemanticIndex\");\n\t\tthis.save();\n\t}\n\n\tgetEmbsearchBinaryPath(): string | undefined {\n\t\treturn this.settings.embsearchBinaryPath;\n\t}\n\n\tgetEmbsearchThresholdBytes(): number {\n\t\treturn this.settings.embsearchThresholdBytes ?? DEFAULT_SETTINGS.embsearchThresholdBytes!;\n\t}\n\n\tgetLight(): boolean {\n\t\treturn this.settings.light ?? DEFAULT_SETTINGS.light!;\n\t}\n\n\tsetLight(enabled: boolean): void {\n\t\tthis.globalSettings.light = enabled;\n\t\tthis.markModified(\"light\");\n\t\tthis.save();\n\t}\n\n\tgetThinkingBudgets(): ThinkingBudgetsSettings | undefined {\n\t\treturn this.settings.thinkingBudgets;\n\t}\n\n\tgetThinkingDisplay(): \"summarized\" | \"omitted\" | undefined {\n\t\treturn this.settings.thinkingDisplay;\n\t}\n\n\tgetShowImages(): boolean {\n\t\treturn this.settings.terminal?.showImages ?? DEFAULT_SETTINGS.terminal!.showImages;\n\t}\n\n\tsetShowImages(show: boolean): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.showImages = show;\n\t\tthis.markModified(\"terminal\", \"showImages\");\n\t\tthis.save();\n\t}\n\n\tgetImageWidthCells(): number {\n\t\tconst width = this.settings.terminal?.imageWidthCells ?? DEFAULT_SETTINGS.terminal!.imageWidthCells;\n\t\tif (typeof width !== \"number\" || !Number.isFinite(width)) {\n\t\t\treturn DEFAULT_SETTINGS.terminal!.imageWidthCells;\n\t\t}\n\t\treturn Math.max(1, Math.floor(width));\n\t}\n\n\tsetImageWidthCells(width: number): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.imageWidthCells = Math.max(1, Math.floor(width));\n\t\tthis.markModified(\"terminal\", \"imageWidthCells\");\n\t\tthis.save();\n\t}\n\n\tgetClearOnShrink(): boolean {\n\t\t// Settings takes precedence, then env var, then default false\n\t\tif (this.settings.terminal?.clearOnShrink !== undefined) {\n\t\t\treturn this.settings.terminal.clearOnShrink;\n\t\t}\n\t\treturn process.env.HOOCODE_CLEAR_ON_SHRINK === \"1\";\n\t}\n\n\tsetClearOnShrink(enabled: boolean): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.clearOnShrink = enabled;\n\t\tthis.markModified(\"terminal\", \"clearOnShrink\");\n\t\tthis.save();\n\t}\n\n\tgetShowTerminalProgress(): boolean {\n\t\treturn this.settings.terminal?.showTerminalProgress ?? DEFAULT_SETTINGS.terminal!.showTerminalProgress;\n\t}\n\n\tsetShowTerminalProgress(enabled: boolean): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.showTerminalProgress = enabled;\n\t\tthis.markModified(\"terminal\", \"showTerminalProgress\");\n\t\tthis.save();\n\t}\n\n\tgetChimeOnTurnComplete(): boolean {\n\t\treturn this.settings.terminal?.chimeOnTurnComplete ?? DEFAULT_SETTINGS.terminal!.chimeOnTurnComplete;\n\t}\n\n\tsetChimeOnTurnComplete(enabled: boolean): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.chimeOnTurnComplete = enabled;\n\t\tthis.markModified(\"terminal\", \"chimeOnTurnComplete\");\n\t\tthis.save();\n\t}\n\n\t// --- Tips ---------------------------------------------------------------\n\t// `seen` and `starNudges` are written by the tip rotation as a side effect of\n\t// showing something, so these setters are called from a timer rather than\n\t// from a user action. They stay cheap: a bounded array and an integer.\n\n\tgetTipsEnabled(): boolean {\n\t\treturn this.settings.tips?.enabled ?? DEFAULT_SETTINGS.tips!.enabled!;\n\t}\n\n\tsetTipsEnabled(enabled: boolean): void {\n\t\tif (!this.globalSettings.tips) {\n\t\t\tthis.globalSettings.tips = {};\n\t\t}\n\t\tthis.globalSettings.tips.enabled = enabled;\n\t\tthis.markModified(\"tips\", \"enabled\");\n\t\tthis.save();\n\t}\n\n\tgetSeenTips(): string[] {\n\t\treturn this.settings.tips?.seen ?? [];\n\t}\n\n\t/**\n\t * Remember that a tip has been shown.\n\t *\n\t * Idempotent, so a tip repeated in a long session does not grow the list. The\n\t * list is bounded by the number of tips that exist, and an id that is no\n\t * longer in `TIPS` is harmless -- it is only ever tested for membership.\n\t */\n\tmarkTipSeen(id: string): void {\n\t\tif (!this.globalSettings.tips) {\n\t\t\tthis.globalSettings.tips = {};\n\t\t}\n\t\tconst seen = this.globalSettings.tips.seen ?? [];\n\t\tif (seen.includes(id)) return;\n\t\tthis.globalSettings.tips.seen = [...seen, id];\n\t\tthis.markModified(\"tips\", \"seen\");\n\t\tthis.save();\n\t}\n\n\tgetStarNudgeCount(): number {\n\t\treturn this.settings.tips?.starNudges ?? 0;\n\t}\n\n\trecordStarNudge(): void {\n\t\tif (!this.globalSettings.tips) {\n\t\t\tthis.globalSettings.tips = {};\n\t\t}\n\t\tthis.globalSettings.tips.starNudges = (this.globalSettings.tips.starNudges ?? 0) + 1;\n\t\tthis.markModified(\"tips\", \"starNudges\");\n\t\tthis.save();\n\t}\n\n\tgetImageAutoResize(): boolean {\n\t\treturn this.settings.images?.autoResize ?? DEFAULT_SETTINGS.images!.autoResize;\n\t}\n\n\tsetImageAutoResize(enabled: boolean): void {\n\t\tif (!this.globalSettings.images) {\n\t\t\tthis.globalSettings.images = {};\n\t\t}\n\t\tthis.globalSettings.images.autoResize = enabled;\n\t\tthis.markModified(\"images\", \"autoResize\");\n\t\tthis.save();\n\t}\n\n\tgetBlockImages(): boolean {\n\t\treturn this.settings.images?.blockImages ?? DEFAULT_SETTINGS.images!.blockImages;\n\t}\n\n\tsetBlockImages(blocked: boolean): void {\n\t\tif (!this.globalSettings.images) {\n\t\t\tthis.globalSettings.images = {};\n\t\t}\n\t\tthis.globalSettings.images.blockImages = blocked;\n\t\tthis.markModified(\"images\", \"blockImages\");\n\t\tthis.save();\n\t}\n\n\tgetEnabledModels(): string[] | undefined {\n\t\treturn this.settings.enabledModels;\n\t}\n\n\tsetEnabledModels(patterns: string[] | undefined): void {\n\t\tthis.globalSettings.enabledModels = patterns;\n\t\tthis.markModified(\"enabledModels\");\n\t\tthis.save();\n\t}\n\n\tgetDoubleEscapeAction(): \"fork\" | \"tree\" | \"none\" {\n\t\treturn this.settings.doubleEscapeAction ?? DEFAULT_SETTINGS.doubleEscapeAction!;\n\t}\n\n\tsetDoubleEscapeAction(action: \"fork\" | \"tree\" | \"none\"): void {\n\t\tthis.globalSettings.doubleEscapeAction = action;\n\t\tthis.markModified(\"doubleEscapeAction\");\n\t\tthis.save();\n\t}\n\n\tgetTreeFilterMode(): \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\" {\n\t\tconst mode = this.settings.treeFilterMode;\n\t\tconst valid = [\"default\", \"no-tools\", \"user-only\", \"labeled-only\", \"all\"];\n\t\treturn mode && valid.includes(mode) ? mode : DEFAULT_SETTINGS.treeFilterMode!;\n\t}\n\n\tsetTreeFilterMode(mode: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\"): void {\n\t\tthis.globalSettings.treeFilterMode = mode;\n\t\tthis.markModified(\"treeFilterMode\");\n\t\tthis.save();\n\t}\n\n\tgetShowHardwareCursor(): boolean {\n\t\treturn this.settings.showHardwareCursor ?? process.env.HOOCODE_HARDWARE_CURSOR === \"1\";\n\t}\n\n\tsetShowHardwareCursor(enabled: boolean): void {\n\t\tthis.globalSettings.showHardwareCursor = enabled;\n\t\tthis.markModified(\"showHardwareCursor\");\n\t\tthis.save();\n\t}\n\n\tgetEditorBorder(): \"rule\" | \"box\" {\n\t\treturn this.settings.editorBorder ?? DEFAULT_SETTINGS.editorBorder!;\n\t}\n\n\tsetEditorBorder(border: \"rule\" | \"box\"): void {\n\t\tthis.globalSettings.editorBorder = border;\n\t\tthis.markModified(\"editorBorder\");\n\t\tthis.save();\n\t}\n\n\tgetEditorPaddingX(): number {\n\t\treturn this.settings.editorPaddingX ?? DEFAULT_SETTINGS.editorPaddingX!;\n\t}\n\n\tsetEditorPaddingX(padding: number): void {\n\t\tthis.globalSettings.editorPaddingX = Math.max(0, Math.min(3, Math.floor(padding)));\n\t\tthis.markModified(\"editorPaddingX\");\n\t\tthis.save();\n\t}\n\n\tgetAutocompleteMaxVisible(): number {\n\t\treturn this.settings.autocompleteMaxVisible ?? DEFAULT_SETTINGS.autocompleteMaxVisible!;\n\t}\n\n\tsetAutocompleteMaxVisible(maxVisible: number): void {\n\t\tthis.globalSettings.autocompleteMaxVisible = Math.max(3, Math.min(20, Math.floor(maxVisible)));\n\t\tthis.markModified(\"autocompleteMaxVisible\");\n\t\tthis.save();\n\t}\n\n\tgetCodeBlockIndent(): string {\n\t\treturn this.settings.markdown?.codeBlockIndent ?? DEFAULT_SETTINGS.markdown!.codeBlockIndent;\n\t}\n\n\tgetWarnings(): WarningSettings {\n\t\treturn { ...DEFAULT_SETTINGS.warnings, ...this.settings.warnings };\n\t}\n\n\tsetWarnings(warnings: WarningSettings): void {\n\t\tthis.globalSettings.warnings = { ...warnings };\n\t\tthis.markModified(\"warnings\");\n\t\tthis.save();\n\t}\n}\n"]}