import type { Transport } from "@mariozechner/pi-ai"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { dirname, join } from "path"; import lockfile from "proper-lockfile"; import { CONFIG_DIR_NAME, getAgentDir } from "../config.js"; export interface CompactionSettings { enabled?: boolean; // default: true reserveTokens?: number; // default: 16384 keepRecentTokens?: number; // default: 20000 } export interface BranchSummarySettings { reserveTokens?: number; // default: 16384 (tokens reserved for prompt + LLM response) skipPrompt?: boolean; // default: false - when true, skips "Summarize branch?" prompt and defaults to no summary } export interface RetrySettings { enabled?: boolean; // default: true maxRetries?: number; // default: 3 baseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s) maxDelayMs?: number; // default: 60000 (max server-requested delay before failing) } export interface TerminalSettings { showImages?: boolean; // default: true (only relevant if terminal supports images) clearOnShrink?: boolean; // default: false (clear empty rows when content shrinks) } export interface ImageSettings { autoResize?: boolean; // default: true (resize images to 2000x2000 max for better model compatibility) blockImages?: boolean; // default: false - when true, prevents all images from being sent to LLM providers } export interface ThinkingBudgetsSettings { minimal?: number; low?: number; medium?: number; high?: number; } export interface MarkdownSettings { codeBlockIndent?: string; // default: " " } export interface GatewaySessionSettings { idleMinutes?: number; maxQueuePerSession?: number; } export interface GatewayWebhookSettings { enabled?: boolean; basePath?: string; secret?: string; } export interface GatewaySettings { enabled?: boolean; bind?: string; port?: number; bearerToken?: string; session?: GatewaySessionSettings; webhook?: GatewayWebhookSettings; } export type TransportSetting = Transport; /** * Package source for npm/git packages. * - String form: load all resources from the package * - Object form: filter which resources to load */ export type PackageSource = | string | { source: string; extensions?: string[]; skills?: string[]; prompts?: string[]; themes?: string[]; }; export interface Settings { lastChangelogVersion?: string; defaultProvider?: string; defaultModel?: string; defaultThinkingLevel?: | "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; transport?: TransportSetting; // default: "sse" steeringMode?: "all" | "one-at-a-time"; followUpMode?: "all" | "one-at-a-time"; theme?: string; compaction?: CompactionSettings; branchSummary?: BranchSummarySettings; retry?: RetrySettings; hideThinkingBlock?: boolean; shellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows) quietStartup?: boolean; shellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., "shopt -s expand_aliases" for alias support) collapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full) packages?: PackageSource[]; // Array of npm/git package sources (string or object with filtering) extensions?: string[]; // Array of local extension file paths or directories skills?: string[]; // Array of local skill file paths or directories prompts?: string[]; // Array of local prompt template paths or directories themes?: string[]; // Array of local theme file paths or directories enableSkillCommands?: boolean; // default: true - register skills as /skill:name commands terminal?: TerminalSettings; images?: ImageSettings; enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag) doubleEscapeAction?: "fork" | "tree" | "none"; // Action for double-escape with empty editor (default: "tree") treeFilterMode?: | "default" | "no-tools" | "user-only" | "labeled-only" | "all"; // Default filter when opening /tree thinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels editorPaddingX?: number; // Horizontal padding for input editor (default: 0) autocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5) showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME markdown?: MarkdownSettings; gateway?: GatewaySettings; } /** Deep merge settings: project/overrides take precedence, nested objects merge recursively */ function deepMergeSettings(base: Settings, overrides: Settings): Settings { const result: Settings = { ...base }; for (const key of Object.keys(overrides) as (keyof Settings)[]) { const overrideValue = overrides[key]; const baseValue = base[key]; if (overrideValue === undefined) { continue; } // For nested objects, merge recursively if ( typeof overrideValue === "object" && overrideValue !== null && !Array.isArray(overrideValue) && typeof baseValue === "object" && baseValue !== null && !Array.isArray(baseValue) ) { (result as Record)[key] = { ...baseValue, ...overrideValue, }; } else { // For primitives and arrays, override value wins (result as Record)[key] = overrideValue; } } return result; } export type SettingsScope = "global" | "project"; export interface SettingsStorage { withLock( scope: SettingsScope, fn: (current: string | undefined) => string | undefined, ): void; } export interface SettingsError { scope: SettingsScope; error: Error; } export class FileSettingsStorage implements SettingsStorage { private globalSettingsPath: string; private projectSettingsPath: string; constructor(cwd: string = process.cwd(), agentDir: string = getAgentDir()) { this.globalSettingsPath = join(agentDir, "settings.json"); this.projectSettingsPath = join(cwd, CONFIG_DIR_NAME, "settings.json"); } withLock( scope: SettingsScope, fn: (current: string | undefined) => string | undefined, ): void { const path = scope === "global" ? this.globalSettingsPath : this.projectSettingsPath; const dir = dirname(path); let release: (() => void) | undefined; try { // Only create directory and lock if file exists or we need to write const fileExists = existsSync(path); if (fileExists) { release = lockfile.lockSync(path, { realpath: false }); } const current = fileExists ? readFileSync(path, "utf-8") : undefined; const next = fn(current); if (next !== undefined) { // Only create directory when we actually need to write if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } if (!release) { release = lockfile.lockSync(path, { realpath: false }); } writeFileSync(path, next, "utf-8"); } } finally { if (release) { release(); } } } } export class InMemorySettingsStorage implements SettingsStorage { private global: string | undefined; private project: string | undefined; withLock( scope: SettingsScope, fn: (current: string | undefined) => string | undefined, ): void { const current = scope === "global" ? this.global : this.project; const next = fn(current); if (next !== undefined) { if (scope === "global") { this.global = next; } else { this.project = next; } } } } export class SettingsManager { private storage: SettingsStorage; private globalSettings: Settings; private projectSettings: Settings; private settings: Settings; private modifiedFields = new Set(); // Track global fields modified during session private modifiedNestedFields = new Map>(); // Track global nested field modifications private modifiedProjectFields = new Set(); // Track project fields modified during session private modifiedProjectNestedFields = new Map>(); // Track project nested field modifications private globalSettingsLoadError: Error | null = null; // Track if global settings file had parse errors private projectSettingsLoadError: Error | null = null; // Track if project settings file had parse errors private writeQueue: Promise = Promise.resolve(); private errors: SettingsError[]; private constructor( storage: SettingsStorage, initialGlobal: Settings, initialProject: Settings, globalLoadError: Error | null = null, projectLoadError: Error | null = null, initialErrors: SettingsError[] = [], ) { this.storage = storage; this.globalSettings = initialGlobal; this.projectSettings = initialProject; this.globalSettingsLoadError = globalLoadError; this.projectSettingsLoadError = projectLoadError; this.errors = [...initialErrors]; this.settings = deepMergeSettings( this.globalSettings, this.projectSettings, ); } /** Create a SettingsManager that loads from files */ static create( cwd: string = process.cwd(), agentDir: string = getAgentDir(), ): SettingsManager { const storage = new FileSettingsStorage(cwd, agentDir); return SettingsManager.fromStorage(storage); } /** Create a SettingsManager from an arbitrary storage backend */ static fromStorage(storage: SettingsStorage): SettingsManager { const globalLoad = SettingsManager.tryLoadFromStorage(storage, "global"); const projectLoad = SettingsManager.tryLoadFromStorage(storage, "project"); const initialErrors: SettingsError[] = []; if (globalLoad.error) { initialErrors.push({ scope: "global", error: globalLoad.error }); } if (projectLoad.error) { initialErrors.push({ scope: "project", error: projectLoad.error }); } return new SettingsManager( storage, globalLoad.settings, projectLoad.settings, globalLoad.error, projectLoad.error, initialErrors, ); } /** Create an in-memory SettingsManager (no file I/O) */ static inMemory(settings: Partial = {}): SettingsManager { const storage = new InMemorySettingsStorage(); return new SettingsManager(storage, settings, {}); } private static loadFromStorage( storage: SettingsStorage, scope: SettingsScope, ): Settings { let content: string | undefined; storage.withLock(scope, (current) => { content = current; return undefined; }); if (!content) { return {}; } const settings = JSON.parse(content); return SettingsManager.migrateSettings(settings); } private static tryLoadFromStorage( storage: SettingsStorage, scope: SettingsScope, ): { settings: Settings; error: Error | null } { try { return { settings: SettingsManager.loadFromStorage(storage, scope), error: null, }; } catch (error) { return { settings: {}, error: error as Error }; } } /** Migrate old settings format to new format */ private static migrateSettings(settings: Record): Settings { // Migrate queueMode -> steeringMode if ("queueMode" in settings && !("steeringMode" in settings)) { settings.steeringMode = settings.queueMode; delete settings.queueMode; } // Migrate legacy websockets boolean -> transport enum if ( !("transport" in settings) && typeof settings.websockets === "boolean" ) { settings.transport = settings.websockets ? "websocket" : "sse"; delete settings.websockets; } // Migrate old skills object format to new array format if ( "skills" in settings && typeof settings.skills === "object" && settings.skills !== null && !Array.isArray(settings.skills) ) { const skillsSettings = settings.skills as { enableSkillCommands?: boolean; customDirectories?: unknown; }; if ( skillsSettings.enableSkillCommands !== undefined && settings.enableSkillCommands === undefined ) { settings.enableSkillCommands = skillsSettings.enableSkillCommands; } if ( Array.isArray(skillsSettings.customDirectories) && skillsSettings.customDirectories.length > 0 ) { settings.skills = skillsSettings.customDirectories; } else { delete settings.skills; } } return settings as Settings; } getGlobalSettings(): Settings { return structuredClone(this.globalSettings); } getProjectSettings(): Settings { return structuredClone(this.projectSettings); } reload(): void { const globalLoad = SettingsManager.tryLoadFromStorage( this.storage, "global", ); if (!globalLoad.error) { this.globalSettings = globalLoad.settings; this.globalSettingsLoadError = null; } else { this.globalSettingsLoadError = globalLoad.error; this.recordError("global", globalLoad.error); } this.modifiedFields.clear(); this.modifiedNestedFields.clear(); this.modifiedProjectFields.clear(); this.modifiedProjectNestedFields.clear(); const projectLoad = SettingsManager.tryLoadFromStorage( this.storage, "project", ); if (!projectLoad.error) { this.projectSettings = projectLoad.settings; this.projectSettingsLoadError = null; } else { this.projectSettingsLoadError = projectLoad.error; this.recordError("project", projectLoad.error); } this.settings = deepMergeSettings( this.globalSettings, this.projectSettings, ); } /** Apply additional overrides on top of current settings */ applyOverrides(overrides: Partial): void { this.settings = deepMergeSettings(this.settings, overrides); } /** Mark a global field as modified during this session */ private markModified(field: keyof Settings, nestedKey?: string): void { this.modifiedFields.add(field); if (nestedKey) { if (!this.modifiedNestedFields.has(field)) { this.modifiedNestedFields.set(field, new Set()); } this.modifiedNestedFields.get(field)!.add(nestedKey); } } /** Mark a project field as modified during this session */ private markProjectModified(field: keyof Settings, nestedKey?: string): void { this.modifiedProjectFields.add(field); if (nestedKey) { if (!this.modifiedProjectNestedFields.has(field)) { this.modifiedProjectNestedFields.set(field, new Set()); } this.modifiedProjectNestedFields.get(field)!.add(nestedKey); } } private recordError(scope: SettingsScope, error: unknown): void { const normalizedError = error instanceof Error ? error : new Error(String(error)); this.errors.push({ scope, error: normalizedError }); } private clearModifiedScope(scope: SettingsScope): void { if (scope === "global") { this.modifiedFields.clear(); this.modifiedNestedFields.clear(); return; } this.modifiedProjectFields.clear(); this.modifiedProjectNestedFields.clear(); } private enqueueWrite(scope: SettingsScope, task: () => void): void { this.writeQueue = this.writeQueue .then(() => { task(); this.clearModifiedScope(scope); }) .catch((error) => { this.recordError(scope, error); }); } private cloneModifiedNestedFields( source: Map>, ): Map> { const snapshot = new Map>(); for (const [key, value] of source.entries()) { snapshot.set(key, new Set(value)); } return snapshot; } private persistScopedSettings( scope: SettingsScope, snapshotSettings: Settings, modifiedFields: Set, modifiedNestedFields: Map>, ): void { this.storage.withLock(scope, (current) => { const currentFileSettings = current ? SettingsManager.migrateSettings( JSON.parse(current) as Record, ) : {}; const mergedSettings: Settings = { ...currentFileSettings }; for (const field of modifiedFields) { const value = snapshotSettings[field]; if ( modifiedNestedFields.has(field) && typeof value === "object" && value !== null ) { const nestedModified = modifiedNestedFields.get(field)!; const baseNested = (currentFileSettings[field] as Record) ?? {}; const inMemoryNested = value as Record; const mergedNested = { ...baseNested }; for (const nestedKey of nestedModified) { mergedNested[nestedKey] = inMemoryNested[nestedKey]; } (mergedSettings as Record)[field] = mergedNested; } else { (mergedSettings as Record)[field] = value; } } return JSON.stringify(mergedSettings, null, 2); }); } private save(): void { this.settings = deepMergeSettings( this.globalSettings, this.projectSettings, ); if (this.globalSettingsLoadError) { return; } const snapshotGlobalSettings = structuredClone(this.globalSettings); const modifiedFields = new Set(this.modifiedFields); const modifiedNestedFields = this.cloneModifiedNestedFields( this.modifiedNestedFields, ); this.enqueueWrite("global", () => { this.persistScopedSettings( "global", snapshotGlobalSettings, modifiedFields, modifiedNestedFields, ); }); } private saveProjectSettings(settings: Settings): void { this.projectSettings = structuredClone(settings); this.settings = deepMergeSettings( this.globalSettings, this.projectSettings, ); if (this.projectSettingsLoadError) { return; } const snapshotProjectSettings = structuredClone(this.projectSettings); const modifiedFields = new Set(this.modifiedProjectFields); const modifiedNestedFields = this.cloneModifiedNestedFields( this.modifiedProjectNestedFields, ); this.enqueueWrite("project", () => { this.persistScopedSettings( "project", snapshotProjectSettings, modifiedFields, modifiedNestedFields, ); }); } async flush(): Promise { await this.writeQueue; } drainErrors(): SettingsError[] { const drained = [...this.errors]; this.errors = []; return drained; } getLastChangelogVersion(): string | undefined { return this.settings.lastChangelogVersion; } setLastChangelogVersion(version: string): void { this.globalSettings.lastChangelogVersion = version; this.markModified("lastChangelogVersion"); this.save(); } getDefaultProvider(): string | undefined { return this.settings.defaultProvider; } getDefaultModel(): string | undefined { return this.settings.defaultModel; } setDefaultProvider(provider: string): void { this.globalSettings.defaultProvider = provider; this.markModified("defaultProvider"); this.save(); } setDefaultModel(modelId: string): void { this.globalSettings.defaultModel = modelId; this.markModified("defaultModel"); this.save(); } setDefaultModelAndProvider(provider: string, modelId: string): void { this.globalSettings.defaultProvider = provider; this.globalSettings.defaultModel = modelId; this.markModified("defaultProvider"); this.markModified("defaultModel"); this.save(); } getSteeringMode(): "all" | "one-at-a-time" { return this.settings.steeringMode || "one-at-a-time"; } setSteeringMode(mode: "all" | "one-at-a-time"): void { this.globalSettings.steeringMode = mode; this.markModified("steeringMode"); this.save(); } getFollowUpMode(): "all" | "one-at-a-time" { return this.settings.followUpMode || "one-at-a-time"; } setFollowUpMode(mode: "all" | "one-at-a-time"): void { this.globalSettings.followUpMode = mode; this.markModified("followUpMode"); this.save(); } getTheme(): string | undefined { return this.settings.theme; } setTheme(theme: string): void { this.globalSettings.theme = theme; this.markModified("theme"); this.save(); } getDefaultThinkingLevel(): | "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | undefined { return this.settings.defaultThinkingLevel; } setDefaultThinkingLevel( level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh", ): void { this.globalSettings.defaultThinkingLevel = level; this.markModified("defaultThinkingLevel"); this.save(); } getTransport(): TransportSetting { return this.settings.transport ?? "sse"; } setTransport(transport: TransportSetting): void { this.globalSettings.transport = transport; this.markModified("transport"); this.save(); } getCompactionEnabled(): boolean { return this.settings.compaction?.enabled ?? true; } setCompactionEnabled(enabled: boolean): void { if (!this.globalSettings.compaction) { this.globalSettings.compaction = {}; } this.globalSettings.compaction.enabled = enabled; this.markModified("compaction", "enabled"); this.save(); } getCompactionReserveTokens(): number { return this.settings.compaction?.reserveTokens ?? 16384; } getCompactionKeepRecentTokens(): number { return this.settings.compaction?.keepRecentTokens ?? 20000; } getCompactionSettings(): { enabled: boolean; reserveTokens: number; keepRecentTokens: number; } { return { enabled: this.getCompactionEnabled(), reserveTokens: this.getCompactionReserveTokens(), keepRecentTokens: this.getCompactionKeepRecentTokens(), }; } getBranchSummarySettings(): { reserveTokens: number; skipPrompt: boolean } { return { reserveTokens: this.settings.branchSummary?.reserveTokens ?? 16384, skipPrompt: this.settings.branchSummary?.skipPrompt ?? false, }; } getBranchSummarySkipPrompt(): boolean { return this.settings.branchSummary?.skipPrompt ?? false; } getRetryEnabled(): boolean { return this.settings.retry?.enabled ?? true; } setRetryEnabled(enabled: boolean): void { if (!this.globalSettings.retry) { this.globalSettings.retry = {}; } this.globalSettings.retry.enabled = enabled; this.markModified("retry", "enabled"); this.save(); } getRetrySettings(): { enabled: boolean; maxRetries: number; baseDelayMs: number; maxDelayMs: number; } { return { enabled: this.getRetryEnabled(), maxRetries: this.settings.retry?.maxRetries ?? 3, baseDelayMs: this.settings.retry?.baseDelayMs ?? 2000, maxDelayMs: this.settings.retry?.maxDelayMs ?? 60000, }; } getHideThinkingBlock(): boolean { return this.settings.hideThinkingBlock ?? false; } setHideThinkingBlock(hide: boolean): void { this.globalSettings.hideThinkingBlock = hide; this.markModified("hideThinkingBlock"); this.save(); } getShellPath(): string | undefined { return this.settings.shellPath; } setShellPath(path: string | undefined): void { this.globalSettings.shellPath = path; this.markModified("shellPath"); this.save(); } getQuietStartup(): boolean { return this.settings.quietStartup ?? false; } setQuietStartup(quiet: boolean): void { this.globalSettings.quietStartup = quiet; this.markModified("quietStartup"); this.save(); } getShellCommandPrefix(): string | undefined { return this.settings.shellCommandPrefix; } setShellCommandPrefix(prefix: string | undefined): void { this.globalSettings.shellCommandPrefix = prefix; this.markModified("shellCommandPrefix"); this.save(); } getCollapseChangelog(): boolean { return this.settings.collapseChangelog ?? false; } setCollapseChangelog(collapse: boolean): void { this.globalSettings.collapseChangelog = collapse; this.markModified("collapseChangelog"); this.save(); } getPackages(): PackageSource[] { return [...(this.settings.packages ?? [])]; } setPackages(packages: PackageSource[]): void { this.globalSettings.packages = packages; this.markModified("packages"); this.save(); } setProjectPackages(packages: PackageSource[]): void { const projectSettings = structuredClone(this.projectSettings); projectSettings.packages = packages; this.markProjectModified("packages"); this.saveProjectSettings(projectSettings); } getExtensionPaths(): string[] { return [...(this.settings.extensions ?? [])]; } setExtensionPaths(paths: string[]): void { this.globalSettings.extensions = paths; this.markModified("extensions"); this.save(); } setProjectExtensionPaths(paths: string[]): void { const projectSettings = structuredClone(this.projectSettings); projectSettings.extensions = paths; this.markProjectModified("extensions"); this.saveProjectSettings(projectSettings); } getSkillPaths(): string[] { return [...(this.settings.skills ?? [])]; } setSkillPaths(paths: string[]): void { this.globalSettings.skills = paths; this.markModified("skills"); this.save(); } setProjectSkillPaths(paths: string[]): void { const projectSettings = structuredClone(this.projectSettings); projectSettings.skills = paths; this.markProjectModified("skills"); this.saveProjectSettings(projectSettings); } getPromptTemplatePaths(): string[] { return [...(this.settings.prompts ?? [])]; } setPromptTemplatePaths(paths: string[]): void { this.globalSettings.prompts = paths; this.markModified("prompts"); this.save(); } setProjectPromptTemplatePaths(paths: string[]): void { const projectSettings = structuredClone(this.projectSettings); projectSettings.prompts = paths; this.markProjectModified("prompts"); this.saveProjectSettings(projectSettings); } getThemePaths(): string[] { return [...(this.settings.themes ?? [])]; } setThemePaths(paths: string[]): void { this.globalSettings.themes = paths; this.markModified("themes"); this.save(); } setProjectThemePaths(paths: string[]): void { const projectSettings = structuredClone(this.projectSettings); projectSettings.themes = paths; this.markProjectModified("themes"); this.saveProjectSettings(projectSettings); } getEnableSkillCommands(): boolean { return this.settings.enableSkillCommands ?? true; } setEnableSkillCommands(enabled: boolean): void { this.globalSettings.enableSkillCommands = enabled; this.markModified("enableSkillCommands"); this.save(); } getThinkingBudgets(): ThinkingBudgetsSettings | undefined { return this.settings.thinkingBudgets; } getShowImages(): boolean { return this.settings.terminal?.showImages ?? true; } setShowImages(show: boolean): void { if (!this.globalSettings.terminal) { this.globalSettings.terminal = {}; } this.globalSettings.terminal.showImages = show; this.markModified("terminal", "showImages"); this.save(); } getClearOnShrink(): boolean { // Settings takes precedence, then env var, then default false if (this.settings.terminal?.clearOnShrink !== undefined) { return this.settings.terminal.clearOnShrink; } return process.env.PI_CLEAR_ON_SHRINK === "1"; } setClearOnShrink(enabled: boolean): void { if (!this.globalSettings.terminal) { this.globalSettings.terminal = {}; } this.globalSettings.terminal.clearOnShrink = enabled; this.markModified("terminal", "clearOnShrink"); this.save(); } getImageAutoResize(): boolean { return this.settings.images?.autoResize ?? true; } setImageAutoResize(enabled: boolean): void { if (!this.globalSettings.images) { this.globalSettings.images = {}; } this.globalSettings.images.autoResize = enabled; this.markModified("images", "autoResize"); this.save(); } getBlockImages(): boolean { return this.settings.images?.blockImages ?? false; } setBlockImages(blocked: boolean): void { if (!this.globalSettings.images) { this.globalSettings.images = {}; } this.globalSettings.images.blockImages = blocked; this.markModified("images", "blockImages"); this.save(); } getEnabledModels(): string[] | undefined { return this.settings.enabledModels; } setEnabledModels(patterns: string[] | undefined): void { this.globalSettings.enabledModels = patterns; this.markModified("enabledModels"); this.save(); } getDoubleEscapeAction(): "fork" | "tree" | "none" { return this.settings.doubleEscapeAction ?? "tree"; } setDoubleEscapeAction(action: "fork" | "tree" | "none"): void { this.globalSettings.doubleEscapeAction = action; this.markModified("doubleEscapeAction"); this.save(); } getTreeFilterMode(): | "default" | "no-tools" | "user-only" | "labeled-only" | "all" { const mode = this.settings.treeFilterMode; const valid = ["default", "no-tools", "user-only", "labeled-only", "all"]; return mode && valid.includes(mode) ? mode : "default"; } setTreeFilterMode( mode: "default" | "no-tools" | "user-only" | "labeled-only" | "all", ): void { this.globalSettings.treeFilterMode = mode; this.markModified("treeFilterMode"); this.save(); } getShowHardwareCursor(): boolean { return ( this.settings.showHardwareCursor ?? process.env.PI_HARDWARE_CURSOR === "1" ); } setShowHardwareCursor(enabled: boolean): void { this.globalSettings.showHardwareCursor = enabled; this.markModified("showHardwareCursor"); this.save(); } getEditorPaddingX(): number { return this.settings.editorPaddingX ?? 0; } setEditorPaddingX(padding: number): void { this.globalSettings.editorPaddingX = Math.max( 0, Math.min(3, Math.floor(padding)), ); this.markModified("editorPaddingX"); this.save(); } getAutocompleteMaxVisible(): number { return this.settings.autocompleteMaxVisible ?? 5; } setAutocompleteMaxVisible(maxVisible: number): void { this.globalSettings.autocompleteMaxVisible = Math.max( 3, Math.min(20, Math.floor(maxVisible)), ); this.markModified("autocompleteMaxVisible"); this.save(); } getCodeBlockIndent(): string { return this.settings.markdown?.codeBlockIndent ?? " "; } getGatewaySettings(): GatewaySettings { return structuredClone(this.settings.gateway ?? {}); } }